我怎么用Codex从零搭建一个脚本分镜图片生成器

零、效果图


一、为什么做这个工具

做短视频、做动画、做漫画脚本的人,几乎都绕不开一个环节------分镜

传统流程是:编剧写好脚本 → 画师根据文字描述画出每一镜的画面 → 反复修改。这个过程慢、贵、且沟通成本高。

于是我想:能不能让 AI 直接根据每一句分镜描述生成图片?

答案是肯定的。现在的图像生成模型(GPT-Image-2、豆包 Seedream 系列)已经能根据文字描述生成相当可用的画面。我要做的,就是把"输入文字 → 出图 → 保存"这个流程封装成一个简单好用的 Web 工具。


二、需求是怎么一步步清晰的

刚开始我的想法很朴素:"调用 OpenAI 的画图 API,输入提示词,出图保存"。

但真正动手前,我和 AI 助手做了一轮 brainstorming,把需求一点点抠清楚。这个过程很重要,模糊的需求一定会导致返工

第 1 轮:交互形态

命令行还是 Web 页面?

我选了 Web 页面。理由很简单:分镜创作是个视觉活,命令行黑框框看着没感觉,Web 页面能直接预览图片、能下载、能反复调整。

第 2 轮:技术栈

前端用什么框架?

我选了 纯 HTML + 原生 JavaScript。不引任何前端框架。为什么?因为这个工具逻辑极简------一个输入框、一个按钮、一张图。上 React 纯属杀鸡用牛刀,反而增加构建复杂度。原生 JS 一个 IIFE 就搞定了。

后端用 Flask。Python 生态对 API 调用友好,Flask 又是最轻量的 Web 框架,单文件就能跑。

第 3 轮:API Key 怎么管

环境变量还是页面输入?

最初我选了页面输入(方便演示),但很快意识到这有安全隐患。最后改成 .env 配置文件 + python-dotenv 自动加载.env.gitignore 排除,密钥永不进版本库。这是更工程化的做法。

第 4 轮:用哪个模型

这是关键转折点。我一开始说"调 OpenAI 的画图 API",但实际我想用的是 gpt-image-2、gpt-image-2-vip、gpt-image-2-highdoubao-seedream-4-0/4-5/5-0 这几个模型。

这些模型并非 OpenAI 官方直出,而是通过一个第三方代理服务提供的(兼容 OpenAI 接口格式)。这一步确认后,后面所有 API 调用逻辑都围绕第三方的文档来写。

第 5 轮:要不要做视频

豆包 Seedream 系列其实也能生成视频。但我果断说 只做图片

理由:视频生成耗时长(分钟级)、文件大、还要轮询任务状态,复杂度比图片高一个数量级。YAGNI(You Ain't Gonna Need It)------先把图片做稳,视频以后再说。这个决策让整个项目复杂度降了一个档。


三、读懂 API 文档:从同步到异步的踩坑

这是整个项目最关键的技术转折。

我最初按 OpenAI 标准接口写代码:

python 复制代码
resp = requests.post(f"{API_BASE_URL}/v1/images/generations", json=payload, headers=headers)
result = resp.json()
image_url = result["data"][0]["url"]  # 直接拿图

结果发现------ToAPIs 的图像生成是异步的

打开文生图官方文档,返回值长这样:

json 复制代码
{
  "id": "task_img_abc123def456",
  "object": "generation.task",
  "model": "gpt-image-2",
  "status": "queued",
  "progress": 0,
  "created_at": 1703884800
}

注意:第一次请求只返回 task_id 和状态,不返回图片! 你得拿着 task_id 去另一个接口轮询:

bash 复制代码
GET /v1/images/generations/{task_id}

直到 status 变成 completed,才能在 result.data[0].url 拿到真正的图片地址。

文档里还贴心地给了轮询策略建议:

makefile 复制代码
初始等待: 2 秒
轮询间隔: 3 秒
最大等待: 120 秒
典型耗时: 5-30 秒

还有一个坑:生成的图片 URL 有效期只有 24 小时。所以必须在拿到 URL 后立刻下载到本地,不能只存 URL。

于是后端逻辑变成了三步走:

  1. 提交任务 → 拿 task_id
  2. 轮询状态 → 每 3 秒查一次,最多 40 次(约 120 秒)
  3. 下载图片 → 保存到 ./images/,返回本地 URL 给前端
python 复制代码
# 提交任务
resp = requests.post(f"{API_BASE_URL}/v1/images/generations", json=payload, headers=headers, timeout=30)
task_id = resp.json().get("id")

# 轮询
for attempt in range(40):
    time.sleep(3)
    status_resp = requests.get(f"{API_BASE_URL}/v1/images/generations/{task_id}", headers=headers, timeout=30)
    status_data = status_resp.json()

    if status_data.get("status") == "completed":
        image_url = status_data["result"]["data"][0]["url"]
        # 下载并保存
        img_resp = requests.get(image_url, timeout=30)
        filename = f"image_{int(time.time())}_{os.urandom(4).hex()}.png"
        with open(os.path.join(IMAGES_DIR, filename), "wb") as f:
            f.write(img_resp.content)
        return jsonify({"image_url": f"/images/{filename}"})
    elif status_data.get("status") == "failed":
        return jsonify({"error": "图片生成失败"}), 500

另外还有一个模型差异细节:Seedream 系列的分辨率参数要走 metadata.resolution ,而 GPT-Image-2 系列直接用 resolution 顶层字段。后端要根据 model 名字判断走哪条路:

python 复制代码
if model.startswith("doubao-seedream") and resolution:
    payload["metadata"] = {"resolution": resolution}

这种"看文档才发现的细节",是远程 API 集成最耗时的部分。没有捷径,就是读文档 + 试错。


四、前端:从单输入框到多输入框的演进

第一版前端就一个 textarea,用户把所有分镜塞进去,用换行分隔。简单,但不好用:

  • 想删中间某一条得手动挪光标
  • 看不清自己一共写了几条
  • 没法对单条单独操作

于是改成 动态多输入框

  • 默认一个输入框
  • 点「+ 添加提示词」按钮新增一个
  • 每个输入框右上角有「删除」按钮
  • 聚焦时才显示删除按钮,避免界面杂乱

核心就是一段 DOM 操作:

javascript 复制代码
function addPromptGroup(promptText) {
  var group = document.createElement("div");
  group.className = "prompt-group";

  var textarea = document.createElement("textarea");
  textarea.className = "promptInput";
  // ...

  var removeBtn = document.createElement("button");
  removeBtn.textContent = "删除";
  removeBtn.addEventListener("click", function() {
    group.remove();
  });

  group.appendChild(textarea);
  group.appendChild(removeBtn);
  promptsContainer.appendChild(group);
}

收集所有提示词时遍历一遍:

javascript 复制代码
function getPrompts() {
  var inputs = promptsContainer.querySelectorAll(".promptInput");
  var prompts = [];
  for (var i = 0; i < inputs.length; i++) {
    var val = inputs[i].value.trim();
    if (val.length > 0) prompts.push(val);
  }
  return prompts;
}

五、依次生成 vs 并行生成:两种模式的设计

分镜往往一画就是十几张。如果一张一张串行生成,10 张图每张 20 秒就要等 3 分钟。如果并行,理论上 20 秒就能全拿回来。

所以加了「处理模式」下拉框:

依次生成(sequential)

javascript 复制代码
for (var i = 0; i < prompts.length; i++) {
  var img = await generateOne(prompts[i], model, size, resolution);
  appendResult({ prompt: prompts[i], url: img.image_url, index: i });
}

特点:一张完成才生成下一张。好处是省 API 配额、出错好定位、前端能看到逐张出现的过程。坏处是慢。

并行生成(parallel)

javascript 复制代码
var tasks = prompts.map(function(p, i) {
  return generateOne(p, model, size, resolution)
    .then(function(img) {
      appendResult({ prompt: p, url: img.image_url, index: i });
    })
    .catch(function(err) {
      appendResult({ prompt: p, error: err.message, index: i });
    });
});
await Promise.all(tasks);

特点:所有任务同时发出,谁先回来谁先显示。快,但要注意的限流(429 错误)。如果配额吃紧,建议用依次模式。

两种模式都用 try/catch 包裹单张生成,一张失败不影响其他张------这对分镜这种"批量但单张独立"的场景很重要。


六、那些调试中的小坑

坑 1:点击生成按钮没反应

部署完第一次点按钮,毫无响应。打开浏览器控制台,发现:

arduino 复制代码
GET http://127.0.0.1:5000/script.js 404 (NOT FOUND)

原因:Flask 默认静态文件目录是 static/,访问路径必须是 /static/script.js,不是 /script.js。改一行:

html 复制代码
<!-- 错误 -->
<script src="script.js"></script>

<!-- 正确 -->
<script src="/static/script.js"></script>

坑 2:API_BASE_URL 写成了完整路径

.env 里我一开始写了:

bash 复制代码
API_BASE_URL=https://xxxxx.com/v1/images/generations

app.py 里又拼了一遍 /v1/images/generations,导致最终 URL 变成:

bash 复制代码
https://xxxx.com/v1/images/generations/v1/images/generations

404。改成只写根域名:

ini 复制代码
API_BASE_URL=https://xxxxx.com

配置项只放根地址,路径在代码里拼------这是惯例,避免重复。

坑 3:pip 命令找不到

PowerShell 里直接敲 pip install 报 "无法识别"。原因是没激活虚拟环境,且系统 Python 没把 pip 加进 PATH。

解法是用 py -m pip 或激活虚拟环境后用 python -m pip

powershell 复制代码
venv\Scripts\Activate.ps1
pip install -r requirements.txt

七、最终项目结构

bash 复制代码
画图程序/
├── app.py              # Flask 后端:异步任务 + 轮询 + 本地保存
├── static/
│   ├── index.html      # 前端:多输入框 + 参数选择 + 结果展示
│   └── script.js       # 前端逻辑:多输入框管理、依次/并行生成
├── .env                # 配置:API_KEY + API_BASE_URL(不提交)
├── .env.example        # 配置模板
├── requirements.txt    # flask / requests / python-dotenv
├── .gitignore          # 排除 .env / images/ / venv/
├── README.md           # 使用文档
├── venv/               # 虚拟环境
└── images/             # 运行时生成图片存放

启动:

powershell 复制代码
cd "D:\Users\opencode\画图程序"
venv\Scripts\Activate.ps1
python app.py

浏览器打开 http://127.0.0.1:5000,界面长这样:

  • 顶部一个提示词输入框(可点「+ 添加提示词」新增)
  • 模型下拉框(6 个模型可选)
  • 宽高比下拉框(1:1 / 4:3 / 16:9 / 9:16 / 21:9 等)
  • 分辨率下拉框(1K / 2K / 4K,仅 Seedream 系列生效)
  • 处理模式下拉框(依次生成 / 并行生成)
  • 生成按钮

点生成后,每张图独立卡片展示:序号 + 提示词预览 + 图片 + 任务 ID + 单独下载按钮。


八、几个关键设计决策的复盘

决策 1:为什么不用 React/Vue

这个工具前端逻辑总共不到 150 行 JS。引入 React 要配 Babel、Webpack、JSX,构建链比业务逻辑还复杂。技术选型要看场景,不是越新越好。 原生 JS + IIFE 对这种小工具是最佳解。

决策 2:为什么后端要做"下载图片到本地"这一步

选择生成的图片 URL 只有 24 小时有效期。如果前端直接用这个 URL,第二天分镜文件就全裂图了。所以后端必须把图片下载到本地 images/ 目录 ,前端拿的是本地路径 /images/xxx.png,永久有效。

这也是为什么有个 /images/<filename> 路由------专门serve本地图片。

决策 3:为什么用 .env 而不是环境变量

环境变量要每次启动前手动 $env:API_KEY = "...",换台机器就忘。.env 文件跟着项目走(虽然不进 git),改一次永久生效,用 python-dotenv 一行加载:

python 复制代码
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("API_KEY")

开发体验比纯环境变量好太多。

最后修改这个文件,修改这两个变量API_BASE_URL 和 API_KEY为实际值:

决策 4:为什么单张失败不阻塞其他张

分镜生成是"批量但单张独立"的场景------第 3 张失败了,第 4、5、6 张照样该生成。所以每张生成单独 try/catch,失败的那张显示错误信息,成功的照常出图。这个容错设计让工具在弱网或 API 抖动时也不至于全盘崩溃。


九、后续可以加什么

这个版本是 MVP(最小可用版本)。如果要继续打磨,我会考虑:

  1. 批量打包下载 ------ 生成完 10 张图后,一键打包成 zip 下载,省得一张张点
  2. 提示词模板 ------ 内置几套分镜模板(如"电影感""赛博朋克""水墨"),点一下自动填到输入框
  3. 历史记录 ------ 把每次生成的 prompt + 图片路径存到 SQLite,方便回溯
  4. 图生图(reference_images) ------ 上传一张参考图,让 AI 基于它改风格。ToAPIs 文档里这个能力是现成的,前端加个上传组件即可
  5. 进度条 ------ 轮询时把 progress 字段(0-100)回传前端,做个真进度条,而不是干等

十、总结

打开后页面样式

输入提示词后等待样式

生成后结果

这个项目本身不复杂,但有几个点值得新手注意:

  1. 需求要抠清楚再动手 ------ 一轮 brainstorming 省下后面 3 轮返工
  2. 读 API 文档比写代码重要 ------ 同步还是异步、参数走顶层还是 metadata、URL 有效期多久,这些细节不看文档全是坑
  3. 小工具别上重框架 ------ 原生 JS + Flask 单文件,部署简单、改起来快、看代码一目了然
  4. 容错要从单点做 ------ 批量任务里每条单独 try/catch,一条挂了不影响其他
  5. 密钥管理要工程化 ------ .env + .gitignore,别图省事硬编码
相关推荐
七牛开发者1 小时前
Agent 小知识|为什么大家都在聊 Agent?
openai·agent·ai编程
FantasticPerson3 小时前
05 · 第一次使用:跑通第一个例子
ai编程
小林ixn3 小时前
手写AI编程Agent:从0到1复刻Trae/Cursor的核心能力
人工智能·aigc·ai编程
Apifox4 小时前
让 AI Agent 稳定使用 Apifox:新版 CLI + Skill 正式上线!
ai编程·测试·命令行
kyriewen5 小时前
一个看不见的撇号,让Claude Code追踪了中国开发者三个月
前端·ai编程·claude
mpp0075 小时前
特赞 Clipo idea 发布:AI 正在进入社媒内容的“投前决策”环节
低代码·ai编程·ai智能开发
纳兰青华5 小时前
在 PVE 上通过 LXC 部署 Ubuntu 24.04:为 OpenClaw 打造轻量高效的 AI 环境
linux·ai编程
北鸟南游6 小时前
学习-claude code 工程化实战的笔记
前端·ai编程·claude
ZJPRENO6 小时前
Cluade 必备技能 context7
ai编程