那天要优化一套语音合成流程时,我原本以为问题很简单:局域网里有 4 台电脑,剪映环境也都在,按照一个固定顺序把任务发出去就行。优先用其他电脑,最后才用当前电脑,听起来已经是一个完整方案。
真正跑起来以后,我才发现“把任务交给另一台电脑”只是表面动作。远端机器可能有软件,却没有同样的模板;可能能打开界面,却不能无头执行;可能已经生成了 wav,却返回了失败状态;也可能只是被一个弹窗挡住了编辑区。更麻烦的是,失败以后如果把整套任务重新跑一遍,就可能重复派发、覆盖半成品,甚至让本来健康的机器也被拖慢。
多机 TTS 的核心不是轮询机器,而是让每次任务都基于能力、健康度和任务类型做一次可解释的选择。
先把“能不能做”放在“轮到谁做”前面
最初的需求是有明确优先级的:几台远端电脑优先,当前电脑作为最后兜底。这个顺序不能直接等同于路由规则,因为“排在前面”不代表“现在可用”。一台电脑也许安装了剪映,但缺少必要的音色模板;也许文件在,却和调度端的脚本版本不一致;还可能看起来在线,实际无法无头启动。
所以我先做了远端预检:逐台确认相关能力是否存在,模板是否可用,再比对执行脚本的哈希。4 台电脑都通过预检、版本也一致以后,才有资格进入候选队列。这里的顺序很重要。我没有先按优先级发任务、失败了再补检查,是因为真正发声以后再发现环境不兼容,代价已经发生了:任务耗时、输出目录被写入,失败原因还可能被后续兜底动作掩盖。
这件事让我重新意识到,路由器首先要回答的不是“选哪台”,而是“哪些机器现在有资格被选”。能力检查是静态门槛,健康度则是动态门槛,两者缺一不可。
如果不处理这一步,最常见的结果不是明显报错,而是任务在远端慢慢消耗时间,最后回到本地重跑。表面上看只是多等了一会儿,实际上调度层失去了对失败的解释能力:究竟是机器不可用,还是任务重复执行,后来很难分清。
路由要发生在真正发声之前
完成预检后,我把路由接到了无头 TTS 的两个正门。一个负责单次发声,一个负责批量合成。关键点不是增加一个“远端模式”按钮,而是让路由发生在真正创建音频之前。
这样做有一个直接好处:任务还没有写入远端产物时,就已经根据任务类型和候选状态决定了执行位置。单次任务可以优先选择最合适的远端机器;批量任务则沿用同一套健康候选,但不因为调用入口不同而出现两套互相矛盾的规则。
本地兜底时,我还加了一层环境标记。它的作用很朴素:一旦路由层已经决定“这次就在本地完成”,后面的批量入口就必须知道这个决定,不能把任务再次分派到远端。否则,本地回退只是表面发生了,内部仍可能二次触发远端路由。
故障回退必须带着“这次已经做过路由”的状态,否则兜底很容易变成重复派发。
我选择把状态放在当前运行环境里,是因为它只影响本次任务,不会改变下一次任务的默认策略。这里没有引入更重的全局状态,也没有让每个调用方重新理解路由细节。一个路由判断应该尽量在共享入口完成,调用方只需要遵守结果。
每次运行都要有自己的边界
远端失败时,我遇到过一个很容易被忽略的问题:输出目录的父目录是共享的。第一次设计回退逻辑时,想把整个输出根目录搬走,再让本地重新生成。这个动作看似能快速清理残留,实际会把其他任务的文件也卷进去。
后来我把隔离范围收窄到本次运行对应的子目录。失败时,只移动这一次运行产生的目录;共享父目录保留,其他任务不受影响。本地重新执行也从新的运行目录开始,不去猜哪些文件属于上一轮。
这里最容易误判的是:看到某个目录“看起来就是这次任务的输出”,就默认它可以整体处理。目录层级表达的是存储关系,不一定表达任务归属。只有运行级子目录才能提供足够清晰的边界。
如果不做这种隔离,远端失败后的清理会带来第 2 个故障:原始任务也许只是生成失败,清理动作却把其他批次的半成品一起移动;接着本地重跑时,系统还可能因为目录已存在而再次失败。故障转移首先要保护数据边界,速度只能排在这之后。
跨平台差异要收进适配层
几台电脑并不是同一台电脑的复制品。macOS 上可以走无头流程,Windows 上却曾经被剪映里的福利弹窗挡住编辑区。即使弹窗处理掉,音色搜索仍遇到了识别问题:第一条结果明明是目标音色,文字识别框太大,把头像和副标题也一起读进来,反而无法确认名称。
我没有在总路由层里堆很多平台判断,而是把这些差异放回机器适配层。Windows 通道先清理固定弹窗,再定位音色名称区域;文字识别不再扫描整块界面,而是收窄到名字那一行。第一次尝试给整块图片加白边,结果识别变差;随后又发现放大到 3 倍太大,1.5 倍虽然能读,但稳定性不够;最后通过直接测试不同放大倍数和边距,确认背景色补边、2 倍放大和字符覆盖率比对更可靠。
这段过程看起来像是在调一个小参数,实际上说明了一个更大的问题:跨平台自动化不能假设“同一个名字在不同系统上会以同样方式呈现”。名称和标识可以共用,交互方式、弹窗状态和识别误差却可能不同。
我的判断是,路由层只负责选谁,适配层负责怎么做。这样做是因为机器差异越多,越不能把平台细节泄漏到所有调用方里。以后换一台电脑,先补它的能力和适配检查,不必重写整个调度流程。
失败后不要立刻相信,也不要立刻惩罚
一次批量任务曾经生成了 wav,却返回了失败状态。这个结果不能简单按失败处理,也不能只看返回码就宣布成功。文件存在说明某个阶段完成了,返回失败说明流程仍有异常,二者必须一起判断。
我把验收拆成两层。第一层确认运行结果是否落在本次目录,文件数量和基本状态是否符合预期;第二层用 ASR 核对音频内容,确认它说的确实是目标文本,而不是只生成了一个可播放但内容错误的文件。只有通过内容核对,音频才算可交付。
机器健康度也不能用一次失败永久拉黑。Windows 通道后来成功跑出一段音频,但它曾经确实在弹窗和 OCR 上耗了很久。因此我加入了 2 小时冷却:某台机器在这段时间内失败过,就暂时排到健康机器后面,而不是每次都先让它消耗约 1 分钟。
这不是惩罚机制,而是一个带时间边界的风险记忆。机器可能只是临时被弹窗挡住,也可能确实处于异常状态;永久降权会浪费可用能力,完全不记忆又会重复踩坑。健康度应该影响下一次选择,但不应该把一次故障永久写成机器的身份。
一套可复用的 6 层流程
把这次处理抽象以后,我得到的是 6 层,而不是一张固定机器顺序表。第 1 层是预检,检查能力、模板和脚本版本;第 2 层是能力选择,按任务类型筛出真正适合的机器;第 3 层是运行隔离,为每次任务创建独立边界;第 4 层是失败回退,只清理本次运行并切换候选;第 5 层是健康冷却,给近期失败机器临时降权;第 6 层是结果验收,用文件状态和 ASR 核对内容。
这 6 层的顺序也不能随意调换。先选机再预检,会把不合格机器送进真实任务;先清理共享父目录再判断归属,会扩大损失;只看返回码不做内容核对,会把错误音频当成成功结果。每一层都不是为了让系统显得复杂,而是为了把一个模糊的“失败”拆成可以处理的原因。
最容易走的错误捷径,是只按固定顺序轮询:远端失败,就从头再跑一遍;再失败,就继续往下试。这种方法在机器永远在线、环境完全一致时看起来很省事,一旦出现弹窗、版本漂移、半成品目录或返回码异常,问题就会被放大。重跑不是故障转移,只有保留运行边界、避免重复派发并记录健康状态,才算真正完成了转移。
一个流程成熟的标志,不是它能在顺利时跑通一次,而是它在下一次失败时,知道该清理什么、该避开什么、该验证什么。可复用的方法不是“优先用某台电脑”,而是让每次路由都有前置资格、失败边界和结果证据。
下次我会怎么做
下次再扩展局域网 TTS,我会先建立每台机器的能力登记:支持哪些任务,是否能无头执行,模板是否齐全,最近一次失败是什么时候。登记只负责提供候选,不替代每次运行前的轻量预检,因为环境会变,电脑也会被重启、弹窗或更新打断。
真正派发前,我会先为任务创建运行级目录,再按任务类型和健康度选择机器。远端失败时,只处理这个运行目录,并把本次失败写入临时冷却状态;本地兜底则携带明确的“已路由”标记,避免再次分派。最后无论返回码是成功还是失败,都要检查实际文件,并用 ASR 核对内容。
这件事让我重新意识到,自动化的价值不只在于让 4 台电脑同时工作,更在于把临场判断变成下次可以重复执行的规则。机器会变化,平台会有差异,失败也一定会发生;真正可靠的设计,是让这些变化只影响当前选择,不至于把整个流程拖回人工重来。