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 矢量绘图层,当前说话方高亮 |
二、安装
需要 Python 3.11 或更新(py -3.11 或 PATH 上那个 python 都认),以及
ffmpeg 放进 PATH。然后:
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 称呼与语速、生成与门禁结果、把改过的脚本存回本期 |
| 合成 | 归属项目、要合成哪几期(只列有脚本的期)、本期标题(改,不是填)、出片开关、实时日志、产物校验报告 |
| 配置 | 全部可调项,按命名空间自动分成十五张卡片:写作目标、门禁阈值、片头片尾、项目、语言模型、角色与音色、音频输出、背景音乐、画面输出、背景、动画、说话人指示、字幕、封面、字体 |
脚本与合成两页只摆最常调的几项,其余全部可调项在「配置」页。 声音与画面不是独立标签,它们是脚本与合成的子配置——摆成并列标签会让人以为 那是一个并列的工序,而不是两页各自的参数。
所有控件的取值范围都由后端下发,前端不硬编码任何选项。地址栏 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 无法解析」那一步,起因被埋在两个阶段之外。
四、产物
产物按类归位到项目的子文件夹里,不摊在一个「集目录」中:
| 位置 | 文件 | 说明 |
|---|---|---|
脚本/ |
<期号>.json |
脚本(含逐句实测时长)。落盘定稿那一份,合成读的就是它 |
音视频/ |
<期号>.mp3 / .aac |
成片音频(按配置) |
音视频/ |
<期号>.mp4 |
横屏视频 |
音视频/ |
<期号>_v.mp4 |
竖屏视频(可关) |
字幕/ |
<期号>.srt / .ass |
字幕 |
图文/ |
<期号>.md |
对话体图文 |
背景/ |
<期号>_bg.png / _bg_v.png |
横竖背景图 |
背景/ |
<期号>_bgm.wav |
内置背景音乐 |
封面/ |
`<期号>_cover_16x9 | 3x4 |
报告/ |
<期号>.manifest.json |
本期被要求产出什么(含视频开关)、产物清单与配置快照 |
报告/ |
<期号>.report.json |
校验报告 |
过程/ |
<期号>/ |
中间件;blocked.lock.json 门禁未通过时出现在这里 |
文件名前缀一律是期号(分支期号 3a 也不会与主期撞名)。
各阶段各自落盘:素材入库即落、地图排完即落、脚本生成即落、出片产物各自归位。 脚本要是不落盘,「先生成、改一改、再合成」这条路就走不通——脚本与合成会被绑成一步。
产物校验只要求「这一期被要求产出什么」。关掉视频开关后还去要视频,
每一次出片都会判不过;判不过就不占期号,项目进度永远停在原地,
而磁盘上每份产物看着都好好的。所以视频开关写进 manifest.json,
不适用的检查项直接不进报告,而不是留一行「不适用」占位。
五、门禁
共 21 条(生成阶段 13 条、产物阶段 8 条;其中 fail 17、warn 4,另有 soft 1),分 fail 与 warn 两级。
条目里的 stage 不是分类标签,是执行权:生成阶段的条目只在脚本阶段执行,产物阶段的
只在合成阶段执行,没有谁越界去替对方判一遍。产物阶段 fail 不过即写 blocked.lock.json
硬中断,不产出次品;warn 在严格模式下同样拦截(script.gate_strict)。脚本阶段不过不拦人
——稿子和问题一起落盘,改稿还是直接出片由人定,合成前只弹一次提醒。此外还有一类 soft:判据本身是估算
值的那些(目前只有总时长偏差一条),不达标只在报告与界面上记「提示·不阻断」,既不拦放行、
也不回灌重写——口径本身只是一个通用语速,拿它把稿子打回去,模型只能围着一个它既测不出、
也控不住的秒数反复改。
严格模式默认开启:宁可不出片,不出错片。
| 阶段 | 门禁 |
|---|---|
| 脚本生成·形式 | JSON 合法、字段完整、同一人连续句数(上限取自素材类型的范式卡)、句长区间、单句时长、情绪标签、情绪档位(不写心情的文体不许出现心情词)、禁用词、可朗读(不含 emoji、图标、不可见字符、网址、命令参数、排版符号)、片头片尾 |
| 脚本生成·软项 | 总时长偏差(≤ 15%,只记账不拦人) |
| 脚本生成·内容 | 语义检、承诺链检(LLM 一次调用判完;不过就定点修补,检不出句号的交人工) |
| 产物 | 音画时长差、实测码率、实测采样率、断词率、字体族名、背景水印、产物完整性、封面三尺寸 |
产物阶段的「产物完整性」与「音画时长差」只在要求视频时才成立;关掉视频开关后 这两项不适用,直接不进报告。要求一个从没被要求的东西,等于让每一期都判不过。
脚本的事归脚本,合成的事归合成。 两个阶段各判各的、各落各的,合成端不碰脚本的账:
| 脚本阶段 | 合成阶段 | |
|---|---|---|
| 判什么 | 生成阶段那 13 条(形式项 + 总时长软项 + 内容检) | 产物阶段那 8 条 |
| 结论落点 | 过程/<期号>/gate.generate.json |
报告/<期号>.report.json |
| 不过怎么办 | 定点修补:只把点名的句子交给模型改,全篇行数一个字不动;结构坏了才整篇重出;指不出句号的交人工。修满轮次仍未过就把这一版稿子连同结论一起落盘,阶段到此为止 | 写 blocked.lock.json 硬中断,不产出次品 |
| 越界防线 | — | 不写稿(没稿子就报错,不顺手现写一份)、不判稿(不拿生成阶段的条目再审一遍) |
两者之间只有一次提醒:点合成时读那份脚本阶段的结论,有未通过项就弹框逐条列出,
人点「仍然合成」才继续,并在清单里留一个 script_issues_ack 作凭证。提醒不是闸门——
不点确认只是不发请求,不是判它过了。框里写明这份记录产生于脚本生成那一刻,不追踪此后
的人工修改:改过没有以手上的稿子为准,不为了把它算准而再跑一遍。
总时长偏差留在生成阶段,但按软门禁办:这一阶段才看得见它,可它判的是估算值——真正的 时长要等合成出来才算数,口径本身只是一个通用语速。不达标只记账,不拿它打回重写。
估时长用一把与音色无关的尺子。 脚本阶段的口径是标准语速 4.39 有效字/秒(≈256 汉字/分钟,锚在国家语委《普通话水平测试实施纲要》的正常语速与朗读口径之间)。目标字数 反推、逐句估时、总时长门禁、容量反推都用它——稿子的时长目标不随「这一期换了哪个音色」变, 一份稿子在哪里估都是同一个数。音色之间快慢的差异换算成一个比例,挂在配置页每个音色下拉 框下面(音色语速 ÷ 标准语速:0.91 即比标准慢,1.00 即同速),没有实测的音色照实写 「尚无实测」,不拿 1.00 冒充量过的数。
措辞禁忌为什么必须在提示词里先说。 只在生成完之后扫描命中,模型事先不知道要避开 什么,命中与否全看运气,几轮重试就是几次抽签。所以词表连同「换成什么」一起前置进系统 提示词;回灌时再给到句——第几句、命中了哪个词、改用什么,并明确「只改这几句,其余照抄」。
定点修补那一侧同样要前置。 只把命中句与替换词回灌给修补模型是不够的:它改这一句时 会顺手写进另一个同样说满的词——实测里模型补长一句就写进了「毫无」,回门禁当场被拦。 所以修补的系统提示词带的是同一份禁忌表(同一个渲染结果,不另存一份),回门禁那一道只当兜底。
改一句不该重打一遍。 第 1 轮整篇写;之后能定点就定点——只把门禁点名的句子交给模型,
输出是一份补丁({edits: [{index, text, emotion}]}),回来按句号原地替换,全篇行数一个
字不动。行数不变是这条路的根基:门禁报的「第 N 句」在补丁前后指的是同一行,一旦允许
增删,编号全漂,就得回头重编一遍。
「不许增删」不靠提示词劝,靠补丁的结构里根本写不出来——只有「改哪一句」和「改成什么」,
没有「加一句」和「删一句」。说话人也不在补丁里:它归代码管(enforce_max_run 压连续句、
pin_intro_outro 写死首末句),给模型留这道口子等于让格式轴上出现两个说话的人。
补丁只给被点名句与它前后各两句,不给全篇。这不是省字那么简单,是实测出来的:把全篇 220 句一起递过去,模型三次里有两次直接不吐 JSON(被上下文带跑,改写起解释性文字);只给 窗口则三次全中。纯形式项(措辞、句长)不带素材,内容项(语义、承诺链)才带——改「编造」 得知道素材支持什么,改「太长」只跟这一句自己的字面有关。
哪些问题能定点,只看它带不带句号。 形式门禁全都自带(它本来就是逐句判的);内容检要 模型自己给,给了才算。分成三类各有各的去处:带句号的定点改;结构坏了(JSON 不合法、字段 残缺)只能整篇重出;内容检自己指不出句号的——不动稿子,交人工。让模型为一句它自己都 说不清的问题重写两百句,只是重新摇一次骰子,还会把已经好的句子一起改坏。
每条提示词都按块分类。 一个推进点位牵扯的资料不止一种:要处理的正文、判断用的参考、 上一步的产物,以及这一步当前该产出什么。全堆在一起,模型只能靠语序猜哪块是依据。所以 每一块都自带名字与用途——块名用【】框起(【素材】【已有计划】【待审脚本】),括注说清它 怎么用(「判断依据」「当前任务:按这里逐条改」)。风格、背景这类与具体内容无关的块,在 括注里写明「以当前任务为准」:冲突时让位的必须是它们。
贵的东西按需跑,便宜的东西每轮全跑。 定点修补之后,形式门禁(走代码、毫秒级)每轮 全套重跑——改一句话的字数会牵动单句时长与总时长,改一个用词可能撞上新的禁用词,只重跑 被触发的那一项会放走改出来的新毛病。内容检(走模型、要读整篇)只重判上一轮没过或没判过 的那几项,已经判通过的那一项本轮不重喂,结论从上一轮带过来——不带走的话报告里会凭空少 一行,「这一项检查通过」的记录就丢了。
每轮写完就落盘。 生成一轮要十几分钟,从前等整段跑完才第一次写盘,中途卡住、被中止、 进程被起停脚本杀掉,手上就什么都没有——稿子明明已经写好了。现在每轮定型完就往出片时读的 那个位置写一份,落的是「当前最新的那一版」,与最终返回的那版一致。
内容检为什么挂在脚本生成阶段。 语义检(台词有没有编造素材之外的东西)与承诺链检 (开头开的那个口子收没收),判断依据都在语义层,词汇匹配做不了——对话体前后用词不同 本来就是正常的,拿重叠率去判是拿错了工具。所以这两项交给模型,一次调用同时判完。
位置比判法更要紧:它接在脚本生成的重试回路里,不通过就带着检出的落点去定点修补
(轮数取 script.max_llm_rounds)。等音频视频都渲染完再检,片子已经定死,检出来也改不动。
内容检的输出必须带落点。 每一项问题要给出 {line, quote, problem}:哪一句、那一句
里的原话、毛病是什么。这不是顺手加个字段,是定点修补的前置条件——检查只说「整篇偏题」
而不说哪一句,补丁就无处可下,只能退回整篇重出,而这正是要被根除的那件事。指不出
是哪一句时要求模型老实填 line: 0,代码据此把它归到「交人工」;句号越界也一律归 0,
不许拿第一句或最接近的一句顶替——填了假句号,改稿的人会去改一句没毛病的台词。
模型没给结论,不等于检查通过。 模型漏返回某一项时,那一项记为「未判定」落进待复核, 不默认放行——把「没检查」记成「检查通过」,报告上的绿灯比不检更坏。 判定为不通过的才参与放行(语义检 fail 级、承诺链 warn 级)。
审校的输出预算与写作同一条口径。 取全局 llm.max_tokens,不另设一个小值:
推理型模型把思考过程也算进这份额度,给小了就是思考吃完、答案一个字不剩,
两项检查一同落成「未判定」。它同时是输出上限,服务端校验「输入 + 上限 ≤ 上下文」,
所以该值要落在模型实际加载的上下文之内——模型自身上限与它无关:
加载时给 8192,配置里写 48640,请求要么被拒(HTTP 400 上下文超限),
要么一路生成到塞满。
素材喂多少由素材容量算出来,不由常数定。 内容检与定点修补从前都把素材截到前 6000 字——
生产里一期素材约 2 万字,只喂进 30%;落在后面的依据在模型眼里就是「不存在」,语义检于是
把本来正确的句子判成编造,再交给定点修补把它改坏。现在按素材容量喂:容量 = 成稿目标 × 1.25 ×
压缩档(偏移 1.25 只进算式、不改变参照系的「1」,与排图体检同一把尺),扣掉估算余量与提示词
模板的占用,剩下的才是素材的;后端窗口够不够不靠程序猜——任务开始时日志会给出具体数字
并提醒「请保证后端上下文窗口 ≥ 输出预算 + 素材折算」。字符与 token 的折合比由后端回传的
prompt_tokens 反标,不写死经验值——中文、数字、标点的折合比差得远。
放不下的时候,两条路分工不同。 写作与定点修补不分批:脚本是单篇产物(片头尾写死、 承诺链跨全篇、总字数由目标时长反推),分批写要引入规划层与段间状态传递,那是另一套架构。 素材超出预算时改用凝缩(每节的逻辑骨架)顶替,这是有损的,日志与素材头部都写明。内容检 则分批核:按预算切段、相邻批重叠 1000 字、每批带整篇脚本,合并规则只有一句——同一句 要在每一批里都被报出来才算真问题(某句的依据只落在某一批里,那一批当然找不到它,撤销它 才对;每一批都找不到,才等价于全文里找不到)。批数超过 8 批判「未核」交人工,不把没核的 那部分记成「核过且没问题」。
成稿规划走分段生成,治字数漂移。 整篇一次生成时模型没有全局计数器,实测同一管线 一期超目标 4 倍、另一期只写六成。成稿规划的项目有地图与各节凝缩,脚本这一步先按凝缩 分组规划(模型不报数字,字数配额由程序按凝缩占比分),再逐段生成、逐段按字数核账 (容差与总时长门禁同一把尺),差额滚入下段配额——误差逐段吸收,总账咬住目标。
路线由项目模式定死,不是一个开关:成稿规划(mapped)走分段——有地图与凝缩,逐期 配额、防重复才有依据;逐期即兴与单集走整篇——当场给料、出完即止,没有凝缩可分。成稿 规划万一本期凝缩读不出来(还没排图、版本对不上),也退回整篇照常出稿,不让人卡在 「一步跑不动」上。
无论走哪条路,成稿都要先过一遍程序硬去重:同一句长句在整篇里出现多次时只留第一次。 模型凑数时会把写过的整段再背一遍(实测一期 302 句里 129 句是重复),那不是改写、是复播, 删掉没有任何信息损失。删了会记一笔「重复凑数」:说明这一期的素材其实撑不满目标时长。
其余全部 fail-closed:不过就写 blocked.lock.json,不出片。
门禁未过时,被拒的脚本与逐项明细会一并落盘(script.rejected.json、gate.generate.json),
避免只留一句「门禁未通过」而无从排查。
六、排版与画面
排版是流式的,不是摆坐标。 背景与封面共用同一个纵向流式执行器:每个元素的位置
由上一个元素的实测包围盒推出来,间距一律写成字号的倍数,集中在 LAYOUT 一张表里。
这条约束是有来历的。曾经用「主标题位置加一百一十八像素」表示标题下方框线的位置, 而字号一百零八的字实测高约一百三十——线正好横穿标题字。这类缺陷调数值调不好: 换字号会穿、换画幅会穿、换字体还是会穿。所以位置只能由实测包围盒推出, 间距只能取自规格表,序列里禁止就地写数值,这条有测试守着。
每个元素落下的实测区间会被记录,测试据此断言相邻元素不压叠、框线始终在主标题之下。
最大的一行是节目名,不是期标题。 背景与封面自上而下同一条层级:
主标题 = 节目名(项目) ← 最大
副标题(项目,可留空)
期标题(地图自动取)
期号 / 版权行 ← 最小
播客卖的是系列品牌,不是这一期讲什么。期标题字数不定,长起来在最大档只能折行,
一期一个样,摆上去就散。字号表按角色命名(hero / second / episode)而不是
按位置,因为主标题换成节目名之后,f_big / f_mid 这种叫法没人分得清。
动画默认静止。 播客重点不在画面上,背景元素还会被缩放带偏。四档动画可选:
| 档位 | 说明 |
|---|---|
| 静止(默认) | 纯静态背景 |
| 缓推 | 全程匀速缩放,幅度可配(默认 1.04)。会带动背景元素位移 |
| 波形 | 半透明声波随音频律动 |
| 频谱 | 频谱条铺底,技术向 |
缓推档的缩放按整段帧数归一。写成「每帧加固定增量并设上限」会让运动全堆在开头: 六十二秒的片子在第 9.5 秒就撞顶,之后 52 秒完全静止。
字体是可选项,两款各管一处。 配置页「字体」卡片里两个下拉——画面字体(背景
与封面)与字幕字体,都在同一张卡上并排摆着。两个下拉都是自绘的:原生 <select>
的选项不接受自定义字体,浏览器直接忽略选项上的 font-family,整列字长得一模一样,
选字体就成了盲选。现在每个候选项按自己的字形渲染同一句样本,一眼可见长什么样;
本机没装的照样列出来、只是暗显不可选。留空即自动挑一款可用的。
缺字形即报错。 字体缺字形时渲染库不报错,直接画豆腐块。绘制前逐个字符检查覆盖, 缺失就报错并指出是哪个字——换符号、换字体,都比出一版带方块字的封面强。这一条在 自选字体之后更要紧:能挑的字体多了,挑到一款没有中文字形的(Inter、JetBrains Mono 这类)是迟早的事,报错会点名是哪个字。
背景波纹按留白排布,不按像素。 三条正弦波原本各自写死间距与振幅,而间距小于 相邻振幅之和——包络从一开始就是重叠的,再加上千分之三的频率差产生拍频, 相位周期性重合,三条线就绞成一团。现在先算单位振幅,按「相邻波带必须留出正留白」 反解出振幅,周期数按可见宽度归一(换画幅疏密不变),三个周期权重取不成整数比, 避免长期同相。
留白是结构条件而不是手感数值:相邻两条的中线间距取「振幅之和 + 留白」,
于是包络净空恒为 留白 × 单位振幅,与相位、频率、画幅都无关。
几何由 wave_layout() 单独算出,绘制与断言走同一份数据,测试量的是真正落笔的那条折线——
把留白改小、振幅调大、周期权重取成整数比,三条反证都会立刻报警。
七、目录结构
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。若宿主环境对
「非临时路径的删除」设了拦截,脚本开头清理上次残留那一步会被拦下——
此时把落点指到系统临时区即可,产物内容与判据完全不变。
572 项单元测试,覆盖最容易出错的地方:字数换算、时长模型、字幕断行、编码参数与 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 属性。项目卡片的按钮是
拼串生成的,拼错一个引号,拼串本身仍是合法字符串——语法检查照过,浏览器要到点下去
才在控制台报错,界面上只表现为「点了没反应」。所以另有一道浏览器外的渲染体检
(_smoke/_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
更新说明
v0.12.2
历史期的成片终于有了试听入口:产物区从前只有「任务刚合成完」一条填充路径,刷新页面、重启服务之后永远显示「尚无产物」。现在历史列表点「报告」同样画出文件链接、成片播放器与门禁表——报告接口本来就算好了这一期的全部产物路径,只是把它扔掉了。更新日志的结构也一并归一:节序、条目形态、引言与计数,15 个版本一个样子。修复一项、变更一项。
修复
- 历史期成片没有试听入口:产物区(文件链接与
<audio>/<video>播放器)只由「任务完成」事件填充,历史期的episode_files在api_report里算好后被丢弃,前端viewReport也只画门禁表——文件在手上、两端各扔一半。修复:api_report把路径表随报告一起回,viewReport改走showOutputs,与「刚合成完」共用同一份渲染,不另写第二份。
变更
- 更新日志结构归一:节序统一为 新增/变更/修复/说明(此前 新增→修复→变更 与 新增→变更→修复 两套并存,v0.11
v0.12 各版又各自为政);39 条裸条目补齐加粗短标题(v0.12.1 六版补引言与全版计数、v0.9.0 补计数句;v0.6.0 引言 11 段 1832 字压缩为 3 段。条目内容零信息损失。- 配置总表:…→- **配置总表**:…,说明节的说明性条目同样处理);v0.10.0
Release files for podcast-maker-ldxs 0.12.2
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-0.12.2-py3-none-any.whl | Python 3 | none | any | Details |
Release files / podcast_maker_ldxs-0.12.2-py3-none-any.whl
| Download URL | podcast_maker_ldxs-0.12.2-py3-none-any.whl |
|---|---|
| Size | 22.0 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ef97739764016fa991929cdf873409f38ee14ec8d68d6b6311d9559c05fb9092
|
|
BLAKE2b-256 checksum How to use checksums |
14edc64137538997070e310b6e11468f59b97d7a09d847d3679bd0a8f48eec97
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.3
|