HeyGen Voice 批量配音——短视频口播,文本清理在前、试听校验在后

文章导读
一次要做几十条短视频口播配音,卡住的往往不是配音环节本身,而是输入文本:来源有剪辑稿、客服话术、活动文案,括号里塞着备注,中英标点混用,数字一会儿写“三十”一会儿写“30”,长度也参差不齐。可行的做法是把这批活拆成一条固定流水线:按条列清单固定边界、按规则清理并分段、逐条试听登记、把问题回流到词表和分段规则。前两步是纯本地文本处理,不依赖任何平台接口;后两步靠固定抽查点控制批量质量。
📋 目录
  1. A 按条列文本清单:每条口播的镜头时长、必须念到的信息、可省内容
  2. B 清理文本:去括号注释、去多余空格、统一标点与数字写法
  3. C 写一个通用批处理骨架:读入清单、按规则清理、按长度分段、导出待生成文件
  4. D 试听校验:每条成片抽查开头与结尾,问题逐条登记
  5. E 问题回流:把读错的词和异常断句补进替换表与分段规则
A A

一次要做几十条短视频口播配音,卡住的往往不是配音环节本身,而是输入文本:来源有剪辑稿、客服话术、活动文案,括号里塞着备注,中英标点混用,数字一会儿写“三十”一会儿写“30”,长度也参差不齐。可行的做法是把这批活拆成一条固定流水线:按条列清单固定边界、按规则清理并分段、逐条试听登记、把问题回流到词表和分段规则。前两步是纯本地文本处理,不依赖任何平台接口;后两步靠固定抽查点控制批量质量。

批量口播配音稳不稳,主要取决于两件事:文本清单是否在生成前就把“必须念到的信息”和“可省内容”分开,以及试听校验是否按固定抽查点逐条登记。建议先把清理规则写成可复现的脚本,再放量生成;每条成片只抽查开头和结尾,问题逐条登记并回流到替换表。边界需要说清楚:脚本只能保证格式一致,多音字、连读、语气这类读法差异仍要人工在文本层面确认,指望词表全自动兜住容易改坏原意。

按条列文本清单:每条口播的镜头时长、必须念到的信息、可省内容

先把每条口播的边界固定下来:这条要念多少秒、哪些信息必须出现、哪些可以删。边界在生成之前定好,后面就不用因为时长超了回头删句子,而删掉的那句往往正好是必念项。清单建议包含这些字段:id、seconds(镜头时长)、words_budget(字数预算)、text(原始口播稿)、must(必须念到的信息)、opt(可省内容)、skip(是否跳过)、note(来源与备注)。

“必须念到的信息”用 must 列登记,分号分隔关键词,例如“产品名;活动时间;价格;CTA”;同时在 text 里用【】把对应片段包住。脚本清理时去掉【】但保留内容,这样标记和删除可以分开做。要注意 must 列是校验用的,不是自动拼接用的:如果某条必须念到的信息只写在 must 列、text 里根本没出现,必须在生成前补进正文。

“可省内容”用 opt 列登记,或者就地用〔〕包住,例如开场寒暄、公司简介、历史沿革。清理阶段默认保留这些内容,只有当段落字数超出预算时才按优先级从后往前删。时长换算成字数,最好先录一条已知字数的样音,算出自己每秒大概几个字,再套进清单;不同主播语速差别明显,直接抄别人的换算值意义不大。

id	seconds	text	must	opt	skip
sp_001	20	【星海云盘】大家好,我们是星海科技(成立于2016年),今天带来……	产品名;活动时间;价格	开场寒暄;公司简介	
sp_002	15	……	产品名;CTA	历史沿革	1

清理文本:去括号注释、去多余空格、统一标点与数字写法

清理的目标是把格式杂乱的输入变成同一种结构,让后面按长度分段时结果可预期。规则要逐条写下来,能写进脚本的就不靠手感:保留 must 标注的内容、英文与型号原样(如 Pro、A100)、数字与单位,以及句末语气词;其余按下面的条目处理。

HeyGen Voice 批量配音——短视频口播,文本清理在前、试听校验在后
  • 括号注释:()、()、【】里的补充说明,除了已登记进 must 的,整体删除。
  • 冗余空白:全角空格、 、零宽字符、行首行尾空格、中文与中文之间的空格,全部去掉;英文单词之间保留一个半角空格。
  • 标点统一:半角 , ? ! ; 转成对应的全角符号;连续句末标点折叠成一个;省略号统一成 …;相连的短横线统一成 ——;标点两侧不留空格。
  • 数字写法统一:整条口播只选一种写法,要么都用阿拉伯数字,要么都用汉字;价格、时间、百分比按同一套规则走。读起来容易出错的写法放进替换表,而不是临时改句子。
  • 平台标记:话题标签、@账号、剪辑指令(如“字幕:xxx”)整行删除,或移进 opt 列留档。
清理前:
【口播】 大家好!!我们是  星海科技(成立于2016年),今天带来“云盘Pro”  — 原价 30 元/月,现在 前100名 只要 9.9 元(限时3天)...

清理后:
大家好!我们是星海科技,今天带来“云盘Pro”——原价30元/月,现在前100名只要9.9元……

上例中“成立于2016年”默认按可省内容处理,所以连括号一起删掉;如果它属于必须念到的信息,就要从括号里拿出来写进正文,同时在 must 列登记。这个判断只能在清单阶段做,放进脚本就没法回头了。

写一个通用批处理骨架:读入清单、按规则清理、按长度分段、导出待生成文件

下面这段只做本地文本处理:读 CSV 清单、按规则清理、按标点优先级切段、写出 JSONL。它不调用任何配音平台接口,替换项是 MAX_CHARS(单段字数上限)和 REPLACE(容易读错的词表)。分段顺序是先按句末标点切句,再把短句合并到不超过上限;单句过长时退到逗号、顿号这类软标点切,找不到切点才硬切。

import csv, re, json

MAX_CHARS = 45
SOFT = ',、;:,;:'
REPLACE = {
    '云盘Pro': '云盘 Pro',
    '9.9': '九点九',
}

def clean(text):
    t = text.strip()
    t = t.replace('\u3000', ' ').replace('\xa0', ' ').replace('\u200b', '')
    t = re.sub(r'[((][^))]{0,40}[))]', '', t)
    for k, v in REPLACE.items():
        t = t.replace(k, v)
    t = re.sub(r'[ \t]+', ' ', t)
    t = re.sub(r'([\u4e00-\u9fff]) (?=[\u4e00-\u9fff])', r'\1', t)
    t = t.replace(',', ',').replace('?', '?').replace('!', '!').replace(';', ';')
    t = re.sub(r'\.{2,}', '…', t)
    t = re.sub(r'-{2,}', '——', t)
    t = re.sub(r'([,。!?;:])\1+', r'\1', t)
    t = re.sub(r'\s*([,。!?;:])\s*', r'\1', t)
    return t.strip()

def split_text(text, max_chars=MAX_CHARS):
    sentences = [s.strip() for s in re.findall(r'[^。!?!?…]*[。!?!?…]?', text) if s.strip()]
    segs, buf = [], ''
    for s in sentences:
        if not buf:
            buf = s
        elif len(buf) + len(s) <= max_chars:
            buf += s
        else:
            segs.append(buf)
            buf = s
        while len(buf) > int(max_chars * 1.5):
            cut = max(buf.rfind(c, 0, max_chars) for c in SOFT)
            if cut <= 0:
                cut = max_chars
            segs.append(buf[:cut + 1])
            buf = buf[cut + 1:]
    if buf:
        segs.append(buf)
    return segs

def main(src, out):
    total = skipped = 0
    rows = []
    with open(src, encoding='utf-8-sig', newline='') as f:
        for row in csv.DictReader(f):
            total += 1
            if (row.get('skip') or '').strip().lower() in ('1', 'y', 'yes', 'true'):
                skipped += 1
                print('[skip]', row.get('id'))
                continue
            cleaned = clean(row.get('text') or '')
            if not cleaned:
                skipped += 1
                print('[empty]', row.get('id'))
                continue
            for i, seg in enumerate(split_text(cleaned), 1):
                rows.append({'id': row.get('id', ''), 'seq': i, 'text': seg,
                             'chars': len(seg), 'must': row.get('must', '')})
    with open(out, 'w', encoding='utf-8') as f:
        for r in rows:
            f.write(json.dumps(r, ensure_ascii=False) + '\n')
    print('清单 %d 条,跳过 %d 条,输出 %d 段' % (total, skipped, len(rows)))
    for r in rows:
        mark = ' [偏长]' if r['chars'] > MAX_CHARS else ''
        print(r['id'], r['seq'], r['chars'], r['text'][:16], mark)

if __name__ == '__main__':
    main('script_list.csv', 'segments.jsonl')

验证方式看运行输出:先看“清单多少条、跳过多少条、输出多少段”,跳过条目会单独打印 id,方便回头确认是空文本还是被标了 skip;再看每段的 id、seq 和字数,超过上限的会带 [偏长] 标记,说明软标点里没找到合适切点,需要人工看一眼。第一次跑建议先用十条以内的小清单试,确认清理规则和分段长度符合预期后再整批跑。

HeyGen Voice 批量配音——短视频口播,文本清理在前、试听校验在后

试听校验:每条成片抽查开头与结尾,问题逐条登记

批量生成之后逐条全听不现实,固定抽查点更可控:每条成片只抽开头和结尾,中间段落按段序号抽检。开头和结尾通常是产品名、价格、CTA 出现的位置,也是错读最容易暴露的位置。抽查结果直接登记成表,字段建议是:条目、段落序号、抽查位(开头/结尾)、听错词(写成实际听到的写法)、断句异常(位置加现象)、判定、处理状态、备注。

id	seq	抽查位	听错词(实际听到)	断句异常	判定	处理状态	备注
sp_001	1	开头	星海云盘→星海云旁	无	打回	已更新词表	下一批重跑
sp_001	3	结尾	无	“云盘Pro”后停半拍,读成两段	打回	已调整分段	同上

通过与打回的判定标准要事先写死,避免每次尺度不一样。通过的条件是:must 列登记的信息全部正确出现、无错读漏读加字、停顿落在标点位置、语速与镜头时长基本匹配、无明显吞字。以下情况打回:must 信息缺失、出现错读或漏读、断句切在词中间、连读导致语义改变、时长明显超出或不足。语气平淡、停顿略长但不影响语义这类灰区,可以记成“待复核”,不阻塞本批使用,但要进回流记录。

问题回流:把读错的词和异常断句补进替换表与分段规则

试听登记完不是结束,问题要回流到脚本和词表里,下一批才少犯同样的错。回流动作按下面几条走:

  1. 听错词写进 REPLACE:原词映射到读音更稳的写法,替换后语义不能变,改完在文本层面通读一遍。
  2. 断句异常先看切点:要么调小 MAX_CHARS,要么把软标点优先级改成先按逗号切,要么在长句里主动补一个顿号或分号。
  3. 数字和单位读错,在词表里展开成读音明确的写法,或反过来统一成阿拉伯数字,同一批里不要两种混用。
  4. 专有名词连读,加分隔或改写,例如“云盘 Pro”中间留一个空格,避免被连成一个词。
  5. 更新脚本后重跑旧清单:用上一批的原始输入重新跑一遍,看打回条目是否还在。
  6. 新一批开工前先跑一次回归,确认词表改动没有波及上一批已经通过的条目。

验证方式是把打回条目单独导出成一个小清单,重跑 clean 和 split_text 之后打印它们的段数与字数,逐条确认词表命中、段落长度在限内;再抽听同一条的开头和结尾,确认读法确实变了。风险边界也需要留一句:替换表是逐条累积的,不适合无脑全局替换,同一个词在不同语境下读法可能不同;为了让机器读对而把句子改得不像人话,就本末倒置了。