模組三|AI 生圖與生片產線
一篇 294 個字的短篇,四個角色,一行指令:
/novel-characters novel/短篇/04.01_沒有痛的殘象.md
十分鐘後拿到這些:
novel/短篇/04.01-角色卡/
├── 04.01_沒有痛的殘象-cast.json 28 KB
├── 04.01_沒有痛的殘象-cast.md 27 KB
├── report.html 95 KB
└── images/*.png 4 張三視圖
四個角色,每個一份:人物畫像、卡通形象提示詞(中英雙份)、音色提示詞(中英雙份)、三視圖提示詞、逐字引文。
而其中一條硬規則是:出圖提示詞裡絕對不准出現角色的名字。
這條規則第一次看到會覺得莫名其妙——描述一個角色,卻不准叫他的名字。今天這篇就從這條規則講起,因為它是 Day 11 那條「參考輸入會全盤繼承」的反面,而反面比正面更難想到。
Day 28 會講 20 章重構成 200 回這件事。這裡先講它造成的一個副作用。
一回短篇大約 300 字。每一回我都要回答同一組問題:
前兩題是創作,後兩題是規格。而後兩題的答案,理論上 200 回都一樣——焰刃永遠是緋紅短鮑伯頭配白黑外骨甲,霜瞳永遠是銀色低馬尾配軌道狙擊槍。
問題是「理論上一樣」跟「實際上一樣」中間隔著 200 次手寫。
我第一版的做法是每回配圖時,在插圖 manifest 裡重新寫一段人物描述。寫到第三回就出現了 Day 11 講過的老毛病:同一個人,第一回寫「銀髮低馬尾」,第三回寫成「銀色長髮」,第五回乾脆只寫「狙擊手」——於是模型畫出一個泛用狙擊手。
每一次重寫,都是新增一個會漂的來源。
而我掃過那 20 份角色設定檔之後的結論是:三視圖那張 PNG 才是真正的規格,文字設定檔只是它的註解——因為三視圖每次生圖都會被讀,錯了下一張圖就會錯;而文字設定檔沒有任何流程會讀它,死掉沒有症狀。這篇要處理的是下一層問題:註解本身要怎麼大量生產而不互相矛盾。
novel-characters 的管線長這樣:
原文 .md
│
├─ chunk 切塊(單次上限 24 塊 / 約 33 萬字元,超了明確報 truncated)
│
├─ 第一趟:抽角色 → roster-NN.json {name, aliases, note, quotes}
│
├─ merge 按名字+別名收斂,notes 累加、quotes 去重,按出現塊數降序
│
├─ 第二趟:出角色卡 → card-<slug>.json {persona, image, voice}
│
├─ validate ⛔ 不能跳
│
└─ render → cast.md + report.html(自動嵌 images/<slug>-turnaround.png)
兩趟分開的理由,是第二趟看不到原文。
這聽起來像缺陷,其實是刻意的:第一趟的 note 欄位如果寫得不夠密,第二趟就編不出東西。所以 skill 的第一趟指示裡直接寫死「後一趟看不到原文,只看得到你寫的 note,所以細節必須落在 note 裡」——用結構逼出品質,而不是用「請寫詳細一點」這種話。
長篇時第一趟可以每塊一個子代理並發跑。我這次的短篇只有一塊,跳過並發,直接自己讀。
$ node scripts/novel-characters.mjs chunk book.txt wd
{ "chunks": 1, "chars": 294, "truncated": false }
一塊,294 字元,沒截斷。chunks == 1 就跳過第一趟的並發,自己讀完寫成 roster-00.json。
$ node scripts/novel-characters.mjs merge wd
[ { "name": "調律者", "aliases": ["我"], ... }, ... ]
merge 這步在單塊時看起來多餘,但它做的事在多塊時很重要:把「陸行遠 / 陸 / 姑娘」收斂成一個人。這次它處理的是「調律者 / 我」——第一人稱敘述者跟角色名的合併。
然後是第二趟出卡,最後:
$ node scripts/novel-characters.mjs validate 04.01_沒有痛的殘象-cast.json book.txt
✓ 4 个角色全部通过校验
這一步是整個 skill 最值得抄的部分。它檢查四件事,而這四件事的共同點是:模型真的會犯,而且犯了不會報錯。

角色卡裡有個 persona.evidence 欄位,放的是原文引用。校驗器拿它逐字比對原始檔案,對不上就 exit 1。
這條規則在這次實際救了我一次,而且是我完全沒預期的方向。
我做完 04.01 的角色卡、validate 顯示「✓ 4 个角色全部通过校验」之後,繼續往下做別的事。中途我回頭再跑了一次同一個指令:
✗ 6 处违规:
[調律者] 引文不是原文逐字片段:綠語不讓我直接接進調律壇。
[調律者] 引文不是原文逐字片段:我點起訊號香,想只測方向。
[綠語] 引文不是原文逐字片段:她說這段東西沒有痛過。
[綠語] 引文不是原文逐字片段:她說沒有傷口的東西,先別急著縫。
[霜瞳] 引文不是原文逐字片段:霜瞳帶回的晶片比綠語的還薄。
[霜瞳] 引文不是原文逐字片段:霜瞳把它放到香案最外側。
角色卡沒動過。是原文被改了。
我在做角色卡的同時,正稿那邊在潤稿。原文從:
綠語不讓我直接接進調律壇。
改成:
我問綠語,能不能直接接進調律壇。
她搖頭,說沒有傷口的東西,先別急著縫。
意思相近,但敘事方向反了——原本是綠語主動阻止,改完變成調律者主動請示、被拒絕。這對角色卡是有影響的:arc 那一欄本來寫「第一次接受同伴的煞車」,改完之後應該是「第一次讓別人決定他能做到哪裡」。
其他五處也一樣:加了「邊緣沒有一點磨損」、「霜瞳自己把它放到最外側」、多出一句「綠語說可以只測方向」。都是潤稿層級的小改,沒有一處會讓角色卡看起來壞掉。
這就是逐字校驗的價值:它不檢查意思對不對(那檢查不了),它只檢查「這句話今天還在不在」。
而衍生物跟來源脫鉤,正是從這種「小到不值得同步」的改動開始的。
這條可以直接搬:任何複製了來源片段的衍生物——文件引程式碼、投影片引數據、測試名稱引需求敘述——都該存一份逐字比對,而且要能重跑。因為語意有沒有漂沒辦法自動檢查,但「這個字串還在不在」可以。

開頭那條規則。理由 skill 寫得很直接:
圖像模型對這些偏見極重,會畫成它記憶裡的角色而不是你的角色。描述這個人,不要叫他的名字。
Day 11 的結論是「參考輸入會全盤繼承,你必須明確關掉不要的維度」。當時講的是參考圖。
而角色名字是一個你沒意識到自己附上去的參考輸入。
模型看到一個名字,會去找它訓練資料裡叫這個名字的角色,然後把那個角色的長相摻進來。你以為你只是在標示「這是誰」,實際上你附了一張看不見的參考圖。
而且這個污染沒有症狀:你不會收到警告,只會拿到一張「怪怪的,但說不上哪裡怪」的圖。
所以校驗器直接掃 image.prompt / image.promptZh / image.turnaround 三個欄位,出現任何角色名、別名就報錯。用機械檢查代替自律。
(這條規則後來害我炸掉一次生圖——因為我把它搬到了另一條規則剛好相反的管線上。明天 Day 19 會講。)
角色卡是中英混合的:persona 和 voice 的描述欄位強制中文(給人看、給我自己判斷),image.prompt / negativePrompt / tags / turnaround / voice.prompt 強制英文(給模型吃)。
skill 的文件裡特別點名一個最容易寫錯的地方:
voice.timbre/pitch/pace/accent/emotion/referenceHint是中文,不是英文。
因為它們就在 voice.prompt(英文)隔壁,寫著寫著就一路英文下去了。
這條規則的存在本身就是證據——它是被真實輸出打出來的,不是設計時想到的。
importance 只能是那四個值protagonist / major / supporting / minor。enum 檢查,最無聊的一條,但模型真的會自己發明 lead、secondary。
四類錯,共同形狀是:都不會爆炸,只會安靜地產出稍微錯一點的東西。
而稍微錯一點的東西,是最貴的那種錯——因為它會通過人工目視。
skill 是通用的,專案是具體的。這次有三個地方我沒照預設走。
skill 預設的 image.style 是 Flat vector cartoon with ink-wash colouring——扁平向量卡通。
而這個專案的既有視覺是另一回事:assets/turnarounds/mj-prompts/ 底下每個角色一份 prompt,開頭全部是同一串骨架:
official game character design sheet, premium industrial sci-fi anime style,
character turnaround presentation, full body front view side view back view,
strict orthographic, neutral standing A-pose, same character shown three times,
exact same outfit and proportions in all views, ...
如果我讓 skill 用預設畫風產一批角色卡,那份卡就會描述一個「扁平向量卡通版的焰刃」,跟 200 回插圖裡的那個人不是同一套視覺。
所以我把四張卡的 image 區塊全部改寫,對齊既有骨架,角色外觀細節逐項照抄 mj-prompts/01-焰刃.txt 那類檔案(緋紅短鮑伯、右前臂內建刀匣、白黑外骨甲配紅色重音線)。
skill 的預設是它自己的意見,不是你的規格。 產出跟既有資產不一致的時候,改的是產出。
skill 的第 8 步是用 codex 內建的 $imagegen 幫主要角色生三視圖。
但這四個角色的官方三視圖早就存在:
assets/turnarounds/char-01-ef-turnaround.png # 焰刃
assets/turnarounds/char-02-turnaround.png # 霜瞳
assets/turnarounds/char-03-turnaround.png # 綠語
assets/turnarounds/char-tuner-turnaround.png # 調律者
重生一輪要花錢,而且畫風一定會飄(skill 自己的文件就寫了「同一批角色各自獨立出圖,畫風會漂」)。
所以我沒生圖,改成 symlink:
ln -sf ../../../../assets/turnarounds/char-01-ef-turnaround.png images/焰刃-turnaround.png
render 那步只認 images/<slug>-turnaround.png 這個路徑,不管它底下是實體檔還是連結。報表裡 12 個 images/ 引用全部指到既有的官方圖。
零成本、零漂移,而且是同一份檔案,不是複製品——三視圖哪天更新,角色卡報表跟著更新。
skill 預設把產出丟在原書同級目錄。這個專案的 novel/短篇/ 底下是 200 份稿源,多丟 3 個檔案 + 一個 images/ 進去會很難看。
改成 novel/短篇/04.01-角色卡/。一回一個資料夾。
做完 04.01 接著做 04.02(同樣四個角色)。這一步才是真正驗證這套流程的地方。
| 欄位 | 04.01 → 04.02 | 為什麼 |
|---|---|---|
image.*(全部) |
原封不動 | 外觀不隨劇情變 |
voice.timbre/pitch/pace/accent |
原封不動 | 音色不隨劇情變 |
voice.emotion |
微調 | 「做決定時句尾壓實」是這一回才有的 |
persona.appearance |
加一件道具 | 綠語這回多掛了共用熱水壺 |
persona.oneLiner |
全部重寫 | 一句話抓這個人「在這一回」做了什麼 |
persona.arc / temperament / motivation |
全部重寫 | 這一回的變化 |
persona.evidence |
全部換 | 綁在這一回的原文 |
這張表就是 Day 11 那條「要固定的東西走不變的通道,要變的東西走可變的通道」的第二次應用——只是這次的「通道」不是 prompt 的參數位置,是 JSON 的欄位分組。
image 和 voice 整塊複製,是因為它們在設計上就不該隨回數變。如果哪一回我發現自己在改 image.prompt,那代表兩件事之一:這一回角色真的換裝了(那要回頭更新三視圖),或者我手滑了(那是 bug)。
欄位分組本身就是一個約束機制:把「該變的」和「不該變的」放進不同的欄位,改動的位置就會告訴你出了什麼事。
這件事必須講清楚,否則前面那條「三視圖才是規格」就被自己推翻了。
novel-characters 產的是衍生物,不是來源。它的來源有兩個:
assets/turnarounds/*.png(三視圖 PNG)角色卡是把這兩個東西合成的一次性快照。它會過期——第 50 回的時候,04.01-角色卡/ 裡的東西早就跟現況無關了。
但這不要緊,因為它從一開始就不是 canon,而是「04.01 這一回的角色狀態」。 它的有效期就是那一回。
真正危險的做法是反過來:拿角色卡當設定檔、下一回從角色卡複製、再下一回從再下一回複製。三回之後你就有了第三個會漂的來源,而且沒有人記得它是從哪裡長出來的。
所以我的規矩是:每一回都從原文 + 三視圖重新產一次,不從上一回的角色卡複製。
看起來比較笨,但笨的那條路沒有累積誤差。
代價一:短篇太短,skill 的分塊機制完全用不到。
294 字元對一個設計來處理數十萬字的東西來說是浪費。真正該用它的是整卷或整部,短篇單回其實只用到「校驗器 + 輸出格式」這兩件事。
我還是用了,理由是輸出格式統一比效率重要——200 回的角色卡如果格式各異,那跟沒有一樣。
代價二:image / voice 整塊複製是我手動做的。
上面那張表講得像是有機制在保障,其實沒有。是我自己在寫 04.02 的時候,把 04.01 的 image 區塊複製過去。
真正該做的是把 image 和 voice 抽成一份 per-character 的共用檔,每回的卡只 $ref 過去。現在沒有,所以「不該變的東西」實際上每回都被重新輸入一次——這正是我這篇在批評的事,只是我還沒修。
代價三:只有引文有防護,歸納摘要沒有。
上面那次原文改動,validate 抓出 6 處引文不符——但它抓不到 persona.arc 那句「第一次接受同伴的煞車」也一起過期了。那句不是引文,是歸納,校驗器不驗它。
我是看到那 6 條錯誤、回去讀新版原文,才順手發現 arc 也要改。如果那 6 句引文剛好都沒被潤到,我就不會回去讀原文,arc 就會一直錯下去。
也就是說:逐字引用有防護,歸納摘要沒有——而角色卡的大部分內容都是歸納摘要。引文校驗實際上是在當「來源變了」的煙霧偵測器,它的覆蓋率取決於引文引得夠不夠多。
代價四:這套流程沒有跨回一致性檢查。
04.01 和 04.02 的焰刃 image.prompt 是不是同一份,目前靠我自己看。沒有腳本會 diff 兩回之間該相同的欄位。
該補的東西很明顯(比對同名角色的 image 區塊、不同就報錯),但我還沒寫。這是「工作路徑旁邊的東西維護不動」的另一個例子——差別只在於這次我知道它會爛。
一、用機械檢查代替自律。
那四類錯(引文不逐字、prompt 混入人名、語言分工錯、enum 亂填)都是「知道規則也還是會犯」的那種錯。寫在文件裡沒有用,寫成 exit 1 才有用。
判斷句:這條規則違反的時候,會有東西叫嗎?
二、逐字比對能做,語意比對不能做,所以把該逐字的部分逐字化。
引用、識別碼、路徑、版本號,這些東西都該存原文並機械比對,而且要能隨時重跑。衍生物不是產出的那一刻壞掉的,是來源後來被改了、而沒有人回頭看衍生物。
我這次就撞到了:正稿在潤,角色卡在另一個資料夾裡靜靜過期。校驗器不懂潤稿改了什麼,但它知道那六個字串已經不存在了。
三、衍生物要標明有效期,不要讓它變成第二來源。
角色卡是快照,快照會過期,這沒問題。有問題的是把快照當成來源、下一份從快照複製。
規矩很簡單:每次都從真正的來源重新生成,不從上一份衍生物複製。
貴一點,但不會累積誤差。而累積誤差是那種你發現的時候已經來不及回頭的東西。
明天 Day 19,把這份角色卡拿去做事:同一張插圖生了九次,兩個坑都是靜默失敗——一條規則被我搬離了它成立的前提,還有一張我從頭到尾沒打開過的立繪。最後救場的不是更長的 prompt。