1
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?

Node.js とブラウザで同じ関数を使い、Python 版との出力差をテストする

1
Posted at

小さな文字列処理でも、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 とツールの関係は、オープンソースツールの紹介ページ にまとめています。

1
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
1
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?