1. 为什么我要用 Claude Code 批量做 18 集 STM32 万年历视频
做嵌入式教学视频最磨人的不是讲原理,而是把同一套工程配置、同一批引脚定义、同一组 FFmpeg 命令在 18 集里反复写对。我这次要产出的是一套 STM32 万年历教学视频,主控 STM32F103C8,RTC 用 DS1302,显示用 128×64 HUB75 全彩点阵屏,开发环境 Keil MDK,视频合成走 FFmpeg。18 集从项目演示、硬件清单、原理图、PCB 打样、焊接上电,一直讲到 Keil 建工程、点屏、DS1302 驱动、字库、源码逐讲和故障排查。
如果每集都手写脚本、手配工程、手敲渲染命令,光是核对 PB0 接 R1、PA1 接 CLK 这种接线表就能把人逼疯。我的做法是:把 Claude Code 当成一个“工程骨架生成器 + 脚本批量改写器”,让它按统一模板产出每集的 script.json、Keil 工程骨架和 FFmpeg 合成命令,我只需要在关键参数上做人工校验。这套流程跑下来,18 集脚本和配置的重复劳动被压掉了大半,剩下的时间全花在真机验证上。
这篇文章适合两类人:一是正在做嵌入式系列教程、被重复配置拖慢进度的创作者;二是想用 Claude Code 管理多集工程配置、又不想每集从零手写的开发者。下面我把可复制的配置片段、Keil 工程模板、FFmpeg 渲染命令和逐集验证动作全部摊开讲。
2. 前置准备:TaoToken 接入与 Claude Code 环境
Claude Code 要稳定跑批量脚本生成,得先把它接到一个可用的模型服务上。我用的是 TaoToken 的 Coding Plan,它按编码场景做了额度规划,适合这种“一次生成 18 集脚本 + 反复改写配置”的长会话任务。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api ,注意 API 地址后面不加 UTM 参数。
接入前先在控制台建一个 API Key,控制台地址 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,Key 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。拿到 Key 之后,Claude Code 侧的配置我放在项目根目录的.claude/settings.json里,这样每集子目录都能继承同一套环境变量,不用重复填。
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的Key", "ANTHROPIC_MODEL": "claude-sonnet-4-5" }, "permissions": { "allow": [ "Read(./**)", "Write(./episode_*/**)", "Bash(ffmpeg:*)", "Bash(python:*)" ], "deny": [ "Read(./secrets/**)" ] } }这里有两个点值得说清楚。第一,ANTHROPIC_BASE_URL指向 TaoToken 的 API 基址后,Claude Code 的所有请求都走这个入口,模型名按你订阅里可用的填。第二,permissions.allow我特意放开了Write(./episode_*/**),因为批量生成时 Claude Code 要往 18 个episode_NN目录里写 script.json 和配置骨架,如果权限收得太死,每写一个文件都弹确认,批量就失去意义了。deny里挡掉 secrets 目录,避免 Key 被误读进上下文。
如果你更习惯在终端里直接对话调试,也可以用模型对话页先试 prompt,地址 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,确认生成格式符合预期后再落到脚本里批量跑。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到字段对不上时对着查。
3. 可复制配置:Claude Code 生成 18 集脚本骨架
3.1 用一份 prompt 模板批量产出 script.json
我不让 Claude Code 自由发挥,而是给它一个固定的 script.json schema,再喂每集的标题和要点。schema 长这样:每集拆成若干场景,每个场景有sid、title、narration、duration_hint四个字段。narration是旁白原文,后面 TTS 直接读它;duration_hint是预估秒数,用来给 FFmpeg 切片做初值,实际时长以 TTS 返回的时间轴为准。
# gen_scripts.py —— 批量调用 Claude Code 生成 18 集 script.json import json, subprocess, pathlib EPISODES = [ (1, "项目介绍与成品演示", ["成品外观", "功能演示", "技术栈概览"]), (2, "硬件清单与元器件认识", ["主控STM32F103C8", "DS1302", "HUB75点阵屏"]), (10, "搭建Keil开发环境", ["MDK安装", "Pack Installer", "新建工程"]), (11, "新建STM32工程", ["四文件夹结构", "添加启动文件", "编译通过"]), (12, "主程序与点屏", ["GPIO初始化", "扫描时序", "点亮第一行"]), # ... 其余集同理 ] PROMPT_TMPL = """你是嵌入式教学视频脚本作者。为第{ep}集《{title}》生成 script.json。 要点:{points} 要求:每个要点拆成1-2个场景,narration 用口语化中文,每段80-150字, duration_hint 给整数秒。只输出 JSON,不要解释。""" for ep, title, points in EPISODES: prompt = PROMPT_TMPL.format(ep=ep, title=title, points="、".join(points)) out = subprocess.run( ["claude", "-p", prompt, "--output-format", "json"], capture_output=True, text=True, encoding="utf-8" ) data = json.loads(out.stdout) d = pathlib.Path(f"episode_{ep:02d}/script") d.mkdir(parents=True, exist_ok=True) (d / "script.json").write_text( json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8" )跑完这一轮,18 个episode_NN/script/script.json就齐了。我实测下来,生成速度主要卡在模型响应上,所以脚本里加了--output-format json让输出可解析,避免它夹带“好的,以下是……”这种废话导致json.loads失败。
3.2 Keil 工程骨架模板
18 集里第 10、11、12 集都要讲 Keil 工程,如果每集让 Claude Code 重新描述一遍文件夹结构,很容易出现“这集说 Start/Library/User,下集说 Core/Drivers”的不一致。我的做法是先手写一份权威的工程骨架,存成keil_template.md,再让 Claude Code 基于它改写每集的讲解脚本。
Project.uvprojx # 工程文件,全部相对路径 Start/ startup_stm32f10x_md.s # 启动文件 stm32f10x.h # CMSIS Library/ stm32f10x_gpio.c stm32f10x_rcc.c stm32f10x_tim.c # ... 标准外设库约50个文件 User/ main.c # 帧渲染 + 扫描 + 时间管理 DS1302.c / DS1302.h # RTC 驱动 led_matrix.c / .h # 点阵底层 Delay.c / Delay.h # ms/us 延时 Objects/ # 编译输出,交付时不发关键约束我在模板里写死:Project.uvprojx里所有路径必须是.\Start、.\Library、.\User、.\Objects这种相对路径,不能出现D:\绝对路径,否则拷到别人电脑就编译不过。另外工程要禁用 JTAG、保留 SWD,因为 PB3/PB4/PA15 被 LED 信号占用了,这条我在第 11 集的脚本里让 Claude Code 明确讲出来。
3.3 FFmpeg 合成命令骨架
视频合成这块,我把每集的渲染拆成三步:静态画面编片段、concat 成静音成片、mux 配音加字幕。Claude Code 负责按每集场景数生成对应的命令列表,我负责校验参数。
# 第一步:单张 PNG 编成 N 帧片段(30fps) ffmpeg -y -loop 1 -i scene_01.png -vf fps=30 -frames:v 150 \ -c:v libx264 -pix_fmt yuv420p scene_01.mp4 # 第二步:concat 所有场景片段成静音成片 ffmpeg -y -f concat -safe 0 -i scenes.txt -c copy silent.mp4 # 第三步:mux 配音 + 烧录 ASS 字幕 + 叠加角标 ffmpeg -y -i silent.mp4 -i narration.mp3 \ -filter_complex "[0:v]movie=logo.png[pad];[0:v][pad]overlay=30:30[v]" \ -map "[v]" -map 1:a -c:v libx264 -pix_fmt yuv420p \ -c:a aac -shortest output/第12集.mp4这里有个我踩过的坑:-loop 1 -i img配-frames:v n是对的,但如果你图省事用-stream_loop -1处理图片,再叠加 zoompan,内存会一路涨到 900MB 以上直接卡死。-stream_loop -1只适合视频输入,图片老老实实用-loop 1。
4. 验证请求:逐集跑通脚本与视频产出
配置写完不能直接批量渲染,得先拿一集做端到端验证。我选第 12 集《主程序与点屏》,因为它同时涉及 Keil 编译和 FFmpeg 合成,最能暴露问题。
第一步,验证 Claude Code 生成的 script.json 结构对不对。用一条命令检查所有集的字段完整性:
python -c " import json, glob for f in sorted(glob.glob('episode_*/script/script.json')): d = json.load(open(f, encoding='utf-8')) assert 'scenes' in d, f for s in d['scenes']: for k in ('sid','title','narration','duration_hint'): assert k in s, (f, s.get('sid')) print(f, 'OK', len(d['scenes']), 'scenes') "跑出来 18 集全部 OK,每集场景数在 4 到 9 之间,说明 schema 约束生效了。
第二步,验证 TTS 时间轴。用 edge-tts 合成第 12 集旁白,拿到句级时间戳,汇总成 timeline.json。这里要注意 edge-tts 的offset和duration单位是 100 纳秒,得除以10_000_000换成秒,否则时间轴会差出十万八千里。
import edge_tts, asyncio, json async def synth(text, out_mp3): comm = edge_tts.Communicate(text, "zh-CN-YunxiNeural") words = [] with open(out_mp3, "wb") as f: async for chunk in comm.stream(): if chunk["type"] in ("WordBoundary", "SentenceBoundary"): t = chunk["offset"] / 10_000_000 dur = chunk["duration"] / 10_000_000 words.append({"text": chunk["text"], "start": t, "dur": dur}) elif chunk["type"] == "audio": f.write(chunk["data"]) return words words = asyncio.run(synth("这一集我们来讲主程序怎么点屏。", "ep12.mp3")) print(json.dumps(words, ensure_ascii=False, indent=2))第三步,验证 FFmpeg 合成。先拿一张测试图跑单场景片段,确认-loop 1 -frames:v能出正常时长的 mp4,再跑完整 concat 和 mux。第 12 集成片跑出来 4 分 12 秒,配音、字幕、画面三轨对齐,没有出现字幕提前或滞后。
第四步,验证 Keil 工程能编译。把Project.uvprojx放到纯英文路径D:\STM32\Wannianli下,双击打开点 Build,0 error 0 warning 通过。这一步很关键,因为 Claude Code 生成的路径如果混进了中文或绝对路径,编译会直接报错。
5. 本篇常见错排查
5.1 Claude Code 生成的 script.json 解析失败
最常见的原因是模型在 JSON 外面包了说明文字。解决办法是在 prompt 里明确“只输出 JSON,不要解释”,并且在解析前做一次清洗:找到第一个{和最后一个},截取中间部分再json.loads。如果还是失败,把--output-format json加上,让 CLI 层帮你兜住格式。
5.2 Keil 编译报 “cannot open source input file”
九成是路径问题。检查Project.uvprojx里的 Include Paths 是不是相对路径,工程文件夹是不是放在纯英文路径下。Keil 对非 ASCII 路径兼容很差,桌面路径带中文用户名就会出问题。另外确认Start/下的startup_stm32f10x_md.s有没有被加进工程,启动文件漏了会报一堆符号未定义。
5.3 FFmpeg 报 “Unable to parse ... as image size”
这个错在 Windows 上特别常见,根源是盘符里的冒号C:/被-filter_complex当成参数分隔符了。解决办法有两个:一是subprocess.run(cwd=BASE)把工作目录切到项目根,filter 串里全用相对路径;二是把overlay=文件.png改成movie=文件.png[pad]当源滤镜,再[main][pad]overlay,避免路径直接出现在 overlay 参数里。
5.4 edge-tts 偶发 NoAudioReceived 或卡死
这是限流和网络挂起导致的。加两层保护:外层用asyncio.wait_for(..., timeout=60)包住流式读取,超时就重试;重试用指数退避,sleep(3.0 * attempt),retries=6。另外把 tts_gen.py 做成断点续跑,每个场景成功后写narration/{sid}.words.json,重跑时跳过已有文件,只补失败的场景,避免一次限流导致整集重来。
5.5 字幕被拆行或遮挡画面
字幕里的品牌名如果带空格,可能被 ASS 拆成两行,用不换行空格\u00a0替代普通空格。底部渐隐区从 y=800 到 1080,字幕落在 y≈940-1060,所以正文和徽章的底边不能低于 y≈706,否则会被渐隐和字幕盖住。这个约束我在 gen_assets.py 里写成了断言,画图时直接校验。
6. 后续怎么把这套流程用起来
如果你也想用 Claude Code 批量产出嵌入式教学视频,我的建议是先把“权威模板”定下来——Keil 工程骨架、引脚接线表、FFmpeg 命令模板各一份,再让 Claude Code 基于模板改写每集脚本。模板是锚,模型是笔,锚定得越死,批量产出越稳。
长期跑这种多集编码任务,Coding Plan 的额度规划比按次调用更省心,地址 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。如果你更想先验证模型对 STM32 脚本的理解,可以到模型对话页试几轮 prompt,地址 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。接入细节和字段说明在文档里,地址 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到配置对不上时对着查最快。
最后留一个我实测有效的习惯:每集渲染完,别急着删中间产物。narration/里的 words.json 和scenes/里的片段留着,下一集如果旁白改了,只需要重跑受影响的那几个场景,不用整集重来。这套断点续跑的思路,比任何“一键生成”都更扛得住真实项目里的反复修改。