返回博客

从零搭一个 AI 有声书配音工作台:用 Agent 把长文变成多角色广播剧,中途接入 4sapi

人工智能5697
从零搭一个 AI 有声书配音工作台:用 Agent 把长文变成多角色广播剧,中途接入 4sapi

一本几十万字的小说丢进去,出来的是分角色、带情绪、能直接听的有声书。这篇我把搭这套配音工作台的全流程写给你。

写在前面

有声书、广播剧这两年特别能打。但传统做法成本高得吓人——请配音演员、租录音棚、后期剪辑,一本书做下来动辄几万块。

AI 语音合成(TTS)这两年进步神速,音色自然到很难听出是机器。于是一个很现实的需求冒出来了:能不能把一段长文本,自动切分、识别角色、分配音色、批量合成,最后拼成一部多角色有声书?

能。而且没那么难。难点不在"会不会调 TTS 接口",而在怎么把一本书的复杂结构,拆成 TTS 能一句句吃下的任务。这篇就带你把这套工作台搭起来,并在中途接入 4sapi.com 这个中转 API统一调用语音模型。


一、先理清楚:一段文本变成有声书要过几关

整本文稿

① 文本切分    长文 → 按段落/句子切成 TTS 能处理的小块

② 角色识别    哪句是旁白?哪句是 A 说的?哪句是 B 说的?

③ 音色分配    给旁白和每个角色各配一个固定音色

④ 批量合成    每个文本块 → 对应音色的音频片段(TTS)

⑤ 拼接成品    所有片段按顺序拼接,加停顿,输出整本音频

这里头 ②靠大语言模型做角色归属判断,④靠语音合成(TTS)模型。两类模型——又是分散在不同地方,这正是要引入中转 API 的原因。


二、架构设计:四个 Agent + 一个中转网关

                   ┌──────────────────────────┐
   上传文稿  ──→    │   Orchestrator 总调度      │
                   └─────────────┬────────────┘
                                 │ 流水线派活
      ┌──────────────┬──────────┼──────────┬──────────────┐
      ↓              ↓          ↓          ↓              ↓
 [切分Agent]   [角色Agent]  [配音Agent]  [拼接Agent]   音色表
      └──────────────┴──────────┼──────────┴──────────────┘

                   ┌──────────────────────────┐
                   │   统一 API 网关 (4sapi)    │  ← 文本/TTS 都走这
                   └──────────────────────────┘

关键还是最底层:所有 Agent 统一走中转网关,不直接对接各家官方接口。 对有声书这种"调用量极大"的场景,好处尤其实在:

我选的网关是 4sapi.com,兼容 OpenAI 格式,对话模型和语音合成模型都能统一调用。


三、中途接入 4sapi:三步把网关接通

第一步:拿 Key、记住 BaseURL

注册 4sapi.com,后台创建 API 令牌(sk-xxxxxxxx),记下接口地址:

https://4sapi.com/v1

放进环境变量,别硬编码:

bash
# .env
FOURSAPI_BASE_URL=https://4sapi.com/v1
FOURSAPI_KEY=sk-你的令牌

第二步:封装统一客户端

python
# gateway.py
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["FOURSAPI_KEY"],
    base_url=os.environ["FOURSAPI_BASE_URL"],   # 指向中转网关
)

def chat(model, messages, **kwargs):
    resp = client.chat.completions.create(model=model, messages=messages, **kwargs)
    return resp.choices[0].message.content

def tts(text, voice, model="tts-1"):
    """语音合成,走同一个网关"""
    resp = client.audio.speech.create(model=model, voice=voice, input=text)
    return resp.content     # 音频二进制

第三步:发请求验证连通

python
if __name__ == "__main__":
    audio = tts("测试一下,网关通了。", voice="zh-female-1")
    open("test.mp3", "wb").write(audio)   # 能生成可播放的音频即成功

能出一个能播的 mp3,说明 TTS 这条路打通了。核心还是「改 base_url + 配 Key」。


四、逐个环节实现:四个 Agent 怎么写

4.1 切分 Agent —— 把长文切成 TTS 能吃的小块

TTS 接口对单次输入长度有限制,几十万字必须切。切的时候要尊重句子边界,别从句子中间切断:

python
import re

def split_agent(text, max_len=300):
    # 按中文句末标点切句,再合并到不超过 max_len 的块
    sentences = re.split(r'(?<=[。!?\n])', text)
    chunks, buf = [], ""
    for s in sentences:
        if len(buf) + len(s) > max_len:
            chunks.append(buf); buf = s
        else:
            buf += s
    if buf: chunks.append(buf)
    return chunks

4.2 角色 Agent —— 判断每一句是谁说的

这是有声书的灵魂。让大模型读上下文,给每个文本块标注说话者——是旁白,还是某个角色:

python
import json

def role_agent(chunks):
    text = "\n".join(f"[{i}] {c}" for i, c in enumerate(chunks))
    prompt = f"""下面是小说片段,每行前面是编号。判断每一段是"旁白"还是某个
角色在说话。输出 JSON 数组,元素含 idx 和 speaker(旁白/角色名)。
{text}"""
    raw = chat(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
    )
    return json.loads(raw)["roles"]

4.3 配音 Agent —— 给每个角色配固定音色

先建一张"角色→音色"映射表,保证同一个角色全书音色统一(这是听感连贯的关键):

python
# 音色分配:旁白用沉稳男声,角色按性别/年龄分配不同音色
VOICE_MAP = {
    "旁白":   "zh-male-narrator",
    "林夏":   "zh-female-1",
    "老陈":   "zh-male-2",
}
DEFAULT_VOICE = "zh-female-2"     # 没预设的角色用默认

def voice_agent(chunk, speaker):
    voice = VOICE_MAP.get(speaker, DEFAULT_VOICE)
    return tts(chunk, voice=voice)    # 合成,走网关

4.4 拼接 Agent —— 把碎片拼成整本

所有片段按原顺序拼接,角色切换处插入短停顿,听感更自然:

python
from pydub import AudioSegment

def merge_agent(audio_segments):
    book = AudioSegment.empty()
    pause = AudioSegment.silent(duration=300)   # 0.3 秒停顿
    for seg_bytes in audio_segments:
        seg = AudioSegment.from_file(io.BytesIO(seg_bytes))
        book += seg + pause
    return book      # 导出 mp3 即整本有声书

五、串起来:总调度跑通一整本

python
def make_audiobook(full_text):
    chunks = split_agent(full_text)          # ① 切分
    roles = role_agent(chunks)               # ② 角色识别
    speaker_of = {r["idx"]: r["speaker"] for r in roles}

    segments = []
    for i, chunk in enumerate(chunks):
        speaker = speaker_of.get(i, "旁白")
        audio = voice_agent(chunk, speaker)   # ③④ 配音+合成
        segments.append(audio)

    return merge_agent(segments)             # ⑤ 拼接成品

文本判断、语音合成——全走同一个 4sapi 网关


六、前端:一个能上传文稿、试听、调音色的工作台

jsx
function AudiobookStudio() {
  const [text, setText] = useState("");
  const [progress, setProgress] = useState(0);

  async function generate() {
    const res = await fetch("/api/audiobook", {
      method: "POST",
      body: JSON.stringify({ text }),
    });
    // 用 SSE 回报"已合成 X / 共 Y 段",更新进度条
  }

  return (
    <div>
      <textarea value={text} onChange={e => setText(e.target.value)}
                placeholder="粘贴或上传文稿…" />
      <VoiceConfigPanel />     {/* 给每个角色挑音色 */}
      <button onClick={generate}>开始生成有声书</button>
      <Progress value={progress} />
    </div>
  );
}

安全提醒:批量 TTS 是真烧钱的环节,一本长篇可能上万次调用。上线前务必做用量上限和频率限制,并给用户一个"预估消耗"提示。另外,上传的文稿要注意版权,别拿没授权的作品商用。


七、几条实战经验

  1. 切分务必尊重句子边界。从句子中间切断,合成出来会有奇怪的断顿,听感立刻崩。
  2. 角色识别一次性批量做,别逐句调。把整章带编号丢给模型一次标注,比一句句问省掉大量 token 和时间。
  3. 音色映射表抽成全局配置。同一角色全书音色必须固定,靠映射表锁死,别让模型每段自由发挥。
  4. 合成做成可断点续传。一本书几千段,跑到一半挂了别从头来——记录已完成的片段,重跑只补缺的。
  5. 小段并发合成。TTS 各段相互独立,开适度并发能把几小时的活压到几十分钟,注意别超网关的速率限制。

写在最后

把一本书变成多角色有声书,难点不在 TTS 本身,而在怎么把一段没有结构的长文,拆成机器能一句句念好的任务流

切分、识角色、配音色、再拼接——四步走通,长文就能自动变成能听的广播剧。

中途接入 4sapi,把"文本模型 + 语音模型分头管理"的麻烦,收敛成了一个网关、一个 Key。业务只管编排,底层模型怎么换都不碰上层代码——这套统一网关的思路,省下的不只是对接成本,更是后续维护的心智负担。


本文代码为说明思路的简化示意,具体模型名称、接口字段请以 4sapi.com 官方文档为准。涉及付费接口,请做好用量控制与版权合规。

标签:AI有声书4sapi代理人文本转语音语音合成

推荐阅读

探索更多前沿洞察与行业干货。