模組三|AI 生圖與生片產線
昨天講角色卡怎麼產、怎麼校驗。那些到頭來都是紙上談兵——真正的驗收是拿它去做事。
這一回要一張插圖,四個角色同框:香案上躺著一枚薄晶片,綠語伸手擋開調律者的探針,訊號香的煙在半空裂成兩道。
生了九次。
用的是 Day 12 講過的那條插圖產線:illustrations.json 寫規格,generate-shortform-illustrations.mjs 讀 manifest、附上參考圖、打 OpenAI Images 的 /v1/images/edits(gpt-image-1)。
「參考圖要先指定職責、角色數超過上限就縮小完整角色數」那條結論,Day 11 已經補進去了,這裡不重講。這篇講的是那條結論沒有涵蓋的兩個坑,以及第九次之後我改做的事。
兩個坑都跟角色卡直接相關,而且都是靜默失敗。
第 4 次,霜瞳被畫成第二個焰刃——畫面上兩個紅髮、沒有銀髮狙擊手。
原因藏在腳本裡。這條產線會依角色名注入身分約束:
if (characters.some((name) => name.includes('霜瞳'))) {
identityNotes.push('霜瞳 is a silver-haired marksman android girl with pale blue eyes, ...');
}
而我在改 prompt 的時候,把角色的中文名從場景描述裡拿掉了——因為我剛寫完角色卡,滿腦子都是昨天講的那條「出圖提示詞不准出現人名」。
名字一拿掉,identityNotes 就不再注入。霜瞳的身分約束靜默失效,模型就拿手上另一張參考圖去補那個位置。
兩條規則都是對的,只是前提不同:
| 規則 | 為什麼 | |
|---|---|---|
| skill 的角色卡 | 禁止人名 | 卡片是通用產物,可能餵給任何模型,而模型對它認得的名字有既有印象 |
| 這條插圖產線 | 必須有人名 | 身分約束靠名字比對觸發,而「霜瞳」這種名字模型根本不認得,不會污染 |
同一個字串,在一條管線裡是污染源,在另一條管線裡是觸發器。
規則不能離開它成立的前提被搬運。 而搬錯的代價是靜默的——沒有錯誤訊息,只有一張看起來還行、但少了一個人的圖。
改成兩人構圖之後畫面乾淨了,但綠語看起來「沒穿衣服」——只有一身白色裝甲板,沒有外衣。
因為我在角色卡和 manifest 裡都只寫了 white-and-teal medical exo-frame。模型照著畫,畫出一具醫療外骨架,完全合理。
而她的立繪上是:白色長版開襟醫療外套、寬袖長擺、青綠分層裙、白色護士帽、球形治療無人機。
我從頭到尾沒有打開過那張立繪。 我讀了焰刃、霜瞳、調律者三張,漏了綠語。角色卡裡那段外觀描述,是我從 mj-prompts/03-綠語.txt 的文字轉寫的,而那份文字本身也只寫了 exo-frame。
於是 Day 11 那條「圖打敗文字」在這裡以另一種方式成立:文字沒寫到的東西,圖上有;而你只讀文字,就永遠不知道自己漏了什麼。
這個坑比坑一更難防。坑一至少有跡可循——回頭讀腳本就會看到 identityNotes 是按名字觸發的。而坑二沒有任何線索:那份文字設定檔本身完全通順,看不出它漏了東西。你只有打開圖,才會知道文字漏了什麼。

九次之後我停手,不再往同一個 API 灌參數。
我把手上的東西整理成一份可交接的出圖規格:
04.01 插圖生成需求
├── 參考圖:char-tuner.png / char-03.png(絕對路徑)
├── 輸出:assets/shortform/04.01-painless-residue.png,1536×1024
├── Prompt:分叉的煙(主體)→ 兩個前景角色(逐項 canon 描述)→ 背景剪影 → 渲染質感
├── Negative:24 條,每一條都對應一次實際失敗
└── 驗收檢查點:8 條,生完逐條比對
然後把這份規格丟給桌面版 codex 去跑。
一次過。煙乾淨地分成一個大 V、綠語外套裙擺完整、探針停在掌心前、背景剛好兩個剪影。
而規格一個字都沒改。換的只是執行的引擎。
這件事的意義不在於「codex 比較強」——換一個模型當然會有不同結果,那沒什麼好講的。意義在於:當我終於能把需求寫成一份別人可以照著做的文件時,它才第一次做對。
前面九次失敗,有六次是因為我腦子裡有規格但沒寫出來(綠語有外套、名字要留著、四個人不能塞太多參考圖)。我以為我在調 prompt,其實我在把自己知道的東西一點一點補進文件裡。
九次失敗真正的產出不是那張圖,是那份 brief。
那份 brief 一開始躺在暫存目錄,session 結束就會消失。
於是我把它收進 novel/短篇/illustration-briefs/,manifest 加一個 briefPath 指過去,reviewNotes 寫明「這是九次失敗的最後一版 prompt,不要拿它重生」。
寫完就發現這違反了昨天那篇的第一條結論——那全都是註解,不是機制。腳本不讀 briefPath,--force 04.01 照樣會用舊 prompt 把好圖蓋掉,而且不會有任何提示。
所以補成會叫的東西,十幾行:
// 有些回的成品是在外部工具用一份手寫 brief 生出來的,
// manifest 裡的 prompt 只是當初失敗的最後一版。這種回直接擋下來,
// 不然一個 --force 就會把好圖蓋成爛圖,而且完全沒有提示。
if (!entry.briefPath || overrideBrief) return false;
console.error(`
這一回的圖不是這支腳本生的,所以不重生。
現有的圖 ${entry.outputPath}
生圖的規格 ${entry.briefPath}
要改圖的話,去改上面那份規格,然後用它原本的工具再生一次。
`);
現在跑 --force 04.01 會停下來、印出 brief 的路徑、exit 1。要硬幹得加 --override-brief。

第一次測這道守門,我看到的是這行:
OPENAI_API_KEY is missing. Source .env.local before running this script.
守門根本沒跑到。因為金鑰檢查寫在最上層,沒設環境變數就直接 exit。
改成真的要打 API 時才檢查。防呆訊息被另一個錯誤蓋掉,等於沒有防呆。
這件事值得單獨記一筆,因為它很容易發生:守門通常是後來補的,而補的位置很自然會在既有的檢查後面。你以為你加了一道防線,實際上那道防線在大部分情況下根本不會被執行到。
加完守門一定要實際觸發它一次,看它到底印出什麼。
代價一:那九次生圖是真的花掉了。
一張 gpt-image-1 高品質 1536×1024 不是免費的,九次就是九次。而其中六次的失敗原因,都是我自己漏掉的資訊,不是模型的問題。
換算下來,讀那張立繪要三十秒,而沒讀它的代價是兩次重生。做這種事之前,把所有來源打開看過一遍是最便宜的一步,我卻是在第八次之後才回頭做。
代價二:成品和產生它的規格,現在是分家的。
illustrations.json 的 prompt 欄位跟實際生出這張圖的 brief 不是同一份東西。守門擋住了「不小心蓋掉」,但沒有解決根本問題——這一回的規格活在 manifest 外面,而 manifest 才是那條產線的主控檔。
真正乾淨的做法是讓腳本能直接吃 brief。目前沒有,所以這一回在 manifest 裡是個特例。
代價三:驗收清單是人在看的。
八條檢查點沒有任何一條是自動的。服裝有沒有退化、角色有沒有被複製、探針跟晶片之間的空隙還在不在——全靠我自己逐條看。
而這正是我在昨天那篇批評過的形狀:寫在文件裡的規則會爛。 差別只在於這次我知道它會爛,也知道自動化它要花多少工(影像判讀,不划算),所以選擇了接受。
代價四:這條路只在「一回一張圖」的量級成立。
兩個角色的上限、逐條人工驗收,都是因為插圖一回只有一張才撐得住。如果哪天要一天出十張,這整套都得換。
一、規則不能離開它成立的前提被搬運。
在 prompt 裡寫角色名、產品名、公司名,你以為只是在標示對象,實際上是在啟用模型腦中關於那個名字的全部既有印象——這是「禁止人名」那條規則的理由。
但我把它搬到另一條管線就炸了,因為那條管線的身分約束正是靠名字觸發的。
每一條規則都有它成立的前提,而前提通常沒有寫在規則旁邊。 搬規則的時候先問:它是為了防什麼?那個東西在新的地方也存在嗎?
這條在工程上到處都是:從別的專案抄來的 lint 設定、複製的 CI 流程、沿用的 code review 準則——它們在原本的地方都對,因為那裡有你沒看見的前提。
二、做不出來的時候,先寫規格,不要一直調參數。
我在同一個 API 上灌了九次參數。真正解決問題的,是停下來把需求寫成一份別人能照著做的文件,然後換一個引擎執行。
一次就對了,而規格一個字都沒改。
判斷句:如果我現在要把這件事交給另一個人或另一個工具做,我交得出東西嗎?
這條完全不限於生圖。做不出來的功能、修不好的 bug、談不攏的需求,卡住的時候寫一份能交接的規格,通常比再試一次有用。
三、只讀文字,永遠不知道自己漏了什麼。
綠語的外套不在任何一份文字設定檔裡,只在那張立繪上。而那份文字設定檔看起來完全通順——沒有任何跡象顯示它漏了東西。
所以「把所有來源打開看過一遍」不是龜毛,是唯一能發現這類漏洞的方法。三十秒的事,我省下來換了兩次重生。
四、加完守門,一定要實際觸發它一次。
我補的那道防呆,第一次測的時候根本沒跑到——被上面的金鑰檢查攔截了。
守門通常是後來補的,補的位置很自然會在既有檢查後面。一道從來沒被觸發過的防線,跟沒有防線是一樣的。
明天 Day 20,模組四開始:秤兒的角色頁為什麼需要 3D,以及在無 build step、純靜態站的限制下,技術選型前該先寫死哪些條件。