HAROLABO Tech Blog

テクノロジーを使いやすく!ハロラボの技術情報発信メディアです。

WordにLLMで自動的にコメントを入れる

とある事情で、Wordに自動でコメントを入れたいという要望があったので、試してみました。

WordのCopilotやGoogleドキュメントのGeminiでコメントの挿入をお願いすると、内部で利用しているpython-docx にその機能がなくてできないよと言われます。

仕方ないので、別の方法を検討します。

今時だと、Coworkとかを使うのでしょうが、ローカルのLM Studioでどこまでできるかを試すため、Officeアドインを使って試してみることにしました。

Officeアドインには、Script Labというプロトタイプを開発するための仕組みがあります。 これを使うと、簡単にローカルで動作するアドインがつくれます。

今回はこれを使って試してみることにします。

セットアップ

Script Labを入れます。

learn.microsoft.com

あとは、LM Studioとモデルをインストールしておきましょう。 今回は、qwen/qwen3-4b-2507を使いました。

techblog.harolabo.com

Wordでコードを書く

  1. Wordファイルを開くと、Script Labメニューが出現していますので、クリックします。

  2. リボンメニューからCodeを選択します。

  3. Scriptでコードエディタが開くので、コードを書きます。

ローカルLLMを呼び出して、分割して文章を入力し、得た出力をコメントとして挿入するコードです。 ChatGPT APIなどを使えば、分割せずに一気に文章を入力することも可能でしょう。

const PARAGRAPHS_PER_CHUNK = 10; // ← 分割数(段落数)

document.getElementById("run").addEventListener("click", () => tryCatch(run));

async function run() {
  await Word.run(async (context) => {
    const paragraphs = context.document.body.paragraphs;
    paragraphs.load("items");
    await context.sync();

    // 1) 全段落テキストをロード(index整合性を優先)
    const items = paragraphs.items;
    for (const p of items) p.load("text");
    await context.sync();

    const paraTexts = items.map(p => (p.text ?? "").trim());

    // 2) 段落配列を N 個ずつに分割
    const chunks = splitIntoChunks(paraTexts, PARAGRAPHS_PER_CHUNK);

    // 3) 各チャンクをAPIに投げ、paragraphIndexを補正して集約
    const allComments = [];
    for (const chunk of chunks) {
      const { startIndex, paragraphs: chunkParas } = chunk;

      const comments = await proofreadWholeDocByLocalOpenAICompat(chunkParas);

      if (!Array.isArray(comments)) continue;

      for (const c of comments) {
        if (typeof c ?.paragraphIndex !== "number") continue;

        // ★ オフセット補正(ここが重要)
        allComments.push({
          paragraphIndex: c.paragraphIndex + startIndex,
          findings: c.findings,
        });
      }
    }

    if (allComments.length === 0) {
      console.log("No comments returned.");
      return;
    }

    // 4) Wordにコメント挿入
    for (const c of allComments) {
      const pi = c.paragraphIndex;
      const findings = c.findings;

      if (pi < 0 || pi >= items.length) continue;
      if (!Array.isArray(findings) || findings.length === 0) continue;

      const commentText = findings
        .filter(f => f && typeof f.message === "string" && f.message.trim())
        .map(f => `• ${f.message}${f.severity ? `(${f.severity})` : ""}`)
        .join("\n");

      if (!commentText) continue;

      items[pi].getRange().insertComment(commentText);
    }

    await context.sync();
    console.log("Done: comments inserted.");
  });
}

/**
 * 段落配列を N 個ずつに分割し、元indexを保持
 */
function splitIntoChunks(array, size) {
  const chunks = [];
  for (let i = 0; i < array.length; i += size) {
    chunks.push({
      startIndex: i,                 // 元の段落index
      paragraphs: array.slice(i, i + size),
    });
  }
  return chunks;
}

/**
 * チャンク(段落配列)を1回で校正
 * 戻り値:
 * [
 *   { paragraphIndex: number, findings: [{type,message,severity?}] }
 * ]
 */
async function proofreadWholeDocByLocalOpenAICompat(paragraphs) {
  const url = "http://localhost:1234/v1/chat/completions";
  const model = "model-identifier";

  const system = `
あなたは日本語論文の校正者です。
入力は「文書の段落配列 paragraphs」です。
各段落について、日本語として不自然/論文として不適切/曖昧な表現を指摘してください。
修正案(書き換え文章)は出さないでください。

必ず次のJSON“だけ”で返してください。説明文は禁止。
{
  "comments": [
    {
      "paragraphIndex": 0,
      "findings": [
        {"type":"grammar|style|logic|term|ambiguity", "message":"短く具体的な指摘", "severity":"low|mid|high"}
      ]
    }
  ]
}

- 指摘がない段落は comments に含めないでください。
- paragraphIndex は paragraphs の 0始まり添字です。
  `.trim();

  const user = JSON.stringify({ paragraphs }, null, 2);

  const res = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      model,
      messages: [
        { role: "system", content: system },
        { role: "user", content: user },
      ],
      temperature: 0.2,
    }),
  });

  const data = await res.json();
  const answer = data ?.choices ?.[0] ?.message ?.content ?? "";

  const parsed = safeJsonParse(answer);
  if (parsed && Array.isArray(parsed.comments)) {
    return parsed.comments.filter(
      x => typeof x ?.paragraphIndex === "number" && Array.isArray(x ?.findings)
    );
  }

  // フォールバック(JSON崩れ)
  if (answer.trim()) {
    return [{
      paragraphIndex: 0,
      findings: [{ type: "raw", message: answer.trim(), severity: "mid" }]
    }];
  }
  return [];
}

function safeJsonParse(s) {
  try {
    const cleaned = s
      .replace(/^```json\s*/i, "")
      .replace(/^```\s*/i, "")
      .replace(/```$/i, "")
      .trim();
    return JSON.parse(cleaned);
  } catch {
    return null;
  }
}

async function tryCatch(callback) {
  try {
    await callback();
  } catch (error) {
    console.error(error);
  }
}

Wordでプロトタイプとして作成したアドインを実行する

  1. ▷ボタンを押します。

  2. Runボタンが出てくるので、押します。

これで書いたコードがアドインとして実行されます。 コメントがローカルLLMで自動生成され、スクリプトで自動で挿入されるようになりました。

いくつか試してみましたが、それっぽいコメントは挿入してくれるものの、やっぱりそのまま使うのは厳しそうな感じでした。 ただ、精度は他のAIを使えば改善できますので、仕組みとしては十分使えそうに思いました。

他のOfficeアプリであるExcelやPowerPointなどでも同じ仕組みが使えると思いますので、どうしてもOfficeファイルを自動で操作したい、といった場合には、選択肢の1つとして検討してみるのもありかと思います。