Cadence —— 让每一句话都正好落在 60 秒里

灵感

起因很小。队友在准备 demo day 的一分钟电梯陈述,录了十一遍,每一遍都卡在 63 秒上下,而规则是硬性 60 秒。他要么删句子(舍不得),要么在 Audacity 里一段段手动拉伸(声音开始出现"水下感")。

我们意识到这不是他一个人的问题:写稿时按字数估时间,说话时按情绪走节奏,两者永远对不上。

查了一圈现有方案,剪辑软件里的"变速适配时长"基本都是整段线性变速——60 秒压到 57 秒就整体加速 5%,听起来像被按了快进。而真正的需求是:停顿该压,拖沓处该提,重点句该原样保留。不是整体变快,是局部重分配。

它做什么

上传一段录音,填一个目标时长。Cadence 输出一版新录音:总长正好等于目标,音高不变,元音不破;同时给出一张节奏热力图,标出你在哪几秒拖了、哪几秒冲了。

我们怎么做的

三层流水线。

第一层:对齐。 不能对整段做一次变速,必须先知道"哪一段对应稿子的哪一句"。我们把录音按能量和过零率切成音节级片段,与脚本的音素序列做动态时间规整(DTW)。代价函数同时惩罚音素距离和时间跳变:

$$ D(i,j) = \min_{k \in \mathcal{N}(j)} \Big[ D(i-1,k) + c(i,j) \Big], \qquad c(i,j) = \lambda \, d_{\text{ph}}(i,j) + (1-\lambda)\,|\Delta t| $$

$d_{\text{ph}}$ 是音素嵌入距离,$\mathcal{N}(j)$ 限定允许的跳跃范围,防止路径为了对齐而"瞬移"。

第二层:分配时长。 拿到对齐路径后,每句在原始音频里的实际时长 $t_i$ 和目标里应得的时长都已知。问题变成:给定总预算 $T$,怎么给每句分配缩放因子 $s_i$?我们没解全局优化——太慢,而且会给出反直觉的结果(把最重要的句子压得最狠)。用的是加权修正:

$$ s_i = 1 - \alpha \cdot \frac{w_i \left(\sum_j t_j - T\right)}{\sum_j w_j t_j} $$

权重 $w_i$ 来自脚本里标记的重点词和该句的信息密度。$\alpha=1$ 时总长精确命中;我们把 $\alpha$ 定在 0.7,剩下的残差用一次小幅二次修正补上——数学上没必要,但 $\alpha$ 小一点,盲听时更不容易被听出来。

第三层:变形。 用 WSOLA(Waveform Similarity Overlap-Add),不用相位声码器。相位声码器在语音上会把辅音抹成气声;WSOLA 在 0.8–1.25 的倍率区间里几乎听不出处理痕迹,而这正好覆盖我们 95% 的实际需求。

前端 React + Web Audio,后端一层薄薄的 FastAPI,重活在 Python worker 里。全程单机,没有 GPU。

我们学到的

数学上最优的路径,听感上是错的。 DTW 的最优路径会倾向于把停顿对齐到停顿——这很合理,但它也会把长停顿"借"给别处,导致输出里出现一句被莫名拉长的句子。最后我们加了一条约束:静音段只允许压缩,不允许拉伸。违反最优性,但听起来对。

别试图学 $\lambda$。 我们花了一个下午想用小回归模型拟合 $\lambda$,失败了。最后是听了四十条样本手工调的。在 hackathon 的时间尺度上,能听的参数比能训练的模型可靠。

关键不在算法,在可解释。 第一版只有一个输出音频,评委听完说"好像快了点?"——因为没有任何东西告诉他哪里变了。加上热力图后,同一段音频,反馈变成"哦,这里我确实拖了"。同样的结果,加上归因,价值完全不同。

遇到的挑战

DTW 的 $O(nm)$ 直接炸了。 45 秒音频切出约 2400 个片段,脚本约 800 个音素,矩阵 190 万格,纯 Python 跑 40 秒,完全无法交互。先换 numpy 向量化降到 3 秒;最后加上带宽约束——只计算 $|\,i - \frac{n}{m}j\,| < B$ 的带内格子——降到 200 毫秒。带宽约束不只是优化,它本身就是一个先验:语音不可能在 1 秒内跨越脚本的 20 个音素。

麦克风的静音不是静音。 按能量阈值切分,笔记本风扇的低频噪声让停顿检测全错。加了 80 Hz 高通和自适应噪声底估计才稳定。

我们差点做了个错的东西。 第三天早上发现,我们一直在优化"压缩精度",能做到 59.98 秒。但用户要的不是精确,是"不要听起来像被处理过"。我们砍掉整层精度优化,把时间花在盲听测试上:每人听二十组,只问一个问题——"哪一个是原始录音?"

我们骄傲的

36 小时内做到:63 秒压到 60 秒,盲听测试里 21/30 次选错,接近随机,也就是基本听不出来。全流程本地运行,音频不出机器。热力图是我们自己最常用的功能,这通常是个好信号。

下一步

多说话人(访谈、双人播客);用句子嵌入替代音素距离做跨语言对齐;把 $\alpha$ 做成用户可拖的滑块——让"处理强度"成为可见的选择,而不是藏在代码里的常数。

Built with

python numpy scipy librosa dtw wsola fastapi react typescript web-audio-api tailwindcss


Built With

  • s
Share this project:

Updates

Submission history