能用本地替换脚本过一遍,但要把它当成「统一写法」的工具,而不是「统一风格」的工具。脚本能处理的是品牌自称有几种写法、语气词写法不一致、禁用词没清干净这三类可以枚举的问题;语气轻重、停顿、句子节奏这类东西脚本判断不了,仍然要人工读一遍。可行的顺序是:把导出的口播稿整理成一行一稿的纯文本,配一张区分整词匹配和任意位置匹配的词表,跑一段通用替换脚本,用 diff 比对替换前后,再人工挑出机器改不了的句子手改。
如果问题只出在品牌自称、语气词写法、禁用词这三类不一致,本地替换脚本够用,成本低、每一步都可回溯。前提是稿件能整理成一行一稿、词表区分整词与任意位置匹配、脚本只写新文件不覆盖原稿。语气、停顿、上下文是否自然,脚本改不了,替换后仍需人工通读一遍。词表里加一条误伤规则就整体重跑,比在替换结果文件上手工修补更稳。
把导出的口播稿整理成一个纯文本文件,一行一稿
替换脚本按行处理最省事,所以输入文件先定死格式:建议命名为 voice_raw.txt,保存为 UTF-8 不带 BOM,行尾统一 LF,这样后面 diff 不会因为换行符差异刷出一屏噪音。约定一行一稿,行首是稿件编号,编号后跟标题、再跟正文,三段之间用同一个分隔符(| 或制表符)隔开。正文内部不要再出现这个分隔符;如果原文分段较多,先把正文里的换行压成空格,保证「一行一稿」成立。
示例行:
001|开场品牌自称|这里是第一条口播正文,第一句先念品牌自称,后面接正文内容
002|第二条稿件标题|第二条口播正文,段落之间的换行已经压成单个空格
编号这一列是后面抽查用的锚点:diff 报出第几行有问题,能直接对回原始稿件。如果导出的是 CSV 或 JSON,通常也不建议直接拿来做替换——口播正文里逗号、引号、顿号很常见,字段错位比多写一步整理脚本更费时间。整理成一行一稿后,先 wc -l voice_raw.txt 看一眼行数是否和导出稿件数量一致。
列出替换词表:品牌自称、语气词、禁用词三类各一行
词表建议单独存成 word_table.csv,固定三列:原词、替换词、是否整词匹配。三类内容各占若干行,方便逐类核对。整词匹配这一列是关键,它决定替换的边界:标 exact 表示原词前后不能再紧邻中文或英数字符(用正则零宽断言实现),标 loose 表示出现在任何位置都替换。短词、常见词、可能是某个更长专名一部分的词,一律标 exact。
原词,替换词,整词匹配
文案狗本狗,文案狗,exact
嗯嗯,嗯,loose
绝对最好,比较合适,exact
第一名的,领先的,exact
禁用词这一类的替换词允许留空,表示直接删除该词,脚本要支持空替换词。容易误伤的词建议在词表末尾集中放,并单独加注释行(以 # 开头)说明原因,例如某个词同时是品牌名的一部分。词表原词里如果本身带逗号,用 CSV 的双引号包起来,不要用逗号去当分隔符凑。
写一段读词表逐行替换的通用脚本骨架
下面这段是通用文本处理,读本地词表、改本地文本文件,和文案狗本身没有接口关系,也不需要任何账号或令牌。把它放在稿件同目录下命名为 replace_script.py,先 cp voice_raw.txt voice_raw.bak.txt 留一份底稿,脚本只写新文件、不覆盖原文件。
import csv, re, pathlib
SRC = pathlib.Path('voice_raw.txt') # 一行一稿的原始稿件
DST = pathlib.Path('voice_replaced.txt') # 替换结果,不覆盖原文件
TBL = pathlib.Path('word_table.csv') # 替换词表
def load_rules(path):
rules = []
with path.open(encoding='utf-8', newline='') as f:
for row in csv.DictReader(f):
src = (row.get('原词') or '').strip()
dst = row.get('替换词') or ''
mode = (row.get('整词匹配') or '').strip().lower()
if not src or src.startswith('#'):
continue
rules.append((src, dst, mode))
rules.sort(key=lambda r: len(r[0]), reverse=True) # 长词先替换
return rules
def build(rules):
compiled = []
for src, dst, mode in rules:
if mode in ('1', 'exact', '是'):
pat = re.compile(r'(?<![0-9A-Za-z\u4e00-\u9fa5])' + re.escape(src) + r'(?![0-9A-Za-z\u4e00-\u9fa5])')
else:
pat = re.compile(re.escape(src))
compiled.append((pat, dst, src))
return compiled
def main():
hits = {}
lines = SRC.read_text(encoding='utf-8').splitlines()
compiled = build(load_rules(TBL))
out = []
for line in lines:
new = line
for pat, dst, src in compiled:
new, n = pat.subn(dst, new)
if n:
hits[src] = hits.get(src, 0) + n
out.append(new)
DST.write_text('\n'.join(out) + '\n', encoding='utf-8')
for src, n in sorted(hits.items()):
print(src, n, sep='\t') # 命中计数打到标准输出,方便核对词表
main()
运行命令:
cd ~/scripts
python3 replace_script.py | tee replace_hits.tsv
最后那几行命中计数是给自己看的:词表里某条规则计数为 0,说明它在整批稿子里没生效,要么写法本来就不一致,要么原词写错了。规则按原词长度倒序排列,是为了避免短词先把长词的一部分吃掉。
用 diff 比对替换前后文件,抽查三行确认没有误替换
不要直接拿 voice_replaced.txt 去用,先比对:
diff -u voice_raw.txt voice_replaced.txt > replace.diff
wc -l replace.diff
sed -n '1,80p' replace.diff
diff 里的行号能直接对回稿件的第几条,改动集中在预期的词上就算正常。再抽查三类各一处:品牌自称用 grep -n '旧写法' voice_replaced.txt,输出为空说明旧写法清干净了;被标成 exact 的词,用 grep -n '替换后的新词' voice_replaced.txt 看它出现的语境是否合理,比如有没有把品牌名的一部分切掉;禁用词用 grep -c '禁用词' voice_replaced.txt,结果应为 0。
发现误替换时的回退做法:原稿没被动过,删掉 voice_replaced.txt 重跑即可;如果只错个别词,改词表而不是改结果文件——把该条改成 exact、或者补一条更长的原词让长词优先命中,然后整批重跑。在替换结果上手改,下一批稿子还会带着同一个错。
脚本跑完再人读一遍,把机器改不了的句子挑出来手改
脚本只能保证「同一个意思写成同一个词」,保证不了语气。通读时按下面的标准挑句:替换后读起来语义不成立的句子(禁用词原本是句子逻辑的一部分,删掉或换掉就断了);品牌自称夹在英文、数字、括号中间,边界判断没覆盖到的;语气词只统一了写法但密度、节奏不统一的段落;同一批稿子里前后呼应或重复的收尾句,只改了一处的;涉及价格、时间、承诺的句子,这类内容脚本本来就不该染指。
手改结果和脚本输出分开存放:脚本输出留在 voice_replaced.txt,手改稿另存为 voice_replaced.manual.txt(或分 auto/、manual/ 两个目录)。手改只动 manual 文件,不回写脚本输出,这样下一批稿子仍然从原始文件走脚本,词表持续积累。如果某处改动在好几个稿子里反复出现,就把它补成词表里的一条规则,下次交给脚本处理。