""" Audio Engine — voice always wins. BGM auto-ducks under voice via sidechain compression; everything gets loudness-normalized before mixdown. """ def build_audio_filter(has_voice: bool, has_bgm: bool, bgm_volume: float = 0.15) -> dict: """ Returns {"extra_inputs": [...], "filter_lines": [...], "audio_out_label": str|None} Caller is responsible for appending voice/bgm as -i inputs in the same order this expects (voice first if present, then bgm). """ lines = [] label = None if has_voice and has_bgm: lines.append("[voice]loudnorm=I=-16:TP=-1.5:LRA=11[voice_n]") lines.append(f"[bgm]volume={bgm_volume}[bgm_v]") # sidechain: bgm ducks whenever voice is present lines.append( "[bgm_v][voice_n]sidechaincompress=threshold=0.05:ratio=8:attack=5:release=300[bgm_duck]" ) lines.append("[voice_n][bgm_duck]amix=inputs=2:duration=first:dropout_transition=2[aout]") label = "aout" elif has_voice: lines.append("[voice]loudnorm=I=-16:TP=-1.5:LRA=11[aout]") label = "aout" elif has_bgm: lines.append(f"[bgm]volume={bgm_volume},loudnorm=I=-18:TP=-1.5:LRA=11[aout]") label = "aout" return {"filter_lines": lines, "audio_out_label": label}