模組三|AI 生圖與生片產線(Day 10–19)
Day 13 為了繞過內容審核換了模型。今天講換模型留下的爛攤子。
這篇原本要寫的是「我怎麼把不同模型的輸出統一成同一個規格」。寫到一半我去實測了 repo,發現我的處理其實沒有解到根本問題,而且最後被推翻了。所以它變成一篇不太一樣的文章。
我的章節封面規格寫在工作流文件裡,很明確:
每回一張圖,尺寸 1536×1024,3:2 landscape。
原本的模型出這個尺寸沒問題。Day 13 換掉的那個新模型,出來的是:
1264 × 848
比原規格小,而且(這是我今天才算出來的)比例也不對。
1536 × 1024 → ratio = 1.50000 (正好 3:2)
1264 × 848 → ratio = 1.49057 (不是)
當時我只注意到「不夠大」,沒注意到「比例不對」。
處理鏈是三段:
nano banana 出圖 1264 × 848 (標稱 1k)
↓ upscale_image(bytedance,參數指定 2k)
放大結果 3216 × 2160 ← 實出遠超過標稱的 2k
↓ 本地 sips 縮到統一規格
落地 2528 × 1696
因為放大再縮小的畫質,比直接放大到目標尺寸好。
放大模型在補細節,補的解析度愈高,它能塞進去的資訊愈多。然後用單純的重採樣縮小,那些細節會被壓實,邊緣更乾淨。
直接請它放大到 2528,它就只補到 2528 的資訊量。
這在影像處理上叫 supersampling,跟遊戲的超採樣抗鋸齒是同一個原理:在比目標更高的解析度下運算,然後降下來。
這條鏈裡有兩個地方,服務給的標籤跟實際輸出對不上:
| 我要求的 | 實際拿到的 |
|---|---|
| 生成 · 標稱 1k | 1264 × 848 |
放大 · 參數指定 2k |
3216 × 2160 |
2k 通常會讓人以為是 2048 寬。實際出來 3216。
另一次紀錄裡也一樣:同一個生成模型,參數要求 16:9、2k(換算約 1344×768),實出 2752×1536。
兩次都超出標稱值,而且倍率不一樣。
所以「2k」「1k」「high」這種標籤,在不同服務、不同模型之間根本不是同一件事。唯一可信的是實際輸出的像素數。
我後來的習慣是:新接一個模型,第一件事是生一張、量尺寸、記下來,不看文件寫什麼。
寫這篇的時候,我想確認一下最後統一成什麼樣,於是量了所有章節封面:
=== 尺寸分布 ===
38 檔 1536 × 1024
1 檔 2528 × 1696
1 檔 2496 × 1664
38 比 1。
那個 2528×1696 就是上面那條鏈的產物。它現在的檔名是:
novel-ch11-ropehold-original-cute.png
而正式在用的 novel-ch11-ropehold.png,是 1536×1024,跟其他 38 張一樣。
也就是說:我當時費力補出來的那個規格,後來被換掉了,退回原本的 1536×1024。

算一下比例就知道了:
1264 × 848 ratio = 1.49057
2528 × 1696 ratio = 1.49057 ← 正好是 2 倍
1536 × 1024 ratio = 1.50000 ← 其他 38 張
2528 × 1696 是 1264 × 848 的精確兩倍。
放大兩倍保留了所有東西,包括錯的比例。
我以為我在解「不夠大」的問題,所以我把它變大了。**但真正的問題是「比例不對」,而放大對這個問題完全沒有效果。**我做了一整條處理鏈,然後產出一張跟其他 38 張比例不一致的圖。
(順帶一提,放大那一步自己也歪了一點:3216×2160 的比例是 1.48889,如果完全等比應該是 3216×2157.6。放大模型多給了 2.4 px 的高度。這個誤差在縮回去的時候被吃掉了,但它說明連放大都不保證嚴格等比。)

這條鏈裡唯一完全正確的決定,是這個:
檔名沿用
assets/novel-ch11-ropehold.png(HTML 零改動),舊圖存.bak-<日期>。
因為 HTML 引用的是檔名。只要檔名不變,圖片怎麼重做、換幾次模型、走幾道後製,下游一行都不用改。
而這正是為什麼後來要換回 1536×1024 時,成本很低:就是換個檔案而已。
檔名是介面。 把它當成介面來守,你就可以自由地改實作。
代價一:我解錯了問題,而且花了整條鏈的成本。
生成 → 放大 → 縮小,三個步驟、兩次 API 呼叫,換來一張比例不對的圖。
錯的原因很單純:我看到「1264 比 1536 小」,就認定問題是尺寸。 沒有去算比例。
如果第一步就把兩組數字相除,整條鏈根本不需要存在,直接生成或裁切到 3:2 就好。
代價二:日誌記的是當下的決定,不是最終狀態。
開發足跡.md 裡寫著「本日定稿」「封面統一規格 2528×1696」。
那是 2026-07-23 的真實狀況。但後來它被推翻了,而日誌沒有更新。
我今天如果只讀日誌不量檔案,會寫出一篇完全錯誤的文章:告訴讀者我的封面規格是 2528×1696,而實際上 38/40 是 1536×1024。
這是這個專案第三次出現同樣的問題了(Day 9 的狀態表落後 180 回、Day 12 的 json/yaml 無人同步)。「定稿」這兩個字在日誌裡的意思是「當天的定稿」。
代價三:那個 -original-cute 檔名。
被取代的檔案改名成 novel-ch11-ropehold-original-cute.png 留在目錄裡。
沒有任何地方說明它是什麼、為什麼留著、可不可以刪。三個月後我看到它,只會知道它跟 ch11 有關,而且很可愛。
被取代的東西如果要留,留的時候就要寫清楚為什麼。 不然它就是下一個沒人敢刪的東西。
一、規格不符的時候,先把每個維度都算出來,再決定要修哪個。
我看到 1264×848 vs 1536×1024,只比了大小。應該要列的是:
| 維度 | 目標 | 實際 | 差在哪 |
|---|---|---|---|
| 寬 | 1536 | 1264 | 小 |
| 高 | 1024 | 848 | 小 |
| 比例 | 1.5000 | 1.4906 | 不同 |
| 色彩空間 | ? | ? | 沒查 |
列表格會逼你看到你沒在看的維度。 憑印象比較只會比到最顯眼的那一個。
二、外部服務的規格標籤不可信,以實測像素為準。
1k、2k、high、standard 這些標籤沒有跨服務的統一定義,而且我這兩次都是實出超過標稱。
接新模型的第一件事:生一張、量、記下來。文件寫什麼不重要。
三、檔名是介面,把它當介面守。
下游(HTML、設定檔、腳本)引用的是檔名。檔名不變,你就可以任意重做內容,換模型、換流程、換規格,下游零改動。
反過來,每次重做都改檔名,你就是在每次重做時都做一次 breaking change。
搭配一條:舊檔用日期後綴備份(.bak-20260723),不要用「old」「new」「final」這種會迅速失去意義的字。
四、你的工程日誌需要一個「後來被推翻了」的機制。
我的日誌是「新到舊」排列的決策紀錄,這個結構很好。但它缺一件事:沒有辦法表達「這條在三天後被否決了」。
於是舊條目永遠看起來像是有效的。
最低成本的補法:發現某個決定被推翻時,回去在舊條目上加一行「⚠️ 已於 <日期> 被 <新決定> 取代」。一行字,但它讓日誌從「事件流」變成「有狀態的紀錄」。
我準備補上這一行。這篇文章本身就是那個發現的過程。
明天 Day 16,模組三的圖產線收尾,講一個更貴的錯誤:一支腳本的「已存在就跳過」邏輯完全失效,20 個角色 100% 重生。 它不報錯、不 crash、輸出看起來完全正常,唯一的症狀是帳單。