小さな文字列処理でも、CLI と Web UI、さらに別言語の API を用意すると、空白の除去や項目の順序が少しずつずれることがあります。
この記事では、自作の MIT ライセンスのツール Anime Prompt Kit を例に、実行環境に依存する処理を分離する方法と、JavaScript・Python 間の出力を比較するテストを紹介します。筆者はこのプロジェクトの開発者です。
まず文字列の仕様を固定する
このツールは画像を生成する API ではありません。7 項目を一定の順番で連結して、画像ツールに入力するためのプロンプトを作ります。
- subject
- outfit
- action
- setting
- style
- lighting
- composition
各項目の前後の空白を除き、空の項目は省略し、残りを , で結合します。指定された項目が文字列でない場合はエラーにします。
composePrompt({ subject: ' explorer ', outfit: ' ', setting: 'forest' });
// 'explorer, forest'
ファイル読み込みを純粋な関数から切り離す
Node.js 用の入口では node:fs を使ってテンプレートを読み込みます。しかし、ブラウザで同じ入口を import すると、この依存が問題になります。
そこで、連結処理を composer.js に分離しました。以下は実際の実装です。
const fields = ['subject', 'outfit', 'action', 'setting', 'style', 'lighting', 'composition'];
export function composePrompt(options = {}) {
if (!options || typeof options !== 'object' || Array.isArray(options)) throw new TypeError('Expected a prompt options object');
for (const key of fields) {
if (options[key] !== undefined && typeof options[key] !== 'string') throw new TypeError(`${key} must be a string`);
}
return fields.map(key => options[key]?.trim()).filter(Boolean).join(', ');
}
このファイルは DOM、ファイルシステム、ネットワークに依存しません。Node.js 側の入口はこの関数を再エクスポートし、ブラウザ側は直接 import します。
import { composePrompt } from './composer.js';
const prompt = composePrompt({
subject: 'a travelling botanist',
setting: 'a quiet greenhouse',
style: 'anime illustration',
});
テンプレートの取得方法は入口ごとに変えます。Node.js ではファイル読み込み、ブラウザでは同じサイト上の JSON を取得する方法です。変更すべき部分を入口に限定すると、連結の仕様を二重に実装せずに済みます。
Python 版は同じデータで比較する
Python 版にも compose_prompt と from_template を用意しています。JavaScript のコードを Python からそのまま呼ぶ設計ではなく、同じ仕様を別実装にして、テストで比較しています。
from anime_prompt_kit import compose_prompt
assert compose_prompt(
subject=' explorer ', outfit=' ', setting='forest'
) == 'explorer, forest'
リポジトリには 12 件のテンプレートがあります。テストでは Python に同梱した JSON とルートの JSON が同じであることを確認した後、全テンプレートに同じ上書きを適用して比較します。
for template in list_templates():
js = subprocess.check_output(
['node', 'cli.js', template['id'], '--setting', 'a snowy station'],
cwd=root,
text=True,
).strip()
self.assertEqual(
from_template(template['id'], setting='a snowy station'),
js,
)
この断片は tests/test_python.py のテストメソッド内の処理です。root はリポジトリのルート、self は unittest.TestCase のインスタンスです。比較テストの実行には Python に加えて Node.js も必要です。
同じ結果になる範囲を言い過ぎない
このテストで確認できるのは、用意したテンプレートと上書き条件での出力一致です。任意の Unicode 空白や、JavaScript と Python のすべての型に対する完全な同等性までは保証しません。
ほかにも、テンプレートを上書きしても元データが変わらないこと、未知のテンプレート ID がエラーになること、CLI の不正な引数で不要な Python のトレースバックを出さないことを確認しています。
画像生成の品質は別の検証対象です。含まれるテンプレートは個別に画像生成テストをしておらず、文字列のテストが画像の品質を保証するわけではありません。
小さなツールで分離する意味
実装が短い場合でも、環境固有の処理とデータ変換を分けると、ブラウザ用の都合が CLI に入り込むのを避けられます。別言語版では、同じ入力を使う比較テストを加えることで、テンプレート更新時のずれを検出できます。
コードは GitHub で公開しています。関連する自作サービス AI Anime Generator とツールの関係は、オープンソースツールの紹介ページ にまとめています。