クリーム色の背景、色付き表示、高コントラスト、大きな文字のほうが読みやすいですか?配色と文字を選ぶと、すべてのページに一度で反映されます。

無料レッスン

紙のページを文字と項目にする

このレッスンのあとで: 読み取った文字の箱を読む順に並べ、項目とその値を取り出せます。

70分のレッスンコマンド7件AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

写真に撮った領収書は、まだ文字ではない

領収書をスマホで撮ると、画像ができます。人には「合計 2,480円」と読めても、コンピューターにはまだ画素の表です。

文字を読み取ると、文字と場所の組が並んだ結果が返ります。このレッスンでは、それを読む順に直し、項目と値を取り出します。

このレッスンのゴール

メモ

読み取った文字の箱を読む順に並べ、項目とその値を取り出せます。

キーワード

ことばいみ
箱一つの文字のまとまりと、その場所
読む順上から下、同じ高さでは左から右に並べること
許容幅同じ行とみなす、高さの差の大きさ
項目名前と値の組。名前は「合計」のようなラベル
確信度読み取りがどれだけ確かかを表す数

学生が入力するセルは、このプレビューでは空白です。

ページは箱のリストである

文字の読み取りは、ページを箱のリストにします。箱は、文字、左上の角の位置、確信度を持ちます。

箱は、読み取った順に返るとは限りません。見た目の順に並べ直さないと、「合計」と金額が離れてしまいます。

ページから出た四つの箱

文字横 x縦 y確信度
店名:101099
ひまわり書店801297
合計:107099
2,480円806961

学生が入力するセルは、このプレビューでは空白です。

読む順には許容幅がいる

同じ行の箱でも、縦の位置は少しずれます。「合計:」は 70、金額は 69 です。縦だけで並べると、金額が先に来てしまいます。

そこで、最初の箱の高さから許容幅以内の箱を、同じ行に入れます。行の中は、左から右に並べます。

よくある間違い

注意

「高さが同じ数のものだけが、同じ行だ」と考えがちです。紙を斜めに撮ると、同じ行でも高さは少しずれます。許容幅を 0 にすると行が割れ、項目の値が見つからなくなります。

項目は、ラベルとその値

領収書では、「合計:」のようなラベルのすぐ右に、値が書かれています。ラベルの次の箱が、その項目の値です。

値がない項目は、空として残します。確信度の低い値は、人が確かめる目印にします。

モデル用の補助を書く(1)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// 応答に含まれるトークン数。なければ null。' 'function count(data, name) {' '  return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

モデル用の補助を書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", {' '      method: \"POST\",' '      headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' '      body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    const d = await res.json();' '    return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' '  } catch (error) {' '    console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' '    return { text: sample, inTokens: null, outTokens: null, recorded: true };' '  }' '}' 'module.exports = { ask };' >> model.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

ページを読むスクリプトを書く(1)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const boxes = [' '  { text: \"2,480円\", x: 80, y: 69, conf: 61 },' '  { text: \"店名:\", x: 10, y: 10, conf: 99 },' '  { text: \"11月3日\", x: 80, y: 43, conf: 88 },' '  { text: \"ひまわり書店\", x: 80, y: 12, conf: 97 },' '  { text: \"合計:\", x: 10, y: 70, conf: 99 },' '  { text: \"日付:\", x: 10, y: 40, conf: 98 },' '];' > page3.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

ページを読むスクリプトを書く(2)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function lines(list, tol) {' '  const sorted = list.slice().sort(function (a, b) { return a.y - b.y || a.x - b.x; });' '  const out = [];' '  sorted.forEach(function (b) {' '    const last = out[out.length - 1];' '    if (last && Math.abs(b.y - last[0].y) <= tol) last.push(b);' '    else out.push([b]);' '  });' '  return out.map(function (l) { return l.sort(function (a, b) { return a.x - b.x; }); });' '}' >> page3.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

ページを読むスクリプトを書く(3)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function fields(ls) {' '  const found = {};' '  ls.forEach(function (line) {' '    line.forEach(function (b, i) {' '      if (b.text.endsWith(\":\") && line[i + 1]) found[b.text.slice(0, -1)] = line[i + 1];' '    });' '  });' '  return found;' '}' >> page3.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

ページを読むスクリプトを書く(4)

コマンド
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const ls = lines(boxes, 5);' '  ls.forEach(function (l) { console.log(l.map(function (b) { return b.text; }).join(\" | \")); });' '  const f = fields(ls);' '  Object.keys(f).forEach(function (k) {' '    const low = f[k].conf < 70 ? \" 要確認\" : \"\";' '    console.log(k + \" = \" + f[k].text + \" (確信度 \" + f[k].conf + \")\" + low);' '  });' '  const q = \"文字の読み取りで確信度が低い項目を、人に確かめてもらう理由を、短い一文で書いてください。\";' '  const r = await ask([{ role: \"user\", content: q }], \"確信度が低い項目は読み間違いの可能性が高く、金額のような大事な値は人が確かめるべきだからです。\", 60);' '  console.log(\"返事: \" + r.text);' '}' 'main();' >> page3.js"

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

ページを読み、項目を取り出す

コマンド
docker exec -w /home/node/project agent node page3.js

ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。

出力の例

行示していること
店名: | ひまわり書店同じ行の箱が、左から右に並んだ
合計: | 2,480円金額の高さが 1 だけ違っても、同じ行に入った
合計 = 2,480円 (確信度 61) 要確認確信度が 70 に満たないので、人が確かめる
返事: ...モデルの一文。先に「記録済みサンプル」の行が出ることもある

学生が入力するセルは、このプレビューでは空白です。

このラボとソースについて: AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

授業で使いますか?このモジュールを学生に割り当てると、教員用ガイド、自動採点つきの課題、学生ごとのラボを使えます。