# spec 01: プロモ動画パイプライン — リポジトリから「動画生成用プロンプト + 参照画像」まで **ID**: 01 **Date**: 2026-09-07 **Status**: Draft rev2 (2026-09-07 ユーザー査読 11 点を反映。Phase 0 着地・Phase A 未着手) **Branch**: main 直接コミット (Kataribe と同じ流儀。Phase 単位で刻む) --- ## Goal 既存アプリのリポジトリと UI スナップショットから、訴求ポイント → シーン構成 → 参照画像 → Veo/Sora に貼れるプロンプト一式を出すデスクトップツール。LLM はローカル CLI (claude -p 等) を サブプロセスで叩く (HTTP 直呼び禁止)。動画の生成そのものはスコープ外。 北極星: **「貼れば動く出力」**。訴求文の巧さでなく、動画生成 UI にそのまま貼れる粒度と、 スナップショットに寄った参照画像の一貫性が売り。 --- ## 構想文 (ユーザー提示 2026-09-07) からの訂正 | 構想文 | 訂正 | 根拠 | |---|---|---| | Flutter + flutter_rust_bridge v2 | **Tauri 2 + Vue 3** | ユーザー決定 2026-09-07。Kataribe の image_gen.rs (1463 行、3 プロバイダ PoC 済み) と設定 UI がそのまま移植できる。この機体に Flutter / dart / frb codegen が無い | | Rust Edition 2024 / 1.78 以上 | edition 2024 / **rust-version 1.85** | 2024 edition は 1.85 から。機体は 1.95 | | ComfyUI は WebSocket で進捗追跡 | **HTTP ポーリング** (v1)。WS は進捗率が要るときだけ | Kataribe が /prompt → /history 1 秒間隔 → /view で実機 Green。WS は複雑さの割に v1 で要る情報が無い | | 「API キー管理負担を抑える」 | **LLM のキー管理を不要にする** に絞る | 画像生成の OpenAI / Gemini はキーが要る。完全に無キーなのは ComfyUI 経路だけ | | Rust がリポジトリを走査してプロンプトを組む | **CLI に委任** (Rust は圧縮マニフェストを stdin に流し、claude に --add-dir + Read/Glob/Grep を許可) | ユーザー決定 2026-09-07。走査器の実装が最小。aider / custom は Read 系ツールが無いのでマニフェストだけで書かせる | | 入力例の endpoint `[http://…](http://…)` | `http://127.0.0.1:8188` | Markdown リンクの残骸。JSON として不正 | | Kataribe で「動画生成 API の呼び方」を調べる | Kataribe に動画生成は無い。**画像生成** (specs 24-27) が移植元 | grep 0 件 (2026-09-07) | --- ## アーキテクチャ (Kataribe の 3 分割と同型) ``` crates/promo_core 純関数のみ (FS・HTTP・プロセスなし)。型 (data_contract の名詞) / schemars で JSON Schema 機械生成 / RepoBrief の刈り込みと整形 / 検査 / fenced JSON 救済 / コピー整形 crates/cli_runner tokio::process。argv 組み立て (純粋) / stream-json 解析 (純粋) / spawn + 本文の運搬 (stdin | 一時ファイル) + stdout/stderr の行ストリーム + timeout + cancel crates/image_gen Kataribe app/src-tauri/src/image_gen.rs の移植 (Tauri 非依存を grep で確認済み) crates/pipeline IO と結線 (Phase B)。RepoBrief の収集 (FS) / 解析→構成の 2 タスク / 検査ループ / export の書き出し。Kataribe の harness に相当 app/ Tauri 2 + Vue 3。HTTP とプロセスは全部 backend。進捗は Tauri event で push ``` - **LLM は書き、Rust は検める。** LLM 出力は schemars 由来の schema で構造化し、Rust が 件数・尺・言語 (video_prompt は英語) を検査して弾く (Kataribe の「LLM は提案し、エンジンが裁く」の縮退形)。 - **走査は CLI の道具に委任、送る土台は Rust が握る。** RepoBrief (tree / README / manifest 先頭) を stdin に流し、CLI には Read / Glob / Grep だけ許す。Write / Edit / Bash は許可しない (**claude のみ構造で保証。agy には許可リストが無く検出のみ** — rev39 / 契約 `IsolationGuarantee`) (ユーザーのリポジトリを書き換える経路を構造的に持たない)。 - **参照画像 = スナップショットの添付。** 「色調・レイアウトを抽出して写像」は、画像を参照として 渡す経路 (Gemini inlineData / OpenAI images/edits / ComfyUI %ref_n%) の上に、VisualIdentity から 合成したスタイルアンカー文を接頭辞で足す形。**プロンプト本文に「スクリーンショットに従え」と 書かない** (Kataribe #85: 入力を指す語は被写体として描かれる)。 --- ## Stories ### P1: 貼れば動く > ユーザーとして、リポジトリと世界観を入れたら、Veo/Sora のプロンプト欄にそのまま貼れる > シーン別プロンプトが出てほしい。なぜなら動画生成 UI 側で書き直す時間が今の律速だから。 **Acceptance**: - Given リポジトリ path と video_concept, When 解析 → 構成を実行, Then ScenePlan (3〜8 scene、 各 video_prompt は英語) が schema 検証を通って GUI に出る - Given scene と コピー先 (Veo / Sora / 汎用), When コピーを押す, Then Veo / Sora は `video_prompt` だけが 入り (比率と尺は各サービスの UI で選ぶ)、汎用は別行に `[aspect 16:9 | 5s]` が付く。`--ar` はどこにも出ない ### P2: 参照画像がアプリに寄る > ユーザーとして、生成された参照画像が自分のアプリの色と UI に見えてほしい。 > なぜなら汎用の綺麗な絵は宣伝にならないから。 **Acceptance**: - Given スナップショット 1 枚以上, When 参照画像を生成, Then 3 プロバイダのどれでもスナップショットが 参照として送られる (encode 純関数の PoC で固定)。枚数は `reference_limits` (openai 既定 1 / gemini 3 / comfy 3) で切り詰め、切り詰めたことを UI に出す - Given スナップショット 0 枚, When 設定を保存, Then palette 3 色の手入力が必須になる ### P3: CLI が無い・落ちた・止まった が見える > ユーザーとして、CLI の不在・認証切れ・ハングが 1 行の理由つきで見えてほしい。 > なぜなら黙って止まるツールは信用できないから。 **Acceptance**: - Given 存在しない executable, When 実行, Then CliError::NotFound がトーストに出る (spawn 前) - Given 認証切れの claude (fixtures/claude_auth_failed.jsonl), When 解析, Then CliError::Auth - Given timeout 経過, When 実行中, Then プロセスが kill され CliError::Timeout --- ## Phase 計画 | Phase | 内容 | PoC (Red→Green) | 状態 | |---|---|---|---| | 0 | 台帳凍結 (本 spec / data_contract / CLAUDE.md) + workspace 骨格 + promo_core 型と schema + cli_runner の argv 組み立てと stream-json 解析 | schema が Scene の必須項目を含む / 認証失敗 fixture が Auth に分類される / claude argv に stdin 本文が載らない / fenced JSON 救済 | **着手** | | A | 成功 fixture 採取 → `structured_output` 確定。cli_runner の実行: spawn / 本文の運搬 (stdin・一時ファイル) / 行ストリーム / timeout / cancel / NotFound / 終端エラーでの打ち切り / **子孫ごと kill** (Windows Job Object・Unix pgid) | `tests/runner.rs` 10 本 (fake_cli 7 モード): stdin 往復 + structured / NotFound / **timeout 後に孫が消える** / cancel / 終了コード + stderr tail / 一時ファイルの往復と削除 / custom の fenced 救済 / 終端 auth で 60s の hang を待たない / process_alive の検算 / scratch が repo 外 | **Done (2026-09-08、Windows 実機)**。Unix 側は未コンパイル | | B | crates/pipeline: RepoBrief の収集 (FS) → 解析 → ScenePlan、Rust 側検査ループ (違反を戻して再生成、最大 2 回)、export 書き出し、live 用 CLI `promo` | collect 4 本 (除外と深さ / README 大小 + manifest 順 / PNG ヘッダと非対応拒否 / 全件エラー) + stages 4 本 (fake runner: 解析 / **違反 → 再生成 → 通過** / 上限で失敗 / 形違い) + export 1 本 + promo_core prompts 3 + export 3。**live: Kataribe で通し** (analyze 62 s + plan 2 attempts 183 s、0.99 USD、7 シーン 30 s) | **Done (2026-09-08)**。`--add-dir` 外の拒否確認は未実施 (未決へ) | | C | image_gen 移植 (provider.rs = Kataribe の写し、以後不触) + `ImageGenerator` trait + 参照上限 + ComfyUI の `/queue` 併読とバックオフ + palette の画素算出 + style anchor + pipeline::reference | Kataribe 同梱 PoC 10 green (live 5 は ignored) + refs 3 + comfy_wait 4 + palette 3 + compose 2 + style 3 + reference 3。**live: Gemini で 2/2 成功 (17 s)**。目視で 1 場面・暖色暗色基調 | **Done (2026-09-08)**。ComfyUI / OpenAI の live は未実施 (`/queue` の形は実機未確認) | | D | Tauri 2 + Vue 3 殻 (`app/`): 3 ペイン (入力 / 結果 / 進捗ログ)、設定 2 タブ (LLM = CLI 認証委任 / 画像 = API キー)、perProvider スロット、settings.json ミラー、プロバイダ別コピー、export、16 command + event | vue-tsc + vite build / vitest 6 (toBackendCli・toBackendConfig の漏れ封鎖・migrate) / src-tauri cargo check・clippy・test 3 | **実装済 (2026-09-08)。GUI 目視はユーザー待ち** | | E | 別リポジトリでの再現。まず LLM ゼロの `promo brief` を 7 本に当て、RepoBrief の一般化を測る → rev4 (tree の出所を git に) | collect 8 本 (既存 4 + git 優先 / 機械生成名の畳み込み / 鍵名の遮蔽 / `venv`)。**実測 7 リポジトリ**: mxf-tool 314→51 / CC-Sakura 400(切り捨て)→122 / outcast 280→214 / Verificator 114→68 / Kataribe 188→176 / Fuseforks 141→107 / KindleScan 25→17 | **rev4 着地 + live 1 本 (2026-09-08)** | | E-live | Verificator (Python + PyAV コア / Tauri GUI、放送用途) に解析 → 構成を通す。スナップショット 0 枚 (`validate_scene_plan(_, 0)` は product カットを要求しない) | brief 9,380 字 / tree 68 行。analyze 0.2857 USD / 27.9 s。plan **attempts 1 (再生成なし)** 0.2710 USD / 81.8 s。合計 **0.557 USD / 110 s**、6 シーン・尺合計 30。`ui_traits` は GUI のソースを実際に読んで書かれていた (frameless title bar / dashed drop zone / monospace path)。`motion_prompt` にシーン間の連続性が自発的に出た | **Done (2026-09-08)**。合成カットはスナップショット待ちで未検証 | --- ## 決定事項 (rev2。取り消し線は rev1 からの変更) 1. **本文は argv に載せない (全 kind、例外なし)** — claude / custom は stdin、aider は runner が一時ファイルへ書き `--message-file` (公式 `-f`)。~~例外は aider (`--message`)~~ は撤回 (査読 1)。`--yes` は存在せず `--yes-always`。 2. **構造化は schemars 単一真実源** — `--json-schema` に渡す schema は `ScenePlan` / `AnalyzedSummary` の型から機械生成。手書き禁止。 3. **fenced JSON 救済を全 kind で持つ** — 規則は「最後の ```json フェンス優先 → 括弧バランスの取れた最上位オブジェクトの最後」。 ~~最初の `{` から最後の `}`~~ は 2 オブジェクトを跨いで拾う (Red で再現) ので廃止 (査読 4)。claude の一次経路は `result.structured_output` (公式文書で確認)。 4. **許可ツールは Read / Glob / Grep のみ、`--add-dir` とセット、cwd は app の作業ディレクトリ** — `-p` は cwd の `.claude/settings.json` の hook と `.mcp.json` を信頼ダイアログなしで実行する (公式)。解析対象を cwd にしない。 `--dangerously-skip-permissions` と `--bare` (OAuth を使わない = キー必須) は既定で付けない。不変条件は純関数のテストで固定 (査読 3)。 5. **タイムアウト既定 600 秒・最小 30、kill は子孫ごと** — Windows は Job Object、Unix は pgid へ SIGTERM → 5s → SIGKILL。 ~~`child.kill()` + wait~~ は Windows で TerminateProcess となり孫が孤児化する (査読 8)。 6. **画像の保存名は `scene_NN_ref_MM.png`** — 同一シーン複数枚で衝突しない (査読 9)。参照の送信枚数はプロバイダ別上限で切り詰め、切り詰めを UI に出す。 7. **video_prompt に `--ar` を書かせず、コピーもプロバイダ別** — Veo / Sora はプロンプトのみ、汎用は別行にメタ。~~コピー時に `--ar` 付与~~ は Midjourney 記法で誤り (査読 5)。 8. **CLI は画像を見ない (v1)。palette はスナップショットの画素から Rust が算出** — image crate で k-means / ヒストグラム (LLM 不要・純関数)、 ユーザー編集可、スナップショット 0 枚なら手入力必須。~~README / テーマ設定から LLM が推定~~ は README にテーマ色が無いリポジトリが大半で接地しない (査読 11)。 9. **RepoBrief は全 kind で同一バイト列 = 最低保証** — 上限は文字数で統一 (tree 400 行 × 100 字 / README 8000 字 / manifest 5 × 2000 字 / 総量 64k 字)。 claude の深掘りは上乗せであって別経路ではない (査読 3)。刈り込みは promo_core (純粋)、収集は pipeline (IO) (査読 6)。 10. **キーの 2 層構造を UI で明示** — LLM は CLI 認証委任 (キー欄なし)、画像は OpenAI / Gemini がキー必須で ComfyUI だけ無キー。「完全無キー」と読める文言を置かない (査読 2)。 11. **ComfyUI の待機と失敗を分ける** — `/history` 未着の間は `/queue` を併読し、どこにも無ければ消失として即失敗。ポーリングは 1→2→4→5s (査読 7)。 12. **製品 UI が映るシーンを最低 1 つ要求する** (2026-09-08 live 起点) — 情景だけでは参照スナップショットの着地点が無く、宣伝としても製品が映らない。 13. **visual_identity は言語設定に関わらず英語** — 画像モデルに渡る文字列なので、copy の言語 (ja) に引きずられない (live で日本語が混入した)。 ## rev3 (2026-09-08、ユーザー FB「スクショに全く従わない、ありえない画面」+ 参考ツイート = 画像ファースト → MiniMax i2v) 14. **カットは `product` と `mood` の 2 種。product は合成、mood だけ生成** — 実スクショの画素を Rust が背景に貼る (`image_gen::compose`)。 モデルには背景だけ描かせ (画面・端末・文字の語は validate で弾く)、参照は送らない。「モデルは舞台を描き、Rust が画面を貼る」。 #85 の「参照を指すな」はキャラの見た目を**寄せる**規律で、UI を**写す**要求には逆だった (failures #8)。 15. **画像ファースト、動画は i2v** — 各カットは静止画 1 枚 (ストア画像兼用) が主役。`motion_prompt` (動きとカメラだけ) を追加し、 `video_prompt` は text-to-video の保険に降格。決定 12 (UI シーン最低 1 つ) は決定 14 の `NoProductCut` 検査に置き換え。 16. **コピー先は MiniMax 限定** — Veo / Sora は指示に従わない (ユーザー実測) ので選択肢から外し、汎用 t2v を保険で 1 つ残す。 MiniMax へは画像ファイルと motion_prompt の組で渡す。 17. **日本語見出しの焼き込みは opt-in (既定 OFF)** — ユーザー方針 (2026-09-08): フォントは「インストール済みのシステムフォント」と 「自分で `app_data/fonts` に置いたもの」の両方から選ぶ。**アプリで焼くか手で焼くかは判断待ち**なので機構だけ用意し、比較材料の 見本 1 枚を出す。フォントは同梱しない (Rust の ab_glyph でラスタライズ、名前は ttf-parser)。 ~~v1 外~~ → 機構は入れたが既定 OFF。決定はユーザーのアンケート結果で。 ## rev4 (2026-09-08、Phase E = 別リポジトリでの再現。RepoBrief の tree が生成物で埋まった) 18. **tree の出所を `git ls-files` にする** (契約 `RepoBrief.tree_source.primary`)。対象が git リポジトリなら 追跡ファイルから深さ 3 以下の行を組み、祖先ディレクトリを補う。git でない / 失敗した時だけ従来の FS walk に落ち、 `EXCLUDED_DIRS` / `EXCLUDED_EXT` はその fallback にだけ効く (`venv` を追加。点なしを取りこぼしていた)。 根拠: 生成物かどうかを一番よく知っているのは除外語の一覧ではなくリポジトリ自身。gitignore 済みの `doxy/html` (227 行) と `vcpkg` (251 行) はこれだけで消える。 19. **追跡された生成物は機械生成名で畳む** (契約 `machine_named_elision`)。子の過半かつ 5 件以上が `[0-9a-f]{16,}` か UUID 断片なら `backend/.sqlx/ (49 entries, elided)` の 1 行にする。 **件数による畳み込みは実装前に棄却** — Fuseforks `specs/` 52 件と outcast `.sqlx/` 49 件は 3 件差で分離不能 (failures #9)。機械生成名は 7 リポジトリで完全分離した。 20. **鍵に見える名前は tree に載せない** (契約 `secret_names`)。`.env*` / `*.pem` / `*.p12` / `*_key` / `*key*.txt` / `*secret*` / `*credential*`。`cli_runner` は claude に `Read/Glob/Grep` + `--add-dir ` を 渡すので、brief がその名前を指すこと自体が鍵の在処を教える経路になる。README 本文の言及は落とさない。 **接地の限界**: git 優先は未コミットの作業を tree から落とす。Phase E の 7 本では消えたのは未追跡の 作業メモ・worktree だけだったが、実装が未コミットのリポジトリでは brief が薄くなる。 LLM を通した live 通し (analyze → plan → 参照画像) は本セッションでは未実施 — CLI の認証が Claude デスクトップの子セッションでは継承されないため (failures #4 / #7)、ユーザー端末での実行が要る。 ## rev5 (2026-09-08、Phase E の合成 live で背景と面のパースが噛み合わなかった) 21. **面の向きを契約に載せる**。`Scene.plate_tilt: Option<{yaw_degrees, pitch_degrees}>` (各 ±35 度、product のみ)。 背景のアングルを書いた LLM が面の向きも書くので、両者が同じものを指す経路ができる。範囲外は `PlateTiltOutOfRange` で再生成に回す。 22. **貼り方を設定で選ぶ** (`PlateMode`、ユーザー判断 2026-09-08「1 でお願い。ただ 1,2 を両方、設定で切り替えられるのがいい」)。 - `perspective` (既定): `image_gen::compose` が面を 3D で yaw/pitch 回転 → ピンホール投影 → 4 点から homography を解いて逆写像 + バイリニアで焼く。影も同じ quad で歪める。 - `frontal`: 面は正対のまま。代わりに `image_prompt` のアングル語を `ProductBackdropAngled` で弾き、背景も正対に保つ。 - **検査と本文がモードに依存する** = `validate_scene_plan` と `scene_prompt` が `PlateMode` を受け取る。 - **傾き 0 は従来の overlay 経路をそのまま通す** (画素等価を PoC で固定)。既存 4 本の PoC を壊さない。 23. 経路は CLI (`--plate perspective|frontal`) と GUI 設定 (画像タブ「製品カットの画面の貼り方」) の両方から。 **live 検算 (2026-09-08、AppPromoVideo 自身 + 実 GUI スクショ)**: analyze 0.2903 USD / 48 s、 plan **attempts 2** (再生成の理由は既存の `ProductBackdropDrawsScreen` = 背景に `screen` が 3 シーン混じった) 0.4237 USD / 135 s、 images 3/3。LLM の傾き指定は意味と一致した — scene 2 `Top-down view` → yaw -10 / pitch -20、 scene 5 `three-quarter angle` → yaw +18 / pitch -12、正対の背景 (scene 4) は `plate_tilt` null。 合成結果は上辺が狭く下辺が広い、机に画面が寝た絵になった。 **接地の限界**: 傾けた絵を MiniMax の i2v に通した検証はまだ無い。①傾ける と ②正面固定 のどちらが動画として 良いかは未決で、だから両方を残した。**→ rev22 で決着 (frontal が既定)。以下はこの時点の状態。**傾けると画面内の文字は読みにくくなる — 許容範囲は未実測。 スクショ自身が持つ 1px の窓枠が、傾けるとエッジで階段状に見える (バイリニアの範囲では消えない)。 影は残っている (実測: 正対 76,476 px / 傾き 66,446 px) が、暗い背景では見えない。 ## rev6 (2026-09-08、MiniMax 実測「判別しにくい文字は作り変えられる」) 24. **canvas をスクショに合わせて拡げる** (契約 `compose.canvas_for_snapshot`)。`1344x768` は固定値で、 窓 (実測 1282x842) より縦が低い。`screen_ratio` を 1.0 まで上げても 0.912 倍にしかならず、 **構造的に必ず縮んでいた** (0.78 では 0.711 倍 = 11px の UI 文字が 7.8px)。 比率を保ったまま等倍で収まる大きさまで canvas を拡げ、長辺 `CANVAS_MAX_LONG_EDGE` (3840px) で頭打ちにする。 実測の組み合わせでは `1344x768 → 1890x1080`、縮小率 1.000。 **理由は rev3 と同じ**: 背景はモデルが描いた柔らかい絵なので拡大が効き、スクショは唯一の硬い情報。 縮める対象を入れ替える。 25. **上限に当たって縮小が避けられない時は進捗に警告を出す** (`スクショを NN% に縮めます`)。黙って劣化させない。 26. **パッケージ内の寸法と形式を揃える** (`compose.fit_to_canvas`)。mood カットはプロバイダの出力をそのまま 保存していたので **JPEG 1376x768 なのに拡張子は .png**、product カット (PNG 1344x768) と食い違っていた。 動画は全フレームが同寸である必要がある。揃えられない時は素のまま保存して警告する (生成には金がかかっている)。 **接地の限界**: 「解像度を上げれば作り変えられない」はユーザーの MiniMax 実測に基づく方向であり、 **等倍にした版での再テストはまだ**。傾けた面の遠い側は等倍より小さくなるので、tilt と可読性は依然として トレードオフの関係にある (角度と可読性の境目は未実測)。 ## rev7 (2026-09-08、ユーザー指摘「再起動すると出力が揮発」→ 調べたらディスク上で破壊されていた) **発見**: `package_dir_name` は `_Promo_Package` だけで run の識別子を持たず、同じアプリに 2 回 実行すると `promo.json` / `scenes.md` / `scene_NN_ref_MM.png` が上書きされ、**過去の出力が消えていた**。 掃除もしないので、新しい run のシーン数が前回より少ないと前回の `scene_07` 等が残って混ざった。 症状は「アプリを再起動すると揮発」だったが、真因はアプリの状態ではなくファイルの側。 27. **run ごとに隔離する** (契約 `ExportPackage.run_isolation`)。`/_Promo_Package/runs//`。 `run_id` = `YYYYMMDD-HHMMSS` (**UTC**。ローカル時刻だと夏時間の切り替わりで順序が壊れる)、同一秒は `-2`, `-3`。 **文字列比較がそのまま時系列順**になるので、一覧の並べ替えに日付解析が要らない。 `promo images /promo.json` は promo.json の親に書くので入れ子でもそのまま動く。 rev6 以前の `/promo.json` は移動も削除もしない (放置)。 28. **索引はキャッシュ、正本はフォルダ** (契約 `RunIndex` / `RunRecord`、`app_data/runs.json`)。 アプリは複数のリポジトリ・複数の出力先を横断して一覧する必要があるので走査だけでは足りない。 索引が指す `run_dir` が消えていたら `missing` として出すだけで**索引からは消さない** (移動しただけかもしれない)。 索引が壊れていても空で続行し、**file は残す** (人が直せる)。索引の書き込み失敗は本流を止めない (生成は成功しているので結果を捨てない)。 29. **比較が要件**。生成は「新しいものを過去と比べて選ぶ」作業なので、一覧・復元だけでなく **2 つの run の同じ scene を左右に並べる**ところまで作る (ユーザー判断 2026-09-08)。 backend `list_runs` / `open_run` / `forget_run`、frontend は `RunsDialog`。 (→ rev31 でダイアログをやめ、全画面の `RunsScreen` に) 30. **`forget_run` は既定でファイルを消さない**。索引から外すだけ。`delete_files` を立てた時のみ削除し、 そのときも `runs/` の形でないフォルダは拒否する (誤爆よけ)。 31. **`copy_package` の名前を組み直す**。run dir をそのままコピーすると日時だけのフォルダ名になり アプリ名が消えるので、`_Promo_Package_` にする。 **live 確認 (2026-09-08、ユーザー実機)**: 同じアプリを 2 回実行して履歴に 2 行 (19:36 / 19:40、4 と 5 シーン、 どちらも gemini で画像あり、0.944 / 0.919 USD)。**上書きは起きず、左右比較も描画された。** そのスクリーンショットから回帰を 1 件回収 — 「面」列が `Perspective` (Rust の識別子) で出ていた。 `RunRecord` の aspect / language / plate_mode は **契約の表記** (`16:9` / `ja` / `perspective`) を入れる。 既存の 2 行は表示専用なのでそのまま残す (解析しないので壊れない)。 **接地の限界**: rev6 以前の既存パッケージを履歴に取り込む移行は**やっていない** — 放置されるだけで ファイルは無事だが、アプリの一覧には出ない。 **`RunRecord` は attempts と違反種別を持たない**ので、「このモードは再生成が起きやすい」を数えられない (**→ rev15 で解消。以下はこの時点の状態**)。 実測 2026-09-08: frontal 1.422 USD / perspective 0.919・0.944 USD (約 1.5 倍) — 原因は `ProductBackdropAngled` による再生成だと**推測しているが確認できていない**。 ## rev8 (2026-09-08、見出しの焼き込みを既定 ON へ) **開いている判断 2 に答えが出た** (ユーザー「既定 ON にして、日本語フォントの自動選択も入れて」)。 字幕ファイル (SRT / WebVTT) は**作らない** — 「そんな本格的なものはいらない、画像に文字を入れるくらい」。 32. **`defaultCaptionSettings().enabled = true`**。機構は rev3 期から入っていて、既定だけが OFF だった。 33. **フォントの自動選択** (`pickCaptionFont`、contract `caption.default_font`)。既定 ON にすると 「ON なのにフォント未選択で何も焼かれない」が**無言で**起きるので、実行直前に解決する。 優先順は ①日本語グリフ必須 ②`app_data/fonts` のもの (source=user) を最優先 ③**太めのゴシック** (画像に焼くので細い明朝・教科書体は写真の上で読めない) ④同点は family 名。 選べなければ焼かずに進み、理由を進捗に出す。 34. 解決の場所は `store.ensureCaptionFont()` = `makeImages()` の先頭。設定画面を一度も開かなくても効く。 **live 確認 (2026-09-08、ユーザー実機)**: 見出しが正しく焼かれた (`シーン構成もプロンプトも、まとめて出力。` / 1886×1886 の 1:1 canvas / 上寄せ / 白 + 落ち影)。 **開いている判断 2 は完全に決着。** **接地の限界**: 自動選択の経路は**まだ通っていない** — ユーザーが設定画面で `NotoSansCJKjp-Black.otf` を 手で選んだため。`pickCaptionFont` が実機で何を引くかは、フォント未選択の状態で 1 回走らせるまで未確認 (実データ 118 件に対して関数単体では `Noto Sans JP` を返すことは確認済み)。 ## rev9 (2026-09-08、見出しを 1 枚ごとに変えたい) **壁**: 見出しは生成時に焼き込まれ、**焼いた後の 1 枚しか残らなかった**。あとから位置や色を変えるには 背景から作り直すしかなく、お金がかかるうえ絵まで変わる (位置を試すたびに別の机になる)。 35. **焼く前の合成を `/base/` に残す** (ユーザー判断: ディスク約 2 倍を受け入れる)。 直下の `scene_NN_ref_MM.png` は今までどおり「焼いた完成品」= MiniMax へ渡すファイル。場所を変えない。 保存に失敗しても本流は止めない (画像は出す) が、焼き直せなくなるので理由を出す。 36. **上書きは `PromoJson.caption_overrides: Map`**。 **LLM のスキーマ (`Scene`) には足さない** — 埋めるのは人であって LLM ではない。 全フィールドが `Option` で、**省略したものは既定 (設定の値) に落ちる** — 「位置だけ変えて他は既定」が成り立つ。 空なら promo.json に書き出さない (既存の出力を汚さない)。 37. **`CaptionSpec.color`** (`#RRGGBB`、読めない指定は白に落ちる)。設定にも「色 (既定)」を足した。 38. **`reburn_caption(run_dir, scene_id, spec)`**: base/ から焼いて直下を置き換え、overrides を promo.json に 書き、data URL を返す。**生成 API は呼ばない。** 毎回 base から焼くので、焼いた上に焼くことがなく劣化しない。 `spec` が null なら見出しを消す (base をそのまま戻す)。 39. UI は結果ペインの各シーンに `CaptionEditor` (フォント / 位置 / 大きさ / 色 + 適用・既定に戻す・消す)。 **→ rev16 でダイアログへ。以下はこの時点の形 (折り畳みパネル)**。 設定側は「全シーンの既定」であることを明記した。 **rev10 の訂正 (同日、ユーザーの問い「背景だけ生成させて、文字はアプリで焼き込んでいるのでは?」から)**: その認識が正しく、焼く行為はアプリ内で無料。コストは**焼く対象を作り直すこと**にあった (背景は `generator.generate` の戻り値をローカル変数で受けるだけで一度も書き出していなかった)。 そして rev9 の選択 (焼く前の**合成画像**を残す) は不十分だった — 合成の時点で `with_caption_band` が **帯の位置を焼き込む**ので、位置を下→上に変えるとプレートに重なる。 40. **`base/` に置くのは背景**にする (product は背景 / mood は絵そのもの。どちらも canvas 同寸の PNG)。 スクショは `snapshots/` にあるので、焼き直しは**合成からやり直す** — 帯が正しく取り直され、 `plate_tilt` もそのまま乗る。ディスクは rev9 と同じ。 41. **`layout_for(canvas, position, tilt)` を帯を決める唯一の場所にする**。生成と焼き直しで共有するので、 帯の計算が二重にならない。生成側もジョブ既定ではなく**そのカットで効く位置**で合成するようになった。 42. **`PromoJson.plate_mode`** (rev10)。焼き直しで傾きを効かせるかを promo.json 自身が持つ (索引に頼らない)。 43. 焼き直しは**元のパスではなく run の `snapshots/` の写し**を読む。元は移動されうるし、run は自己完結すべき。 **接地の限界**: live 未実施。**rev10 より前に作った run は焼き直せない** (`base/` が無い、または中身が 合成画像) — `reburn_caption` はその旨をエラーで返す。移行はしない (作り直せば済む)。 ディスクは run あたり約 2 倍 (6 シーンで 18 MB → 36 MB)。古い run の掃除機構は無い。 **傾きを GUI から変える UI はまだ無い** — 機構としては無料でできるようになったが、今回は入れていない。 ## rev11 (2026-09-09、はめ込みも 1 枚ごとに) rev10 で**合成からやり直せる**ようになったので、見出しだけでなくプレートの置き方も無料で変えられる。 ユーザー要望「傾きも変えられたほうがいい、はめ込み位置とかもね」— より複雑な PR 動画を作るには 1 枚ごとの調整が要る、という理由。 44. **`Layout.x_offset_ratio`** を追加。横方向のずらしが無く中央固定だった (正対・傾きの両経路に効かせる)。 45. **`PromoJson.plate_overrides: Map`** (product のみ)。 `yaw_degrees` / `pitch_degrees` / `screen_ratio` / `x_offset_ratio` / `y_offset_ratio` の全てが `Option`。 **傾きの上書きは LLM が書いた `scene.plate_tilt` に勝つ**が、触っていない軸は LLM の値のまま。 **縦位置を指定すると見出しの帯のずらしを置き換える** (人が決めた位置を優先)。 46. **範囲は Rust 側で丸める** (`PlateOverride::clamped`、傾き ±35 / 比 0.2〜0.95 / ずらし ±0.4)。 UI の入力を信用しない。 47. `reburn_caption` が `plate` も受ける。空の指定は「既定に戻す」なので行ごと消す。 48. UI は `CaptionEditor` に「はめ込み」節を追加 (product のみ表示)。**空欄 = 既定のまま**。 `copy_text` が無い product カットでも開けるようにした (見出しの入力群は隠す)。 **接地の限界**: live 未実施。数値入力なので**結果を見るまで当たりが分からない** — スライダーや ドラッグでの直接操作、プレビューの即時反映は入れていない。rev10 より前の run は焼き直せない。 ## rev12 (2026-09-09、スライダー / ドラッグ / コピー文の書き換え) GUI 実測を受けたユーザー要望。「数値入力では結果を見るまで当たりが分からない」への処方と、 コピー文そのものを直したいという追加。 49. **スライダー + プレビューのドラッグ**。`@change` (離した時) でのみ焼き直す — ドラッグ中に 走らせると 1886² の再合成が連発する。ドラッグはプレビュー画像の上で行い、ずらしは canvas 比なので 表示サイズの比 (`getBoundingClientRect`) でそのまま換算できる。編集パネル内にプレビューを置いたので、 スライダーと同じ場所で完結する。 50. **コピー文の書き換え** (`reburn_caption` の `new_copy`)。`scene.copy_text` を**その場で**書き換える — `scenes_markdown` もクリップボードも plan を読むので、それだけで全部揃う。書き換えたら scenes.md も書き直す (パッケージの中で食い違わせない)。(→ rev54 から、コピー文を変えない焼き直しでも**毎回**書き直す) 51. **`PromoJson.original_copy`**: LLM が最初に書いた文を生成時に 1 度だけ控える。以後触らない。 「最初の文に戻す」ができ、うっかり消しても LLM の出力を失わない。 52. コピー文を空にすると見出しを焼かない (見出しを消す操作と同義)。 **接地の限界**: live 未実施。**焼き直しの所要時間は未計測** — 1886² の再合成 + PNG エンコードなので、 スライダーを離すたびに待ちが入る。体感が悪ければ、縮小版でのプレビューと確定時のみ原寸、という 二段構えが要る (今は入れていない)。 ## rev13 (2026-09-09、重さの実測 / 確定してから焼く / スナップショットの選び直し) 53. **debug の画像処理を最適化する** (`[profile.dev.package.*] opt-level = 3`、root と `app/src-tauri` の **両方**に。src-tauri は独立 workspace)。実測 1886² の再合成が release 252 ms / debug 17.9 s で **71 倍** 差があり、GUI の重さはここが主因だった (failures #16)。**17.9 s → 0.64 s**。 54. **スライダーとドラッグは値を変えるだけ**にし、焼き直しは「適用」に集約 (ユーザー提案)。 押し忘れが分かるよう、未反映のあいだはボタンを「適用 (未反映)」にする。 55. **スナップショットの選び直し** (`PlateOverride.snapshot_index`)。LLM が選んだ画面がコピー文に 合わないことがあるため。**人の指定が LLM に勝ち**、指定が無ければ `scene.snapshot_index` のまま。 生成時と焼き直しの両方で効く。 **接地の限界**: live 未実施。0.64 s は**テストのビルドでの実測**で、GUI から押した時の体感 (IPC で data URL を送る分を含む) は測っていない。release ビルド (`npm run tauri build`) なら 0.25 s + IPC。 ## rev14 (2026-09-09、見出しの縦位置と、予定位置の枠) 56. **見出しの縦位置を数値で** (`Caption.y_ratio` / `CaptionSpec` / `CaptionOverride`)。 canvas 高さ比で文字ブロックの上端を直接指定する。省略すると従来どおり `position` + 余白。 canvas からはみ出す値は端で丸める (`caption_top` を公開して焼き込みと共有)。 57. **予定位置を枠で重ねる** (`plate_preview` → `compose::plate_quad`)。 **座標は合成と同じ関数から出す** — 射影の数式を TS 側に写すと必ず食い違うため。 画像は作らずヘッダから寸法を読むだけなので、スライダーを動かしている間ずっと呼べる。 傾けると枠は台形になる (PoC で「yaw 正なら右辺が短い」を固定)。 応答の追い越しは連番で捨てる (古い応答で新しい枠を上書きしない)。 58. 枠を画像に重ねるため、プレビューは `object-fit: contain` を使わず**画像そのものの箱**にする。 contain だとレターボックスのぶん枠がズレる。 **live 確認 (2026-09-09)**: 枠が画像とズレた → 実 run で座標を検算したところ **`plate_quad` は正しかった** (4 シーンで数 px 以内)。真因は CSS で、`position: absolute; inset: 0` では SVG が伸びない (置換要素なので 固有サイズ 300×150 で描かれる)。`width/height: 100%` を明示して解決 (failures #17)。 枠は**いじっている間だけ**出す (適用すると消える)。 **接地の限界**: `plate_quad` は合成の実測位置と ±2px で一致することを PoC で固定したが、 **見出しの文字がどこに来るかは枠に含めていない** (文字の実寸はフォントを開かないと出ないため)。 縦位置スライダーの初期つまみ位置は目安の値で、実際の既定位置とは厳密には一致しない。 ## rev15 (2026-09-09、再生成の回数と種別を残す) **動機は測定であって表示ではない。** rev7 の live で frontal の run だけ費用が約 1.5 倍 (1.422 USD 対 0.919 / 0.944) だったが、attempts も違反種別も**どこにも永続化していなかった**ので 「`ProductBackdropAngled` で再生成が発火した」という推測を確認できないまま残っていた (CLAUDE.md「次の候補」1 件)。データは最初から `StageReport` にあり、捨てていたのは書き出しの一箇所だけ。 59. **正本は promo.json、索引はその写し** (契約 `RunStats`)。`RunRecord` にだけ持たせるのは足りない — ①索引はキャッシュで `forget_run` や破損で消える ②**`promo` CLI には索引が無い**ので、 同一リポジトリで両モードを回す一番安い対照実験が数えられない。`plate_mode` が rev10 で 「索引に頼らず promo.json 自身が持つ」と決めたのと同じ判断。CLI と GUI の両方が書く。 60. **持つのは種別 (`violation_kind`) であって文 (`describe_violation`) ではない**。後者は scene_id や 語が埋まるので、同じ種類の違反でも文字列が変わり集計できない。種別文字列は promo.json に残る = 契約。 61. **記録が無いことを「1 回」に落とさない**。`PromoJson.run_stats` は `Option`、`RunRecord.plan_attempts` の 0 は「記録なし」。rev14 以前の 3 行を 1 と描くと、集計に偽の分母が混ざる (PoC で固定: Rust 2 本 + vitest 1 本)。GUI は「—」と出し、2 回以上を強調する。 62. `analyze` は構造検査を持たず常に attempts 1 なので**持たない** (常に 1 の列は読む側を惑わせる)。 **既にある反証**: rev5 の live (**perspective**) が `ProductBackdropDrawsScreen` で attempts 2 になっている (0.2903 + 0.4237 = 0.714 USD)。つまり **perspective でも再生成は起きるし、起きても 1.5 倍にはならなかった**。 frontal の 1.5 倍を attempts だけで説明できる保証は無く、rev15 が用意したのは説明ではなく**数える手段**。 **接地の限界**: **live で 1 件も記録していない**。実装と PoC は green だが、実際の run から `run_stats` が書かれた promo.json はまだ 1 つも無い。仮説の検証には最低でも同一リポジトリ・同一 スナップショットで perspective / frontal を各数本走らせる必要があり、それは LLM 費用のかかる作業。 rev14 以前の 3 行 (frontal 1 / perspective 2) は遡って埋められない — 索引にも promo.json にも attempts が無いため。**過去は数えられない、これから数える。** (→ 2026-10-02 閉じた (ユーザー判断): 記録が溜まっていた — frontal 8 本 平均 1.283 USD / perspective 5 本 平均 1.394 USD。 1 回で通った run どうしでも 0.911〜1.316 とばらつき、1.5 倍は再現しない。frontal の再生成の主因は mode に依らない `ProductBackdropDrawsScreen` (4 本)、 `ProductBackdropAngled` は 2 本 (単独は 1 本)。リポジトリとモデルが mode と重なっており対照実験ではない。数字の出所は CLAUDE.md「閉じた」と history.md 2026-10-02) ## rev16 (2026-09-09、編集はダイアログで) **ユーザー判断**「見出し/はめ込みについてはトグルのアコーディオン表示ではなく、ダイアログページにしよう。 大きい画面で確認できるようにしたい」。 63. **`CaptionEditor` を折り畳みパネルからダイアログへ。** 結果ペインの中に畳むと絵が列幅 (`max-width: 420px`) に縛られ、**焼き上がりを確かめるという用途に足りていなかった**。 ダイアログは左が絵・右がつまみの 2 段組で、絵は `92vh` からダイアログの他の要素を引いた残りまで使う。 幅 1000px 未満では 1 列に落として絵を上に置く。 64. **`Teleport` で body に出す。** この部品は `overflow: auto` の列 (`.center`) の中に居るので、 そこに `position: fixed` を置くこと自体は動くが、将来 `transform` が祖先に入ると包含ブロックが 移って壊れる。履歴・設定と同じ「画面の上に出す」形に揃える。z-index は 45 (両ダイアログの 40 の上)。 65. **mood カットにもプレビューを出す。** 従来は絵が `isProduct` の中にしか無く、mood の見出しを 変えても**焼き上がりを見る場所が無かった**。ドラッグははめ込みの操作なので product のみのまま。 66. **未適用のまま閉じない。** `dirty` の時、閉じる / 背景クリック / Esc は確認を挟む。 折り畳みだった頃は畳んでも値がその場に残っていたが、ダイアログは閉じると視界から消えるため。 67. 枠の重ね方は **rev14 のまま** — `.stage` を画像に縮め (`display: inline-block`)、SVG に `width/height: 100%` を明示する。`inset: 0` では伸びない (failures #17)。大きさが変わっても ポリゴンは canvas 比の 0..100 座標なので、この 2 つが保たれていれば追従する。 **接地の限界**: **私は画面を見ていない** (headless)。`vue-tsc` + `vite build` + vitest 22 green は 通ったが、これらは**レイアウトが意図どおりに見えるかを何も検証しない**。特に `max-height: calc(92vh - 190px)` の 190px は他の要素の高さの見積もりで、**実測していない** — はみ出す / 余るなら数値を直す必要がある。ダイアログでの枠のズレも未確認 (rev14 の実測は 420px 幅のパネルでのもの)。純関数が増えていないので PoC も無い (Red を観測していない)。 ## rev17 (2026-09-09、縦位置スライダーが焼き直しに届いていなかった) **ユーザー報告**「スライダーを動かしてもプレビューの文字が変化しない」。大きさ・色・フォントは効くのに **縦位置 (`y_ratio`) だけ絵が動かない**。値は promo.json に残る。 68. **`Caption` の組み立てを `CaptionSpec::to_caption` 1 箇所に。** rev14 で `y_ratio` を足したとき、 生成側 (`generate_references`) には入れたが、焼き直し側 (`reburn_caption`) は `Caption` を 手で組み直していて写し忘れていた。保存経路は別なので**値だけは残り**、「設定は効いているのに 絵が動かない」という形に化けていた (failures #18)。 69. `burn_caption` が持っていた縦位置の式の写しを `caption_top` に寄せた (同じ数式を 2 つ持たない)。 **PoC (Red→Green を実測)**: ①全フィールドが `Caption` に届く ②**縦位置を変えると文字が実際に動く** (`assert_ne!` + 上下の判定)。`cap.y_ratio = self.y_ratio;` の 1 行を消すと**両方落ちる**ことを確認した — つまり修正前の状態を再現し、縦位置を変えても画像がバイト等価になることを見ている。 ## rev18 (2026-09-09、位置の選択を外し、見出しの予定位置も枠に) **ユーザー判断**「文字の位置というのは縦位置のスライダーができたのでいらない」 「文字の位置や大きさも予定位置がプレビューでわかったほうがよい」。 70. **「位置」(top/bottom) の選択を UI から外す。** 縦位置スライダーが上位互換。 ただし `layout_for` は**帯をどちら側に空けるか**を知る必要があるので、 `CaptionSpec::effective_position()` で `y_ratio` から導く (上半分なら Top、無ければ設定の値)。 **これが無いと、見出しを上へ動かしたのに面が下の帯を避け続ける**という食い違いが残る。 契約のフィールド自体は残す (`y_ratio` を使わない経路の既定として要る)。 71. **版組みを焼き込みから取り出す** (`image_gen::caption_layout`)。自動縮小の後の `px`、行送り、 行ごとの `x / width / top` を返す。**`burn_caption` はこの結果をそのまま描く** ので、 枠と焼き上がりが構造的に一致する (`plate_quad` と同じ作法)。画素は触らないので速い。 72. `plate_preview` が `caption_lines` (1 行 1 枠) も返す。`quad` は `Option` になった (**mood カットには面が無い**が、見出しはあるので mood でも枠が要る)。 編集中のコピー文は引数 `copy` で渡す — promo.json はまだ適用前の古い文を持っているため。 73. フォントは 1 ファイル数 MB〜数十 MB。プレビューは打鍵とスライダーのたびに走るので、 **直前に読んだ 1 つだけ持ち回す** (同じフォントを触り続ける操作が大半)。 74. 見出しの枠は面と**別の色** (琥珀 / 青)。どちらの枠を見ているか迷わせない。 **PoC**: `caption_layout` 4 本 — ①**焼いた墨が返した枠の中に収まる** (落ち影のぶんだけ緩める) ②縦位置で枠が動く ③はみ出す行は縮み、**縮んだ後の値**が返る (指定値を返すと枠が嘘になる) ④空文字は枠なし。`effective_position` 3 本。 **接地の限界**: **私は画面を見ていない。** 枠が実際に文字と重なって見えるかは未確認で、 PoC が保証しているのは**焼き上がりの墨が枠の中にある**ことだけ (ダイアログ上での見え方ではない)。 `px` が実測なのに対し、`line_h = px * 1.3` の行送りは字面より高いので、**枠は文字よりやや大きく見える**はず。 フォントの持ち回しは 1 つだけなので、フォントを次々に切り替える操作では毎回読み直しになる (未実測)。 ## rev19 (2026-09-09、run にスナップショットを足せるようにする) **ユーザー判断**「コピーにあった画像が無いときにスクショを取り直してそれを貼るということもあり得るので、 リスト以外にも新たに足せるようにしたい」。 75. **`add_run_snapshot(run_dir, path)`**。既存の run に画像を足し、`snapshot_paths` に追記して `snapshots/` に写す。**生成はやり直さない** — 足した画像ははめ込みの材料になるだけ。 76. **番号は一覧の長さで決める。ファイルを数えない。** 契約は `snapshot_paths[i]` ↔ `snapshots/snapshot_{i+1:02}.*` なので、欠番や孤児があるフォルダで ファイルを数えると番号がずれ、`snapshot_index` が**別の画像**を指す。 77. **同じ番号の孤児は拡張子を問わず先に消す。** 残すと `read_run_snapshot` の前方一致が `snapshot_03.png` と `snapshot_03.jpg` のどちらを拾うか決まらない。 78. **命名を `promo_core::snapshot_prefix` / `snapshot_file_name` 1 箇所に。** 同じ式が 書き出し・読み出し・追加の 3 箇所にあった。failures #18 に書いた処方の一般形をそのまま当てた (「同じ構造体・同じ名前を 2 箇所以上で組み立てていたら関数にして PoC を 1 本置く」)。 79. **重複排除はしない。** 撮り直しは**同じパスのまま中身が変わる**ので、パスで弾くと新しい画像を拒む。 80. 経路は貼り付け (Ctrl+V) とファイル選択の 2 つ。**貼り付けは window にしか来ない**ので 所有者を 1 つに決める (`pasteFocus`) — ダイアログが開いている間、入力ペインの一覧は拾わない。 足したらそのまま選んだ状態にする (足す動機は「これを使いたい」なので選び直させない)。 **→ rev20 で撤去。** ダイアログに取り込み口を置いたこと自体が誤りで、所有権の調停はその誤りの 後始末だった (`pasteFocus` は 1 日と保たずに消えた機構)。以下はこの時点の形。 **PoC**: `snapshot_file_name` / `snapshot_prefix` 2 本 (番号と名前の対応、100 枚目以降も前方一致が 壊れない、`09` の前方一致が `090` を巻き込まない) + `pasteOwner` 3 本 (**rev20 で撤去**)。 **接地の限界**: **画面は見ていない。** 貼り付けの経路は実機でしか通らない (クリップボードの画像 → FileReader → base64 → backend)。**ドラッグ&ドロップは足していない** — `onDragDropEvent` は webview に 1 つの待ち受けで、入力ペインが既に持っているため。 孤児の掃除と番号のずれは PoC が無い (ファイルシステムを触る側)。同じ run に何十枚も足したときの 一覧の見え方も未確認。 ## rev20 (2026-09-09、取り込み口は入力ペイン 1 つ / 一覧に後から足したぶんも出す) **ユーザー判断**「というか入力のスナップショットに足したら、それをリストで開けるようにすればいいんじゃない? 今は最初に解析を押したときのスナップショットしか足せなくなっている」。 **rev19 の設計の誤りを正すもの。** ダイアログに独自の取り込み口 (ファイル選択 + 貼り付け) を付けたが、 入力ペインには既にドロップ / 貼り付け / ファイル選択が揃っている。**同じ機能を 2 箇所に置いたせいで** 貼り付けの所有権という調停が必要になり、そのための機構 (`pasteFocus`) まで生えた。 本当に足りなかったのは取り込み口ではなく、**run 作成後に入力ペインへ足した画像が一覧に出ないこと**。 81. **ダイアログから取り込み口を撤去** (`pasteFocus` ごと)。入力ペインが唯一の入口。 82. **一覧 = run に写したもの + 入力ペインで後から足したもの** (`snapshots.ts::snapshotChoices`、純関数)。 並びは run の番号順が先、後から足したぶんが下 (「入力に追加」と出す)。 83. **入力ペインから外されても run のぶんは一覧に残す。** run は自己完結していて (rev10)、 入力の一覧は**この run の履歴ではない**。消すと過去の run の再現ができなくなる。 84. **写すのは選ばれた時** (`add_run_snapshot`、rev19 の機構をそのまま使う)。 `read_run_snapshot` は run の写しを読むので、入力ペインのパスを直に指すことはできない。 **PoC**: `snapshotChoices` 4 本 (合流と重複の除去 / 入力から外れても run のぶんは残る / 並びと番号 / 入力が空でも run は出る)。 **一般化**: **「足せない」という報告に対して、足す口を新設したのが早すぎた。** 足りないのは入口ではなく**見えている範囲**だった、という形は他にもありそうなので記録しておく。 機能を足す前に「既にある入口から入ったものが、なぜここに出てこないのか」を先に問う。 **接地の限界**: **画面は見ていない。** 「入力に追加」を選んだ瞬間に写す経路は実機でしか通らない。 `snapshotChoices` はパスで突き合わせるので、**同じパスのまま中身を撮り直した場合**は run の古い写しが選ばれ続ける (run が自己完結する以上そうなるが、撮り直しの用途とは噛み合わない — その時は入力ペインで別名の画像として足す必要がある)。**これは未解決。** ## rev21 (2026-09-09、つまみは実効値を指す) **ユーザー指摘**「0° は正面で、傾き画像にしたときは傾き数値の° にしたほうがよい」。 絵が 18° 傾いていてもスライダーは 0° を指していた (failures #19)。 85. **つまみの基準は backend が出す** — `plate_preview` が合成と同じ `tilt_of` の値を `tilt: [yaw, pitch]` で返す。`PlateMode::Frontal` や `plate_tilt` 無しでは `[0, 0]` = 本当に正面。 **UI 側で「たぶんこれが既定」を組み立てない** (`plate_quad` / `caption_layout` と同じ作法)。 86. **値の出どころまで見せる** (`plate.ts::tiltValue` / `tiltLabel`、純関数)。 `0° (正面)` / `18° (LLM)` / `18°` の 3 通り。数字だけだと「自分で 18° にした」と 「LLM が 18° と書いた」が区別できず、「既定に戻す」を押すべきかが判断できない。 87. **開いたら保存済みの上書きをつまみへ戻す** (指摘の外、同じ型の 2 件目)。 適用して開き直すと全つまみが既定に戻っていた。`promo.json` には保存されているのに、 ダイアログが毎回 null から始めていた。 88. **`reburn_caption` が書き換えた後の `promo` を返す。** 上書きを書くのは backend なので、 frontend が手元で真似ると食い違う (87 の根拠になる値)。`generate_images` と同じ形に揃えた。 89. ダイアログを開くたびに `plate_preview` を呼ぶ。従来はフォント未読み込みのときだけ呼んでいた (枠は `dirty` の間しか出ないので実害が無かった) が、実効の傾きは**開いた時点で要る**。 **PoC**: `tiltValue` / `tiltLabel` 4 本 — 未指定は LLM の値を指す / 0 は正面と出る / 自分の値には印を付けない / LLM と同じ数値を自分で選んだら自分の値として出る。 **接地の限界**: **画面は見ていない。** つまみの位置が実際に動いて見えるかは未確認。 87 と 88 は**ユーザーの指摘の外**で私の判断で入れた — 同じ型の嘘だが、報告されたのは傾きだけ。 保存済みの上書きの読み戻しには PoC が無い (`loadSaved` は store と props に依存する)。 ## rev22 (2026-09-09、既定は正面固定へ — 開いている判断 1 の決着) **ユーザー観測**「Minimax はうまくできています。ただ斜めにすると動画が動かしすぎるので、 正面がデフォルトのほうがいいかもね」。X に投稿された 15 秒の実物つき。 **rev5 から開いていた唯一の判断がここで閉じた。** 90. **`PlateMode` の既定を `Frontal` へ。** 静止画としての整合 (背景のパースと面のパースが合う) と、 そこから起こした**動画の落ち着き**は別の話で、製品の目的は後者。傾ける経路は残す — 捨てる根拠は無く、設定と `--plate perspective` で選べる。 91. **既存の run は影響を受けない。** rev10 で promo.json 自身が `plate_mode` を持つので、 焼き直しは作られた時のモードのまま動く (PoC で固定)。 92. **保存済みの設定は上書きしない** (rev8 の precedent)。localStorage に `perspective` がある人は そのまま。**既定の変更が効くのは新規の環境・CLI・`plateMode` を持たない保存だけ** — つまり**この判断を出したユーザー自身の GUI には効かない**。設定で 1 度切り替える必要がある。 **PoC**: Rust 2 本 (既定が Frontal / 契約の表記は変えない / 保存済みの `perspective` が勝つ) + vitest (既定が frontal / 未知の値は既定に落ちる / **保存済みの明示値は勝つ**)。 **この決定で変わる副作用**: `frontal` では `scene_prompt` が「傾けてよい」を送らなくなり、 `ProductBackdropAngled` の検査が効く。2026-09-08 に「frontal の run だけ費用 1.5 倍」と観測した 経路が**既定になる**。rev15 で `RunStats` (attempts と違反種別) を残すようにしたので、 **次の live からは推測ではなく数えられる。** **接地の限界**: 根拠は**ユーザーの実機観測 1 件**で、対照実験ではない (同じ素材で両モードを 通して比べたわけではない)。「動かしすぎ」の定量化もしていない。私は動画を見ていない。 それでも既定を変えたのは、これが製品の使い手の判断であり、両方の経路が残っているため。 ## rev23 (2026-09-10、撮り直した画面が一覧に出ない — 契約違反の回収) **症状** (持ち越し、2026-09-09 から): 同じパスにスクリーンショットを撮り直しても、はめ込みの 一覧に新しいほうが現れない。選べるのは run の写し (古いバイト列) だけ。エラーは出ない。 **これは設計の穴ではなく、凍結済みの契約への違反だった。** `RunSnapshots.add.no_dedup` は 「撮り直しは同じパスで中身が変わるので、パスでの重複排除は新しい画像を拒むことになる」と 名指しで書いてある。backend (`add_run_snapshot`) は守っていて、同じパスを 2 度受ける。 **その禁止を、契約を書いた同じ rev20 が UI 側 (`snapshotChoices`) に実装していた** — 守られた 入口へ到達する経路が塞がれていた (failures #20)。 **rev20 は症状自体は見つけて `これは未解決` と書いている。** 直らなかったのは、そこに 「run が自己完結する以上そうなる」という**誤った必然性**を添えたから。自己完結 (rev10) が 強制するのは焼き直しが写しを読むことだけで、一覧の重複排除の鍵がパスであることは強制しない。 迂回策 (別名で足す) の併記と合わせて、**開いている問いが閉じた問いに化けていた**。 93. **重複排除の鍵をパスから中身へ移す。** `stale_run_snapshots(run_dir, snapshot_paths)` が 「今の元ファイルのバイト列が、**同じパスで写したどの写しとも一致しない**」ものを返す。 パスは 2 度足せるので、照合は index 単位ではなく**同じパスの写し全部**に対して行う (撮り直しを足した後は一致する写しが現れて済んだ状態になる)。 94. **run の写しは古いまま残す。** rev10 の自己完結を崩さない — 過去の run が黙って変わっては いけない。撮り直しは**新しい番号として足す**もので、上書きではない。 95. **数えるのは編集ダイアログを開く直前。** 撮り直しはアプリの外で起きるので、監視ではなく 見る直前の計測にする。コストは先に長さで弾いて抑える (同じ長さの時だけ読んで照合)。 96. **黙るのは 2 つの場合** — 元ファイルが読めない (消された / 移された) 時と、写しが無い時。 どちらも run は焼き直せるか、別のエラーで既に報せている。報せるのは「新しい画像が選べない まま埋もれている」の一点だけ。 97. **一覧は同じ名前で 2 行並ぶ。** 上が run の写し (`N 枚目`)、下が今のファイル (`撮り直し`)。 肩書きで出どころを名乗らせる (rev21 の「つまみは実効値を指す」と同じ作法)。 98. **サムネイルのキャッシュも捨てる。** `snapshotUrls` はパスをキーにしているので、撮り直し前の 絵を出したままだと「変わっていない」と読める。同じ欠陥の**表示側の面**。 **PoC**: backend 5 本 (撮り直し / 元が消えた / 写しが無い / 2 度足した後 / **同じ長さで中身が違う**) + vitest 2 本。Red は実装前のスタブ (「撮り直しを一切見ない」= 今の挙動) で観測した。 **測り方についての観察**: 5 本のうち **Red になったのは 1 本だけ**で、残り 4 本はスタブでも通る。 つまりその 4 本だけでは今回のバグを検出できない。長さで弾く近道を通らない 「同じ長さで中身が違う」1 本を別に足し、バイト照合の行を消すと落ちることを確認した (2026-09-09 の「通ったテストは実装を検証したとは限らない」の適用)。 **接地の限界**: **私は画面を見ていない。** 2 行並ぶ一覧が実際に読めるか、「撮り直し」の肩書きで 分かるかは目視していない。rev16〜21 と同じ状態。 **目視 (ユーザー、2026-09-11)**: 確認済み。一覧に `4 枚目 — snapshot_04.png` と `撮り直し — snapshot_04.png` が上下に並んだ (スクリーンショットで確認)。 ## rev24 (2026-09-11、mood にも面を足せるように — 自由度) **ユーザー判断**「1 枚めがデフォルトはめ込みなしになるのは構わないが、何もないときに はめ込みを追加できないのが自由度が低いと思う」。 **先に事実確認をした。** 「1 枚目が mood」は**仕様ではない** — 検査に位置の規則は無く、 あるのは `NoProductCut` (product が 1 つ以上) という下限だけ。そうなるのは `scene_prompt` の 一文 (`use mood only for the opening or a transition`) の誘導で、これは 2026-09-08 の live で **7 シーン全部が情景になり製品が 1 枚も映らなかった**ことの回収として入れたもの。 1 枚目を mood にすること自体は狙っていない。**実測**: 手元に残る promo.json 10 個すべてで 1 枚目が mood、mood はちょうど 1 つ (AppPromoVideo 5 / Fuseforks 4 / Lorekeel 1)。例外なし。 99. **貼るかどうかの決定を 1 箇所に出す。** `pipeline::reference::plate_snapshot_index(scene, plate) -> Option`。それまで `reburn_caption` と `plate_preview` が**別々に同じ式**を持っており、 #19 (枠が嘘をつく) の再発条件が揃っていた。焼き込みと枠は必ずここを通る。 100. **`mood` は人が足した時だけ貼る。** `PlateOverride.snapshot_index` を mood にも効かせる。 既定は絵そのままなので、**既存の run の見え方は変わらない**。 101. **`cut_kind` は書き換えない。** 書き換えると `image_prompt` が背景の記述でなくなり (`ProductBackdropDrawsScreen` と食い違う)、参照画像を作り直したときに mood の絵を失う。 種別は「何として生成されたか」の記録として残し、面は上書きとして持つ。 102. **LLM が mood に番号を書いていても無視する。** 契約では null であるべき値なので、それを 根拠に貼ると「誰も足していないのに面が出る」。足すのは人の操作だけ。 103. **UI: はめ込みの節はどのシーンにも出す。** mood の既定は「はめ込みなし (絵のまま)」。 つまみ (傾き / 大きさ / 位置) は**面がある時だけ**出す。編集ボタンは**どのシーンでも押せる** — 以前は「文も面も無い」と押せず、mood に面を足す経路がそこで塞がっていた。 **PoC**: `plate_snapshot_index` 2 本 (mood は人が足した時だけ・外すと戻る・LLM の番号は無視 / product は rev13 の優先順位を保つ)。Red は実装前のスタブ (「mood には決して貼らない」= 今の挙動) で 観測した。**product 側の 1 本はスタブでも通る** — 既存の挙動を変えていないことの確認になる。 **接地の限界**: **私は画面を見ていない。** mood の絵は背景ではないので、空きの無いところに面を 置くと絵に重なる。**黙って綺麗にはならない** — 大きさと位置で逃がす前提で、その手触りは未確認。 ダイアログにその旨を出してはいるが、読めるかは目視していない。 **目視 (ユーザー、2026-09-11)**: 確認済み。シーン 1 (mood) で `2 枚目 — snapshot_05.png` を選ぶと 予定位置の枠が絵に重なって出た (スクリーンショットで確認)。枠の `plate_preview` は焼き込みと同じ `plate_snapshot_index` を通るので、mood で「貼る」判定が効いている証拠になる。 ## rev25 (2026-09-11、run に実際のモデルを残す) **ユーザーの問い**「LLM のモデル名は `opus` `sonnet` の短縮名ではなく `claude-sonnet-5` のような正式名で 入力するべきか」。 **事実確認**: CLI の `--model` はエイリアス (系列の最新版に追従) と正式名の両方を受ける (手元の `claude --help`)。アプリは入力をそのまま渡し、空欄なら `--model` を付けない。**ところが run は、どの モデルで走ったかをどこにも残していなかった** — Fuseforks の最新 run の promo.json に `model` の文字列は 0 件、`PromoJson` にも `RunStats` にも項目が無い。エイリアスのまま新しい版が出ると、開いている判断 1 (frontal の費用) の比較の途中でモデルが入れ替わっても、後から確かめる手段が無かった。 **回答**: 比較の再現性のために正式名を勧める。ただし rev25 以降は、エイリアスや空欄でも実名が残る。 104. **init 行が名乗る実名を読む。** stream-json の `system/init` にはトップレベルの `model` があり、 エイリアスや空欄で走らせても**解決後の名前**が入る (fixture `claude_json_schema_ok.jsonl` で確認: `claude-haiku-4-5-20251001`)。`ParsedLine::Init` → `StreamFold` → `RunOk` へ運ぶ。aider / custom は None。 105. **`RunStats.models: Option>`。** analyze + plan の全試行の実名を重複なく整列。 **None = 記録なし** (rev24 以前) / **Some([]) = CLI が名乗らなかった**。空で埋めて区別を消さない (rev15 の `run_stats` と同じ作法)。 106. **組み立てを `pipeline::stages::run_stats` 1 箇所へ。** CLI (`promo run`) と GUI が `RunStats::new(r2.attempts, …, r1.cost_usd + r2.cost_usd)` を別々に手組みしていた — 項目を足すたびに 両側を思い出す必要がある、#18 の再発条件そのもの。 107. **索引 (`RunRecord`) にはまだ写さない。** 履歴の一覧に列を足す時に写す。 **PoC**: cli_runner 1 本 (実物 fixture から実名が取れる / model の無い init は None) / promo_core 2 本 (重複なく整列・空でも Some / 旧 `run_stats` は None として読める) / pipeline 2 本 (試行をまたいで運ぶ / `run_stats` が全項目を運ぶ)。Red は各層のスタブ (今の挙動 = 読まない・残さない・運ばない) で **3 回**観測した。 **Red にならなかったものを申告する**: 旧 `run_stats` の読み込み (serde default の後方互換を見張る役) と、 `run_stats` の全項目 (新設の関数なので「今の挙動」が存在しない)。 **接地の限界**: live の run はまだ走らせていない — 実物の promo.json で `models` が埋まるのは次の live から。 fixture は 2026-09-08 の採取で、現行の CLI の init 行が同じ形かは未確認。形が変わっていれば `Some([])` として記録されるので、黙って欠けることはない。 ## rev26 (2026-09-11、確認はアプリ内のメッセージボックスで) **ユーザー報告** (スクリーンショットつき)「メッセージボックスですが、ブラウザの confirm や message を使うと ローカルの URL が出てしまうので、Web モーダルでメッセージボックスを作ってください」。 シーン編集を未適用のまま閉じると、見出しに `localhost:1421 の内容` と出ていた。 使っていた箇所は **2 つだけ** — シーン編集の未適用確認 (`CaptionEditor.vue`) と、run のフォルダ削除 (`RunsDialog.vue`)。`alert` と `prompt` は使っていなかった。 108. **`dialog.ts` の `ask(opts) -> Promise` に集める。** 表示は `MessageBox.vue` を App.vue に 1 つ。 **一度に 1 件** — 出ている間の次の `ask` は待たせ、答えた順に返す (重ねると、どちらに答えたか分からない)。 109. **Esc と背景クリックは否定側。** 呼び出し側は、取り消しが安全側になる文言を選ぶ (「編集に戻る」「キャンセル」)。 110. **`danger` (削除など) は最初の焦点を否定側に置く。** Enter の連打で消さないため。肯定側は警告色。 111. **keydown は capture 段階で受けて伝播を止める。** 止めないと、下のシーン編集の Esc も同じ打鍵で反応し、 確認の後ろで画面が閉じる。`askClose` の側でも、確認が出ている間は同じ確認を二重に積まない。 112. **機械の網 `noBrowserDialogs.test.ts`。** src の `.vue` / `.ts` を読み、標準ダイアログの呼び出しで落ちる。 新しく書いた画面が標準ダイアログに戻っても、ここで捕まる。 **PoC**: `dialog.ts` 4 本 (スタブで 3 本 Red。「何も出ていない時の answer は何もしない」はスタブでも成り立つので Red にならない — 申告) / 網 2 本 (本物の 2 か所で Red → 置き換えて Green / 網そのものの検出力)。 **測り方についての観察 2 件**: ①**初版の網は誤検出した** — 画面の文言「video prompt (text-to-video fallback)」の `prompt (` に反応した (名前と括弧の間の空白を許していた)。空白を許さないようにし、その行を「反応してはいけない例」として網自身のテストに入れた。 ②**初版は vitest では通ったが build で落ちた** — `node:fs` でソースを読んでいたが、build の型検査 (vue-tsc) は テストも対象にしており、このプロジェクトには Node の型定義が無い。`@types/node` の追加 (ダウンロード) と、 型検査からテストを外す案 (他のテストの型検査まで失う) を捨て、Vite の `import.meta.glob` (`?raw`) で読むように書き直した。 書き直した網が本物で落ちるかは、ソースに `confirm(` を一時的に差し込んで Red を見てから戻して確かめた。 あわせて「glob が実際にファイルを拾っている」ことも網のテストに入れた (拾わないと常に空で通る)。 **接地の限界**: vitest は node 環境で **DOM が無い**。次の 4 点はテストで確かめられていない — Esc が確認だけを閉じて下のシーン編集を閉じないこと / Tab が 2 つのボタンの間に留まること / 閉じた後に元の焦点へ戻ること / 重なり順 (70) がすべてのダイアログの上に来ること。GUI の目視が要る。 **目視 (ユーザー、2026-09-11)**: OK。確認手順 4 点 (Esc で確認だけが閉じる / Tab が 2 つのボタンに留まる / 削除の最初の焦点が「キャンセル」/ 未適用確認が出る) を提示し、結果 OK の報告を受けた。 ## rev27 (2026-09-11、文字を少し大きく・列の枠を外す — 試行) **ユーザー** (スクリーンショットつき、ライトテーマ)「文字を少しだけ大きくして、入力、結果、進捗の枠をなくしてみたい」。 **次の主題 (UI を簡単にする) で、ユーザーが具体的な方向を出した最初の手。** 113. **文字の大きさをトークンにまとめ、全段 +1px。** px の直書きが 37 か所 (10 / 11 / 12 / 13 / 14 / 18 px) に 散らばっていた。基準 (`body` の 13px) だけを上げると、ラベルや補足 (11〜12px 固定) は変わらず 「少しだけ大きく」にならない。`main.css` の `:root` に `--fs-xs〜--fs-xl` を置いて各所から参照させ、 値を 11 / 12 / 13 / 14 / 15 / 19 px にした。**次に大きさを調整するときは main.css の 1 か所で済む**。 Lightbox の矢印 (40px) は文字ではなく記号なので対象外。 114. **列の枠を外す。** 3 つの列の直下の `.panel` から背景・枠線・角丸を外した (App.vue の scoped + `:deep`)。 共通の `.panel` はダイアログ 4 種が使うので変えていない — 変えるとダイアログの枠まで消える。 (→ rev31 で履歴は全画面になり `.panel` を使わなくなった。今は 3 種) **PoC**: なし。**見た目の変更で、vitest は DOM の無い環境なので Red を観測していない** (申告)。 機械で確かめたのは ①px の直書きが Lightbox の記号 1 か所を除いて残っていない ②vitest 38 / build green。 **未決 (試行)**: 採否はユーザーの目視で決める。枠を外すと、列の境目は 10px の隙間と余白だけになる。 読みにくければ「枠線だけ残して背景は平ら」または「列の間に細い区切り線」の案がある。 ## rev28 (2026-09-11、書体を IBM Plex Sans JP の Bold に・文字 1.5 倍 — 試行) **ユーザー**「文字をフォントを IBMPlexSansJP-Bold にして 1.5 倍くらいにしてみて」。rev27 (試行) の上に重ねる。 **事実確認**: IBM Plex Sans JP は、この機体に**ユーザーごとのフォント**として 8 ウェイト入っている (`%LOCALAPPDATA%\Microsoft\Windows\Fonts`、アプリのフォント一覧でのファミリー名は `IBM Plex Sans JP`)。 システム全体のフォントフォルダには無い。CSS で名前を指定しても見つからなければ黙って別の書体になるので、先に確かめた。 115. **書体をトークン `--font-ui` に。** 先頭が IBM Plex Sans JP、無ければ Segoe UI → Noto Sans JP。 **同梱はしていない** — 入っていない環境 (公開後の他の利用者を含む) では Segoe UI に落ちる。 (→ rev30 のデザイン修正で書体は Inter + Noto Sans JP に替わり、123 で**同梱**にした。この項の「同梱しない」は rev30 で閉じた) 116. **倍率を変数 1 つに。** `--fs-scale: 1.5`。各段 = rev27 の値 × scale (16.5 / 18 / 19.5 / 21 / 22.5 / 28.5 px)。 「1.5 倍くらい」は rev27 の後の大きさに対する倍率として解釈した。**試し直す時は scale だけ変える**。 117. **太さもトークンに。** 本文 `--fw-base` / 見出し・ボタン `--fw-semi` (旧 600) / `--fw-bold` (旧 700) を全部 700。 本文だけを Bold にすると、SemiBold のまま残る見出しやボタンが本文より**細く**見えて強弱が逆転するため。 「(mood)」の横の 400 は、わざと細くしている補足なので残した。 **PoC**: なし — 見た目の変更で Red は観測していない (申告)。機械で確かめたのは、大きさ・太さ・書体の直書きが 意図した例外 (Lightbox の記号 40px / 補足の 400 / 等幅の Consolas) を除いて残っていないことと、vitest 38 / build green。 **接地の限界**: ①WebView2 が**ユーザーごとにインストールしたフォント**を CSS の名前で拾えるかは未確認。拾えなければ Segoe UI の Bold に見える。 ②**文字は 1.5 倍でも、幅と高さは px のまま** — 列の幅 (340 / 320px) やボタン・タイトルバー・ダイアログの寸法は広がっていない。 詰まる・折り返す・はみ出す箇所が出る可能性が高い。 ③公開前に採用するなら、フォントの同梱 (OFL) を別に判断する必要がある。 ## rev29 (2026-09-11、見出しと項目の大きさを分ける・説明文とログを Medium に — 試行) **ユーザー** (rev28 を見て)「見出しの大きさはこれくらいがいいと思うが、内部の項目はちょっと小さくしたほうがいいかな」。 作業中に続けて「進捗のログも medium で普通サイズで」「(UI スナップショットの案内やシーン編集の注意書き) のような 説明文は bold でなく medium で小さめにしていい」。rev27・28 (試行) の上に重ねる。 118. **倍率を 2 本に分ける。** rev28 までは 1 本の `--fs-scale` が全段に掛かっており、見出し (`.panel h2` は 12〜13px の段) と項目が同じ段を共有していたので、倍率を下げると見出しも小さくなる。見出し用 `--fs-scale-heading` (1.5 のまま) と見出し用の段 `--fs-h-md / h-lg / h-xl` を足し、見出し 5 か所 (入力・結果・進捗 / 結果のアプリ名とキャッチ / メッセージボックスのタイトル / 設定の小見出し) を そちらへ切り替えた (当初はタイトルバーも含めて 6 か所 — 121 で外した)。項目の `--fs-scale` は 1.5 → **1.25**。見出しの実寸は rev28 と同じ。 「比較」「はめ込み」の `

` / `

` は大きさを変数で持たず周りに対する比率なので、項目と一緒に縮むが項目より大きいまま。 119. **説明文と補足を Medium・小さめに。** `.muted` (色だけの定義だった) に `--fw-medium` (500) と `--fs-sm` を足した。 ユーザーの例の 1 つ (スナップショットの案内) は `note` の付かない `muted` なので、`note` だけでは届かない。 **`.muted` は 47 か所** — 説明文以外 (ログの時刻 / 表の `plate_mode` 列 / 「コピー先」のラベル / Lightbox の番号など) も 同じになる。戻したいものは個別に外す。設定ダイアログの `.note` (自前で `--fs-md`) も `--fs-sm` に揃えた。 120. **進捗ログを Medium・本文の大きさに。** ログは `mono` (Consolas) だったが Consolas には Medium が無いので、 等幅をやめて UI の書体の Medium・`--fs-base` にした。時刻の桁は `tabular-nums` で揃える。 「medium」は太さと解釈した (大きさの段 md の意味なら切り替える、とユーザーに伝えた)。 121. **タイトルバーのアプリ名は試行の対象外。** ユーザー「ウィンドウタイトルバーのアプリタイトルのフォントと大きさは 変えなくていい」。`.brand` を試行中の変数 (見出し用の段・書体・太さ) から外し、固定値の変数 `--font-chrome` (Segoe UI 系) / `--fs-chrome` (12px) / `--fw-chrome` (700) を参照させた — **試行前の値そのもの**で、 倍率も掛けない。値を部品に直書きしない決まりは保ったまま、「ここは試行の対象外」という意図を変数名に残す。 中の「実行中」の chip は状態表示なので `.chip` のまま (大きさは項目の倍率、書体は `.brand` から Segoe UI を受け継ぐ)。 **PoC**: なし — 見た目の変更で Red は観測していない (申告)。機械で確かめたのは、見出し用の段の参照が 5 か所 (121 の後)、 `--fw-medium` の参照が 2 か所 (`.muted` とログ)、ログに `mono` が残っていない、大きさの px 直書きが Lightbox の記号だけ、 vitest 38 / build green。 **接地の限界**: 画面は見ていない。等幅をやめたログで長いパスや stderr が読みやすいか、`.muted` に巻き込まれた 表の列やラベルが小さすぎないかは、ユーザーの目視で決まる。 ## rev30 (2026-09-11、デザインの修正と UI の多言語化 — ユーザーの手を検めて回収) **ユーザー**「デザインを修正し、UI の多言語化をこちらでおこなっておきました」。会話の外での作業で、未コミットの差分として受け取った。 差分の中身: 書体を Inter + Noto Sans JP に / 太さ 500・600・700 / 形のトークン (角丸・高さ・影・遷移) / アイコン部品 `Icon.vue` / 左ペインの区切り線 / UI を 3 言語 (ja / en / zh-CN) に切り替える `i18n.ts` (localStorage `apppromo.ui_lang`) とタイトルバーの切り替え。 こちらは差分を検めて 4 点を報告し、ユーザーの「提案の順で 1 から」で回収した。途中で 5 点目 (127) を見つけた。 122. **消えたトークン `--fs-h-xl` を戻す。** 書体トークンの行を差し替えた範囲に定義も入っていて、見出しの段 md / lg / xl のうち xl だけが 抜けていた。ScenePanel の結果のアプリ名 (`.app`) が参照しており、宣言ごと無効になって**黙って親の大きさに落ちていた**。 参照を別の段に替えるとデザインの判断を上書きするので、定義を HEAD と同じ値で戻した。 網 `cssTokens.test.ts`: フォールバックの無い `var(--x)` は、src のどこかで定義されていること。 123. **書体は同梱する** (ユーザー判断: Inter と Noto の両方。npm からの取得も承認)。index.html の Google Fonts の `` は、 CSP (`style-src 'self' 'unsafe-inline'`、`font-src` 無し = `default-src 'self'`) で読めない形だった。この機体に Inter は入っていない (Noto Sans JP は Windows のコンポーネントとして入っている — WinSxS に 10.0.26100 系) ので、英字の書体は指定どおりに出ていなかったはず (実行時は未確認)。 `@fontsource-variable/inter` 5.3.0 と `@fontsource-variable/noto-sans-jp` 5.3.0 (OFL-1.1) を main.ts で import した。 **宣言名は `Inter Variable` / `Noto Sans JP Variable`** — トークンに `Inter` と書くと、手元に入っていない限り当たらない。 dist は woff2 131 本・5.5 MB、`data:` への埋め込み 0 (埋め込むと CSP で読めない)、HTML と CSS の外部 URL 0。CSS は `@font-face` の分 23 KB → 123 KB。 網 `fonts.test.ts`: index.html が外部の CSS・フォントを読まない / main.ts が import する / `--font-ui` と `--font-chrome` の先頭がパッケージの宣言名。 124. **多言語化の残りを辞書へ。** 移っていたのは一部で、設定・履歴・シーン編集のダイアログ、トーストとログ、確認ダイアログの既定値、 傾きのラベル (`0° (正面)`)、履歴の「再生成」列の説明が日本語の直書きのまま (8 ファイル約 180 行) — 英語を選んでもそこだけ日本語で出た。 辞書に 152 キーを足して移した。**日本語の文言は変えていない**: 移行前の控えから日本語の断片 275 個を取り出し、すべて辞書か今のソースに 残っていること、足した ja の値の断片が移行前のソースにあったことを機械で突き合わせた (1 個だけ出た不一致は、`