ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VoiceStudio:本地文本转语音与音频后处理批量工作台

VoiceStudio:本地文本转语音与音频后处理批量工作台 有声书和播客做多了绕不开一个很现实的问题稿子写完只是第一步真正耗时间的是把文字变成能听的声音。找配音外包一集二十分钟的成品报价不低改一个数字都要重录自己录环境噪声、口水音、气息不稳返工率极高。我去年帮朋友收尾一档三十多集的有声内容中途把整条链路推倒重做了一遍最后收敛成一个本地工作台我把它叫 VoiceStudio。它不是什么新奇概念本质是把文本规范化、语音合成、音频后处理、批量调度这四件事塞进一套可复现的流程里让一个人也能稳定产出接近成品质量的音频。这篇文章面向三类人想给自己内容配旁白但预算有限的创作者、需要批量生成语音素材的产品和测试同学、以及想把语音链路完整跑通一次的工程新手。下面我把设计思路、参数算法、实操代码和踩过的坑全部摊开讲能直接抄的部分我都会给出来。1. VoiceStudio 整体设计与思路拆解1.1 先想清楚要解决的真实问题很多人一上手就去追最像真人的模型结果跑完发现整体流程根本跑不通。我在项目初期犯过同样的错花了两天调一个语音克隆模型最后卡在文本里的日期、金额、英文缩写全读错客户第一耳朵就听出来了。所以 VoiceStudio 的第一条设计原则是——先解决读得对再解决读得好听最后才谈像不像。这三件事的优先级不能颠倒因为错误的读音是硬伤音色差一点反而观众能接受。具体拆下来实际需求无非这么几类一是长文本批量转语音一次要处理几万字二是固定音色的一致性同一档节目三十集不能每集声音都变三是音频质量要过关不能有明显底噪、忽大忽小的音量四是可控出问题能定位到是文本环节还是模型环节。这四个需求决定了整个系统的形态——它不能只是一个模型推理脚本必须是一个带队列、带缓存、带日志的流水线。我先用一句话定义它VoiceStudio 是一个把非结构化文本经过规范化、切分、合成、后处理稳定输出为可交付音频文件的工作台。注意稳定和可交付这两个词它们才是工程价值的所在也是绝大多数个人项目夭折的地方——demo 能跑量产就崩。1.2 三层架构前端、模型层、工程层整个系统我拆成了三层这样出问题的时候能快速判断该看哪一层。文本前端层负责把所有输入变成模型能吃的干净音素序列。这一层做的事情比想象中多全角半角统一、繁简转换、数字转读法2024读二零二四还是两千零二十四、单位符号展开3.5kg要读成三点五千克、中英混读的语种切分。这一层做不好后面模型再强也白搭。模型层负责声学建模和波形还原也就是从音素序列到梅尔频谱、再从梅尔频谱到波形。这一层是大家最关注的但说实话对最终交付质量的影响可能只占一半。工程层负责调度、并发、缓存、日志、失败重试、格式转换。这一层最不起眼但它决定了你能不能睡觉的时候让它自己跑完五千条音频。我后来的习惯是任何一条流水线先把工程层搭起来再接模型因为模型可以换工程骨架搭好了就不用动。三层之间通过明确的中间产物解耦前端输出带音素标注的 JSON模型层输出梅尔频谱的 npy 文件后处理层输出 wav最终封装成 mp3 或 flac。每一步都可单独重跑这在调试时能省掉大量时间——改一个数字读法不用重新跑模型。1.3 为什么坚持本地优先而不是全云端这里要说清楚我的取舍逻辑不是反对云服务而是场景决定的。批量合成语音有个特点单次请求便宜但量大之后成本线性增长而且网络往返带来的不确定性很难处理。五千条音频如果走外部接口排队、限流、偶发失败重试光是把失败率压下去就要写一堆补偿逻辑。本地部署的好处是成本固定、可重复、数据不出本机。坏处也明显需要一块像样的显卡环境配置有门槛模型更新要自己维护。我的判断标准很简单——如果这个项目你预计要跑超过两万次合成本地就更划算如果只是偶尔生成十几条直接用现成服务省事得多。还有一点容易被忽略本地部署才方便做精细化的后处理链。合成出来的原始波形几乎不能直接用必须过降噪、响度归一化、去齿音、限幅。这些处理放在本地 ffmpeg 里跑参数随便调一遍不满意再跑一遍成本几乎为零。这个反复试错的空间是本地方案最大的隐性价值。2. 核心模块拆解与关键参数解析2.1 文本前端读对字比声音好听更重要文本规范化Text Normalization业内简称 TN是整条链路里最容易被低估的环节。我统计过第一批出问题的样本差不多七成的抱怨集中在读音上电话号码被当成了一个巨大的整数读出来版本号v2.1读成了v二点一英文缩写AI读成了字母拼读还是单词读音没控制住。我的做法是写一套规则引擎加词典兜底顺序是先做字符归一化再做规则匹配最后用自定义词典覆盖。规则用正则按优先级排序执行顺序很关键比如必须先处理日期再处理数字否则2024-05-01里的数字会先被单独展开。import re # 规则按优先级从高到低执行顺序不能乱 RULES [ # 日期2024-05-01 - 二零二四年五月一日 (re.compile(r(\d{4})-(\d{1,2})-(\d{1,2})), lambda m: f{m.group(1)}年{m.group(2)}月{m.group(3)}日), # 时间14:30 - 十四点三十分 (re.compile(r(\d{1,2}):(\d{2})), lambda m: f{m.group(1)}点{m.group(2)}分), # 版本号v2.1 - v二点一保留字母 (re.compile(r\bv(\d(?:\.\d))\b, re.I), lambda m: v m.group(1).replace(., 点)), # 百分号35% - 百分之三十五 (re.compile(r(\d(?:\.\d)?)%), lambda m: f百分之{m.group(1)}), ] def normalize(text: str) - str: for pattern, repl in RULES: text pattern.sub(repl, text) return text数字读法要区分场景年份、编号、电话号码按逐位读金额、数量按数值读。这个判断得靠上下文关键词比如前面出现第编号电话就逐位读。我一开始图省事全用逐位读结果1500元读成一五零零元立刻被听出来。后来加了一张上下文关键词表准确率才上来。中英混读是另一个大坑。中文模型遇到英文单词往往逐字母读英文模型遇到中文又直接跳过。可行方案是做语种切分把文本按语言切成片段分别用对应前端处理标注上语种标签再送进多语种模型。切分逻辑我是按连续 ASCII 字符视作英文片段、连续 CJK 字符视作中文片段来做的效果够用。要注意标点归属——句末的句号应该跟在前一个片段后面否则韵律会断得很奇怪。注意文本规范化不需要追求百分之百覆盖但必须留一个人工校对清单。我通常让脚本把所有被规则改动过的片段打印出来扫一眼就能发现误判比合成完再听效率高得多。还有个小细节数字、英文、标点在送进模型前要统一成半角还是全角必须和训练数据保持一致。有些开源模型训练时用的是全角标点你喂半角进去韵律预测会莫名其妙地变差。这个坑我踩了整整一天最后靠对比训练数据的预处理脚本才发现。2.2 声学模型与声码器的搭配选择模型层要拆成两半看声学模型负责什么内容用什么语调声码器负责把频谱还原成波形。这两部分可以独立替换理解这一点非常重要因为它意味着你可以用一个成熟的声学模型配一个更新的声码器来提升音质而不必重训整条链路。声学模型的主流路线大致两类。一类是自回归结构逐帧预测音质自然但推理慢、容易在长句上出现重复或漏字另一类是非自回归结构一次性预测整段频谱速度快、稳定但早期版本的自然度稍弱。我的选择倾向很明确批量生产场景优先选非自回归因为稳定性和速度的收益远大于那一点点自然度差距。长文本合成最怕的就是某一条卡住或者读漏自回归模型在这方面的不确定性会成倍放大运维成本。声码器这边从早期的基于循环网络的结构到后来的生成对抗网络方案再到现在的多周期判别器方案音质提升很明显。选型时我关注三个指标实时率RTF、显存占用、对异常频谱的鲁棒性。RTF 小于 1 表示能实时生成实际项目中我要求 RTF 低于 0.1也就是十秒音频一秒出这样批量任务才跑得动。参数上几个关键点必须对上号否则频谱和声码器对不上会出电音或者嘶嘶声参数常见取值说明采样率22050 / 24000 / 44100 Hz必须与声码器训练时一致不一致会变调帧移 hop_length256 / 300决定时间分辨率22050 下 256 约等于 86 帧/秒窗长 win_length1024 / 1200一般为帧移的 4 倍FFT 点数 n_fft1024 / 2048需大于等于窗长梅尔维度80 / 100 / 128声学模型和声码器必须一致这里有个简单的换算必须记住音频时长秒 帧数 × hop_length / 采样率。22050 采样率、hop 256 的情况下一秒音频对应 22050 / 256 ≈ 86.13 帧。做批量任务的时候我会先用这个公式估算总时长和磁盘占用避免跑到一半发现硬盘满了。按 22050 采样率、16 位单声道算一分钟 wav 大约 2.6 MB五千条每条一分钟就是 13 GB这个量级心里得有数。2.3 音频后处理链从裸波形到可交付成品模型直接吐出来的波形直接发布是不行的。我固定跑这么一条链去直流偏移 → 降噪 → 均衡 → 齿音抑制 → 响度归一化 → 真峰值限幅。顺序不是随便定的。降噪要放在最前面因为后面任何增益类操作都会把噪声一起放大。响度归一化要放在限幅之前否则归一化之后的峰值可能超限。齿音抑制放在均衡之后因为均衡如果提升了高频齿音会更明显顺序反了就白做。响度这块必须讲清楚因为它是听起来专业和听起来业余的分水岭。行业里用的是基于门限的响度测量方法ITU-R BS.1770 标准单位是 LUFS。不同平台的投放目标不一样投放场景目标响度真峰值上限播客/有声书-16 LUFS-1.5 dBTP通用流媒体-14 LUFS-1.0 dBTP广播级-23 LUFS-1.0 dBTP为什么强调真峰值True Peak因为采样点峰值不超不代表转换成模拟信号后不超。真峰值会做上采样插值捕捉到采样点之间的过冲。如果只按采样峰值卡在 0 dBFS播放设备解码后可能削波出现刺耳的破音。ffmpeg 里做响度归一化推荐用两遍法第一遍测量第二遍应用# 第一遍只测量输出 JSON 报告 ffmpeg -i raw.wav -af loudnormI-16:TP-1.5:LRA11:print_formatjson -f null - # 第二遍把测量值填进去做线性归一化 ffmpeg -i raw.wav -af loudnormI-16:TP-1.5:LRA11:\ measured_I-22.3:measured_TP-3.1:\ measured_LRA6.2:measured_thresh-33.0:\ offset0.4:lineartrue -ar 24000 -c:a pcm_s16le out.wav为什么不用一遍法因为一遍法内部是动态调整会引入压缩感声音会瘪。两遍法把测量和应用分开用的是线性增益音色不变。实测下来两遍法处理后的音频在听感上明显更开阔尤其是动态范围大的素材。提示降噪不要开太猛。我见过太多人把降噪拉到最大结果人声出现类似水下的金属感。经验值是降噪强度控制在 6 到 12 dB 之间宁可留一点点底噪也不要让声音失真。底噪在最终混音里几乎听不见失真却藏不住。去直流偏移容易被完全忽略。某些模型的输出会有微小的直流分量表现为波形整体偏离零轴。这个偏移在听感上不明显但会让后续的归一化和限幅判断失准。处理方法很简单一句高通滤波就能解决频率设在 20 到 40 Hz 之间人耳听不到这个范围不会损伤音质。3. 从零搭一套可用的 VoiceStudio3.1 环境和依赖怎么装先给结论不要用最新版本的框架用稳定版并锁定版本号。语音这条链路依赖链条长音频库、深度学习框架、推理运行时之间有版本约束跨版本升级带来的坑远大于收益。我的习惯是把所有依赖写进一个锁定文件环境重建时一字不差。核心依赖大致分四块深度学习框架提供张量计算和模型加载、音频读写库、文本处理库、以及音视频工具 ffmpeg。ffmpeg 一定要装带 libmp3lame 和 loudnorm 滤镜的完整版很多精简版缺滤镜跑起来会报 No such filter。# 创建独立环境Python 版本我用 3.10兼容性最好 python -m venv venv source venv/bin/activate # 核心依赖版本号按你实际验证过的锁死 pip install torch2.1.0 torchaudio2.1.0 pip install numpy1.24.3 soundfile0.12.1 librosa0.10.1 pip install fastapi0.104.1 uvicorn0.24.0 pip install pypinyin0.49.1 jieba0.42.1 opencc-python-reimplemented # ffmpeg 建议用系统包管理器装完整版 # Ubuntu: sudo apt install ffmpeg # macOS: brew install ffmpeg ffmpeg -filters | grep loudnorm # 验证 loudnorm 滤镜存在显卡驱动和推理运行时这块要特别注意版本匹配。如果用 GPU 推理深度学习框架版本、CUDA 版本、驱动版本三者必须兼容错一个就会出现能 import 但一跑就崩的诡异现象。我的排查习惯是先跑一句最小验证创建一个张量丢到 GPU 上做一次矩阵乘法能跑通说明这条链路没问题再去怀疑模型本身。如果只有 CPU 或者显卡显存较小比如 6 GB 以下那就别硬上大模型。选参数量小的模型配合半精度推理速度能接受。我实测过一个中等规模的声学模型加一个轻量声码器CPU 上 RTF 大概在 0.5 到 1.5 之间也就是生成一分钟音频要 30 到 90 秒换到一块中端显卡上RTF 能压到 0.05 左右差距是几十倍。所以如果你的量级超过几百条一块显卡的投入是非常值的。3.2 目录结构与配置设计目录结构要按输入、中间产物、输出、日志四类分开别混在一起。好处是清理缓存特别方便改模型的时候直接把中间产物目录删掉重跑就行不用怕误删原始数据。voicestudio/ ├── config/ │ ├── default.yaml # 全局默认配置 │ └── voices/ # 每个音色一份配置 ├── data/ │ ├── input/ # 原始文本 │ ├── normalized/ # 规范化后的文本 │ ├── mel/ # 中间产物梅尔频谱 │ └── output/ # 最终音频 ├── logs/ │ ├── synth.log # 合成日志 │ └── errors.log # 失败记录 ├── core/ │ ├── tn.py # 文本规范化 │ ├── g2p.py # 音素转换 │ ├── synth.py # 合成主逻辑 │ └── postprocess.py # 后处理链 └── run_batch.py # 批处理入口配置文件用 YAML 而不是写死在代码里理由很实际参数需要反复试写在配置里改完重启就行写在代码里每次都怕改错。下面这份是我的默认配置几个关键项我都会解释。audio: sample_rate: 22050 hop_length: 256 win_length: 1024 n_fft: 1024 mel_channels: 80 synth: device: cuda # cuda 或 cpu batch_size: 8 # 显存 8G 以内建议 4~8别贪大 max_chars_per_chunk: 120 # 单次合成的最大字符数超了会切分 silence_ms: 180 # 句间静音太短会连读太长会拖沓 postprocess: denoise_db: 9 highpass_hz: 30 target_lufs: -16 true_peak_db: -1.5 output_format: wav runtime: max_workers: 2 # 并发生成数显存不够就调小 retry: 2max_chars_per_chunk这个参数特别重要。文本太长一次性送进模型容易在中间出现漏读、重复甚至显存爆掉。我的经验值是中文按 100 到 150 字切一段英文按 200 到 300 字符切一段。切分点优先选标点没有标点就用逗号再没有就硬切但硬切的时候要在切点插短暂停顿否则听感上会很突兀。silence_ms是句间静音时长。180 毫秒是个比较舒服的值接近正常朗读的换气间隔。设成 50 毫秒整段听起来像赶稿子设成 500 毫秒又像在等对方回答。有声书场景我会把它调到 250 到 350 毫秒因为听众需要一点消化时间。3.3 核心链路代码主流程其实就是一条直线关键是把每一步的产物落盘方便断点重跑。import json from pathlib import Path from core import tn, g2p, synth, postprocess def synthesize_one(text: str, voice_cfg: dict, work_dir: Path) - Path: 单条文本 - 成品音频 # 1. 文本规范化 norm_text tn.normalize(text) (work_dir / normalized.txt).write_text(norm_text, encodingutf-8) # 2. 音素转换带语种标签 phonemes g2p.convert(norm_text, langvoice_cfg[lang]) # 3. 分片合成长文本必须切 chunks synth.split_chunks(phonemes, max_charsvoice_cfg[max_chars_per_chunk]) wavs [] for i, chunk in enumerate(chunks): mel synth.text_to_mel(chunk, voice_cfg) wav synth.mel_to_wav(mel, voice_cfg) wavs.append(wav) # 4. 拼接并插入静音 merged synth.merge_with_silence(wavs, silence_msvoice_cfg[silence_ms]) # 5. 后处理 out postprocess.run_pipeline(merged, voice_cfg[postprocess]) out_path work_dir / output.wav out.save(out_path) return out_path每一步落盘的意义在于如果第 5 步的参数不满意可以直接读第 4 步的中间文件重跑不用再花时间合成。我实测过后处理链跑一遍一分钟音频大概 2 到 3 秒而合成要 5 到 20 秒差了一个数量级。调参阶段这个设计能省掉大量等待时间。后处理链的实现我把每一步单独写成函数串起来跑import subprocess def run_pipeline(wav_path: Path, cfg: dict) - Path: filters [ fhighpassf{cfg[highpass_hz]}, # 去直流和低频噪声 fafftdnnr{cfg[denoise_db]}, # 频域降噪 equalizerf200:tq:w1:g-2, # 削一点浑浊的 200Hz deesser, # 齿音抑制 floudnormI{cfg[target_lufs]}:TP{cfg[true_peak_db]}:LRA11, alimiterlimit0.95, # 兜底限幅 ] out wav_path.with_name(final.wav) cmd [ ffmpeg, -y, -i, str(wav_path), -af, ,.join(filters), -ar, 24000, -ac, 1, -c:a, pcm_s16le, str(out) ] subprocess.run(cmd, checkTrue, capture_outputTrue) return out这里equalizer那一行是我个人的偏好人声在 200 Hz 附近容易堆积出闷的感觉稍微削 2 dB 会通透一些。deesser是 ffmpeg 内置的齿音抑制参数用默认值就够。最后的alimiter是兜底防止某些素材经过响度处理后仍然过冲。要提醒一点afftdn的nr参数单位是分贝不是百分比。设成 9 表示降噪 9 dB设成 30 就是灾难性的失真。我第一次用的时候想当然填了 30出来的人声像在水里说话。3.4 批量合成与并发控制批量任务的核心矛盾是显存有限但任务很多。我的做法是用一个固定大小的任务队列加线程池同时跑的任务数不超过显存能承受的上限。from concurrent.futures import ThreadPoolExecutor, as_completed from pathlib import Path import logging logging.basicConfig( filenamelogs/synth.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s ) def run_batch(tasks: list, voice_cfg: dict, max_workers: int 2): results {ok: 0, fail: 0, failed_ids: []} with ThreadPoolExecutor(max_workersmax_workers) as pool: futures { pool.submit(synthesize_one, t[text], voice_cfg, Path(t[work_dir])): t[id] for t in tasks } for fut in as_completed(futures): tid futures[fut] try: fut.result() results[ok] 1 logging.info(fOK {tid}) except Exception as e: results[fail] 1 results[failed_ids].append(tid) logging.error(fFAIL {tid}: {e}) return results几个关键点。第一失败必须记录任务 ID 而不是只记异常因为批量跑完之后你要能精确地重跑失败的那些而不是全部重来。第二并发数要和显存匹配判断方法很简单从 1 开始往上加直到显存占用稳定在 85% 左右就停。加到爆显存再往回退纯属浪费时间。第三日志一定要带时间戳和任务 ID出问题的时候能顺着日志把整个过程还原出来。我一般还会在批处理外面套一层重试。第一次跑完之后把失败列表单独拿出来跑第二遍很多失败是偶发的显存碎片或者临时IO问题重试一次大部分能过。重试两次还失败的那就是文本或者配置本身有问题需要人工看。提示批处理启动前先跑三条样本确认音色、语速、音量都符合预期再全量跑。我吃过一次亏配置里音色路径写错了一晚上跑完三千条第二天发现全是错的声音只能全部重来。4. 常见问题与排查技巧实录4.1 问题速查表把这段时间遇到的典型问题整理成一张表方便按现象快速定位。现象可能原因排查方法解决声音发闷、像隔了层布高频被削、降噪过猛关掉降噪单独听降噪降到 12 dB 以内检查均衡是否过量削高频有金属感、水下音降噪强度过高对比原始波形降噪降到 6~9 dB或改用更温和的降噪方式声音忽快忽慢分片长度差异大、静音不均检查 chunk 长度分布限制单段字符数浮动范围固定静音时长长句漏字或重复自回归模型长序列不稳定看频谱是否在某处重复缩短分片长度或换非自回归模型整体音量偏小响度归一化未生效测量输出 LUFS确认 loudnorm 滤镜存在且参数正确转 mp3 后爆音编码器前峰值超限检查真峰值归一化时把真峰值卡在 -1.5 dBTP高音区出现电音采样率不匹配核对配置与模型三者采样率、帧移、梅尔维度统一某个词读音固定错文本规范化规则误判打印规范化前后对比加自定义词典覆盖显存溢出并发过高或分片过长看显存曲线降并发、缩短分片进度突然卡住单条任务死锁或IO阻塞看最后一条日志加重试和超时机制4.2 几个文档里不会写的坑第一坑是静音段也会被降噪处理。ffmpeg 的频域降噪对纯静音段会做一些奇怪的估计有时候反而在静音里引入微弱的呼吸声。解决办法是在拼接阶段让静音段完全为零或者在后处理时对静音段做门限处理低于某个阈值的直接归零。第二坑是音频拼接处的爆音。两段波形直接首尾相接如果接点处振幅不为零就会产生一个突变的阶跃听起来是啪的一声。解决方法是拼接时加极短的交叉淡化或者保证每段首尾的振幅接近零。我是用 5 毫秒的线性交叉淡化解决的听感上完全察觉不到。第三坑是不同批次的音频响度不一致。这是因为后处理链的响度归一化是有状态的素材本身动态范围不同处理结果会有偏差。我的做法是先在整批素材上跑一遍测量取中位数作为统一目标再逐条应用。这样整批的响度分布会紧得多连续播放不会出现忽大忽小。第四坑是文件系统的大目录性能问题。中间产物全部塞在一个目录里跑了几万条之后ls都要等好几秒。解决办法是按任务 ID 的前两位做二级目录分片或者干脆用哈希分桶。这个是工程细节但量一大就会变成瓶颈。第五坑是模型加载的内存泄漏。如果用 web 服务常驻内存的方式提供服务反复加载卸载模型偶尔会残留显存。我的做法是服务启动时一次性加载好所有需要的模型运行期只做推理不动态加载。如果需要切换音色就把所有音色模型都常驻用音色 ID 索引。显存换稳定性这个交易很划算。5. 性能调优与效果评估5.1 推理加速的几条路速度优化的收益递减很厉害所以要从性价比最高的开始做。第一层是批处理。单条推理的时候显卡的计算单元大量空闲。把 8 条短的合到一起跑一次总耗时可能只比单条多 20%。这是性价比最高的一招但要注意长度对齐把长度相近的排在一批避免为了对齐做大量填充。第二层是半精度推理。把模型权重和中间激活值降到 16 位浮点显存占用减半速度提升大概 30% 到 50%。音质损失在听感上几乎察觉不到我对比过全精度和半精度的输出用频谱对比工具看差异极小。唯一要注意的是某些算子在半精度下数值不稳定如果出现异常值把这几层单独保持全精度。第三层是模型编译和算子融合。把模型导出成中间表示格式用推理运行时优化能再快一截。这层收益视模型结构而定改动成本也高一些通常放到最后做。还有一个经常被忽略的点IO 优化。音频读写和磁盘操作在批量任务里占的时间比想象中大。我的做法是把中间产物改用内存文件系统暂存跑完再落盘实测能省掉 15% 左右的总时间。如果磁盘是机械盘这个提升会更明显。优化到后面你会发现瓶颈往往不在模型而在数据搬运和文件操作上。所以别一上来就想着换更快的模型先用性能分析工具看清楚时间花在哪里。我一般的顺序是先跑一遍带计时的最小样本把预处理、推理、后处理、IO 四块的时间占比列出来再决定优化哪一块。5.2 怎么判断这个声音能不能交付主观判断很容易被自己的耳朵带偏尤其是自己听了几十遍之后。我后来固定了一套客观检查流程先过机器再过耳朵。客观指标上我最看重三个响度是否落在目标区间偏差不超过 1 LUFS、真峰值是否低于上限、静音段是否有残留噪声。这三个都能用工具一键检测。再加一个语速检测算出每秒字数如果某条明显偏离平均值超过 20%大概率是分片或静音出了问题值得单独听一遍。import subprocess, json def measure_loudness(path: str) - dict: cmd [ ffmpeg, -i, path, -af, loudnormprint_formatjson, -f, null, - ] r subprocess.run(cmd, capture_outputTrue, textTrue) # ffmpeg 把 JSON 输出到 stderr tail r.stderr[r.stderr.rfind({):r.stderr.rfind(}) 1] return json.loads(tail) def check(path: str, target_lufs-16, max_tp-1.5) - bool: m measure_loudness(path) ok_loud abs(float(m[input_i]) - target_lufs) 1.0 ok_peak float(m[input_tp]) max_tp if not ok_loud: print(f[响度超标] {path}: {m[input_i]} LUFS) if not ok_peak: print(f[峰值超标] {path}: {m[input_tp]} dBTP) return ok_loud and ok_peak主观检查我有个小技巧把待检查的音频和一条已知合格的音频交替播放。人的听觉记忆很短单独听一条很难判断质量但两条交替听差异会立刻暴露。我习惯用同一个参考音频作为基准每条都和它对比。还有一点是关于风格一致性的。批量合成的音频即使音色相同也可能因为文本内容不同导致语调差异很大——一段全是短句的文本和一段长句为主的文本合成出来的节奏感完全不一样。我的做法是在后处理之后加一个统一的整体效果比如统一的轻微压缩和一致的响度目标让整批素材听起来像同一个环境下录的。这个处理能把拼凑感压下去不少。最后一个经验是留出人工抽检比例。全量自动检查能过滤掉明显的技术问题但好不好听这件事还是要靠人。我通常按 5% 到 10% 的比例随机抽检重点听句子衔接处、数字和英文出现的位置、以及每一条的开头和结尾。这三个位置是最容易出问题的也都是听众最容易注意到的。如果后面要扩展我第一个会加的是多音色管理把音色、语速、情感参数做成可组合的配置让同一个脚本能按需输出不同风格。第二个是对齐校验把合成音频和原始文本做时间对齐自动定位到具体哪个字读错了这样抽检效率能再提一个台阶。
返回列表