这次构建的是一个偏“场次播报 + 本地语音合成 + 批量导出”的小型自动化工作流:以260812 本命巡演 石家庄站这类信息作为输入,批量产出用于群内通知、日程提醒、彩排版语音短讯。整套流程完全跑在本地,语音引擎可以接本地开源模型,也可以接在线授权服务,接口层做成统一封装,后面换引擎不用动业务代码。
先说清楚边界:这套东西不会去复刻歌手本人声音,也不做任何真人音色的未授权模拟。合法做法是使用开源音色、授权音色或你自己录制的素材,合成内容是场次播报和通知文案,不参与任何伪造、仿冒或未授权商业用途。能用得住的本地 AI 工具,前提永远是授权链路清晰、使用边界明确。
文章会从环境准备、目录结构、批量任务、接口 API、性能观察、常见问题六个方向展开。整个过程可以先跑通最小样例,再做正式场次批量生成。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 本地语音合成 + 批量音频转场通知工作流 |
| 输入数据 | 文本、JSONL 场次信息、批量文本目录 |
| 输出格式 | mp3 / wav,目录可指定 |
| 合成引擎 | 预留接口,可接本地 TTS 模型或在线授权 TTS 服务 |
| 支持平台 | Windows、Linux、macOS,取决于语音引擎和依赖 |
| 建议配置 | CPU 可运行基础场景;模型推理需按具体引擎确认 GPU |
| 启动方式 | Python 脚本批量处理 / FastAPI 服务接口 |
| API 能力 | 支持单条合成、批量合成、任务状态查看 |
| 批量任务 | 支持目录批量处理、JSONL 批量导入、断点续跑 |
| 合规要求 | 不使用真人未经授权音色,不用于伪造和仿冒场景 |
这个能力表对应的不是某个闭环商业软件,而是一个可自己实现的工程结构。核心价值在于三层解耦:文本输入层、TTS 引擎层、音频输出层分开,后续引擎升级、文案调整、导出格式改造都不需要重写整套流程。
2. 适用场景与使用边界
2.1 适合谁
- 巡演或演出项目的执行人员:需要把场次信息快速生成手机播报、车载提示或语音备忘录。
- 做本地自动化脚本的技术同学:想把文本通知变成可批量复用的语音文件,并开放一个内部调用 API。
- 想做语音合成评测的开发者:需要一套统一接口来对比不同 TTS 引擎在中文长句、数字播报、时间表达上的效果。
- 需要批量处理音频素材的内容团队:先合成一批草稿音频,再进入人工精修,减少录制成本。
2.2 不适合什么
- 不适合做真人歌手声音的无授权克隆。本地语音模型虽然可以训练音色,但使用真实艺人、真实公众人物的声音,必须获得明确授权。本文不提供相关素材、训练教程或调用实现。
- 不适合用语音合成伪造身份信息。验证码语音、银行通知、客服录音等高风险身份关联场景,不能使用不可信或未授权的合成音色。
- 不适合对版权文本做批量商业语音化。歌词、书籍、杂志长文等是否有朗读权、复制权、信息网络传播权,需要单独确认授权范围。
2.3 使用边界梳理
| 场景 | 是否可行 | 前置条件 |
|---|---|---|
| 把“请于 8 月 12 日 18:30 到石家庄站集合”生成语音提醒 | 可以 | 文案自有或获授权 |
| 用开源音色生成播报样音 | 可以 | 确认模型与音色许可 |
| 复刻某真实歌手的音色并生成其名义音频 | 不可以 | 需要艺人本人或权利方明确授权 |
| 用合成音频伪装联系人身份 | 不可以 | 无任何合法前提 |
3. 环境准备与前置条件
在开始写代码前,先准备好本地环境。
3.1 基础环境
建议使用 Python 3.10 或 3.11,64 位系统。如果你只需要纯 CPU 做简单 TTS,普通办公电脑即可;如果要换更大的本地语音推理模型,才需要考虑独立显卡和显存。
需要提前安装的工具:
- Python 虚拟环境管理工具,比如
venv或conda。 - FFmpeg,用于音频格式转换、采样率调整和拼接。
- 一个可用的 TTS 引擎。这里不会绑死某个具体模型。你可以选择本地开源模型、官方 SDK、或自建模型服务。
FFmpeg 安装成功后,命令行执行ffmpeg -version能看到版本信息。
3.2 FFmpeg 检查方式
ffmpeg -version如果把 ffmpeg 安装到了自定义目录,需要把可执行文件目录加入PATH。Windows 用户可以使用终端执行:
where ffmpeg如果找不到,需要手动配置系统环境变量,或在 Python 代码中显式指定ffmpeg路径。
3.3 Python 虚拟环境准备
python -m venv .venvWindows 激活:
.venv\Scripts\activateLinux / macOS 激活:
source .venv/bin/activate激活后,确认当前 Python 路径来自虚拟环境:
where python3.4 音频输出目录规划
建议保持一个清晰的输入输出结构:
audio_batch_project/ ├── .venv/ ├── app.py ├── batch_generator.py ├── engine_adapter.py ├── requirements.txt ├── config.json ├── data/ │ ├── input/ │ │ └── schedule.jsonl │ └── output/ │ └── audio/ ├── logs/ │ └── run.log输入文件放在data/input/,合成的音频统一输出到data/output/audio/,日志落在logs/。这样批量任务出问题时,能快速定位是哪个文件、哪个字段、哪条任务失败。
4. 本地部署与启动
4.1 依赖清单
requirements.txt只需要保留与核心流程相关的内容:
fastapi==0.111.0 uvicorn==0.30.1 pydantic==2.7.4 python-multipart==0.0.9 requests==2.32.3如果你用的是某个具体 TTS 引擎,需要额外查看它的官方安装文档,把对应依赖追加到requirements.txt里。不要盲目从网络上复制一长串依赖清单,没用的包只会增加环境冲突概率。
4.2 语音引擎适配层
为了让业务脚本不绑定具体 TTS 服务,先定义一个统一的引擎接口engine_adapter.py:
import abc class TTSEngine(abc.ABC): """统一 TTS 引擎接口,所有具体引擎需要实现 synthesize 方法。""" @abc.abstractmethod def synthesize(self, text: str, output_path: str) -> None: """把 text 合成语音并写入 output_path。""" raise NotImplementedError class DemoTTSEngine(TTSEngine): """一个最简的演示适配器,实际使用时应换成已授权且可运行的引擎实现。""" def synthesize(self, text: str, output_path: str) -> None: # 这里只演示接口写法,不真正生成有用音频。 with open(output_path, "wb") as f: f.write(b"# demo audio placeholder")在实际项目中,你需要新建一个engine_xxx.py,把官方 SDK 的调用包成TTSEngine子类。这样后续换引擎时,只新增文件即可,不再改动批量任务和 API 代码。
4.3 场次信息批量生成脚本
处理批量任务时,建议把输入做成 JSONL 格式,每行是一个独立合成任务。这样即使某个任务失败,也不会影响整个文件读取。
新建batch_generator.py:
import json import os import time from pathlib import Path from engine_adapter import TTSEngine def load_tasks(input_file: str): """读取 JSONL 任务,每行格式见 data/input/schedule.jsonl""" tasks = [] with open(input_file, "r", encoding="utf-8") as f: for line_number, line in enumerate(f, start=1): line = line.strip() if not line: continue task = json.loads(line) task["_line_number"] = line_number tasks.append(task) return tasks def build_audio_path(task: dict, output_dir: str) -> str: """根据任务字段生成输出文件名。""" date_str = task.get("date", "unknown") order_id = task.get("id", "task") return os.path.join(output_dir, f"{date_str}_{order_id}.mp3") def run_batch(engine: TTSEngine, input_file: str, output_dir: str, fail_log: str) -> None: tasks = load_tasks(input_file) os.makedirs(output_dir, exist_ok=True) failed_file = Path(fail_log) failed_file.parent.mkdir(parents=True, exist_ok=True) failed_tasks = [] success_count = 0 for task in tasks: text = task.get("text", "") if not text.strip(): failed_tasks.append({"line": task["_line_number"], "reason": "empty text"}) continue output_path = build_audio_path(task, output_dir) try: engine.synthesize(text, output_path) success_count += 1 print(f"[OK] {output_path}") except Exception as exc: failed_tasks.append({"line": task["_line_number"], "reason": str(exc)}) # 避免短任务一次性把 CPU/网络占满 time.sleep(0.2) with open(fail_log, "w", encoding="utf-8") as f: json.dump(failed_tasks, f, ensure_ascii=False, indent=2) print(f"完成,成功 {success_count},失败 {len(failed_tasks)} 条,失败明细见 {fail_log}") if __name__ == "__main__": import importlib import sys # 在这里切换引擎,可改为你自己的引擎类 engine_cls = DemoTTSEngine demo_engine = engine_cls() run_batch( engine=demo_engine, input_file=sys.argv[1] if len(sys.argv) > 1 else "data/input/schedule.jsonl", output_dir=sys.argv[2] if len(sys.argv) > 2 else "data/output/audio", fail_log="logs/failed.json" )执行方式:
python batch_generator.py data/input/schedule.jsonl data/output/audio任务结束以后,优先看日志里的失败数量,不要凭耳朵判断哪些文件没生成。
4.4 API 服务启动
除了批量脚本,还可以用 FastAPI 提供一个轻量服务,方便后续接入内部系统或自动化工具。
新建app.py:
import os import uuid from pathlib import Path from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from engine_adapter import DemoTTSEngine class SynthesizeRequest(BaseModel): text: str = Field(..., description="需要合成的文本") task_id: str = Field(default="", description="可选,传自己的任务编号") app = FastAPI(title="场次播报语音合成 API") output_root = Path("data/output/audio") output_root.mkdir(parents=True, exist_ok=True) engine = DemoTTSEngine() @app.post("/api/synthesize") def synthesize(req: SynthesizeRequest): if not req.text.strip(): raise HTTPException(status_code=400, detail="text 不能为空") task_id = req.task_id or uuid.uuid4().hex output_path = output_root / f"{task_id}.mp3" try: engine.synthesize(req.text, str(output_path)) except Exception as exc: raise HTTPException(status_code=500, detail=f"合成失败: {exc}") return { "task_id": task_id, "output_file": str(output_path), "status": "success" } @app.get("/health") def health(): return {"status": "ok"}启动服务:
uvicorn app:app --host 127.0.0.1 --port 7860如果要自定义端口,可以换成:
uvicorn app:app --host 127.0.0.1 --port 9000如果 7860 端口已被占用,终端会报address already in use,换个端口即可。
4.5 接入更自然的语音引擎
上面的DemoTTSEngine只是接口演示,不会生成可用音频。真正使用的时候,把DemoTTSEngine替换成你选定的 TTS 实现。替换时要重点看这些信息:
- 是本地推理还是请求外部 API;如果走外部 API,是否要求本地端点和密钥。
- 支持 WAV、MP3 还是原始 PCM。
- 最大可处理文本长度。
- 对并发请求是否有限制。
- 模型的授权协议是否允许当前业务使用。
只要实现synthesize方法,批量脚本和 API 都不用改。
5. 功能测试与效果验证
5.1 最小文本合成测试
测试目标不是验证音色多好听,而是确认整条链路能跑通。
输入文本:
请各位演职人员于八月十二日十八点三十分到达石家庄站集合,统一乘坐班车前往场地。预期结果:
- 脚本正常结束。
- 输出目录出现对应音频文件。
- 日志中
失败 0。 - 音频文件不为空,时长与文字长度比例合理。
如果更换为真实语音引擎后,要打开音频文件听一遍,重点检查:
- “八月十二日”是否被读成自然日期。
- “十八点三十分”是否读成时间。
- “石家庄站”是否出现词内断句错误。
如果日期或时间读错,就说明当前引擎的数字转写规则不够好,需要调整文本预处理,不要直接以为合成引擎没有问题。
5.2 JSONL 批量任务测试
准备一份最小 JSONL:
{"id": "001", "date": "0812", "text": "明天下午两点进行彩排,请提前十五分钟到场。"} {"id": "002", "date": "0812", "text": "石家庄站演出结束时间是二十二点,请大家合理安排返程。"} {"id": "003", "date": "0813", "text": "请在后台领取工作证件,并保管好个人物品。"}执行批量命令:
python batch_generator.py data/input/schedule.jsonl data/output/audio判断成功的标准:
- 三个音频文件都生成。
logs/failed.json中的失败数量为 0。- 文件名能清楚区分任务编号,例如
0812_001.mp3。
如果某个任务失败,打开logs/failed.json,里面会记录是哪一行、什么原因。比如文本为空、引擎超时、输出目录不可写,都能在失败原因里找到。
5.3 API 接口测试
用 curl 测试接口:
curl -X POST http://127.0.0.1:7860/api/synthesize \ -H "Content-Type: application/json" \ -d '{"task_id": "test_api_001", "text": "八号门入场,请出示工作证件。"}'预期返回值:
{ "task_id": "test_api_001", "output_file": "data/output/audio/test_api_001.mp3", "status": "success" }如果返回 500,多半是引擎调用失败;如果返回 404,检查app.py中的路由是否写对,以及 uvicorn 是否监听了正确端口。
5.4 长文本与分批策略测试
语音合成引擎通常有最大长度限制。不要等到正式任务跑挂了再处理,可以先准备一篇上千字的长文本,测试当前引擎的边界。
当文本超过引擎上限时,最简单的策略是先按标点切句,再分批合成,最后用 FFmpeg 拼接。
例如切成一句合成一个文件后拼接:
ffmpeg -f concat -safe 0 -i file_list.txt -c copy output.mp3file_list.txt的格式:
file 'part_001.mp3' file 'part_002.mp3' file 'part_003.mp3'但需要注意,拼接前要保证所有分段音频使用相同的采样率、声道数和编码格式。如果合成引擎输出 WAV,可以先用 FFmpeg 统一转成 MP3 或 PCM,再进行拼接。
6. 接口 API 与批量任务设计
6.1 API 调用示例
Python 请求示例:
import requests API_URL = "http://127.0.0.1:7860" payload = { "task_id": "shijiazhuang_0812_001", "text": "请于八月十二日十八点三十分前完成设备测试。" } response = requests.post(f"{API_URL}/api/synthesize", json=payload, timeout=60) print(response.status_code) print(response.json())批量生产环境下,不建议直接在业务代码里一个任务一个POST去请求。更好的方式是把任务先落库或落文件,再由本地脚本批量消费,脚本内部加失败重试和超时控制。
6.2 批量任务建议
| 设计点 | 建议 |
|---|---|
| 输入来源 | JSONL 文件或数据库任务表 |
| 输出位置 | 按任务日期分目录 |
| 日志 | 记录每个任务的成功/失败、耗时、输出路径 |
| 重试 | 单个任务失败重试 2 次,仍然失败才进入失败清单 |
| 幂等 | 同一任务重复执行时,能覆盖旧音频而不是生成重复文件 |
| 并发 | 先跑单线程,确认稳定后再提升并发数 |
6.3 使用 Python 内置队列跑并发任务
如果选用的 TTS 引擎允许并发,可以用concurrent.futures做简单并发:
from concurrent.futures import ThreadPoolExecutor, as_completed def submit_task(engine, text, output_path): try: engine.synthesize(text, output_path) return output_path, None except Exception as exc: return output_path, str(exc) with ThreadPoolExecutor(max_workers=2) as executor: futures = [ executor.submit(submit_task, engine, task["text"], "...") for task in tasks ] for future in as_completed(futures): output_path, err = future.result() if err: print("failed", output_path, err) else: print("ok", output_path)并发数不要一开始就调到 16。语音合成服务有的是 CPU 密集,有的是外部 API 限流,盲目的高并发只会让失败率上升。
7. 资源占用与性能观察
7.1 怎么看资源占用
当你把合成引擎接到真实模型后,观察资源占用是判断模型能否稳定运行的重要一步。
Windows 用户可以使用任务管理器,或者用命令直接看显存:
nvidia-smiLinux 用户同样用nvidia-smi查看 GPU 占用。如果是纯 CPU 推理,则需要关注 CPU 和内存。最直接的方法是打开实时监控,再启动一条合成任务,记录峰值占用。
不要根据“某个模型默认占用多少显存”的记忆去做判断,不同版本、不同量化参数、不同输入长度,显存占用差别很大。第一次跑务必要实测。
7.2 影响性能的因素
- 输入文本长度:越长推理时间越久,长文本还可能超出引擎最大 token 限制。
- 输出采样率:采样率越高,生成的音频数据量越大。
- 音频格式:WAV 文件通常比 MP3 更大。
- 并发线程数:过高的并发会增加内存占用。
- 合成模型的参数量:更大模型通常音质更自然,但推理会变慢。
如果做批量合成,建议先跑 10 条测试数据,观察单条耗时和资源占用,再估算 1000 条任务的总体耗时。
7.3 如何降低资源占用
- 不使用时关闭 API 服务,只保留批量脚本。
- 如果引擎支持量化版本,可以测试低精度版本在可接受音质下是否能跑。
- 单批任务控制在 50~100 条之间,完成后重启进程,避免长时间累积内存。
- 输出音频可以先合成低采样率版本,确认内容没问题后再合成高音质版本。
8. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
启动时报ffmpeg not found | 没安装 FFmpeg 或没配置环境变量 | 终端执行ffmpeg -version | 安装 FFmpeg 并加入 PATH |
| 批量任务全部失败 | 输入 JSONL 格式错误或引擎不可用 | 打开错误日志,检查第一条失败原因 | 先用最小文本单独测一次合成 |
| 服务启动时端口被占用 | 7860 或 9000 被其他进程使用 | 终端查看端口占用 | 换端口启动 |
| API 返回 500 | 引擎报错、文本太长、模型未加载 | 查看 uvicorn 终端输出 | 缩短文本或检查引擎服务状态 |
| 音频文件为空 | TTS 引擎没有正确写入数据 | 检查输出目录权限和报错日志 | 确认输出目录可写,重新调用 |
| 生成的音频里时间数字读错 | 文本预处理不足 | 单独测试日期/时间表达 | 在合成前把文本改写成更适合口语的格式 |
| 批量任务执行到一半卡住 | 引擎内部死锁或网络超时 | 看 CPU/GPU 占用和进程状态 | 为单条任务加超时控制,增加重试机制 |
| 拼接音频后时长不对 | 分段采样率或编码格式不一致 | 用 ffprobe 检查各文件格式 | 使用 FFmpeg 统一格式后再拼接 |
8.1 日期时间表达处理建议
文本直接写成“8月12日18:30”,有些合成引擎会读错。稳妥做法是在预处理阶段先把时间整理成适合语音合成的格式:“八月十二日十八点三十分”。这里的核心是不要让引擎去猜测数字的读法,而是把读音确定下来。
8.2 失败任务恢复建议
批量任务中断后,不需要重新跑全部数据。建议在任务清单里增加一个status字段,记录 pending、running、done、failed。每次重新运行,只处理非 done 的任务,已经成功的音频不会重复生成。如果你的输入还停留在简单 JSONL 阶段,可以把成功任务的文件名记录到一个completed.txt里,下次跳过这些任务。
9. 最佳实践与使用建议
9.1 先跑通最小闭环
任何语音合成项目,第一优先级都是跑通最小闭环:“输入一行文本,输出一个 mp3”。这一步成功以后,再接入批量任务、API 服务、长文本拼接。很多人一开始就搭 FastAPI 服务,结果引擎本身没跑通,调试时每层都可能是故障点。
9.2 沉淀一套可复用的测试用例
准备 10 条固定测试文本,覆盖中文数字、日期、时间、地名、英文单词、长句。每次更换语音引擎或升级依赖后,都先跑一遍这批用例,通过后再上正式任务。不要等生产任务出问题才发现音色数字读错。
9.3 目录职责分开
输入、输出、日志不要混在一起放。推荐:
data/ ├── input/ │ ├── schedule.jsonl │ └── retry_tasks.jsonl ├── output/ │ ├── audio/ │ └── completed/ └── debug/输出音频放到audio/,已经完成的文本任务记录到completed/,调试音频单独放debug/。这样后面做自动清理和归档时,不需要人工判断哪个文件是哪个任务的产物。
9.4 接口服务只在内网开放
FastAPI 里的host默认写127.0.0.1,只允许本机访问。如果需要让同一局域网内的其他设备调用,可以改为0.0.0.0,但必须确认网络环境安全。不要把没有任何鉴权的合成 API 直接暴露到公网。
9.5 版权与授权
这块再来一个实用的检查清单:
- 合成文本是否为自有内容或已获授权。
- 使用的语音模型或音色授权是否覆盖你的使用场景。
- 输出音频是否会被用于公开传播或商业用途。
- 是否涉及真实个人姓名、肖像、声音特征。
- 是否涉及未公开的场次安排、内部工作信息。
如果某一条回答不了,建议先暂停,确认清楚再跑任务。
10. 总结与下一步
这次围绕“260812 本命巡演 石家庄站”场次信息,完整实现了一个轻量本地语音合成工作流:目录规划、统一引擎接口、JSONL 批量任务、FastAPI 服务、失败日志恢复、长文本拼接策略,都覆盖到了。整个链路先跑最小样例,再扩展批量任务和 API,是最稳妥的推进方式。
建议下一步优先做四件事:
- 把
DemoTTSEngine替换为一个音质满足需求、授权状态清晰的语音引擎,并跑通 10 条固定测试用例。 - 处理日期、时间、地名等容易读错的文本,建立一套预处理函数。
- 用约 50 条真实场次数据执行一次批量测试,记录成功率、错误原因和总耗时。
- 如果有稳定需求,把 FastAPI 服务跑在一个固定端口,接上统一鉴权和日志收集。
最容易踩的坑通常是:输入格式不规范、TTS 引擎文本长度超限、端口占用、引擎授权边界不清。只要每一步都留日志和失败清单,问题大多能在十分钟内定位。项目本身不难,难的是一开始就把输入、输出、引擎、接口层级理顺。