Automated release: version and notes generated from pull requests merged since 1.12.0.
Added
audio.py --voicetakes a strength:--voice light|medium|strong, with a bare--voicemeaningmedium— byte for byte the chain it has always produced, so existing calls and MCP requests are unchanged.lightishighpass=f=80,acompressor=threshold=-18dB:ratio=2:attack=5:release=80:makeup=1(rumble and level only, for a good room);mediumishighpass=f=80,deesser=i=0.4,afftdn=nf=-25:tn=1,acompressor=threshold=-18dB:ratio=3:attack=5:release=80:makeup=2;strongismediumfollowed bydeesser=i=0.6,acompressor=threshold=-24dB:ratio=4:attack=5:release=120:makeup=3,alimiter=limit=0.891251:level=disabled, for phone and laptop audio.- The ducking parameters are sayable:
--duck-threshold DB(default −26.02 dBFS, the 0.05 linear the bed has used since 1.4, so the default filter string is unchanged),--duck-attack MS(20) and--duck-release MS(400) next to the existing--duck-amount.--jsongains anaudioblock reporting the voice level, the stereo width, the effects bed and the duck settings the run actually used. audio.py --stereo-widen 0..1widens the stereo image (extrastereo=m=1+2*amount), applied after the channel layout is settled. It needs a real stereo source and says so: widening scales the side signal (L−R), which is exactly zero once a mono track has been duplicated to two identical channels, so a 1-channel input is refused (kind: input) whether or not--stereois given, and more than two channels are refused unless--downmixfolds them to stereo first — in which case the widening runs on the fold-down. Nothing is downmixed implicitly.audio.py --effects FILE/--effects-volume DBmixes a third track (sound effects, atmos) into the bed. It is never ducked: effects are cut to the picture, so dipping them under speech would move them off their own frames.loudness.py --jsonnow reports the loudness range on both sides (measured.input_lrafor the input,result.input_lrafor the written file) and echoes the requestedtargets(lufs, tp, lra);--lra Nis documented inreferences/scripts.mdand the README.check.py --platform podcastgains two informational rows:chapters(PASS when the container carries at least one marker, WARNnoneotherwise, withmetadata.py --chaptersas the fix) andchannels(PASS for mono or stereo, WARN above — podcast players downmix 5.1 unpredictably). Neither can FAIL a delivery and neither appears for another platform.render.pyproject keys:"audio": {"stems": {"dialogue": DB, "music": DB, "effects": DB}}names one level per element of the mix, mapping to--gain,--music-volumeand--effects-volume(an explicit flag next to a stem wins; a stemseffectslevel with no"audio": {"effects": "sfx.wav"}file is refused), and"chapters"— a chapters file path or an inline list of{"at": TIME, "title": STR}— runsmetadata.pyon the delivered file as the last stage beforecheck, so the markers are in the file that ships. The stage plans like every other one:--dry-run/--planprints themetadata.pycommand and listschaptersinstages, so the plan the user approves is the run."audio": {"voice": "light"|"medium"|"strong"}picks the voice strength;truestill meansmedium. Both new objects are validated against the project schema, so a misspelled stem or chapter key is refused by name.- The audio guards say what is missing instead of ignoring it: a duck parameter (
--duck-threshold,--duck-attack,--duck-release,--duck-amount) without--duck, and--duckwithout--music, are refused (kind: input) the way a typed dynamics parameter without its switch already was. In arender.pyproject, anaudio.stems.musiclevel with no"music"file is refused likestems.effectswithout an"effects"file, andchaptersentries (a missingatortitle, a chapters file that does not exist) are validated before the first ffmpeg call rather than after the render has already delivered an unchaptered file. tests/fixtures/mcp_tools.jsonregenerated:audiogainsduck_threshold,duck_attack,duck_release,stereo_widen,effectsandeffects_volumeandvoicebecomes alight|medium|strongenum. A client still sending the 1.12 boolean{"voice": true}keeps working:trueis the bare flag, which ismedium;docs/contract.mdandaudio.py --helpsay so.
Docs
- Not shipped: a
loudness.py --dialoguespeech gate.loudnorm's EBU R128 integrated measurement already applies the −70 LUFS absolute and −10 LU relative gates, so gating the measurement on speech spans moved the result by at most 0.6 LU on every fixture (including one that is half digital silence) — insidecheck.py's own ±1 LU tolerance — for the cost of a second decode pass.references/gotchas.md#loudness-and-ambiencerecords it. docs/roadmap.mdmarks 1.13.0 done and records the decision not to addaudio.py --chapters:metadata.py episode.mp4 --chapters chapters.txtalready writes chapter markers with every stream copied, and a second spelling inside a tool that re-encodes the audio would be the worse one.render.py'schapterskey is the project-level answer instead.- feat: the audio bed — voice levels, stereo widen, duck parameters, effects stem, loudness --dialogue, podcast rows, project chapters (#221)
- evals: 18 prompts in th hi he ru el vi id tr it, 8 delivery prompts, 9 trigger queries, grader rules (#220)
- docs: eval iteration 13 at 1.12.0 (50 prompts, nine languages) (#219)
What's Changed
- docs: eval iteration 13 at 1.12.0 (50 prompts, nine languages) by @claude[bot] in #219
- evals: prompts in th hi he ru el vi id tr it, delivery prompts, trigger queries by @claude[bot] in #220
- feat: the audio bed — voice levels, stereo widen, duck parameters, effects stem, loudness --dialogue, podcast rows, project chapters by @claude[bot] in #221
Full Changelog: v1.12.0...v1.13.0