Skip to main content

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。画地图分五步,一次长调用变成一批小调用:

三件长活都在后台跑:素材入库即探查、排图、插入建议——全是「模型慢慢算」的活(探查几分钟、排图几十分钟),点了按钮弹窗里就有实时进度与逐条日志(凝缩到第几节一目了然),弹窗可以收起、任务照跑,跑完自动重开面板。同一项目这些活互相排他(它们写的是同一批探查文件),别的项目不受影响。

  1. 结构从稿子里读出来(#、一、、Word 标题样式),不喂模型。层级归一、同系列 归纳、附录挑出来单列,每条记下它的起止行与所属章节链——都是可查的事实。
  2. 定凝缩的单元层级:按这批素材的文体判断哪一级才算一个完整的表达单元(方法论看 论证链、小说看情节单元、采访看话题板块……判据写在范式卡里)。这一步定的是颗粒:选 篇一级,凝缩 39 次;选章一级,就是 179 次。判据不是"标题看起来像不像一个完整话题" ——越具体的标题越像,照着猜只会把层级判细、把单元切碎。
  3. 逐单元凝缩:一次调用只看一个单元的原文,做的是保逻辑的有损压缩——留下它说清楚了 什么、反对什么、有哪几条主线,以及跟着每条主线的细节区分。几十万字的原文进不了 上下文,几十条凝缩可以——每次的上下文与输出都不随全书体量增长。按哪种文体压,由 范式卡里的 condense 一项给出(与排图用的"重点判据"是两个字段,见下)。 凝缩带着位置落盘:压的是原文的哪一块(第几行到第几行)、属于哪一篇,由取料那一步 实测的范围回填,不是另算一遍。
  4. 模型合并与切分:拿一份凝缩清单(每条带行号范围、所属章节与字数)回答"哪些节合起来 是一期、这一期叫什么、讲什么"。分出几组就是几期——期数由内容结构定,不由字数除。 它不抄标题、不编期号、不算字数,这些都由代码做,所以也就没有"抄错一条落点就取不到料" 这件事。分完做压比体检——每期原文量对照压缩档核对,两个方向各有红线:超过上限 (成稿目标 × 1.25 × 档位)是一期塞不下,低于下限(成稿目标 × 1.25 × 1.2)是料不够——写不满时模型 只能把写过的段落再背一遍凑数。把问题清单喂回去整体重排(凝缩复用,只是重新分组, 最多重试 2 次);重排仍超上限就报错停下,仍低于下限则落警告交人。
  5. 按落点取原文:生成某一期时取回该期涵盖的单元原文(含子节),不截断—— 照地图上标的行号切,切走的与凝缩时看的是同一块。

两级之间只有「章节标题 + 行号」这一个标识,所以 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 倍、另一期只写六成。成稿规划的项目有地图与各节凝缩,脚本这一步走三步:

  1. 逻辑拆分——模型按内容把节分组成段(哪几节讲的是同一件事),只回节序号; 递给它的清单只给「节号 + 标题 + 凝缩」,一个体量数字都不给(分组看语义,摆着 字数会把它往"按字数摊匀"上带)。分组过三道归一:越界/重复丢掉、按序号排序 (不许重排,讲述顺序由地图定死)、漏掉的节补成独立一段。两次答不出合法分组 就退回「一节一段」。
  2. 逐段过桶——桶逐个逻辑段过输入额度(装得下就是一段、装不下在组内按整节切成 几段);两个逻辑段之间是话题转折,绝不并进同一段。切分单位只有整节凝缩:单个节 自己就超额度时报错要求调倍率或拆期,不再从节中间切——半个节取不出料、也算不出账。
  3. 规划轮写段主旨——给每段写一句段主旨(与期主旨同层级:这一段讲的是什么事), 并给全篇起标题。段数钉死,不能增删段、也不许写"从哪里起到哪里止"。这一轮同样 不看字数——段边界是程序定的、配额是程序摊的,字数摆进来只会把"这一段讲什么" 带成"这一段该多长"。

然后逐段生成、逐段按字数核账(容差 = 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 路径,把两条路都跑通,产物落在临时目录, 跑完还原配置,不动真实产物:

  1. 逐期即兴:立项 → 脚本带标题 → 出片 → 期号前进 → 续接。
  2. 单集:立项(单集)→ 计划期数恒为一期、出完不再排下一期;画面照样印得出节目名。
  3. 成稿规划:立项(成稿规划)→ 上传 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.py 5 项:三类词表(人称叙事 / 产品定位论证 / 口语措辞)逐行扫描、命中即报行号;体例区那条款必须在场;每个版本段必须有加粗 小标题。词表扫的是正文,跳过头部体例区——那里写着词表本身,扫进去是自证。
  • 另在 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)

Table of built distributions (wheels) for podcast-maker-ldxs 1.0.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

1.0.0 This release

1 release file

0.42.0

1 release file

0.41.2

1 release file

0.41.1

1 release file

0.40.1

1 release file

0.39.0

1 release file

0.34.4

1 release file

0.12.2

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page