Podcast Maker
播客制作智能体。素材进,成品出:脚本 → 声音 → 字幕 → 画面 → 产物校验,一条链走完。
定位很明确:播客的重点在听,不在看。所以视频侧只提供固定档位(背景、封面、动画、说话人指示、BGM), 够用、稳定、可复现,不追花哨。真正花力气的地方是脚本、时长、字幕和产物校验。
一、它解决什么
上一代播客工具留下的七个问题,这一版逐条堵死。
| 编号 | 现象 | 根因 | 本版的修法 |
|---|---|---|---|
| 1 | 设了 192 kbps,导出实测只有 160 kbps | 采样率 22050 Hz 属 MPEG-2 LSF,码率上限 160 kbps,编码器静默钳制 | 内置采样率-码率上限表,越界在编码前直接报错 |
| 2 | 字幕在词中间断开(Claude-3|.5) |
逐字符判定西文 token,- 与 . 不匹配 |
改为正则区间判定,token 整体不可切 |
| 3 | 音画漂移数秒 | 用 -shortest 让 ffmpeg 自己决定长度 |
显式传 -t <音频时长>,并对齐断言 |
| 4 | 调语速后音调变尖 | 用采样点插值做变速 | 统一走 atempo,变速不变调 |
| 5 | 字幕不显示中文 | 把字体文件路径填进 ASS 的 Fontname | 传字体族名 + fontsdir,并用真实族名 |
| 6 | 重跑覆盖上一集产物 | 目录名用序号,已存在就往下写 | 时间戳 + 标题 slug,已存在即报错 |
| 7 | 视频里看不出谁在说话 | 无说话人指示 | ASS 矢量绘图层,当前说话方高亮 |
二、功能
按制作的次序列全。每一项都标出它落在哪 —— 目录结构见「九、目录结构」。
| 工序 | 能做什么 | 产物 / 落点 |
|---|---|---|
| 项目 | 立项(三种规划方式之一,选定即定死);节目设定:节目名、副标题、受众、音色、称呼、期号;项目列表与进度;归档(下线,可恢复)与删除(点两下确认,无回收站);未归属产物认领 | 项目目录十个子文件夹、_projects.json |
| 素材 | 导入 md / txt / docx 或粘贴正文;按 # 锚点切章,标题行原样保留;入库即探查(层级归一、同系列归纳、附属页单列);逐单元凝缩——保逻辑的有损压缩,带着行号落盘 |
素材/:index.json 与每份 <sid>.md |
| 期数地图 | 五步排图:读结构 → 定凝缩单元层级 → 逐单元凝缩 → 模型合并与切分(分出几组就是几期)→ 按落点取原文;压比体检与整体重排;产能预检;插入分支(3a / 3b 期号,同一锚点连续插入续用字母) |
地图/ |
| 脚本 | 成稿规划走分段生成(逻辑拆分 → 过桶 → 规划段主旨 → 逐段生成并核账);逐期即兴与单集走整篇生成;逐句估时;形式门禁 12 条;内容检(承诺链必检、语义检可开关);定点修补——只把点名的句子交给模型改;片头尾在整期定稿那一刻逐字粘上 | 脚本/<期号>.json(含逐句实测时长) |
| 声音 | 本地 Qwen3-TTS(按需搭建,音色档案跟着项目走,同一句话的波形可复现)或备选 Edge-TTS;语气指令按素材类型的 emotion_level 档位下发 |
音视频/<期号>.mp3 / .aac |
| 字幕 | 四份同源、同次生成:srt(播放器)、lrc(音频平台歌词位)、txt(整秒)、clean.txt(整秒去掉方括号);断行均衡、不在词中间断开;两档版式(歌词纵滚 / 单行横滚) |
字幕/ |
| 画面 | 背景与封面共用同一个纵向流式排版器;四档动画(静止 / 缓推 / 波形 / 频谱);说话人指示;横竖两版 | 音视频/<期号>.mp4;背景落 背景/,封面落 封面/ |
| 产物校验 | 九步编排;产物阶段门禁 8 条;本期被要求产出什么由 manifest 定(关掉视频就不再要求视频);未过写 blocked.lock.json 留痕,产物照落、照常记账 |
报告/<期号>.manifest.json、报告/<期号>.report.json |
| 运行环境 | 探测 ffmpeg / ffprobe;缺失时给一键安装与自装指引;已就绪时显示实际路径、来源与版本 |
bin/(不进仓库) |
| 界面与后台 | 四个标签页与配置页;批任务(串行、单期失败跳过、连续三期同一原因熔断、软中止);实时日志与进度;生成走后台任务,中途刷新或关页面不丢 | — |
界面上的任何取值范围都由后端下发,前端不硬编码选项。地址栏 hash 就是路由
(#project #script #render #config),可以把某一页直接发给别人。
三、安装
需要 Python 3.11 或更新(py -3.11 或 PATH 上那个 python 都认),以及
ffmpeg(要含 ffprobe)。它不随本仓分发:
启动后到配置页最上面那张「运行环境」卡里会探测,缺了可以点一键安装
(从国内镜像下到项目 bin/,约十几秒,装完当场可用、不用重启);
你也可以自己装,把解出来的 bin 目录放进 PATH 或项目 bin/ 都认。然后:
python -m pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
(Windows 双击 setup.bat 会自动做这一步并起服务。)
| 依赖 | 用途 | 许可证 |
|---|---|---|
| edge-tts | 备选语音引擎(调微软 Edge 的在线语音接口) | LGPL-3.0 |
| Pillow | 背景图与封面绘制 | HPND |
| ffmpeg / ffprobe | 拼接、混音、编码、合成 | LGPL/GPL |
主程序的依赖只有这两个包,其余全部走标准库。本地语音服务那套(torch 等,约 4.8 GB) 不进主程序依赖,它待在自己的环境里,见下。
下载源统一写在 tools/sources.py(PyPI 用清华,内有阿里云 / 腾讯云 / 中科大备选;
PyTorch 的 CUDA 轮子用上海交大,备选官方源)。脚本与依赖清单里各抄了一份,
tests/test_sources.py 盯着几处不许走散。
两条语音路的版权状况不同,差别不在「本地还是云端」:
- Qwen3-TTS(默认):模型权重与代码均为 Apache-2.0(阿里云通义千问团队),
推理全程在本机,不经过任何第三方服务。音色跟着项目走:每个项目立项后在
合成开工前自动录一段 5~8 秒的参考音频(用模型自带的九个合成音色之一当基准,
不是真人录音;文案固定为陈述+疑问+惊叹三句型混合——Base 克隆会整条继承
ref 的韵律先验,混合文案让输出全局韵律更生动,工具自带念全校验防残废音频),
落在项目的「音色」目录里,整期所有句子都以它为音色源头;
换项目互不影响,要与别的项目一致就把那份目录整个复制过来。用 Base 变体 +
ICL 而不是内置音色表,是因为句间音色漂移小一半以上(F0 相对极差 46.9% → 22.5%)。
它那套依赖(PyTorch 等,约 4.8 GB)不进主程序依赖,也不同住一个环境——按需搭建:
配置页把「语音引擎」选成 Qwen3-TTS,那张卡上就会出现环境状态与一个「搭建」按钮,
点一次即可,环境 / 依赖 / 模型都由程序自己装,不需要你预装任何 Python 包。
语气也走这条路:脚本每句的
emotion标签会折成语气指令交给它(只认真情绪, 语篇功能标签不转——「用过渡的语气说」那种指令对模型是纯噪音)。给不给语气、 给到什么程度,由素材类型的范式卡决定(emotion_level):论述类、教程、论文这些 「不写心情」的文体,合成侧整句不发情绪指令——脚本里即便出现「平静」这类词也不发 (它在那一档是被当作「不加修饰」的代码字填进来的,不是心情);小说、散文这类「只到略」的 文体,拼的是「用略带X的语气说」。实测里「明显/非常」两档方向会反转(同一个词在 肯定句上抬高起伏、在中性句上又压低),所以不进产品。档位只有一个来源——写脚本 能填什么、合成拼什么、约束解码的枚举里有几个标签,取的是同一个值;风格倾向里 「情绪密度」那一项也按它发挥:不写心情的文体只谈功能标签怎么分布,允许略情绪才谈 起伏多大。同一句话的波形 是确定的:种子按(文本, 音色标识)派生(克隆时标识是参考音频的内容指纹), 出了问题能照原样复现。 之后不用管它:合成开工前先探一次,没在线就地拉起来,整期(整批)跑完自动停掉、 把显存还回去;已经在跑的服务一律不动。细节见tts_service/README.md。 - Edge-TTS(备选):
edge-tts这个 Python 客户端库是 LGPL-3.0;但它合成的 声音来自微软 Edge 浏览器的在线朗读接口——那不是微软公开授权、发放凭据的 API, 而是浏览器自用通道,条款与可用性都由微软单方面决定;产出的音频同样受微软服务条款约束。 想要权属清晰的声音,用本地那条。
设置页如实显示服务状态;起不来时把原因写进设置页与任务日志,不假装可用。 探活分的是环境没建、依赖没装齐、模型没下、只是没在跑四种状态—— 前三样的动作都在配置页那张卡上(点「搭建」),第四样什么都不用做,合成时会自动拉起。 这四句措辞互不相同,因为把「该去下模型」说成「该去装包」就是让人白忙一场。
素材只支持 md / txt / docx。PDF 不支持:它是出版格式而非记录格式,章节层级 是从源文档投影出来的,要拿回来只能反推,而反推就是猜;而且它的文本顺序是排版 顺序(双栏、页边注、页眉页脚都按坐标给),不是阅读顺序。上传 PDF 会明确提示 「请提供源文件」。
Windows 下可直接双击 setup.bat:杀残留进程 → 检查依赖 → 起服务。
(或者直接双击 _run.bat 跳过检查。)
四、启动
python main.py # 默认 http://127.0.0.1:8811
python main.py --port 8812 # 指定端口
python main.py --check # 只测 LLM 后端连通性
python main.py --voices # 列出可用音色
python main.py --fonts # 列出可用中文字体
python main.py --continue <树根> --episode 3 # 从锁文件恢复续跑第 3 期
--continue 收的是树根(项目目录,或单集的目录),期号由 --episode 给。省略期号
时从锁文件反推,但只有「恰好一期卡在门禁上」才自动定,其余一律把选择权交回来——
猜错了就是拿着另一期的产物当这一期的接着做,而这种错在产物上看不出来。
界面是深色顶栏加白画布的单页,四个标签页按制作顺序排列:
| 标签页 | 管什么 |
|---|---|
| 项目 | 立项(含规划方式)、项目列表与进度、成稿入库、期数地图与插入、期号、未归属产物认领 |
| 脚本 | 归属项目、要生成哪几期、素材导入、锚点切章、风格预设、目标时长、A/B 称呼与语速、生成与门禁结果、把改过的脚本存回本期 |
| 合成 | 归属项目、要合成哪几期(只列有脚本的期)、本期标题(改,不是填)、出片开关、实时日志、产物校验报告 |
| 配置 | 全部可调项,按命名空间自动分成十六张卡片(顺序即工序:写作目标、门禁阈值、片头片尾、项目、语言模型、角色与音色、音频输出、背景音乐、AIGC 标识、画面输出、背景、动画、说话人提示、字幕、封面、字体);列表最上面另有一张独立的「运行环境」卡,管 ffmpeg 的探测与安装 |
脚本与合成两页只摆最常调的几项,其余全部可调项在「配置」页。 声音与画面不是独立标签,它们是脚本与合成的子配置——摆成并列标签会让人以为 那是一个并列的工序,而不是两页各自的参数。
所有控件的取值范围都由后端下发,前端不硬编码任何选项。地址栏 hash 就是路由
(#project #script #render #config),可以直接把某一页发给别人。
项目与期数
一档节目一个项目。立项就是建项目目录——此后素材、地图、脚本、成品各归其位, 都在这个项目的子文件夹里。项目记住节目名、副标题、受众、风格倾向、音色、称呼与期号,出片时自动往后排:
| 场景 | 行为 |
|---|---|
| 计划期数留空 | 由期数地图定:按凝缩与文体做合并与切分,分出几组就是几期 |
| 计划期数填了 | 以人为准,排图照这个数排(超出「地图期数上限」时报错) |
| 压缩档 | 「写作目标」里选 1:3 / 1:5 / 1:10(默认 1:5):一期装多少原文按档位折算,排图照此核对每期分量——口径:1 = 成稿目标(每期时长 × 标准语速,不含偏移),x = 1 × 1.25 × 档位;上限 = 档位 × 1.25,下限 = 1.25 × 1.2(25 分钟一期约 9900 字原文;不足就写不满,只能靠复读凑字数),破线回炉重排 |
| 脚本页选了项目 | 期号自动取项目的「下一期」,生成的脚本直接挂在该项目下 |
| 出片完成 | 产物落了盘就记入项目、下一期号自动进位,门禁过没过不改变这件事(过没过写在报告里,历史列表上标出未过的期) |
| 同一项目再生成 | 自动续接到下一期,不必手填期号 |
| 节目名 / 副标题 / 受众 | 整档节目共用一份,卡片上改一次,往后各期都跟着变:节目名是画面上最大的那行,受众印在片头那句「面向…的听众」里。受众在排地图时由模型给第一版,之后以你填的为准(重排地图不覆盖已填的) |
| 只出一集 | 照样立项——规划方式选「单集」,不排图、出完即完结。没有第二条路可走 |
| 项目设定 vs 全局配置 | 项目优先,且只作用于本次运行,不回写全局 |
节目名、副标题与受众不出现在配置页——它们是项目设定,不是全局默认值。改的地方是项目 卡片上的「节目名 / 副标题 / 受众」。前两项改完立刻反映到画面;受众改的是片头句,要重出 那一期才会变(已经落盘的脚本里那句话是粘好的文字,不回溯)。
项目目录长这样(十个子文件夹在立项那一刻一次建齐):
<输出目录>/
_projects.json 登记表:只存身份与设定
20260912-121051/ 目录名即项目 id
素材/ 地图/ 脚本/ 封面/ 背景/
音视频/ 字幕/ 图文/ 报告/ 过程/
未归属的产物仍单独列出,不自动并入任何项目:归属是人决定的,猜错了比不猜 更糟。这一栏是历史账——磁盘上早先躺着的不归属产物得有人认领;界面上已经不会再产生 新的这类目录(只出一集也是一个项目)。
项目下线有两条路,分得很清,也可以叠加(先归档、再删除):
| 动作 | 做什么 | 产物 |
|---|---|---|
| 归档 | 只翻一个标记,项目从各处列表里下线 | 全部保留,随时「恢复」 |
| 删除 | 登记表条目拔掉,项目目录(素材、地图、脚本、各期成品)整个删掉 | 不留 |
删除要点两下:第一下只是把按钮点亮(六秒没下文自动熄灭),第二下才动手。删掉的是 一整个目录,没有回收站,所以不提供「一键完成」的省事。
一次做多期
脚本与合成是两个页签,就是两步,不是一步。脚本可以批量生成,改好某一期再合上, 然后批量合成——中间随时可以停下来改某一期。
脚本页与合成页的「选期」都是下拉里带勾选,收起时只显示已选几期,不占地方:
| 页签 | 可选的期 | 批量时的行为 |
|---|---|---|
| 脚本 | 该项目的全部期(含还没脚本的) | 串行生成,单期失败跳过 |
| 合成 | 只列有脚本的期 | 读盘上定稿那一份,串行出片,单期失败跳过 |
合成页不列没脚本的期,是为了让「选了一期却没脚本」这个状态不会产生——门禁在入口, 不做事后补救。批量跑的时候:
- 串行。 语音合成与视频渲染吃满机器,并着跑只会互相拖慢。
- 单期失败跳过,失败的期号与原因记下来,重勾一次就能补。
- 连续三期同一个原因失败就停下,后面几期不再空跑(模型后端挂了、合成服务没起这类 系统性问题,跳过等于把同一个错误重复 N 遍)。
- 中止是软的:点「中止」后当前这一期做完就停,不硬杀线程——硬杀会把正写着的文件 截断,半截的清单比没有更坏。
规划方式:立项时定死
立项必须选一种,选定之后不可更改——中途换模式会让已排的地图失去来处。
| 方式 | 适用 | 行为 |
|---|---|---|
| 成稿规划 | 手上有成稿(一本书、一部报题集),要一次排完 | 成稿入库 → 排期数地图 → 按图逐期出片,每期素材由落点自动取。排图之前不进脚本页与合成页的可选集 |
| 逐期即兴 | 每期临时定讲什么 | 每期自己给素材,期号只往后走;立了项就能选 |
| 单集 | 只出一集 | 不排地图,素材当场给,出完即完结(不再有「下一期」)。同样是一个项目:有节目名、有封面、产物落在自己目录 |
三种方式回答的是同一个问题——本期素材从哪来、要不要排图。只出一集也走这条路: 从前它是「不选项目」,产物落进一个没人认领的目录、画面印不出节目名;收编之后流程 只剩一条:立项 → 脚本 → 合成。
本功能上线前立的项目读作「未定(旧项目)」,在项目卡片上补选一次即可, 补选之后同样定死。
受众(片头里「面向…的听众」那一句)是跟着地图来的,所以成稿规划有它,逐期即兴与单集 没有——留空就整段消失,片头退化成「欢迎收听《节目名》。」,其余部分不受影响;要填就上项目 卡片填一次,全档节目共用。
成稿与期数地图
成稿落在项目的 素材/ 下:一份 index.json 记索引,每份素材一个 <sid>.md。画地图分五步,一次长调用变成一批小调用:
三件长活都在后台跑:素材入库即探查、排图、插入建议——全是「模型慢慢算」的活(探查几分钟、排图几十分钟),点了按钮弹窗里就有实时进度与逐条日志(凝缩到第几节一目了然),弹窗可以收起、任务照跑,跑完自动重开面板。同一项目这些活互相排他(它们写的是同一批探查文件),别的项目不受影响。
- 结构从稿子里读出来(
#、一、、Word 标题样式),不喂模型。层级归一、同系列 归纳、附录挑出来单列,每条记下它的起止行与所属章节链——都是可查的事实。 - 定凝缩的单元层级:按这批素材的文体判断哪一级才算一个完整的表达单元(方法论看 论证链、小说看情节单元、采访看话题板块……判据写在范式卡里)。这一步定的是颗粒:选 篇一级,凝缩 39 次;选章一级,就是 179 次。判据不是"标题看起来像不像一个完整话题" ——越具体的标题越像,照着猜只会把层级判细、把单元切碎。
- 逐单元凝缩:一次调用只看一个单元的原文,做的是保逻辑的有损压缩——留下它说清楚了
什么、反对什么、有哪几条主线,以及跟着每条主线的细节区分。几十万字的原文进不了
上下文,几十条凝缩可以——每次的上下文与输出都不随全书体量增长。按哪种文体压,由
范式卡里的
condense一项给出(与排图用的"重点判据"是两个字段,见下)。 凝缩带着位置落盘:压的是原文的哪一块(第几行到第几行)、属于哪一篇,由取料那一步 实测的范围回填,不是另算一遍。 - 模型合并与切分:拿一份凝缩清单(每条带行号范围、所属章节与字数)回答"哪些节合起来 是一期、这一期叫什么、讲什么"。分出几组就是几期——期数由内容结构定,不由字数除。 它不抄标题、不编期号、不算字数,这些都由代码做,所以也就没有"抄错一条落点就取不到料" 这件事。分完做压比体检——每期原文量对照压缩档核对,两个方向各有红线:超过上限 (成稿目标 × 1.25 × 档位)是一期塞不下,低于下限(成稿目标 × 1.25 × 1.2)是料不够——写不满时模型 只能把写过的段落再背一遍凑数。把问题清单喂回去整体重排(凝缩复用,只是重新分组, 最多重试 2 次);重排仍超上限就报错停下,仍低于下限则落警告交人。
- 按落点取原文:生成某一期时取回该期涵盖的单元原文(含子节),不截断—— 照地图上标的行号切,切走的与凝缩时看的是同一块。
两级之间只有「章节标题 + 行号」这一个标识,所以 md 上传时标题行会原样保留:# 是结构锚点,
不是装饰,剥掉之后按章切分就无从谈起。地图长这样:
| 期号 | 标题 | 主旨 | 要点 | 素材落点 |
|---|---|---|---|---|
| 1 | 链与两头 | 判断的两头与中间的链 | 确定性交代码;解释空间给模型 | 书稿.md · 第一章 链与两头 |
| 2 | 代码编期号 | 期号错位看不出来 | 模型编期号会错位且看不出来 | 书稿.md · 第二章 代码编期号 |
期号与落点一律由代码填。 模型只给分组、标题、主旨、要点。落点是「素材 + 标题 + 行号」——同一本书里两节都叫「小结」是常态,只按标题取会取到靠前那一条,而地图上写的 是同一串字,错了也看不出来。
期数不是算出来的。 早先的做法是:总字数除「一期容量(目标时长 × 语速)」得期数, 再把「共 N 期,请正好排出 N 期」写进提示词。素材按逻辑需要 100 期时,那等于命令模型把 超出来的内容无声塞进别的期里,每一期都超载。现在分组的结果就是期数;「地图期数 上限」是排完之后的红线,超了报错要求合并,不做排前截断。排图之前倒有一道产能预检: 每期连下限(素材不足成稿目标 × 1.25 × 1.2)都摊不到的,当场报错拦下,不烧一次注定失败的排图; 素材远多于期数能消化的,警告期数偏少,不拦。
漏掉的单元会被补回来。 模型给的分组经 _clean_groups() 归一成一个不重不漏的划分:
越界与重复的序号丢弃、分组间顺序打结时按首个序号重排、没被任何一期提到的单元补成
独立一期。漏掉的不是"少讲一点",是那几节从此不在任何一期里,而地图看上去完整。
凝缩拿不到就停下。 凝缩结果是分组的依据,没有它排出来的图是按字数硬切的——那种图 看着完整,才是最难的假成功。
凝缩要留住"判断合并所需的东西"。 排图那一步只看凝缩、不看原文,所以凝缩里没有 的信息,排图永远看不到:两节讲同一条论证链的不同环节,凝缩里都写成"讲了 X 的重要性", 模型就看不出它们该合。因此凝缩不设长度上限与条数上限——40 字装不下一条带条件与反例的 主干,而"取前 6 条"那道砍在清单里也要撤掉。
插入与排图是同一条管线,只是容器换了。 画地图是在空根容器里建 1、2、3……,插入是 选定锚点期做父容器,在它下面建 3a、3b……。所以插入走的也是那套「逐单元凝缩 → 序号 分组 → 落点由代码映射 → 压比体检」:新素材的每一节先单独凝缩(已凝缩过的节直接用缓存), 模型只判断插在哪一期之后、把序号分组成期,落点由程序按分到的单元直接取用——抄错落点 这类错从根上不存在了。
插入分支
项目定了 60 期之后又发现另一本书可作佐证,可以把它插进现有计划。插入面板自带上传区: 要插的那份还没入库,就地传(md / txt / docx 或粘贴正文),入库并探查完自动勾上它, 不必退到「成稿」面板传完再回来。已经排进地图的素材会标出「已在地图中」并默认不打勾, 不勾任何素材时后端也只处理还没排进地图的——同一份素材插两遍,期号会产出 3a、3b 而 内容与既有各期重复。模型对照带主旨的已有计划简报定插入点并在锚点下分组建期, 看过、改过再落库:
1、2、3、3a、4、…、21、21a、21b、21c、…、60
分支期号由后端编(branch_no()),锚点不变、同一锚点再插一批续用后面的字母
(3a 3b → 3c 3d),超过 26 个进位成双字母(3z → 3aa)。规则只有这一处,
界面与出片阶段都不再各编一套。落库位置固定在锚点分支链的末尾:先插的批排在前面,
后插的接着往下排,不按字母序乱插。有地图的项目,「下一期」由地图决定——分支期号夹在主期
之间,靠数字进位算不出来(bump_episode("3a") 会得到 "4a",把分支当成了新主线)。
接 LLM
界面里选后端,模型名可以从本机可用模型里挑,也可以直接填一个列表里没有的名字:
python main.py --backend lm-studio --model <模型名>
python main.py --backend ollama --base-url http://127.0.0.1:11434 --model qwen3:8b
生成脚本必须走 LLM。没有可用后端时直接报错中断,不会退化成模板填充。
推理型与普通模型都要能跑,这是客户端的责任。 用哪种模型由使用者自己定,程序不替他 决定要不要思考——所以不会主动传"别思考"这类字段(只留一个透传口)。真跑了推理,日志里 带「思考 N token」,代价看得见。
后端之间的差异在这里处理:明确拒绝某个可选字段(HTTP 400/422)时逐个摘掉重试,摘一个
标一个,字段之间不连坐——次序是「辅助字段先摘、约束解码最后」,response_format 是三者里
最值钱的,不能因为后端不认一个 usage 开关就把它一起丢掉(那种情况下 meta.usage_dropped
标的是 usage,不是约束解码)。超时、5xx、返回非 JSON 不降级——它们与字段无关,摘字段治
不了,当成"字段不认"只会在日志上写下一个假结论、把真实起因丢掉。思考与答案混在同一个
content 里的后端,会剥掉成对的思考段,找 JSON 时逐个花括号试解析并取最后一个成立的
(没带思考标记的后端是"先想后答",第一个成功的多半是思考里的草稿)。
超时判「卡死」,不判「慢」。 对话一律走流式:后端吐一个字收一个字,收到就重置静默 计时。两道闸门分工不同——
| 配置项 | 判的是 | 默认 |
|---|---|---|
llm.idle_timeout |
多久没有下一个字 → 判后端卡死 | 300 秒 |
llm.timeout(总时限) |
整次生成最久允许多久 → 判写得太久 | 3600 秒 |
从前只有一个总时长上限,而且回复要整段等,于是"模型在慢慢写"和"后端已经死了"被同一个数
一起判死:写一期长稿必然撞墙,日志上却写着「后端响应超时」。本机实测(0gm-1.0-35b-a3b
冷加载 + system 1.7k + 素材 20.3k 字)第一个字要等 57.7 秒——预填充本身就要几十秒,
静默时限按最坏情况给,不是按"答一句话要多久"给。
流式中途断掉(没有 [DONE]、也没有 finish_reason)会直接报错并带上已收字数,不把半截
稿子当成品交给下游——否则错误会推迟到「JSON 无法解析」那一步,起因被埋在两个阶段之外。
五、怎么用
三种规划方式只在素材从哪来这一步分岔,脚本、合成、产物三段合流。 每一步的细节在各节里,这一节只给路径与按钮名。
5.1 走完一期(逐期即兴)
| 步 | 在哪 | 做什么 | 结果 |
|---|---|---|---|
| 1 | 项目页 | 点「立项」,规划方式选「逐期即兴」,填节目名(副标题与受众可留空) | 项目目录一次建齐 |
| 2 | 脚本页 | 选归属项目(期号自动取「下一期」),导入本期素材,选风格预设与目标时长 | 素材落 素材/ |
| 3 | 脚本页 | 点「生成脚本」 | 后台任务:弹窗里带实时日志与进度;先写正文,再内容检,最后形式门禁 |
| 4 | 脚本页 | 看门禁结果。有未过项就按点名改,改完点「存回本期」 | 定稿落 脚本/<期号>.json |
| 5 | 合成页 | 选归属项目与要合成的期(只列有脚本的期),确认本期标题,开关出片项,点「开始合成」 | 串行出片,单期失败跳过 |
| 6 | 合成页 | 看产物校验表,点「▶ 试听」 | 产物按类归位,报告落 报告/ |
脚本阶段的未过项不拦人:稿子与问题一起落盘;点合成时再弹一次提醒, 逐条列出,点「仍然合成」才继续。
5.2 一次排完(成稿规划)
| 步 | 在哪 | 做什么 |
|---|---|---|
| 1 | 项目页 | 立项选「成稿规划」。计划期数留空则由地图定,填了以人为准 |
| 2 | 项目页 → 成稿 | 传成稿(md / txt / docx)。入库即探查 |
| 3 | 项目页 | 点「排地图」(已有地图时是「重新排图」)。排完可在卡片上改标题、主旨、要点;动了要点就点「保存并重排地图」;只想改设定不重排,点「只保存(保留旧地图)」 |
| 4 | 脚本页 | 逐期或批量生成脚本。这一路走分段生成,按落点取原文 |
| 5 | 合成页 | 同 5.1 的第 5、6 步 |
排图之前,这个项目不进脚本页与合成页的可选集——期号还没定,选了也无从生成。
5.3 只出一集(单集)
立项选「单集」:不排地图、不设计划期数,素材当场给,出完即完结(不再有「下一期」)。 产物照样落在自己的项目目录里,画面也印得出节目名。
5.4 常见操作
| 想做的事 | 怎么做 |
|---|---|
| 改某一期再重合成 | 脚本页选该期 → 改 → 「存回本期」→ 合成页重新合成。合成读的是盘上定稿那一份 |
| 一期没跑完就断了 | python main.py --continue <树根> --episode <期号>。省略期号时,只有「恰好一期卡在门禁上」才自动定 |
| 中途停下 | 点「中止」:当前这一步做完就停,不硬杀线程——硬杀会把正写着的文件截断 |
| 重出一期 | 合成页重新勾该期,再点「开始合成」 |
| 换音色 | 配置页「角色与音色」卡改;项目卡片上还能按项目覆盖。稿子的时长目标不随音色变 |
| 语音环境没搭 | 配置页「角色与音色」卡上点「搭建本地语音环境」,环境、依赖、模型都由程序自己装 |
| 缺 ffmpeg | 配置页「运行环境」卡会报出来,点「一键安装」下到项目 bin/;也可以自己装,装完点「重新探测」 |
| 改节目名 / 副标题 / 受众 | 项目卡片上改。前两项立刻反映到画面;受众改的是片头句,要重出那一期才变 |
| 项目下线 | 项目卡片上「归档」(全部保留,可「恢复」)或「删除」(登记表条目与项目目录一并抹掉,点两下确认) |
| 查历史产物 | 历史列表点「报告」:文件链接、成片播放器与门禁表 |
5.5 配置页十六张卡片
配置页按命名空间自动分组,卡片顺序就是制作的顺序。
| 卡片 | 管什么 |
|---|---|
| 写作目标 | 决定脚本写多长、写成什么调子 |
| 门禁阈值 | 不达标的脚本直接拦下,不进入合成 |
| 片头片尾 | 片头尾由结构写死,整期定稿那一刻粘上;模型只写正文 |
| 项目 | 品牌行、标语、版权行——每期画面上都印的那几行字 |
| 语言模型 | 脚本由本地或自建模型生成,此处指定用哪一个 |
| 角色与音色 | 两位主持人的称呼、音色与语速。音色随所选引擎切换——同屏只显示当前引擎那一套 |
| 音频输出 | 导出的音频文件参数;码率受采样率的物理上限约束 |
| 背景音乐 | 背景音乐的来源、音量与人声闪避 |
| AIGC 标识 | AI 生成内容的合规标识:元数据隐式标识与片头语音声明 |
| 画面输出 | 画幅、帧率与编码质量 |
| 背景 | 背景配色档位 |
| 动画 | 背景是否运动。播客重点不在画面,默认静止 |
| 说话人提示 | 画面如何区分当前说话人 |
| 字幕 | 字幕版式、字号与边距 |
| 封面 | 封面档位;封面上的字与背景共用同一份,取自项目与配置 |
| 字体 | 画面与字幕各用哪款字体。下拉里每款都按自己的字形渲染 |
列表最上面另有一张独立的「运行环境」卡(不归命名空间),管 ffmpeg 的探测与安装。 脚本页与合成页只摆最常调的几项,其余全部可调项都在这里。
5.6 出问题先看哪里
| 症状 | 先看 |
|---|---|
| 生成脚本报「没有可用后端」 | 配置页「语言模型」卡;python main.py --check 测连通性。生成脚本必须走 LLM,没有后端时直接报错中断,不会退化成模板填充 |
| 合成起不来,日志说语音服务有问题 | 配置页「角色与音色」卡把状态分成四种:环境没建、依赖没装齐、模型没下、只是没在跑。前三种都点「搭建本地语音环境」,第四种什么都不用做——合成时会自动拉起 |
| 一期出片了但校验不过 | 合成页的产物校验表,或 报告/<期号>.report.json。每一项都写明判据与实测值 |
| 界面点了没反应 | 先看服务还在不在(控制台窗口是否关掉)。生成与合成都是后台任务,关掉浏览器不影响任务,重新打开还能看到 |
| 字幕不显示中文 | 配置页「字体」卡换一款带中文字形的字体。缺字形时程序直接报错并点名是哪个字,不会画出方块 |
| 输出码率对不上 | 先核对采样率——码率上限由它决定,越界在编码前直接报错,不会静默钳制 |
| 稿子时长总差一截 | 「总时长偏差」是软门禁,只记账不拦人:真正的时长要等合成出来才算数 |
日志与产物都在盘上。 任务日志随任务落盘,产物按类归位,本期被要求产出什么、
实际产出了什么、当时的配置快照,都在这一期的 manifest.json 与 report.json 里。
界面关了、服务重启了,盘上那份都还在。
六、产物
产物按类归位到项目的子文件夹里,不摊在一个「集目录」中:
| 位置 | 文件 | 说明 |
|---|---|---|
脚本/ |
<期号>.json |
脚本(含逐句实测时长)。落盘定稿那一份,合成读的就是它 |
音视频/ |
<期号>.mp3 / .aac |
成片音频(按配置) |
音视频/ |
<期号>.mp4 |
横屏视频 |
音视频/ |
<期号>_v.mp4 |
竖屏视频(可关) |
字幕/ |
<期号>.srt / .lrc / .txt / <期号>_clean.txt |
四份同一份源、同一次生成:.srt 给播放器,.lrc 给音频平台的歌词位,.txt 整秒,_clean.txt 是整秒去掉时间戳方括号的那一份。进片的 .ass 在 过程/ 里拼 |
图文/ |
<期号>.md |
对话体图文 |
背景/ |
<期号>_bg.png / _bg_v.png |
横竖背景图 |
背景/ |
<期号>_bgm.wav |
内置背景音乐 |
封面/ |
<期号>_cover_16x9|3x4|1x1.png |
封面三尺寸 |
报告/ |
<期号>.manifest.json |
本期被要求产出什么(含视频开关)、产物清单与配置快照 |
报告/ |
<期号>.report.json |
校验报告 |
过程/ |
<期号>/ |
中间件;blocked.lock.json 门禁未通过时出现在这里 |
文件名前缀一律是期号(分支期号 3a 也不会与主期撞名)。
各阶段各自落盘:素材入库即落、地图排完即落、脚本生成即落、出片产物各自归位。 脚本要是不落盘,「先生成、改一改、再合成」这条路就走不通——脚本与合成会被绑成一步。
落盘的那一份是成品:正文 + 片头尾。 片头(「欢迎收听《节目名》,面向…的听众」「本期讲述 期标题,播讲人甲乙」)与片尾(「这里是《节目名》,欢迎关注。」)由程序按模板逐字拼出来, 在所有门禁、内容检与定点修补都做完之后、整期定稿那一刻才粘上去——模型、门禁、修补看到的 自始至终只有正文。这样两件事同时成立:门禁判的是模型真正写的那部分,而片子该有的固定标识 一句不少。
标签序列(片头档位 × 回顾开关,四种):标准档+回顾=开场/回顾/承接/正文…/收束;
标准档=开场/承接/正文…/收束;简档+回顾=开场/回顾/正文…/收束;简档=开场/正文…/收束。
开场只有一个——片头第二句挂的是词表内的中性档「承接」(它是「接开场的话头往下讲本期」,
不是第二个开场)。前期回顾固定在第 2 句(紧跟片头第一句,不分档位):它是「上期讲到哪儿」
的交代,得在开场之后紧接着让人听到,摆到正文后面就成尾声了。开场/回顾/收束是程序专用标签
(PROGRAM_ONLY_TAGS),只在重判与脚本页标签下拉两处出现——盘上稿子里有它们,下拉里
没有的话那一格就没有任何选项能被选中,浏览器会退回显示第一项。
前期回顾是三条,不是一段。 引用的料一物一句:上期标题、期主旨、前三段段主旨各占一句
上期《{上一期的标题}》。
聊的是{上一期的期主旨}。
讲了{上一期的前三段段主旨}等。
三个引用值进位前只剃尾部的句读标点与空白——三条模板各自在句末补自己的句号,不剃尾会拼出 「。、」「。。」;头不剃,中间一个字不动。拼完不再按字数切:从前是把料拼成一整段(实测长到 195 / 241 字)再按正文那把尺切句,切点落在长度上,于是切出过「…收窄至"仅填空"的本质,」下一句以 「的架构迭代」开头这种半句话。句子边界现在由结构决定,最多三句。任一路引用值取不到(第 1 期、 本期不在地图上、上一期没留下段主旨)就三条一句都不粘——留「上期《》。」这种半句比少说难看。
整期只粘一次。 片头尾不进轮:它不参与生成、不过门禁、不被定点修补、不核字数——逐字拼出来 的东西,没有「模型照没照做」可验。所以:
- 中间产物一律是裸正文。 分段生成时每段写完落的是半期正文;整期稿子在门禁那几轮里落的也是 当时那一版正文。中途卡住、被中止、进程被杀,手上那份就如实是「写到这儿的正文」,不会伪装成 一整期。
- 定稿那一次粘,且只粘一次。 门禁过了定稿,轮次用尽也定稿——改成功没改成功,交到手上的那 一份都带片头尾。这一份同时落盘(出片读的是盘上那份)与返回(界面拿的是返回值),两处共用 同一个对象。从前是每轮落盘都粘、同一轮返回时又粘一遍,两轮跑下来日志冒三行「片头尾已粘上」, 读的人分不清是粘重了还是落了几次盘;现在整期一行。
粘过的稿子形态像成品,混进中间产物里就再也分不出哪份写完、哪份没写完——所以粘合点要数得清: 整期一次,粘在定稿那一刻。
是「粘」不是「替换」。 从前那版拿片头句盖掉正文第 1 句、拿片尾句盖掉正文最后 1 句, 句数不变所以门禁看不出来,实际每期正文头尾各丢一句——第 2 句应答的那句、倒数第二句问的那句, 听感上就是「没错」开头、问题悬空。现在只往两头加,正文一句不动、一句不删。
片头里那句受众是节目设定(audience):画地图时问一次——同一档节目各期必须逐字一致,
每期各生成一次必然漂移,那就不是节目的标识了——落进项目,在项目卡片上可改,人填过不覆盖。
单集与逐期即兴没有地图,那一小段自动消失(片头退化成「欢迎收听《节目名》。」),不留
「面向的听众」这种半句被 TTS 字正腔圆地念出来。
产物校验只要求「这一期被要求产出什么」。关掉视频开关后还去要视频,
每一次出片都会判不过;判不过就不占期号,项目进度永远停在原地,
而磁盘上每份产物看着都好好的。所以视频开关写进 manifest.json,
不适用的检查项直接不进报告,而不是留一行「不适用」占位。
七、门禁
共 20 条(生成阶段 12 条、产物阶段 8 条;其中 fail 16、warn 4,另有 soft 1),分 fail 与 warn 两级。
条目里的 stage 不是分类标签,是执行权:生成阶段的条目只在脚本阶段执行,产物阶段的
只在合成阶段执行,没有谁越界去替对方判一遍。产物阶段不过时写 blocked.lock.json 留痕、
照常出片照常记账——产物与报告都落了盘,要不要带着问题出片由人定;过没过写在报告里,批量跑完
时未过的期单独列出来(不会只剩一片「成功」)。warn 在严格模式下同样判不过(同样是留痕、出片、
记账)。脚本阶段不过不拦人
——稿子和问题一起落盘,改稿还是直接出片由人定,合成前只弹一次提醒。此外还有一类 soft:判据本身是估算
值的那些(目前只有总时长偏差一条),不达标只在报告与界面上记「提示·不阻断」,既不拦放行、
也不回灌重写——口径本身只是一个通用语速,拿它把稿子打回去,模型只能围着一个它既测不出、
也控不住的秒数反复改。
严格模式默认开启:宁可标出来未过,不放过一个错项。产物阶段不过照样出片、照样记账, 过没过写在报告里、在历史列表上标出来——盘上有什么、账上是什么、报告里判的是什么,三份一致。
| 阶段 | 门禁 |
|---|---|
| 脚本生成·形式 | JSON 合法、字段完整、同一人连续句数(上限取自素材类型的范式卡)、句长区间、单句时长、情绪标签、情绪档位(不写心情的文体不许出现心情词)、禁用词、可朗读(不含 emoji、图标、不可见字符、网址、命令参数、排版符号) |
| 脚本生成·软项 | 总时长偏差(≤ 15%,只记账不拦人) |
| 脚本生成·内容 | 承诺链检(LLM,必检);语义检(LLM,配置开关、默认关)。两段串行:形式门禁成环 → 内容检成环;不过就定点修补,检不出句号的记「未修好」 |
| 产物 | 音画时长差、实测码率、实测采样率、断词率、字体族名、背景水印、产物完整性、封面三尺寸 |
产物阶段的「产物完整性」与「音画时长差」只在要求视频时才成立;关掉视频开关后 这两项不适用,直接不进报告。要求一个从没被要求的东西,等于让每一期都判不过。
片头尾没有门禁,因为它不在这份稿子里。 它由程序在整期定稿那一刻逐字粘上(见「六、产物」), 模型从头到尾没参与,也就没有「它照没照做」可验。硬留一条判据只有两种下场:恒真(白占一格), 或者拿正文去验首末句、每轮报「未命中」,然后让模型去改一句它从来没写过的句子。 同理,语义检的判据范围就是呈上去的整份正文,不必再为片头尾单开豁免——判据范围与结构要求 冲突时,错的是判据。
粘合句进不了逐句判据,是同一条理由的另一面。 片头/前期回顾/片尾带着程序专用标签
(PROGRAM_ONLY_TAGS)出现在稿子里(脚本页重判读的就是盘上成品整份、含片头尾),门禁按标签
认出它们之后,句长、单句时长、禁用词、可朗读、句尾标点、语篇词表这六项一律跳过;连说账上它们
还当分隔符——前后两段正文各算各的,不因为它被算成「连着说」。整篇量照算(句数、总时长):
粘合句确实在稿子里、确实要念,报告里注明跳过了几句。
脚本的事归脚本,合成的事归合成。 两个阶段各判各的、各落各的,合成端不碰脚本的账:
| 脚本阶段 | 合成阶段 | |
|---|---|---|
| 判什么 | 生成阶段那 12 条(形式项 + 总时长软项 + 内容检) | 产物阶段那 8 条 |
| 结论落点 | 过程/<期号>/gate.generate.json |
报告/<期号>.report.json |
| 不过怎么办 | 定点修补:只把点名的句子交给模型改,全篇行数一个字不动;结构坏了才整篇重出;指不出句号的交人工。修满轮次仍未过就把这一版稿子连同结论一起落盘,阶段到此为止 | 写 blocked.lock.json 留痕(--continue 靠它认期),产物落盘即出片、即记账;过没过写进报告,界面在历史列表标出未过的期 |
| 越界防线 | — | 不写稿(没稿子就报错,不现写一份)、不判稿(不拿生成阶段的条目再审一遍) |
两者之间只有一次提醒:点合成时读那份脚本阶段的结论,有未通过项就弹框逐条列出,
人点「仍然合成」才继续,并在清单里留一个 script_issues_ack 作凭证。提醒不是闸门——
不点确认只是不发请求,不是判它过了。框里写明这份记录产生于脚本生成那一刻,不追踪此后
的人工修改:改过没有以手上的稿子为准,不为了把它算准而再跑一遍。
总时长偏差留在生成阶段,但按软门禁办:这一阶段才看得见它,可它判的是估算值——真正的 时长要等合成出来才算数,口径本身只是一个通用语速。不达标只记账,不拿它打回重写。
估时长用一把与音色无关的尺子。 脚本阶段的口径是标准语速 4.39 有效字/秒(≈256 汉字/分钟,锚在国家语委《普通话水平测试实施纲要》的正常语速与朗读口径之间)。目标字数 反推、逐句估时、总时长门禁、容量反推都用它——稿子的时长目标不随「这一期换了哪个音色」变, 一份稿子在哪里估都是同一个数。音色之间快慢的差异换算成一个比例,挂在配置页每个音色下拉 框下面(音色语速 ÷ 标准语速:0.91 即比标准慢,1.00 即同速),没有实测的音色照实写 「尚无实测」,不拿 1.00 冒充量过的数。
措辞禁忌为什么必须在提示词里先说。 只在生成完之后扫描命中,模型事先不知道要避开 什么,命中与否全看运气,几轮重试就是几次抽签。所以词表连同「换成什么」一起前置进系统 提示词;回灌时再给到句——第几句、命中了哪个词、改用什么,并明确「只改这几句,其余照抄」。
定点修补那一侧同样要前置。 只把命中句与替换词回灌给修补模型是不够的:它改这一句时 会连带写进另一个同样说满的词——实测里模型补长一句就写进了「毫无」,回门禁当场被拦。 所以修补的系统提示词带的是同一份禁忌表(同一个渲染结果,不另存一份),回门禁那一道只当兜底。
改一句不该重打一遍。 第 1 轮整篇写;之后能定点就定点——只把门禁点名的句子交给模型,
输出是一份补丁({edits: [{index, text, emotion, absorb?}]}),回来按句号原地替换。
行数默认一个字不动——这是这条路的根基:门禁报的「第 N 句」在补丁前后指的是同一行,
一旦允许增删,编号全漂,就得回头重编一遍。唯一的例外是并句 absorb:门禁点名
「同一人连着说超限」时,把这一段并进第一句(absorb 是并掉的句数),行数因此变少。
并句的字数判据是一个恒非空的区间。 从前判据要「并后 ≥ 原句合计 × 70%」,提示词又硬性
要求「每句 ≤ 40 字」——原句合计 > 57 字时两条同时满足是不可能的,而 70% 这条一个字都没进
提示词。真机上并句因此一次没成功过(当时本机 A 捧哏上限 1 句,「并成 1 句」正是最易无解的
情形,第 2 期 15 处全没修好;v0.38.0 起放宽为 2)。现在写成区间:常规 [单句上限 × 70%, 单句上限](本机
28~40);原句合计连 28 都不到时退成 [原句合计 × 70%, 原句合计]。区间恒非空,且
上限必然 ≤ 单句上限——并出来的句子不会再被句长门禁打回。判据与处方调同一个函数
(absorb_span),处方里直接写给模型「每句 28~40 字」这种具体数字,不是只给一句原则。
「不许凭空增删、不许换人」不靠提示词劝,靠补丁的结构里写不出来——没有「加一句」,
「删一句」只有 absorb 这一种表达且只给超限用,说话人根本不在 schema 里:把 A 的
一句问话翻给 B,就成了 B 自己问自己,那是拿格式代替语义(靠硬翻说话人的那个后处理
enforce_max_run 已在 v0.34.0 取下)。片头尾仍由 glue_intro_outro 在整期定稿
那一刻粘上,不进补丁。
另外,补丁按句号改句子,而句号只在正文那一份上算得准——所以循环里跑的一直是正文,
粘好的成品只出现在定稿那一刻(generate._finish 里的 _glued_draft),中间产物全是裸正文。
补丁只给被点名句与它前后各两句,不给全篇。这不是省字那么简单,是实测出来的:把全篇 220 句一起递过去,模型三次里有两次直接不吐 JSON(被上下文带跑,改写起解释性文字);只给 窗口则三次全中。纯形式项(措辞、句长)不带素材,内容项(语义、承诺链)才带——改「编造」 得知道素材支持什么,改「太长」只跟这一句自己的字面有关。
哪些问题能定点,只看它带不带句号。 形式门禁全都自带(它本来就是逐句判的);内容检要 模型自己给,给了才算。分成三类各有各的去处:带句号的定点改;结构坏了(JSON 不合法、字段 残缺)只能整篇重出;内容检自己指不出句号的——不动稿子,交人工。让模型为一句它自己都 说不清的问题重写两百句,只是重新摇一次骰子,还会把已经好的句子一起改坏。
每条提示词都按块分类。 一个推进点位牵扯的资料不止一种:要处理的正文、判断用的参考、 上一步的产物,以及这一步当前该产出什么。全堆在一起,模型只能靠语序猜哪块是依据。所以 每一块都自带名字与用途——块名用【】框起(【素材】【已有计划】【待审脚本】),括注说清它 怎么用(「判断依据」「当前任务:按这里逐条改」)。风格、背景这类与具体内容无关的块,在 括注里写明「以当前任务为准」:冲突时让位的必须是它们。
贵的东西按需跑,便宜的东西每轮全跑。 定点修补之后,形式门禁(走代码、毫秒级)每轮 全套重跑——改一句话的字数会牵动单句时长与总时长,改一个用词可能撞上新的禁用词,只重跑 被触发的那一项会放走改出来的新毛病。内容检(走模型、要读整篇)只重判上一轮没过或没判过 的那几项,已经判通过的那一项本轮不重喂,结论从上一轮带过来——不带走的话报告里会凭空少 一行,「这一项检查通过」的记录就丢了。
每轮写完就落盘。 生成一轮要十几分钟,从前等整段跑完才第一次写盘,中途卡住、被中止、 进程被起停脚本杀掉,手上就什么都没有——稿子明明已经写好了。现在每轮定型完就往出片时读的 那个位置写一份,落的是「当前最新的那一版」,与最终返回的那版一致。
内容检为什么挂在脚本生成阶段。 语义检(台词有没有编造素材之外的东西)与承诺链检 (开头开的那个口子收没收),判断依据都在语义层,词汇匹配做不了——对话体前后用词不同 本来就是正常的,拿重叠率去判是拿错了工具。所以这两项交给模型。
位置比判法更要紧:它接在脚本生成里,不通过就带着检出的落点去定点修补。等音频视频都
渲染完再检,片子已经定死,检出来也改不动。脚本生成是三段串行(v0.36.0 定序):
出货(写正文,只求一份可用稿子;产物不可用按 script.segment_parse_rounds 重试)→
检查(内容检,修到过或修满 script.check_rounds 都往下走)→ 门禁(形式门禁,
修到过或修满 script.gate_rounds 就落盘定稿)。修满仍不过不空转,带着问题交人。门禁排
最后是因为它每轮重新判形式、并把内容检的结论并回同一份报告——形式结论永远对着最终稿。
三段内部没有重写——重写只属于出货段,那是「写」的事,不是「判」和「改」的事;
补丁则是逐条落地:单条不合格只拒该条(原因递回下一轮提示词),其余照落,不像从前
一条坏就整份作废、一轮十几分钟作废。语义检要逐句核素材、装不下还得分批,是全链最贵的
一项,所以做成了配置开关、默认关(script.check_semantic);承诺链检必检——
它不看素材、只看脚本自己,一次调用判完。
内容检的输出必须带落点,两块形状不同。 语义项是 {line, quote, problem};承诺项是
{line, promise, how, speaker, close_after}——承诺提在哪一句、承诺是什么、怎样才算闭合、
这句闭合的话由谁来说、建议插在第几句之后。这不是加个字段,是定点修补的前置条件——
检查只说「整篇偏题」而不说哪一句,补丁就无处可下。
落点不许是 0。 契约把 line 收紧为「≥1 且不超总句数」,提示词也不再教模型填 0,
而要求它填其中最该改的那一句。模型仍给不出句号时,程序拿 quote 去稿子反查(去空白后
比对子串,不花模型调用);反查不到就记一笔「未修好」,不早退、不重写。承诺项的修法是
插入:补丁新增 inserts 形态,在建议位置插一句带说话人的新话——旧契约不许增行,
逼得模型去删承诺句,问题在报告上消失、稿子一字未动。
模型没给结论,不等于检查通过。 模型漏返回某一项时,那一项记为「未判定」落进待复核, 不默认放行——把「没检查」记成「检查通过」,报告上的绿灯比不检更坏。 判定为不通过的才参与放行(语义检 fail 级、承诺链 warn 级)。
审校的输出预算与写作同一条口径。 取全局 llm.max_tokens,不另设一个小值:
推理型模型把思考过程也算进这份额度,给小了就是思考吃完、答案一个字不剩,
两项检查一同落成「未判定」。它同时是输出上限,服务端校验「输入 + 上限 ≤ 上下文」,
所以该值要落在模型实际加载的上下文之内——模型自身上限与它无关:
加载时给 8192,配置里写 48640,请求要么被拒(HTTP 400 上下文超限),
要么一路生成到塞满。
能喂多少字由输入额度算出来,不由常数定。 内容检与定点修补从前都把素材截到前 6000 字—— 生产里一期素材约 2 万字,只喂进 30%;落在后面的依据在模型眼里就是「不存在」,语义检于是 把本来正确的句子判成编造,再交给定点修补把它改坏。
三把尺各管各的,别用一个数兼两件事。 「喂多少字」里面藏着两个不同的问题,从前混成 一个,于是两个不同单位的数被拿来做减法再比大小——一期 6.4 万字符的技术文档在「朗读字数」 那把尺下压比合格,到写作侧被判「超出容量」,原文一个字没进提示词:
| 层 | 回答什么 | 尺 | 不合格怎么办 |
|---|---|---|---|
| 画地图 | 这一期该讲多少料 | 压比 / 朗读字数(成稿目标 × 1.25 × 压缩档,与排图体检同一把尺) | 重排;两轮仍超则报错要求拆期 |
| 单次调用 | 这一次能装多少 | token:输入额度 = llm.max_tokens × llm.input_ratio |
调倍率,或回地图拆期(后端窗口由你自己开,程序不探也不管) |
| 划分段 | 这些料要分几次喂完 | 逻辑拆分(模型按内容分组,只回节序号)+ 装箱(纯算术,逐段过额度) | 按整节切,绝不丢原文;单节自己就超容则报错要求调倍率 / 拆期 |
画地图那个数可以往下透传,但只当换算用——透传下去的是「本期成稿目标字数」,拿去摊
段配额;它不参与「这次装不装得下」的判断。倍率调大调小,地图一个字节不动;改时长改
档位,倍率也不用动。后端窗口不归程序管——够不够自己调,程序既不探也不提要求;
任务开始时日志只报这次准备喂多少(单次额度 = 最大输出 × 倍率、素材折成多少 token),
放不下就切段喂完,原文一字不丢。字符与 token 的折合比由后端回传的
prompt_tokens 反标(取中位数),不写死经验值——中文、数字、标点的折合比差得远;
没有样本时按 1 字 = 1 token 顶格算,折算只准偏保守。
放不下的时候,切段而不是砍料。 写作先由模型按内容把节分组成逻辑段,再让桶逐个
逻辑段过输入额度:装得下就是一段,装不下在组内按整节切成几段(见下一段)——
最终真正分别喂给模型的段叫写作段,一个逻辑段至少落成一个写作段,所以写作段数 ≥
逻辑段数(日志那行「N 个逻辑段 → M 个写作段」就是这两个数);
段内若仍装不下(折算比有波动、或已写正文比配额长了一截),把那几节按整节分批分几次
喂完——不丢原文、也不换成凝缩。只有装箱覆盖不到的边角才退到有损降级:可用额度已
用光、或拿不到逐节原文时改用凝缩(每节逻辑骨架)顶替,连凝缩都没有(逐期即兴没排过图)
才按额度截原文;两者都是有损的,日志与素材头部都写明。内容检则分批核:每批能装
多少由输入额度折回来(budget_chars_for_check),按它切段、相邻批重叠 1000 字、每批带
整篇脚本(脚本不切片——句与素材段之间没有可靠映射,硬切只会制造新误判),合并规则只有
一句——同一句要在每一批里都被报出来才算真问题(某句的依据只落在某一批里,那一批当然
找不到它,撤销它才对;每一批都找不到,才等价于全文里找不到)。批数超过 8 批判「未核」
交人工,不把没核的那部分记成「核过且没问题」。
成稿规划走分段生成,治字数漂移。 整篇一次生成时模型没有全局计数器,实测同一管线 一期超目标 4 倍、另一期只写六成。成稿规划的项目有地图与各节凝缩,脚本这一步走三步:
- 逻辑拆分——模型按内容把节分组成段(哪几节讲的是同一件事),只回节序号; 递给它的清单只给「节号 + 标题 + 凝缩」,一个体量数字都不给(分组看语义,摆着 字数会把它往"按字数摊匀"上带)。分组过三道归一:越界/重复丢掉、按序号排序 (不许重排,讲述顺序由地图定死)、漏掉的节补成独立一段。两次答不出合法分组 就退回「一节一段」。
- 逐段过桶——桶逐个逻辑段过输入额度(装得下就是一段、装不下在组内按整节切成 几段);两个逻辑段之间是话题转折,绝不并进同一段。切分单位只有整节凝缩:单个节 自己就超额度时报错要求调倍率或拆期,不再从节中间切——半个节取不出料、也算不出账。
- 规划轮写段主旨——给每段写一句段主旨(与期主旨同层级:这一段讲的是什么事), 并给全篇起标题。段数钉死,不能增删段、也不许写"从哪里起到哪里止"。这一轮同样 不看字数——段边界是程序定的、配额是程序摊的,字数摆进来只会把"这一段讲什么" 带成"这一段该多长"。
然后逐段生成、逐段按字数核账(容差 = max(配额 × 15%, 地板 × 15%),与总时长门禁同一把尺), 差额滚入下段配额——误差逐段吸收,总账咬住目标。每段只喂本段那几节的原文,不再把整期素材 过一遍闸门。超差不重写整段、也不改已有句子,只按字数增删(少了插句、多了压删),而这两条 路的落地校验都不判句长——句长是全篇一把尺的量,归门禁判、两个方向都有处方;写作阶段替它 把关,代价是把「一条坏句」放大成「整段停摆」(真机一条 7 字句让 40+ 条新增整批作废,那一段的 缺口 1872 字一轮都没再试)。补丁落地失败也只作废那一轮(稿子一字不动、轮次照减),不收工。
每轮的顺序是「补字 → 查重 → 替换 → 计算」。 第一轮只写作、轮末算一次账;之后每轮从 上一轮末的差额决定补字还是压字,补完立刻查重、就地替换,替换完再重新核账。「计算」挪到 轮末是本轮收口,差额供下一轮开头补字用。补字是复读的源头——第 2 期补出 33 句逐字复读,就是 因为补字轮看不到本段以外的正文却要它「挑正文还没讲到的点」;现在补字轮也带上全篇已写 正文(与首写轮视野对齐),补完马上查、当场换。
查重判整篇,动刀只动本段。 重复只能按句指认,而「段 2 复读段 1」这种只有整篇范围
看得见——所以 find_repeats 拿整篇句列表判、只报本段内后出现的副本;apply_replace 换成
新句时行数一个字不变(没有并句、没有插入),并且新句必须不短于被替换那句、不长于单句
上限——换短了核账又报缺口,下一轮又补、又可能复读,绕回老路。替换提示词把事情说清楚:问题
发生在本段、原文素材也只给本段,但要看得到所有已写的——【本段原文】与【已写脚本】(全篇
正文,逐句带编号)分开摆。
段配额全是加法:段配额 = 段内各节配额之和、段 token = 段内各节 token 之和、段原文 = 段内各节原文按序拼。从前那套「块配额 = 整节配额 × 块字符 / 整节字符」的除法连同它的误差 一起消失——配额一把尺、内容另一把尺,两边不同源时数字必然对不上。
配额摊完还有一道地板,但它只防一件事:提示词里那句「约 N 句」不许与配额矛盾—— 「约 N 句」带一个软句数下限,配额低于「N 句 × 每句最少字」时,提示词就一边要模型写 15 句、 一边只给它不足 120 字的额度。所以地板取的是「软句数下限 × 每句最少字」(本机 120), 不是碎段合并那把「× 期望句长」(本机 360)的产能尺——那把尺判的是「这一段的料够不够 单开一段」,拿它当地板等于把按料摊好的配额抬高三倍,同一期里这几段偷偷换了压比。
段容差的下限就是这根地板的派生量(本机 120 × 15% = 18 字)。从前它是一个独立常数(60), 没有算式出处、还让「各段容差之和 = 全篇容差」这笔账对不上;改用地板的派生量后,全篇各段容差 之和恰是 15% × 全篇配额(配额本身低于地板的段除外),账自洽。
配额权重是「这一节的原文有效字」(凝缩那一步算出的 chars)——单位必须与成稿目标
同源:成稿目标出自 chars_for_target(时长 × 速度 × STANDARD_K,STANDARD_K 的单位
是有效字/秒),地图压比也是拿期素材的有效字比成稿目标,三处同一把尺,配额的占比才和
压比可比。
三个数各有各的用途,别混:
| 量 | 单位 | 归谁 |
|---|---|---|
原文有效字(chars) |
有效字 | 配额权重——这一节能写出多少稿 |
原始字符数(loc.chars / 取到的原文长度) |
字符 | 装箱折 token——这一节占多少输入(排版符号与西文数字也在里面,它们念不成本) |
| 摘要自己的字数(gist + points) | 字符 | 与料无关,已无消费者 |
用错尺的后果是同一件事:那一节被派了它写不出的字数,只能把刚写过的话换个说法再讲一遍。 第 2 期实测——按摘要字数摊,段 2 被多派 776 字、压比压到 1.50 贴死下限,去重删掉 64 句; 改按原始字符数摊仍不齐;只有按有效字摊,两段压比才同时等于整期的 2.09。
路线由项目模式定死,不是一个开关:成稿规划(mapped)走分段——有地图与凝缩,逐期 配额、防重复才有依据;逐期即兴与单集走整篇——当场给料、出完即止,没有凝缩可分。成稿 规划万一本期凝缩读不出来(还没排图、版本对不上),也退回整篇照常出稿,不让人卡在 「一步跑不动」上。
无论走哪条路,成稿最后都过一遍程序硬去重,但它是最后兜底,不是主力(v0.37.0): 分段路现在靠「补字 → 查重 → 替换」当场把重复换掉,走到这一刀时还剩重复才删。 判据是归一化(去空白与标点)后完全同字、且 ≥ 12 字,措辞相近的拦不住——那是改写,得靠 提示词那三条纪律。删了会记一笔:照实说删了几句、本期压比多少、缺口多少字,不再推断 「这说明素材撑不满目标时长」——第 2 期料是目标的 2.09 倍,把责任推给素材方向是错的。
其余全部 fail-closed:不过就照实报,不伪造一份「看着对」的结果——产物阶段的门禁不过写
blocked.lock.json 留痕、照常出片照常记账(过没过写进报告,历史列表上标出来),脚本阶段的
门禁不过把稿子连同结论一起落盘。
门禁未过时,被拒的脚本与逐项明细会一并落盘(script.rejected.json、gate.generate.json),
避免只留一句「门禁未通过」而无从排查。
八、排版与画面
排版是流式的,不是摆坐标。 背景与封面共用同一个纵向流式执行器:每个元素的位置
由上一个元素的实测包围盒推出来,间距一律写成字号的倍数,集中在 LAYOUT 一张表里。
这条约束是有来历的。曾经用「主标题位置加一百一十八像素」表示标题下方框线的位置, 而字号一百零八的字实测高约一百三十——线正好横穿标题字。这类缺陷调数值调不好: 换字号会穿、换画幅会穿、换字体还是会穿。所以位置只能由实测包围盒推出, 间距只能取自规格表,序列里禁止就地写数值,这条有测试守着。
每个元素落下的实测区间会被记录,测试据此断言相邻元素不压叠、框线始终在主标题之下。
最大的一行是节目名,不是期标题。 自上而下的层级固定为:
主标题 = 节目名(项目) ← 最大
副标题(项目,可留空)
期标题(地图自动取) ← 只在背景上
期数(项目进度) ← 只在背景上
标语(全局配置,可留空)
版权行(全局配置) ← 最小
播客卖的是系列品牌,不是这一期讲什么。期标题字数不定,长起来在最大档只能折行,
一期一个样,摆上去就散。字号表按角色命名(hero / second / episode)而不是
按位置,因为主标题换成节目名之后,f_big / f_mid 这种叫法没人分得清。
画面上印哪几行,出处只有一张表。 FRAME_LINES 一行一项,写明它跟谁走、值从
哪来、印在哪张画面上:
| 行 | 跟谁 | 封面 | 背景 |
|---|---|---|---|
| 品牌行 | 全局配置 | 印 | 印 |
| 主标题(节目名) | 项目 | 印 | 印 |
| 副标题 | 项目 | 印 | 印 |
| 标语 | 全局配置 | 印 | 印 |
| 版权行 | 全局配置 | 印 | 印 |
| 期标题 | 期 | — | 印 |
| 期数 | 期 | — | 印 |
两张画面由同一个构造函数按这张表取件。封面跟项目、不跟期,所以期标题与期数不 进封面,各期封面是同一张;背景跟期,两样都印。要加一行、要换归属,改这一处 就够——从前封面与背景各写一份元素序列,同一个元素在两处各描述一遍,改一处忘一处, 两张画面就各印各的了。
动画默认静止。 播客重点不在画面上,背景元素还会被缩放带偏。四档动画可选:
| 档位 | 说明 |
|---|---|
| 静止(默认) | 纯静态背景 |
| 缓推 | 全程匀速缩放,幅度可配(默认 1.04)。会带动背景元素位移 |
| 波形 | 半透明声波随音频律动 |
| 频谱 | 频谱条铺底,技术向 |
缓推档的缩放按整段帧数归一。写成「每帧加固定增量并设上限」会让运动全堆在开头: 六十二秒的片子在第 9.5 秒就撞顶,之后 52 秒完全静止。
字体是可选项,两款各管一处。 配置页「字体」卡片里两个下拉——画面字体(背景
与封面)与字幕字体,都在同一张卡上并排摆着。两个下拉都是自绘的:原生 <select>
的选项不接受自定义字体,浏览器直接忽略选项上的 font-family,整列字长得一模一样,
选字体就成了盲选。现在每个候选项按自己的字形渲染同一句样本,一眼可见长什么样;
本机没装的照样列出来、只是暗显不可选。留空即自动挑一款可用的。
缺字形即报错。 字体缺字形时渲染库不报错,直接画豆腐块。绘制前逐个字符检查覆盖, 缺失就报错并指出是哪个字——换符号、换字体,都比出一版带方块字的封面强。这一条在 自选字体之后更要紧:能挑的字体多了,挑到一款没有中文字形的(Inter、JetBrains Mono 这类)是迟早的事,报错会点名是哪个字。
背景波纹按留白排布,不按像素。 三条正弦波原本各自写死间距与振幅,而间距小于 相邻振幅之和——包络从一开始就是重叠的,再加上千分之三的频率差产生拍频, 相位周期性重合,三条线就绞成一团。现在先算单位振幅,按「相邻波带必须留出正留白」 反解出振幅,周期数按可见宽度归一(换画幅疏密不变),三个周期权重取不成整数比, 避免长期同相。
留白是结构条件而不是手感数值:相邻两条的中线间距取「振幅之和 + 留白」,
于是包络净空恒为 留白 × 单位振幅,与相位、频率、画幅都无关。
几何由 wave_layout() 单独算出,绘制与断言走同一份数据,测试量的是真正落笔的那条折线——
把留白改小、振幅调大、周期权重取成整数比,三条反证都会立刻报警。
字幕两档,只差一个轴。 两档共用同一个框:左右取 margin_lr、底边取画面底减 margin_v、
高度是「行数 × 行距」、填充与描边同一套写法——差别只在文字往哪个方向走。
| 档位 | 轴 | 一句话 |
|---|---|---|
| 歌词 | 纵 | 框内自动折行,换句时整块上滚(可见句数、框行数、上滚时长可配) |
| 单行滚动 | 横 | 一句一行、不折字;装不下就在框里滚过框口 |
横滚滚的是溢出的那一截,不是整句。 跑的是一段三段式:①句首静止(文字左缘贴框左,先让人读到
开头)②匀速左移(位移只等于「文字宽 − 框宽」)③句尾静止(文字右缘贴框右,结尾留在框里)。
速度取自语速(文字宽 ÷ 句时长),于是滚动时长 = 句时长 × 位移 ÷ 文字宽,恒小于句时长——
任何长度的句子都滚得完,不需要容差也不需要兜底分支。句首静止时长等于「框宽 ÷ 速度」,横屏默认
值下约 9.8 秒,与句子长短无关,正好是读完框内那一眼的时间。
跑马灯式「整句从框外滚到框外」要拿「整句过框」的时间,而观众只需要看没进过框的那部分—— 多滚的那一截是白滚的:按句时长硬塞会快过朗读三成,按朗读同步则滚不完。从前的单行档更直接: 它不滚,超出的部分被裁掉(104 字那句实渲只剩中间约 33 字)。
字宽不用字号顶替,是实渲标定出来的:汉字 0.751、全角标点 0.820、半角 0.438(× 字号),
前后端读同一组常量。按「一个汉字一个字号」估会宽出三成,横滚终点会跑过框。这组数由测试钉死,
换字体就得重标。
九、目录结构
podcast-maker/
├── main.py 入口(Web / 自检 / 续跑)
├── setup.bat 一键启动
├── requirements.txt
├── podcast_maker/
│ ├── config_manager.py 配置总表、枚举点位、预设、门禁定义
│ ├── layout.py 项目目录布局的唯一定义处:子目录名(含「音色」)、各期产物路径
│ ├── llm_client.py LLM 客户端(约束解码、后端兼容、思考段剥离与 JSON 提取)
│ ├── ingest.py 素材导入与锚点切章
│ ├── script_engine.py 脚本生成、格式收束、片头尾粘合、门禁、锁文件
│ ├── duration_model.py 字数 ↔ 时长换算、标准语速与音色校准
│ ├── tts_engine.py 语音合成
│ ├── audio_engine.py 拼接、混音、编码
│ ├── subtitle_engine.py 断行、SRT/ASS 生成、时间轴
│ ├── assets_factory.py 背景、封面、BGM(含声波几何 wave_layout)
│ ├── video_engine.py 动画、说话人指示、合成
│ ├── project_store.py 项目登记表:规划方式、期号记忆、期数地图与分支期号、进度
│ ├── source_store.py 素材库:成稿入库、章节清单、按落点取原文(含行号定位)
│ ├── probe.py 结构探查:层级与原文位置、按文体定凝缩单元、准入、逐单元凝缩
│ ├── paradigms.py 素材范式:切分/整合/重点/推进四段(排图)+ condense(凝缩专用)
│ ├── planner.py 期数地图:合并与切分(期数由此定)、压比体检与整体重排、分组归一、落点、插入(与排图同管线)
│ ├── pipeline.py 九步编排与产物校验、后台任务(起任务/进度日志/同项目互斥)
│ └── web_ui.py 界面与 HTTP 接口(含选期、批任务与中止)
├── tools/
│ ├── ui_smoke.py 真实浏览器界面冒烟
│ └── e2e_http.py 走 HTTP 的端到端验收
└── tests/ 单元测试
十、测试
python -m unittest discover -s tests -v # 单元测试
python main.py --port 8812 # 另开一个窗口起服务
python tools\ui_smoke.py --url http://127.0.0.1:8812 --shot-dir _smoke
python tools\e2e_http.py --url http://127.0.0.1:8812
python tools\e2e_http.py --url http://127.0.0.1:8812 --scratch "$TEMP/pm_e2e/episodes"
--scratch 是验收产物的落点,默认在仓库内的 _smoke/_scratch。若宿主环境对
「非临时路径的删除」设了拦截,脚本开头清理上次残留那一步会被拦下——
此时把落点指到系统临时区即可,产物内容与判据完全不变。
1283 项单元测试,覆盖最容易出错的地方:字数换算、时长模型、字幕断行与均衡折行、 框几何与字宽标定(系数与实渲量出的宽对账)、两档共用一个框、单行横滚的三段式与「恒滚得完」、 回顾三条模板与只剃尾、配置迁移(旧档位与改名的点位)、 编码参数与 BGM 来源解析、 门禁规则、滤镜转义、排版包围盒与动画缩放曲线、配置点位对账、脚本契约、项目期号推进、 规划方式定死(三种)与单集出完即完结、素材类型落库、期数地图与分支期号、 素材索引完整性与按落点取原文、 料源按范式分流(成稿规划忽略手填、逐期即兴与单集用递来的)、 画面字体换一款真的换出另一张图、 项目目录布局与各期产物归位、批量的串行与失败跳过与熔断与软中止、 批量合成读盘上定稿那一份脚本、删除把登记表条目与项目目录一并抹掉、 单元位置与取料口径一致、按文体定凝缩单元层级、 md 锚点存活、结构探查与编号形态、声波包络不相交。
排版测试不看渲染图,只断言实测包围盒:相邻元素不压叠、框线在主标题之下、 字号取六十到二百四十共四档都不越界。
声波测试同样不看渲染图:按实际绘制点逐像素比对相邻两条波的上下关系, 任意 x 上都不允许反转。这一条必须由测试守着——留白被改小、振幅被调大, 界面上只表现为「波浪线又缠上了」,没有任何一处会报错。
配置点位测试做静态对账:扫描管线源码里读过的每个点位,与配置总表逐条比对。 悄悄读一个没声明的点位,取值永远落回默认值、界面上也没有控件去驱动它, 症状只出现在产出的图上——这类缺陷靠人看不出,只能靠对账。同一份对账还管三件事: 每个点位都必须有界面入口、枚举档位的标签不得等于值本身、每个点位都必须有阶段归属。
界面冒烟为什么必须用真浏览器
单元测试跑在 Python 里,看不见界面。而最贵的两类界面缺陷只在浏览器里现形:
控件找不到自己(点号被当成类选择符,getElementById 全返回 null,事件一个都没绑上,
滑块滑了不生效)、下拉只剩英文裸值。所以这一层用 Playwright 跑真浏览器,判据包括:
- 配置页控件数量与后端下发的点位数一致
- 界面里每个控件都在登记表里(没有「有控件、没登记」的漏网)
- 拖动滑块后服务端配置真的变了(回读确认)
- 枚举下拉的标签不是英文裸值;音色有候选可挑;模型名是下拉且点开能列全部
(判据不是"有个能打字的框"——
datalist那种"输入框 + 候选"是浏览器的补全, 值非空时候选被筛成命中项,框里填着qwen/qwen3.5-35b-a3b就只剩它自己, 点开列不出其余 14 个。列表外的名字走末项「手动填写…」) - 立项可选三种规划方式且默认选中一项;选「单集」时计划期数与起始期号收起,切回 成稿规划再展开;脚本页有归属项目下拉并给出说明
- 两款字体下拉都是自绘且每个候选项真的带各自的
font-family——只验「有选项」 会漏掉换回原生select时的形状:标签都在、值也对,字却全长一个样 - 单集卡片不写「下一期」,也没有「改下一期号」按钮
- 合成页只列有脚本的期(没脚本的期不进可选项)
- 选期表:全选 / 清空 / 单勾与计数一致,未选项目时整个收起
- 勾了多期走批任务,不落到单期接口
- 门禁在接口层也拦得住(缺脚本的批合成、空脚本存盘、中止不存在的任务)
- 删除要点两下:第一下只点亮、一个请求都不发,第二下才发
action=delete - 插入面板自带入库区;已排进地图的素材默认不勾并标出;一份素材都没有时面板照样打开
- 控件 id 不含点号;无 console error / pageerror
后两条是补齐的教训:脚本页曾经整页读一个不存在的下拉(s-project),
一按生成就抛异常,而配置页那八十几项全都正常。控件数量对得上、值读得出,
都不代表这份界面能被用起来。
选期与批量那几条走的是浏览器内的桩:项目表、期表、批任务接口在页面里就地换掉。 这样「多期是不是真的走了批任务」「合成页是不是真的只列有脚本的期」能在真浏览器里 答出来,而服务端一期活都不干、落盘一个字节都不动——冒烟不该有副作用。桩里另接了 单期接口:多期若从那儿漏过去,判据就记一笔;删除请求也只记不做。
还有一类缺陷上面这些全都照不到:运行时拼出来的 HTML 属性。项目卡片的按钮是
拼串生成的,拼错一个引号,拼串本身仍是合法字符串——语法检查照过,浏览器要到点下去
才在控制台报错,界面上只表现为「点了没反应」。所以另有一道浏览器外的渲染体检
(tools/probes/check_buttons.js):把卡片渲染函数真跑一遍,把拼出来的每个事件属性拿回来
逐个校验语法(修前 24 个里有 2 个不可解析),一并验下拉的过滤(归档项目、没排图的
成稿项目都不该进选项)。
端到端为什么要走 HTTP
单元测试各测一层,唯独串联处没人测:标题从脚本传到合成、期号从项目记忆里接着往下排。
tools/e2e_http.py 走界面用的那条 HTTP 路径,把两条路都跑通,产物落在临时目录,
跑完还原配置,不动真实产物:
- 逐期即兴:立项 → 脚本带标题 → 出片 → 期号前进 → 续接。
- 单集:立项(单集)→ 计划期数恒为一期、出完不再排下一期;画面照样印得出节目名。
- 成稿规划:立项(成稿规划)→ 上传 md 书稿(走 base64,验锚点还在)→ 排地图 (逐节凝缩后分组,就地核对每期落点都切得出料)→ 生成脚本(素材区已收起,验日志写明 按落点取用、标题取自地图)→ 在插入面板里就地传第二份素材(验库里份数增加、面板自行 重画并勾上它)→ 让模型定插入点 → 落库(故意递一个错的期号,验后端没有采信)→ 核地图 顺序与分支期号。
第二条是补齐的教训:只有真跑一遍才发现,排过地图的项目在生成脚本时仍被"素材为空"拦下。
十一、许可
Apache License 2.0,见 LICENSE。第三方组件声明见 NOTICE。
主程序与默认语音引擎这条链路上的许可都是宽松型——主程序 Apache-2.0、Qwen3-TTS
权重 Apache-2.0、推理加速层 faster-qwen3-tts MIT——商用不需要额外授权或付费,
义务只有保留版权与许可声明、修改处标注、附免责声明。唯一的例外是备选的 edge-tts:
库本身 LGPL-3.0,但产出的音频来自微软浏览器自用通道,受微软服务条款约束、权属不清。
要商用就选本地那条,并注意两件许可之外的事:参考音频别用真人录音(那份音频 由程序用模型自带的合成音色生成,走这条链路出来的声音不涉及他人声音权;换成真人 录音去克隆就落进《民法典》第 1023 条声音权保护的范围),发布时按《人工智能生成 合成内容标识办法》加 AI 生成标识。
作者:wUwproject
更新说明
v1.0.0
本次改动:README 补齐功能与用法、更新日志体例收紧(改写日志正文 60 处、README 新增 2 节、新增测试 6 项)
问题
- 更新日志写成现场对白:段里出现人称叙事(写明是谁提的)、产品定位论证 (替产品该不该分发辩护)与口语措辞(只在口头成立的词)。读到它的人 是后来查这件事的人,这三类内容都不承载「改了什么」,只把技术结论埋在话里。
- README 只有设计论证,没有功能与用法:通篇逐条讲的是「为什么这样做」, 使用者读完仍不知道有哪些功能、按什么顺序点、产物落在哪;配置页十六张卡片 只列了名字,没说各管什么。
修法
- 更新日志正文全量改写:人称叙事、产品定位论证、口语措辞三类一律去掉,只留 现象、根因、修法、验证。体例区补一条文风条款。
- README 新增两节:**「二、功能」按工序列能力与产物落点;「五、怎么用」**给 三种规划方式各自的操作路径(写明界面按钮名)、常见操作、配置页十六张卡片速查、 出问题先看哪里。原「四、产物」及其后各节序号顺次后移,交叉引用同步。
测试(1277 → 1283 项)
- 新增
tests/test_changelog_tone.py5 项:三类词表(人称叙事 / 产品定位论证 / 口语措辞)逐行扫描、命中即报行号;体例区那条款必须在场;每个版本段必须有加粗 小标题。词表扫的是正文,跳过头部体例区——那里写着词表本身,扫进去是自证。 - 另在
tests/test_config_keys.py的版本齐步组加 1 项:ARCHITECTURE.md抬头那个版本号也要与VERSION一致 —— 本次三端都齐了,唯独漏在架构册。 - README 的测试计数同步为 1283。
版本:1.0.0
功能面到此冻结。此后按语义版本管理:补丁修缺陷,次版本加功能,主版本只在 不兼容变更时动。
Release files for podcast-maker-ldxs 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| podcast_maker_ldxs-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Release files / podcast_maker_ldxs-1.0.0-py3-none-any.whl
| Download URL | podcast_maker_ldxs-1.0.0-py3-none-any.whl |
|---|---|
| Size | 22.1 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d58264b8d0baf2c757b991e850b2cdbd02545a100059acf1a56632fc5cc19555
|
|
BLAKE2b-256 checksum How to use checksums |
7809d7e6ec7d161a531a6895cb67e5eb3b941be408f92c02e85aab85fb41dc9c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|