Qwen-ImageでバイリンガルOGP画像をローカル生成する

780mcomfyuiimage-generationsite-meta

小型PCから出た琥珀色の画像が青緑のガラス枠を通って2枚のカードへ分かれる様子

14枚の背景画像が完成しました。過去の記事用が13枚、この記事用が1枚です。すべて最初の目視確認で採用しました。この記事では、サイト全体で使い始めた画像フローを記録します。

1枚の背景から2種類の公開画像を作る

このブログでは各記事に英語版と日本語版があります。文字を含まない背景は両方で共有します。ビルド用スクリプトがMarkdownのfrontmatterから各言語のタイトルを読み、その言語専用のソーシャルカードを描画します。

素材パス用途
元の背景画像public/og/source/<translationKey>.png再生成用に残す可逆な入力
表示用の背景public/og/display/<translationKey>.webp記事上部と一覧のサムネイル
各言語のカードpublic/og/posts/<translationKey>-<locale>.jpgOpen GraphとTwitterのメタデータ

背景はComfyUIで生成した1216×640です。ページ容量を抑える表示用ファイルは品質82のWebPにします。ソーシャルカードは1200×630のJPEGで出力します。記事ページの見出しはHTMLのまま残り、その下に文字なしの背景が入ります。

780Mで背景を生成する

画像生成には、このサイトの他のローカル生成でも使っているComfyUI-TheRock環境を使います。背景を作るモデルはQwen-Image-2512です。

設定
diffusionモデルqwen-image-2512-Q4_K_M.gguf
テキストエンコーダーqwen_2.5_vl_7b_fp8_scaled.safetensors
VAEqwen_image_vae.safetensors
画像サイズ1216×640
サンプリング20 steps、CFG 2.5、Euler、simple scheduler

プロンプトでは、記事の内容を1つの物理的な場面に置き換えます。左側3分の1には静かな余白を確保し、主題を右寄りに置きます。共通のスタイル指定には、暗いスレート色、控えめな青緑、暖色の光を1つ、浅い被写界深度を入れました。細かなフィルムグレインも指定しています。ネガティブプロンプトには文字、ロゴ、署名、UI要素を入れています。

場面説明だけを渡すプロンプトで、背景への偶発的な文字混入を抑えます。正確な表記はfrontmatterから合成スクリプトへ渡します。一晩空けた後のコールド実行は、開始から成功まで18分52秒でした。その後に連続実行した10枚は、同じ設定で平均15分45秒です。この記事用のジョブ開始時にはH3の動画生成が重なっており、09:58:49に終了しました。Qwenのsamplerは17分36秒動き、PNGは10:16:30に保存されました。競合終了からファイル保存までの17分41秒を、この画像の処理時間として数えます。14枚分の処理時間は合計3時間48分3秒です。

ComfyUIの完了ファイルを取り込む

Save Imageノードにはog_minus_zeroのような一定のprefixを設定しました。インポーターは一致する最新ファイルを探し、記事のtranslationKeyをファイル名にしてコピーします。

npm run og:posts -- --import-comfy "C:\path\to\ComfyUI\output"

取り込み済みの背景は、その場でカードまで出力されます。生成待ちのsourceはskipとして記録され、その記事にはサイト共通のソーシャル画像が残ります。キューの処理中もサイト側の実装を進められます。

生成後に正確な文字を合成する

文字合成には@napi-rs/canvasを使います。Windows側の英語フォントはSegoe UI Bold、日本語はMeiryo Boldです。タイトルと背景の間には暗い横方向のグラデーションを敷きます。JPEG品質は86に固定しました。

英語は空白で改行できます。日本語の改行には追加処理が必要でした。最初の単語境界はIntl.Segmenterから取り、その後でカタカナの連続、句読点、21分のような数値と単位、MiniMax Music 3のようなラテン文字の製品名をまとめます。

初期の行バランス処理は21 / 分動 / かしたという分割を作りました。その後のgreedyな改行では、ループだけが最後の行に残りました。現在のスクリプトは長いタイトル用に検証付きの改行ヒントを受け取ります。ヒント内の文字列がfrontmatterのタイトル全体と一致するか確認し、タイトルだけが変更された場合はエラーで止まります。短いタイトルは自動改行のままです。

Astroへつなぐ

画像を持つ記事には、各言語のaltテキストをfrontmatterへ追加します。

image:
  alt: "小型PCの上のガラスケース内に浮かぶ琥珀色の音声波形"

2種類の画像パスはtranslationKeyから決まります。記事一覧では表示用WebPを遅延読み込みのサムネイルとして使います。隣に同じリンク先の記事タイトルがあるため、サムネイルのalt属性は空です。記事ページでは説明付きのaltテキストを持つ画像として同じWebPを読み込みます。各言語のJPEGはソーシャル用metaタグとBlogPostingの構造化データに入ります。

任意のimage項目は、確認済みの背景と英日カードが揃った時点で有効にします。この項目がない記事には従来のテキスト中心の表示が残ります。

公開

この画像フローは、この記事と同時に公開しました。14枚の背景画像を各記事ページに表示し、28枚の各言語向けソーシャルカードへつないでいます。すべての背景画像が目視確認を通りました。