🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

TL;DR

  • Claude Opus 用於角色設定和CSV格式的劇本製作
  • Nano Banana Pro 用於生成角色圖片、背景和漫畫頁面
  • Python 用於構建CSV驅動的自動生成管道
  • Canva 用於修正錯字(Nano Banana Pro對於日文文本的表現較差)
  • 完成的漫畫已在 LINE Mini App 公開
  • 影片版本可在這裡

完成的漫畫的縮圖

前言

我正在進行個人開發的料理應用程式「CookForYou」。

在開發過程中,我注意到一件事:幾乎沒有系統地教導料理「擺盤」的內容。雖然食譜影片無數,烹飪影片也很多,但解釋「為什麼這種擺盤看起來更美味」的內容卻異常稀少。

專業廚師可以憑感覺來了解,但普通人往往止於「好像無法擺得很有格調...」。

因此,我決定製作一個可以學習擺盤基本概念的內容。不過,僅僅文字解釋是很難被閱讀的。此外,委託專業人士製作漫畫的費用通常高達幾十萬元。因此,我想到了一個 「完全由AI製作漫畫的管道」

為什麼是漫畫

  • 視覺化傳達:料理的擺盤完全依賴「外觀」。透過漫畫展示前後對比,可以一目瞭然。
  • 透過故事來記憶:將「撒黑芝麻會增加高級感」的知識,透過角色對話來傳遞,可以留下深刻印象。
  • 容易在社交媒體上擴散:直式漫畫形式與Instagram和TikTok非常契合。

為什麼現在可以做到

在2024年底至2025年間,Google的圖像生成AI Nano Banana Pro(API名稱: gemini-3-pro-image-preview)的能力得到了顯著提升。

特別重要的是 「提供參考圖片可以保持角色的一致性」 這一功能。這使得可以生成同一角色出現在連貫的漫畫頁面中。

整體架構

重點:先生成設定畫(角色和背景),然後將其作為參考圖片傳遞給Nano Banana Pro,以保持整體漫畫的一致性。

步驟1:角色設計(與Claude Opus協商)

為什麼需要角色設計?

Nano Banana Pro在收到參考圖片後,可以在保持該角色和背景的情況下生成新的場景。這意味著,如果先製作「設定畫」,就可以在整個漫畫中保持角色的一致性

反過來說,如果每次都沒有設定畫,而只是用提示詞指定「黑髮的男性」,那麼每頁的臉型可能都會變化。

因此,首先我與Claude進行了角色設定的討論。設定了一對像「四葉妹妹」那樣的日常風格角色,一位是烹飪新手的男友,另一位是廚藝高超的女友。

角色設定

優太(25歲,男性)

  • 職業:IT工程師
  • 料理技能:幾乎為零(泡麵、炒飯程度)
  • 性格:認真但笨拙,從失敗中學習(「原來如此!」)
  • 服裝:素T恤+運動褲

美奈(24歲,女性)

  • 職業:事務工作(料理是興趣)
  • 料理技能:中至高級,自然能夠擺盤
  • 性格:做事很有條理但不會過於嚴厲,會以優雅的方式指出問題
  • 服裝:簡單的毛衣+圍裙

舞台設定

  • 1LDK公寓(同居三個月)
  • IKEA/無印風格的簡約北歐風室內設計
  • 櫃檯式廚房,雙口IH爐,圓形餐桌

步驟2:生成角色圖片(Nano Banana Pro)

根據角色設定,使用Nano Banana Pro生成了設定畫。

提示詞範例(優太)

簡單漫畫角色設計圖,風格為東浩紀的「四葉妹妹」,25歲的年輕日本男性,短黑髮,圓潤友善的眼睛,柔和的臉部特徵,穿著素灰色T恤和黑色運動褲,站姿並附帶三種表情變化(正常、驚訝帶有汗珠、快樂帶有閃亮效果),全身正中間,白色背景,干淨的線條藝術,平面顏色,溫暖且親和的表情,漫畫插圖,柔和色調

生成結果

生成結果1
生成結果2

背景也同樣生成。

生成結果3
生成結果4

步驟3:CSV驅動的漫畫生成管道

CSV格式

與Claude協商後,決定了以下的CSV格式。

頁面,框,框大小,場景描述,角色,對話,漫畫部分提示,實拍料理提示,備註
1,1,大,優太把和食擺盤成西餐風格,優太,我做了肉燉蘿蔔!放在大盤子上了!,"被攝體:優太站在餐桌前,手拿著一個白色的大圓盤,臉上帶著得意的笑容。構圖:桌子對面優太的上半身。地點:1LDK公寓的餐廳,晚上。風格:四葉妹妹風,彩色,全色彩。","肉燉蘿蔔堆在白色圓盤上,呈現出西餐風格,讓人感到不和諧",導入
1,2,中,美奈微妙的反應,美奈,看起來好像很好吃...但有點不像和食,“被攝體:美奈坐在桌子前,看著盤子,歪著頭。構圖:半身像。風格:四葉妹妹風,彩色。”,,  

重點在於「漫畫部分提示」和「實拍料理提示」的分開。我原本打算提供實拍菜餚的照片來做組合,但這次請Nano Banana Pro製作漫畫風格的料理。

Python 程式碼

tools/generate_image.py

調用Nano Banana Pro API的基本模組。

"""
Nano Banana Pro 圖像生成(支持多圖)

用法:
    python -m tools.generate_image "貓的插圖"
    python -m tools.generate_image "貓的插圖" -o cat.png
    python -m tools.generate_image "貓" "狗" "鳥" -o cat.png dog.png bird.png
    python -m tools.generate_image "風景" --aspect 16:9 --size 2K
"""

import argparse
import sys
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from io import BytesIO

from PIL import Image
from google.genai import types
from google import genai
from retry import retry

def load_and_resize_images(image_paths: list[str | Path], size: tuple[int, int] = (200, 200)) -> list[bytes]:
    """讀取圖像並調整大小,返回字節數據列表

    參數:
        image_paths: 圖像文件的路徑列表
        size: 調整後的大小 (width, height)

    返回:
        調整大小的圖像的字節數據列表
    """
    images = []
    for path in image_paths:
        img = Image.open(path)
        img_resized = img.resize(size, Image.Resampling.LANCZOS)
        buffer = BytesIO()
        img_resized.save(buffer, format="PNG")
        images.append(buffer.getvalue())
    return images

@retry(
    exceptions=Exception,
    tries=5,
    delay=2,
    backoff=2,
    jitter=(0, 1),
)
def generate_image(
    prompt: str,
    aspect_ratio: str = "1:1",
    image_size: str = "1K",
    model: str = "gemini-3-pro-image-preview",
    input_images: list[bytes] | None = None,
) -> dict | None:
    """在Nano Banana Pro中生成圖像

    參數:
        prompt: 生成提示
        aspect_ratio: 圖像比例 ("1:1", "16:9", "9:16", "3:2", "4:3")
        image_size: 解析度 ("1K", "2K", "4K")
        model: 使用的模型名稱
        input_images: 輸入圖像的字節數據列表(可選)

    返回:
        {"data": bytes, "text": str | None, "prompt": str} 失敗時返回 None
    """
    client = genai.Client(vertexai=True, project="your-project", location="global")

    contents = []
    if input_images:
        for img_data in input_images:
            contents.append(types.Part.from_bytes(data=img_data, mime_type="image/png"))
    contents.append(prompt)

    response = client.models.generate_content(
        model=model,
        contents=contents,
        config=types.GenerateContentConfig(
            response_modalities=["TEXT", "IMAGE"],
            image_config=types.ImageConfig(
                aspect_ratio=aspect_ratio,
                image_size=image_size,
            ),
        ),
    )

    result = {"data": None, "text": None, "prompt": prompt}

    for part in response.candidates[0].content.parts:
        if hasattr(part, "inline_data") and part.inline_data:
            result["data"] = part.inline_data.data
        elif hasattr(part, "text") and part.text:
            result["text"] = part.text

    if not result["data"]:
        raise ValueError("生成圖像失敗:未返回圖像數據")

    return result

def generate_images(
    prompts: list[str],
    aspect_ratio: str = "1:1",
    image_size: str = "1K",
    model: str = "gemini-3-pro-image-preview",
    max_workers: int = 3,
) -> list[dict]:
    """同時生成多個圖像

    參數:
        prompts: 生成提示的列表
        aspect_ratio: 圖像比例
        image_size: 解析度
        model: 使用的模型名稱
        max_workers: 並行執行的數量

    返回:
        生成結果的列表 [{ "data": bytes, "text": str | None, "prompt": str}, ...]
    """
    results = []

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        future_to_prompt = {
            executor.submit(generate_image, prompt=prompt, aspect_ratio=aspect_ratio, image_size=image_size, model=model): prompt
            for prompt in prompts
        }

        for future in as_completed(future_to_prompt):
            prompt = future_to_prompt[future]
            try:
                result = future.result()
                results.append(result)
            except Exception as e:
                print(f"生成圖像時發生錯誤 '{prompt}': {e}", file=sys.stderr)
                results.append({"data": None, "text": None, "prompt": prompt, "error": str(e)})

    return results

def save_image(data: bytes, output_path: str) -> str:
    """保存圖像"""
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    Path(output_path).write_bytes(data)
    return output_path

main.py

讀取CSV並生成漫畫頁面的主腳本。

"""
漫畫生成主腳本

從data/base的圖像中讀取,調整為200x200大小,
並基於CSV文件逐頁生成漫畫
"""

import argparse
import csv
import sys
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path

from tqdm import tqdm
from tools.generate_image import generate_image, load_and_resize_images

PROMPT_PREFIX = """請根據這個角色和房子創建漫畫
【重要!!】
時間序列必須順序進行:右上->左上->右下->左下
不要出現左下 -> 右下的情況!!
"""

def load_csv(csv_path: Path) -> dict[int, list[dict]]:
    """讀取CSV文件並按頁面分組

    參數:
        csv_path: CSV文件的路徑

    返回:
        以頁面編號為鍵,頁面信息列表為值的字典
    """
    pages = defaultdict(list)

    with open(csv_path, "r", encoding="utf-8-sig") as f:
        reader = csv.DictReader(f)
        for row in reader:
            page_num = int(row["頁面"].strip())
            pages[page_num].append(row)

    return dict(sorted(pages.items()))

def build_page_prompt(page_data: list[dict]) -> str:
    """根據頁面數據建立提示
    """
    prompt_parts = []

    for i, panel in enumerate(page_data, 1):
        panel_size = panel.get("框大小", "")
        scene = panel.get("場景描述", "")
        character = panel.get("角色", "")
        dialogue = panel.get("對話", "")
        manga_prompt = panel.get("漫畫部分提示", "")

        prompt_parts.append(f"框{i}({panel_size})")

        if scene:
            prompt_parts.append(f"【場景】{scene}")
        if character:
            prompt_parts.append(f"【{character}】")
        if manga_prompt:
            prompt_parts.append(f"【提示】{manga_prompt}")
        if dialogue:
            prompt_parts.append(f"【對話】{dialogue}")

        prompt_parts.append("")

    return "\n".join(prompt_parts)

def main():
    parser = argparse.ArgumentParser(description="從CSV文件生成漫畫")
    parser.add_argument("csv_file", type=Path, help="CSV文件的路徑")
    args = parser.parse_args()

    csv_path = args.csv_file
    base_dir = Path("data/base")

    print(f"正在從 {csv_path} 加載CSV...")
    pages = load_csv(csv_path)
    print(f"找到 {len(pages)} 頁")

    # 讀取參考圖片(角色設定畫、背景等)
    image_files = sorted(base_dir.glob("*.png")) + sorted(base_dir.glob("*.jpg"))
    print(f"加載 {len(image_files)} 張參考圖片...")
    resized_images = load_and_resize_images(image_files, size=(200, 200))

    output_dir = Path("output") / csv_path.stem
    output_dir.mkdir(parents=True, exist_ok=True)

    def generate_page(page_num: int, page_data: list[dict]) -> tuple[int, bool, str]:
        """生成頁面的函數"""
        output_file = output_dir / f"{page_num}.png"

        if output_file.exists():
            return (page_num, False, f"頁面 {page_num}: 已存在,跳過...")

        page_prompt = build_page_prompt(page_data)
        full_prompt = PROMPT_PREFIX + page_prompt

        result = generate_image(
            prompt=full_prompt,
            aspect_ratio="9:16",
            image_size="2K",
            input_images=resized_images,
        )

        output_file.write_bytes(result["data"])
        return (page_num, True, f"頁面 {page_num} 已保存到 {output_file}")

    # 並行生成
    pages_to_generate = [
        (page_num, page_data)
        for page_num, page_data in pages.items()
        if not (output_dir / f"{page_num}.png").exists()
    ]

    print(f"正在並行生成 {len(pages_to_generate)} 頁...")

    with ThreadPoolExecutor(max_workers=3) as executor:
        future_to_page = {
            executor.submit(generate_page, page_num, page_data): page_num
            for page_num, page_data in pages_to_generate
        }

        with tqdm(total=len(pages_to_generate), desc="生成頁面") as pbar:
            for future in as_completed(future_to_page):
                page_num, success, message = future.result()
                pbar.update(1)

    print(f"完成!輸出: {output_dir}")

if __name__ == "__main__":
    main()

convert_to_webp.py

將生成的圖片轉換為WebP格式以優化大小。

"""將PNG/JPG轉換為WebP以減小文件大小
"""

from pathlib import Path
from PIL import Image
from tqdm import tqdm

def convert_to_webp(input_path: Path, output_path: Path, quality: int = 85):
    """將圖像轉換為WebP格式"""
    with Image.open(input_path) as img:
        if img.mode == 'RGBA':
            img.save(output_path, 'WEBP', quality=quality, method=6)
        else:
            img = img.convert('RGB')
            img.save(output_path, 'WEBP', quality=quality, method=6)

def main():
    finished_dir = Path("output")
    compressed_dir = Path("compressed")

    image_extensions = {'.png', '.jpg', '.jpeg', '.PNG', '.JPG', '.JPEG'}

    all_images = []
    for subdir in finished_dir.iterdir():
        if subdir.is_dir():
            for img_file in subdir.iterdir():
                if img_file.suffix in image_extensions:
                    all_images.append((subdir, img_file))

    print(f"找到 {len(all_images)} 張圖片要轉換")

    for subdir, img_file in tqdm(all_images, desc="轉換為WebP"):
        relative_subdir = subdir.relative_to(finished_dir)
        output_subdir = compressed_dir / relative_subdir
        output_subdir.mkdir(parents=True, exist_ok=True)

        output_file = output_subdir / f"{img_file.stem}.webp"

        if not output_file.exists():
            convert_to_webp(img_file, output_file)

    print(f"輸出: {compressed_dir}")

if __name__ == "__main__":
    main()

目錄結構

project/
├── data/
│   └── base/           # 參考圖片(角色設定畫、背景)
│       ├── yuta.png
│       ├── mina.png
│       ├── kitchen.png
│       └── dining.png
├── output/
│   └── chapter1/       # 生成的漫畫頁面
│       ├── 1.png
│       ├── 2.png
│       └── ...
├── compressed/
│   └── chapter1/       # 壓縮為webp格式的漫畫頁面
│       ├── 1.png
│       ├── 2.png
│       └── ...
├── main.py
└── tools/
    └── generate_image.py

生成結果

生成結果1
生成結果2

步驟4:在Canva中修正錯字

Nano Banana Pro的圖像生成非常出色,但在日文文本的生成上有困難

之前

之前

之後

之後

由於「始」旁邊有不明的文字,因此我在Canva中進行了修正。

修正小技巧

  1. 將圖片上傳到Canva
  2. 從「素材」→「圖形」中放置一個白色方框以隱藏錯誤
  3. 用文本工具輸入正確的字詞
  4. 調整字體和位置

這個步驟是手動進行的,但每頁大約需要2-3分鐘即可完成。

步驟5:WebP轉換與網路公開

最後,使用 convert_to_webp.py 進行圖片大小的優化,然後在網路上公開。

python convert_to_webp.py

透過WebP轉換,圖像大小減少了約90%(約6MB → 約600KB)。

學習與挑戰

進步的地方

角色一致性:傳遞參考圖片後,頁面間角色的外觀幾乎統一
CSV驅動的效率提升:通過CSV管理劇本,修正和添加變得更便捷
並行生成:可以同時生成多個頁面,因此1個章節(5-10頁)在幾分鐘內即可完成

挑戰

⚠️ 日文文本:Nano Banana Pro對於日文的文字生成表現較差,必須手動在Canva中修正
⚠️ 框架控制:遵循「右上→左上→右下→左下」的順序主導指令有時會崩潰。即使在提示中強調,有時仍然失序
⚠️ 料理描寫:雖然可以繪製漫畫風格的料理,但其「美味感」不如實拍菜餚

部分公開

前言

前言1
前言2
前言3
前言4
前言5
前言6

色彩篇

色彩篇1
色彩篇2

續集在網路上

第二章(色彩篇)的一部分已在Qiita上公開,但其餘內容可以在LINE Mini App上完全免費閱讀。

📖 擺盤的七大支柱 - 完整版
https://miniapp.line.me/2008548551-A86l0jJv/manga/how-to-plate

希望對對料理擺盤有興趣的朋友和想利用AI製作漫畫的人有所幫助!

職業合作諮詢

我作為自由工作者在AI/LLM相關的開發上工作。

擅長領域:

  • 🤖 LLM應用程式開發(RAG、代理、聊天機器人)
  • 🔧 LINE Bot / LIFF開發
  • 📊 技能抽取・匹配系統(正在開發中)

成果:

  • 對LangChain / LangChainJS / Flowise / Ragas / Dify的OSS貢獻
  • 出席Google Cloud Next Tokyo 2023演講
  • 獲得博士學位(東京大學),PNAS發表

無論是「能不能做這種東西?」這種程度的諮詢都可以,
隨時歡迎私訊我。

隨時歡迎洽詢!💪
(如果能通過多個平台聯繫我會更可靠)

參考鏈接


標籤: #Claude #NanoBananaPro #Gemini #生成AI #Python #個人開發 #漫畫 #圖像生成


原文出處:https://qiita.com/yongyong/items/cabcfb8c91b857cc164f


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝6   💬3   ❤️2
141
🥈
我愛JS
💬1  
6
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付