2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Officeアドイン編】エクセルでローカルLLM翻訳

Posted at

ExcelのOfficeアドインとして、ローカルで動作する翻訳アプリを作ってみました!
以前にExcel-DNA(C#)でxllアドインとしてローカルLLMを組み込んだモノを作りましたが、今回React + Transformers.jsでモダンなUIと柔軟な構成で実装しています。
以下では、開発環境から構築手順、翻訳モデルの組み込み、UIの工夫、そしてExcelへの統合まで紹介していきます。

テンプレートはYeomanジェネレーターで作っています。

最終的にエクセルのサイドパネルでこのようなものをつくります。
image.png

最終的なコードは以下です。

以前はExcel-DNA(C#)でローカルLLMのxllアドインを作りました。

環境

  • Windows11
  • Node.js v22.19.0
  • npm 10.9.3
  • VSCode
  • Excel2024

プロジェクトの作成

yo office

? Choose a project type: Office Add-in Task Pane project using React framework
? Choose a script type: JavaScript
? What do you want to name your add-in? ReactTest
? Which Office client application would you like to support? Excel

transformers.jsで翻訳アプリ

翻訳アプリをOfficeアドインテンプレートへ

transformersを追加
npm i @huggingface/transformers
cssを追加
npm install --save-dev style-loader css-loader
  • テンプレートLocalTranslatorForExcel/src内のファイルは削除して、react-translator/src/内のファイルをコピーする

webpack.config.jsの設定

以下のように必要な設定を変更していきます。

  • ファイル名が変わるのでentryの変更
  • commandsは使わないので不要
    entry: {
      polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
      react: ["react", "react-dom"],
      // 変更
      // taskpane: {
      index: {
        // import: ["./src/taskpane/index.jsx", "./src/taskpane/taskpane.html"],
        import: ["./src/main.jsx", "./src/index.html"],
        dependOn: "react",
      },
      // 不要
      // commands: "./src/commands/commands.js",
    },
    // entryに合わせて変更
    plugins: [
      // new HtmlWebpackPlugin({
      //   filename: "taskpane.html",
      //   template: "./src/taskpane/taskpane.html",
      //   chunks: ["polyfill", "taskpane", "react"],
      // }),
      new HtmlWebpackPlugin({
        filename: "index.html",
        template: "./src/index.html",
        chunks: ["polyfill", "index", "react"],
      }),
    // 不要
      // new HtmlWebpackPlugin({
      //   filename: "commands.html",
      //   template: "./src/commands/commands.html",
      //   chunks: ["polyfill", "commands"],
      // }),
  • cssファイルを使うので追加
    // cssを追加
    resolve: {
      extensions: [".js", ".jsx", ".html", ".css"],
    },
module: {
      rules: [
            // cssについて追加
        {
          test: /\.css$/i,
          use: ['style-loader', 'css-loader'],
          exclude: /node_modules/,
        },
    ]
}
  • WebSocketについて追加
devServer: {
      // websocketの追加
      client: {
        webSocketURL: {
          hostname: "localhost",
          port: 3000,
          pathname: "/ws",
          protocol: "wss",
        },
      },
    },

manifest.xml

  • entry変更に合わせての変更
  <DefaultSettings>
    <!-- 変更 -->
    <!-- <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/> -->
    <SourceLocation DefaultValue="https://localhost:3000/index.html"/>

      <bt:Urls>
        <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://go.microsoft.com/fwlink/?LinkId=276812"/>
        <!-- 不要 -->
        <!-- <bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/> -->
        <!-- 変更 -->
        <!-- <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/> -->
        <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/index.html"/>
      </bt:Urls>

その他

  • App.jsx(その他も)でreactが読み込めないエラーになるのでimportを追加
App.jsx
// エラー
// Uncaught ReferenceError: React is not defined
//     at App (App.jsx:107:3)
import React from "react";
  • favicon.icoを設定していないとブラウザが探してエラーになるのでindex.htmlへ追加
index.html
<!-- ブラウザが探すので設定していないとエラーになる -->
    <!-- favicon.ico:1   Failed to load resource: the server responded with a status of 404 () -->
    <link rel="icon" href="../assets/favicon.ico" type="image/x-icon">
  • エクセルのサイドパネルに表示するので小さい幅へのレスポンシブ対応
App.css 広げる
App.css
#root {
  /* レスポンシブ対応のために。固定ではなく100% */
  width: 100%;
  max-width: 1280px;
  margin: 0 auto;
  padding: 2rem;
  text-align: center;
}

.language-container {
  display: flex;
  gap: 20px;
  justify-content: center;
}

.textbox-container {
  display: flex;
  justify-content: center;
  gap: 20px;
  /* 固定値は入れない */
  /* width: 800px; */
}

.textbox-container > textarea,
.language-selector {
  width: 50%;
}

.language-selector > select {
  width: 150px;
}

.progress-container {
  position: relative;
  font-size: 14px;
  color: white;
  background-color: #e9ecef;
  border: solid 1px;
  border-radius: 8px;
  text-align: left;
  overflow: hidden;
}

.progress-bar {
  padding: 0 4px;
  z-index: 0;
  top: 0;
  width: 1%;
  overflow: hidden;
  background-color: #007bff;
  white-space: nowrap;
}

.progress-text {
  z-index: 2;
}

.selector-container {
  display: flex;
  gap: 20px;
}

.progress-bars-container {
  padding: 8px;
  height: 140px;
}

.container {
  margin: 25px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}


/* メディアクエリ対応 */
@media screen and (max-width: 768px) {
  .textbox-container {
    width: 100%;
    flex-direction: column;
  }
  .textbox-container > textarea,
  .language-selector {
    width: 100%;
  }

  /* .language-container {
    flex-direction: column;
  } */

  button {
    width: 100%;
  }
  .progress-bars-container {
    width: 100%;
  }
}

.button-group {
  display: flex;
  gap: 1rem;
  justify-content: center;
  margin-top: 1rem;
}

翻訳言語の入れ替え

  • 翻訳対象を入れ替えるボタンを追加します
App.jsx 広げる
App.jsx
// sourceとtarget入れ替えの状態管理
  const [shouldTranslateAfterSwap, setShouldTranslateAfterSwap] = useState(false);
  useEffect(() => {
    if (shouldTranslateAfterSwap) {
      translate();
      setShouldTranslateAfterSwap(false);
    }
  }, [sourceLanguage, targetLanguage, input]);

  // sourceとtargetを入れ替える
  // 翻訳結果を空白にする
  // 翻訳の実行
  const swapLanguages = () => {
    setSourceLanguage(targetLanguage);
    setTargetLanguage(sourceLanguage);
    setInput(output);
    setOutput("");
    setShouldTranslateAfterSwap(true);
  };

  return (
    <>
      <h1>Local Translator for Excel!</h1>
      <h2>エクセルでローカル翻訳</h2>

      <div className="container">
        <div className="button-group">
          <button disabled={disabled} onClick={translate}>
            Translate
          </button>
          {/* 入れ替えボタン */}
          <button onClick={swapLanguages}>Swap</button>
        </div>
        <div className="language-container">
          <LanguageSelector
            type={"Source"}
            value={sourceLanguage}
            onChange={(x) => setSourceLanguage(x.target.value)}
          />
          <LanguageSelector
            type={"Target"}
            value={targetLanguage}
            onChange={(x) => setTargetLanguage(x.target.value)}
          />
        </div>

        <div className="textbox-container">
          <textarea value={input} rows={3} onChange={(e) => setInput(e.target.value)}></textarea>
          <textarea value={output} rows={3} readOnly></textarea>
        </div>
      </div>

      <div className="progress-bars-container">
        {ready === false && <label>Loading models... (only run once)</label>}
        {progressItems.map((data) => (
          <div key={data.file}>
            <Progress text={data.file} percentage={data.progress} />
          </div>
        ))}
      </div>
    </>
  );
}

  • 変更が反映されるようにdefaultLanguageからvalueに変更
LanguageSelector.jsx 広げる
LanguageSelector.jsx
export default function LanguageSelector({ type, onChange, value }) {
  return (
    <div className="language-selector">
      <label>{type}: </label>
      <select onChange={onChange} value={value}>
        {Object.entries(LANGUAGES).map(([key, value]) => {
          return (
            <option key={key} value={value}>
              {key}
            </option>
          );
        })}
      </select>
    </div>
  );
}

エクセルで使用

  • 初回のモデルダウンロードのみインターネット接続が必要です
  • 以下のような翻訳をローカルにて行えます
    翻訳gif.gif

GitHub Pagesにホスト

GitHub Pages サイトの公開元を設定する

  • npm run buildでdistフォルダに出力されます
  • 設定を変更する
    ここを変えればbuild時にmanifest.xmlが変わる
webpack.config.js
// const urlProd = "https://www.contoso.com/"; // CHANGE THIS TO YOUR PRODUCTION DEPLOYMENT LOCATION
const urlProd = "https://msmsrep.github.io/LocalTranslatorForExcel/dist/";
  • エクセルに読み込むのはdistに出力されたmanifest.xmlです

次回はサイドロードについての予定です

参考

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?