Postprod improvements: denoise, phone EQ, ad muting, ducking, voice mappings

- Add host mic noise reduction (afftdn + anlmdn)
- Add phone EQ bandpass on caller stem
- Mute music during ads with 2s lookahead/tail
- Increase ducking release to 3s to reduce pumping
- Add Inworld voice mappings for all regular callers
- Recording toggle endpoint, stem sync fixes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 03:59:08 -07:00
parent 75f15ba2d2
commit 95c2d06435
6 changed files with 216 additions and 96 deletions

View File

@@ -2377,8 +2377,8 @@ async def set_on_air(state: dict):
def _run_postprod():
try:
result = subprocess.run(
[python, "postprod.py", str(stems_dir), "-o", str(output_file)],
capture_output=True, text=True, timeout=300,
[python, "postprod.py", str(stems_dir), "-o", "episode.mp3"],
capture_output=True, text=True, timeout=600,
)
if result.returncode == 0:
add_log(f"Post-production complete -> {output_file}")
@@ -3927,44 +3927,37 @@ async def server_status():
# --- Stem Recording ---
@app.post("/api/recording/start")
async def start_stem_recording():
if audio_service.stem_recorder is not None:
raise HTTPException(400, "Recording already in progress")
from datetime import datetime
dir_name = datetime.now().strftime("%Y-%m-%d_%H%M%S")
recordings_dir = Path("recordings") / dir_name
import sounddevice as sd
device_info = sd.query_devices(audio_service.output_device) if audio_service.output_device is not None else None
sr = int(device_info["default_samplerate"]) if device_info else 48000
recorder = StemRecorder(recordings_dir, sample_rate=sr)
recorder.start()
audio_service.stem_recorder = recorder
audio_service.start_stem_mic()
add_log(f"Stem recording started -> {recordings_dir}")
# Auto go on-air
@app.post("/api/recording/toggle")
async def toggle_stem_recording():
"""Toggle recording on/off. Also toggles on-air state."""
global _show_on_air
if not _show_on_air:
_show_on_air = True
_start_host_audio_sender()
audio_service.start_host_stream(_host_audio_sync_callback)
threading.Thread(target=_update_on_air_cdn, args=(True,), daemon=True).start()
add_log("Show auto-set to ON AIR")
return {"status": "recording", "dir": str(recordings_dir), "on_air": _show_on_air}
@app.post("/api/recording/stop")
async def stop_stem_recording():
if audio_service.stem_recorder is None:
raise HTTPException(400, "No recording in progress")
# START recording
from datetime import datetime
dir_name = datetime.now().strftime("%Y-%m-%d_%H%M%S")
recordings_dir = Path("recordings") / dir_name
import sounddevice as sd
device_info = sd.query_devices(audio_service.output_device) if audio_service.output_device is not None else None
sr = int(device_info["default_samplerate"]) if device_info else 48000
recorder = StemRecorder(recordings_dir, sample_rate=sr)
recorder.start()
audio_service.stem_recorder = recorder
audio_service.start_stem_mic()
add_log(f"Stem recording started -> {recordings_dir}")
if not _show_on_air:
_show_on_air = True
_start_host_audio_sender()
audio_service.start_host_stream(_host_audio_sync_callback)
threading.Thread(target=_update_on_air_cdn, args=(True,), daemon=True).start()
add_log("Show auto-set to ON AIR")
return {"on_air": _show_on_air, "recording": True}
# STOP recording
audio_service.stop_stem_mic()
stems_dir = audio_service.stem_recorder.output_dir
paths = audio_service.stem_recorder.stop()
audio_service.stem_recorder = None
add_log(f"Stem recording stopped. Running post-production...")
# Auto go off-air
global _show_on_air
if _show_on_air:
_show_on_air = False
audio_service.stop_host_stream()
@@ -3978,8 +3971,8 @@ async def stop_stem_recording():
def _run_postprod():
try:
result = subprocess.run(
[python, "postprod.py", str(stems_dir), "-o", str(output_file)],
capture_output=True, text=True, timeout=300,
[python, "postprod.py", str(stems_dir), "-o", "episode.mp3"],
capture_output=True, text=True, timeout=600,
)
if result.returncode == 0:
add_log(f"Post-production complete -> {output_file}")
@@ -3989,7 +3982,7 @@ async def stop_stem_recording():
add_log(f"Post-production error: {e}")
threading.Thread(target=_run_postprod, daemon=True).start()
return {"status": "stopped", "stems": paths, "processing": str(output_file), "on_air": _show_on_air}
return {"on_air": _show_on_air, "recording": False}
@app.post("/api/recording/process")