跳到主内容
快讯直播
AI智模界
教程

ElevenLabs v4 多语种配音:情绪标签与批量合成

这套流程最终能做出什么

把一份中文脚本,变成多语种的成品音频:中文、英文、西班牙语各一版,同一个音色贯穿全片,句子之间有情绪起伏(兴奋、压低声音、停顿),并且整批文件可以自动拼接成一条完整的节目音频。

具体交付物是这样几样:

  • 一份 script.jsonl:每行一个配音块,带语言标记和情绪标签
  • 一个 out/ 目录:每个配音块一个 mp3,文件名与脚本 id 对应
  • 一个 manifest.json:记录每个块用了什么文本、生成了哪个文件
  • 每个语种一条拼接好的成品音频,响度统一,可直接上传播客或视频平台

整套流程的核心只有三件事:脚本分块、标签标注、音色与参数锁定。三者缺一个,批量出来就会听起来像好几个人配的。

---

前置条件清单

开工前确认这几项,缺哪项先补哪项:

1. 账号与 API Key:在官方控制台生成一个 API Key,具备文字转语音的调用权限。配额、并发上限、计费方式以官方页面当前说明为准。

2. 一个固定的音色:也就是 voice_id。可以选自带的预设音色,也可以用自己的音频素材做声音克隆。长期批量产出时,优先选一个已经保存到账号里的音色,避免每次现场生成导致音色漂移。

3. Python 环境:能跑 requests 即可,Python 3.10 及以上比较省事。

4. ffmpeg:用于拼接和响度处理,命令行能直接调用。

5. 官方模型列表:打开官方文档的 models 页面,确认当前可用的多语种模型标识(model_id)和它支持的音频标签写法。版本与标识以官方文档为准,不要凭记忆写。

6. 术语表:品牌名、人名、专业词的统一写法。多语种场景下,这份表决定了三个语种读出来是不是同一个东西。

环境变量先设好:

```bash

export ELEVENLABS_API_KEY="你的 key"

```

---

分步骤

第 1 步:把脚本切成"可配音块"

不要一次性把三千字丢给接口。单次请求过长容易在中间出现语气漂移,出错重来的成本也高。切成 300~800 字的块,边界放在句末标点处。

```python

chunk.py

import re

MAX_CHARS = 700

def split_script(text, max_chars=MAX_CHARS):

blocks = []

for para in re.split(r"\n+", text.strip()):

para = para.strip()

if not para:

continue

buf = ""

按句末标点切,保留标点

for sent in re.split(r"(?<=[。!?!?;;])", para):

if not sent:

continue

if len(buf) + len(sent) <= max_chars:

buf += sent

else:

if buf:

blocks.append(buf.strip())

buf = sent

if buf:

blocks.append(buf.strip())

return blocks

if __name__ == "__main__":

raw = open("raw_script.txt", encoding="utf-8").read()

for i, b in enumerate(split_script(raw), 1):

print(i, len(b), b[:40])

```

判断切得好不好的标准:随便拿一个块单独听,语义是完整的,语气不会半路断掉。如果某块结尾是逗号或者半句话,把它和前一块合并。

第 2 步:加情绪标签

在需要表演变化的位置插入方括号标签,例如兴奋、低语、停顿、笑声这类。具体支持哪些标签、大小写是否敏感,以官方文档当前给出的列表为准,不要自己造词,否则标签会被当成正文念出来。

标签的使用原则:

  • 一条块里放 1~2 个就够,堆多了表演会变得夸张、不自然
  • 标签放在它要生效的那句话前面,而不是段尾
  • 停顿类标签不要用来做"留白",长留白交给后期剪辑更可控

转成 JSONL,每行一个块:

```json

{"id": "zh_001", "lang": "zh", "text": "[excited] 这次更新,把配音流程整个换了一遍。"}

{"id": "zh_002", "lang": "zh", "text": "[pause] 你需要的,只是一份排好版的脚本。"}

{"id": "zh_003", "lang": "zh", "text": "[whispers] 剩下的事情,交给批处理。"}

```

id 里带语言前缀(zh_、en_、es_),后面拼接和分组都靠它。

第 3 步:生产多语种版本

把中文底稿翻译成英文、西班牙语等目标语言。翻译时要求标签原样保留、位置不变,可以这样给翻译工具下指令:

> 把下面的文本翻译成英语。保持方括号标签 [xxx] 原样不动,位置也不变。专有名词按下述术语表处理:……

翻译完要人工过一遍,重点看三件事:

1. 标签有没有被翻译掉或改写

2. 数字、单位、缩写是否改成了适合朗读的写法(比如 3.5kg 写成 three point five kilograms)

3. 目标语言里有没有过长且无标点的句子——有的话手动断句,否则合成出来会一口气念完

每个语种生成一份独立的 JSONL,命名如 script.zh.jsonl、script.en.jsonl。

第 4 步:锁定音色与参数

音色一致性不是靠"选同一个音色"就够,而是靠所有参数在所有语种所有块上完全一致。把参数写进配置文件,不要散落在代码里:

```json

{

"voice_id": "REPLACE_WITH_VOICE_ID",

"model_id": "REPLACE_WITH_MODEL_ID_FROM_OFFICIAL_DOCS",

"output_format": "REPLACE_WITH_SUPPORTED_FORMAT",

"voice_settings": {

"stability": 0.5,

"similarity_boost": 0.75,

"style": 0.0,

"use_speaker_boost": true

}

}

```

说明几点:

  • model_id 和 output_format 的取值,从官方文档当前页面复制,不同账号可用的值可能有差异
  • voice_settings 里各项的名字和取值范围,不同模型支持情况不完全一样,以官方文档为准
  • 一旦定下这组参数,所有语种共用。中日英西混排时分别调参,音色会明显分裂
  • 配置文件放进版本管理,以后想复现某期节目,checkout 回去就能重跑

第 5 步:先跑一条基线

别急着批量。先拿一条 30 秒左右的块,用配置里的参数跑一遍,听三件事:

1. 音色是不是你要的

2. 情绪标签有没有生效

3. 换一个语种跑同一句话,音色是否还是"同一个人"

这一步花五分钟,能省掉后面整批重做的几小时。基线不满意就调参数,调好了再把值写回配置文件。

第 6 步:批量合成

下面是完整的批量脚本,包含重试和上下文传递:

```python

synth_batch.py

import json, os, pathlib, sys, time

import requests

BASE = "https://api.elevenlabs.io/v1/text-to-speech"

API_KEY = os.environ["ELEVENLABS_API_KEY"]

HEADERS = {"xi-api-key": API_KEY, "Content-Type": "application/json"}

cfg = json.loads(pathlib.Path("config.json").read_text(encoding="utf-8"))

OUT = pathlib.Path("out")

OUT.mkdir(exist_ok=True)

def synth(text, prev_text=None, next_text=None, retries=5):

url = f"{BASE}/{cfg['voice_id']}"

payload = {

"text": text,

"model_id": cfg["model_id"],

"voice_settings": cfg["voice_settings"],

"output_format": cfg["output_format"],

}

上下文能让相邻块的语气更连贯

if prev_text:

payload["previous_text"] = prev_text

if next_text:

payload["next_text"] = next_text

for attempt in range(retries):

r = requests.post(url, headers=HEADERS, json=payload, timeout=180)

if r.status_code == 200:

return r.content

if r.status_code in (429, 500, 502, 503, 504):

wait = min(2 ** attempt, 30)

print(f" [{r.status_code}] 第 {attempt+1} 次重试,等待 {wait}s")

time.sleep(wait)

continue

raise RuntimeError(f"{r.status_code}: {r.text[:300]}")

raise RuntimeError("重试次数用尽")

def run(jsonl_path):

rows = [json.loads(l) for l in

pathlib.Path(jsonl_path).read_text(encoding="utf-8").splitlines() if l.strip()]

manifest = []

for i, row in enumerate(rows):

上下文只在同语种内传递,跨语言会干扰发音

prev_text = rows[i - 1]["text"] if i > 0 else None

next_text = rows[i + 1]["text"] if i + 1 < len(rows) else None

audio = synth(row["text"], prev_text, next_text)

fp = OUT / f"{row['id']}.mp3"

fp.write_bytes(audio)

manifest.append({"id": row["id"], "lang": row["lang"],

"file": str(fp), "chars": len(row["text"])})

print(row["id"], "ok")

time.sleep(0.4) # 控制请求节奏,具体限速以账号方案为准

return manifest

if __name__ == "__main__":

all_items = []

for f in sys.argv[1:]:

all_items += run(f)

pathlib.Path("manifest.json").write_text(

json.dumps(all_items, ensure_ascii=False, indent=2), encoding="utf-8")

print("完成,共", len(all_items), "块")

```

运行:

```bash

python synth_batch.py script.zh.jsonl script.en.jsonl script.es.jsonl

```

注意 previous_text / next_text 只在同一语种内部传,跨语言传会让模型在中文里带出英文口音。

第 7 步:拼接与响度统一

先按语种生成 ffmpeg 的拼接清单:

```python

make_list.py

import json, pathlib

m = json.loads(pathlib.Path("manifest.json").read_text(encoding="utf-8"))

lang = "zh"

with open(f"list_{lang}.txt", "w", encoding="utf-8") as f:

for item in m:

if item["id"].startswith(lang + "_"):

f.write(f"file '{item['file']}'\n")

```

```bash

python make_list.py

拼接

ffmpeg -f concat -safe 0 -i list_zh.txt -c copy joined_zh.mp3

统一响度与采样率,便于直接上传平台

ffmpeg -i joined_zh.mp3 \

-af loudnorm=I=-16:TP=-1.5:LRA=11 \

-ar 44100 -b:a 192k final_zh.mp3

```

如果 -c copy 拼接报错,改成重编码:

```bash

ffmpeg -f concat -safe 0 -i list_zh.txt -c:a libmp3lame -b:a 192k joined_zh.mp3

```

三个语种各跑一遍,得到 final_zh.mp3、final_en.mp3、final_es.mp3。

第 8 步:抽检与交付

整批跑完不要直接发布。抽检清单:

  • 随机挑 5 个块单独听,确认没有念标签、没有漏字
  • 听每个语种的第一块和最后一块,确认音色没有变化
  • 听每两个块的接缝处,确认语气没有突然断裂
  • 确认成品音频总时长与脚本预估时长接近(差太多说明有块生成失败)

---

常见坑与排错

音色前后不一致

九成情况是参数不一致:某次调用漏传了 voice_settings,或者中途换了 model_id。排查方法是把每次请求的 payload 打到日志里,逐字段比对。另一个原因是块边界断在句子中间,previous_text / next_text 能缓解,但把边界挪到句末更彻底。

情绪标签被当成正文念出来

先确认这个标签在当前 model_id 的支持列表里(以官方文档为准)。其次是位置问题——标签写在句子中间容易失效,写在句首更稳。还有一种情况是翻译环节把标签改写成了别的词,检查 JSONL 里标签是不是原样。

多语种读音错误

专有名词靠"文字注音"解决,比如把品牌名按目标语言的发音习惯拼写后再提交。同一段文本里混两种语言,除非模型明确支持,否则拆成两个块分别合成。

数字和单位读得别扭

统一在脚本阶段就写成朗读形式。2024年3月 写成 二零二四年三月 还是 两千零二十四年三月,取决于你要哪种语感;英文里 $3.5M 写成 three point five million dollars 更稳。这一步在文本层做,比后期修音频便宜。

返回 429 或超时

脚本里已经有指数退避,把 time.sleep(0.4) 的间隔调大、或者分批在不同时间段跑。并发数不要开太高,具体上限以账号方案说明为准。

拼接处有爆音或静音

不同块的输出格式如果被改动过,采样率不一致会让拼接出问题。统一 output_format,拼接后统一重采样到同一采样率即可。

字符计费与标签

标签是否计入计费字符、试听是否消耗额度,这类规则各账号方案不同,以官方页面当前说明为准。批量跑之前先用小样本估一次成本。

---

下一步建议

1. 加字幕时间戳:拿到音频后做一次强制对齐,产出 srt,视频和播客都能直接用。

2. 把脚本与配置进版本管理:script.*.jsonl + config.json + manifest.json 一起提交,任何一期节目都能精确复现。

3. 定时任务化:脚本稳定之后,把 synth_batch.py 挂到定时任务里,新稿子入库就自动出音频。

4. 建一个音色库:为不同内容线(播客、课程、短视频)各固定一个 voice_id 和一组参数,写进文档,避免每次都从头试。

5. 做一次 A/B 听测:同一段文本用两组 voice_settings 各生成一版,找几个真实听众盲听打分,用数据决定长期参数。

6. 留意官方更新:模型标识、支持的标签、音频格式都会随版本迭代变化,定期回官方文档核对一遍配置里的值。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。