Powered by AppSignal & Oban Pro

Showcase: the 思 rescue — Coconut's raison d'être in four acts

showcase_phoneme_timing.livemd

Showcase: the 思 rescue — Coconut's raison d'être in four acts

Coconut 的存在意义,四幕演示:思 的元音被对齐压到约 0 帧(不可闻)→ 用户用两个 patch 重塑思的时值 → 用户把「小心思」整体左移 30 tick,检验干预数据是否存活 → 用户在最长的长音上画一条先抑后扬的音高曲线(区间锚定的 curve_pitch patch,带控制点的 Bezier)。

运行环境

本 notebook 需要在项目内运行:右上角 RuntimeMix standalone,路径指向 umbrella 根目录(coconut_intervention/),复用已编译的 _build,无需 Mix.install

同一逻辑的 CLI / IEx 版本在旁边的 showcase_phoneme_timing.exs(文件头有完整旁白与用法);本 notebook 复用其中的模块,逐幕执行。

Setup

# 阻止 .exs 末尾的一次性自动运行(那是给 CLI 的)
System.put_env("SHOWCASE_NO_AUTORUN", "1")

# Mix standalone runtime 的 cwd 是 umbrella 根目录;若 cwd 恰好在 examples/ 下则走第二个候选
exs_path =
  Enum.find(
    ["apps/coconut_oi/examples/showcase_phoneme_timing.exs", "showcase_phoneme_timing.exs"],
    &File.exists?/1
  ) ||
    raise "showcase_phoneme_timing.exs not found — set the runtime to Mix standalone at the umbrella root"

Code.require_file(exs_path)

加载乐谱(《雨后甜点-女声》前 35 拍)、声库、渲染/检查图,构建 workspace。需要声库与 ONNX 模型在位;首次编译图较慢。

ctx = ShowcasePhonemeTiming.setup()
# 自定义:ShowcasePhonemeTiming.setup(voicebank: "...", beats: 35, seed: 42)
# (扩散噪声默认种子为 0,输出完全可复现;想换一版采样就显式换 seed)

Act 1 · baseline:check + render,找到被吞掉的元音

不做任何编辑跑一遍检查链与渲染,定位 思(小心思 位于 16.5–18 拍)。其元音被对齐压到可闻线(8 帧 ≈ 93ms)以下。

ctx = ShowcasePhonemeTiming.act1_baseline(ctx)

Act 2 · 抢救思:边界即 preutterance + 重塑两侧边缘

两个 patch(数值均为 tick @128BPM):

  • [[1, -60]]:把 心|思 边界左移,让咝音提前开始(心 的元音尾巴买单——没有 preutterance 端口);
  • [[0, 24], [1, 120]]:s|i0 边界右移 +2 帧,元音结束边界再右移 +10 帧(后继 交 的声母买单)。

守恒由构造保证,无需 donor 计算。

ctx = ShowcasePhonemeTiming.act2_rescue_si(ctx)

Act 3 · 把小心思左移 30 tick:干预数据还活着吗?

裁掉前一个休止符的尾部腾出空间,三个音整体左移。检查链重跑,报告 patch 是否随 score 编辑存活(锚点搬运 + 摘要匹配)。

ctx = ShowcasePhonemeTiming.act3_drag_trio(ctx)

Act 4 · 画一条音高曲线:画的 vs 唱的

在最长的长音上挂一条先抑后扬的 Bezier 曲线(4 个控制点:起点原调 → 30% 处 −1.5 半音 → 65% 处 +0.3 → 终点回原调)。区间锚定(Ordinal{refs: [note, note]}),assemble 期光栅化成 pins 进 pitch_predict

图里绿线是画下去的曲线(空心圆点是控制点),橙线是模型实际唱出来的——两者的缝隙就是 pins 口的「强建议、非钳制」语义;灰虚线是没有曲线时模型原本会唱的。

ctx = ShowcasePhonemeTiming.act4_draw_curve(ctx)

Outputs

每幕的 WAV / debug JSON / PNG 落在 apps/coconut_oi/tmp/showcase/

ShowcasePhonemeTiming.summary(ctx)

四幕图像对比(前三幕缩放到 16–20 拍,curve 幕缩放到曲线区间):

images =
  for tag <- ["before", "after", "moved", "curve"],
      path = Path.join(ShowcasePhonemeTiming.out_dir(), "#{tag}.png"),
      File.exists?(path) do
    {String.to_atom(tag), Kino.Image.new(File.read!(path), "image/png")}
  end

if images == [], do: "(先按顺序运行上面的四幕)", else: Kino.Layout.tabs(images)

备注

  • patch 的 tick 数值(60/24/120、位移 30)与曲线形状(−1.5/+0.3)是 .exs 里的模块属性;想换参数,编辑 showcase_phoneme_timing.exs 后重跑 Setup cell 重新 require 即可。
  • frames/frames_raw(pitch)是 pitch 域的 raw/override 对:frames_raw 是剥掉 pitch patch 的对照渲染(上游 timing 干预保留)——本轮没有 pitch patch 时两者恒等不占字段;curves 键携带控制点与光栅化折线供绘图叠加。图例里 rendered pitch = 干预全部生效的渲染结果,raw pitch (pins/curves off) = pitch 域的对照。