# INSTRUMENT-AUSNAHME: Victor-Go — Soundscape: Titelmusik + Atmo + SFX für Holmes/Watson Hörspiel
"""
Baut watson_mathe_final.mp3:
  - Intro: Titelmusik 8s solo
  - Atmo: Kaminfeuer durchgehend bei -28dB
  - SFX: Papierrascheln, Uhrtickern, Federkiel — positions-genau eingebaut
  - Dialog: alle 53 Clips mit Pausen (PAUSE_AFTER)
  - Outro: Titelmusik zurück, 8s Ausklang

Freesound API Key: fa1qGiJ4ugA1c8BRKogNKp4y5R1JM3wwNEqbzxDu
"""
import json, time, urllib.request, urllib.parse
from pathlib import Path
from pydub import AudioSegment, effects

# ─── Pfade ───────────────────────────────────────────────────────────────────
CLIPS       = Path("/Users/victorholland/Vibe Coding/dispatcher/cockpit/watson_demo_clips")
SFX_DIR     = Path("/Users/victorholland/Vibe Coding/dispatcher/cockpit/watson_sfx")
TITLE_MUSIC = Path("/Users/victorholland/Desktop/Sherlock Holmes Titelmusik.mp3")
OUT         = CLIPS / "watson_mathe_final.mp3"
LOG         = Path("/tmp/watson_soundscape.log")

SFX_DIR.mkdir(exist_ok=True)

FREESOUND_KEY = "fa1qGiJ4ugA1c8BRKogNKp4y5R1JM3wwNEqbzxDu"

DIALOG = [
    ("d01","holmes"), ("d02","watson"), ("d03","holmes"), ("d04","watson"),
    ("d05","holmes"), ("d06","holmes"), ("d07","watson"), ("d08","holmes"),
    ("d09","watson"), ("d10","holmes"), ("d11","watson"), ("d12","holmes"),
    ("d13","watson"), ("d14","holmes"), ("d15","watson"), ("d16","holmes"),
    ("d17","holmes"), ("d18","watson"), ("d19","holmes"), ("d20","watson"),
    ("d21","holmes"), ("d22","watson"), ("d23","holmes"), ("d24","watson"),
    ("d25","holmes"), ("d26","watson"), ("d27","holmes"), ("d28","watson"),
    ("d29","holmes"), ("d30","watson"), ("d31","holmes"), ("d32","watson"),
    ("d33","holmes"), ("d34","watson"), ("d35","holmes"), ("d36","watson"),
    ("d37","holmes"), ("d38","watson"), ("d39","holmes"), ("d40","watson"),
    ("d41","holmes"), ("d42","watson"), ("d43","holmes"), ("d44","watson"),
    ("d45","holmes"), ("d46","watson"), ("d47","holmes"), ("d48","watson"),
    ("d49","holmes"), ("d50","watson"), ("d51","holmes"), ("d52","watson"),
    ("d53","holmes"),
]

# Pausen nach bestimmten Clips (ms)
PAUSE_AFTER = {
    "d01": 400, "d03": 300, "d05": 700,
    "d08": 600, "d10": 700, "d11": 200, "d13": 400, "d15": 200,
    "d16": 900,   # Themenübergang Pythagoras
    "d17": 500, "d19": 800, "d21": 400, "d23": 600, "d25": 500,
    "d28": 900,   # Themenübergang LGS
    "d29": 800, "d31": 400, "d33": 300,
    "d35": 700,   # Themenübergang Textaufgaben
    "d37": 700, "d38": 300, "d39": 700,
    "d40": 900, "d41": 500, "d42": 700, "d43": 700,
    "d44": 300, "d45": 500,
}

# SFX-Positionen relativ zum Dialog-Start: {cue_label: (clip_id, offset_within_pause_ms, sfx_file)}
# SFX werden in die jeweilige Pause NACH dem genannten Clip gelegt
SFX_CUES = {
    "paper_at_start":   ("d01", -800, "paper_rustle.mp3"),    # 800ms vor d01-Ende = bei Papier
    "clock_thinking_1": ("d10", 150,  "clock_ticking.mp3"),   # kurz nach d10-Ende, Watson denkt
    "clock_thinking_2": ("d19", 200,  "clock_ticking.mp3"),   # Zirkus-Pause
    "clock_chapter_3":  ("d28", 100,  "clock_ticking.mp3"),   # LGS-Übergang
    "pen_writing_1":    ("d38", -600, "pen_writing.mp3"),      # Watson rechnet in d38
    "pen_writing_2":    ("d40", -800, "pen_writing.mp3"),      # Watson löst in d40
}

LEAD_MS = 30
TAIL_MS = 200

def log(msg):
    print(msg, flush=True)
    with LOG.open("a") as f:
        f.write(msg + "\n")

# ─── Freesound Download ───────────────────────────────────────────────────────
def freesound_search(query, dur_min=1, dur_max=60):
    url = (f"https://freesound.org/apiv2/search/text/"
           f"?query={urllib.parse.quote(query)}"
           f"&filter=duration:[{dur_min}%20TO%20{dur_max}]"
           f"&fields=id,name,duration,previews,avg_rating,num_downloads"
           f"&sort=downloads_desc"
           f"&page_size=5"
           f"&token={FREESOUND_KEY}")
    with urllib.request.urlopen(url, timeout=30) as r:
        data = json.loads(r.read())
    return data.get("results", [])

def download_sfx(query, out_name, dur_min=1, dur_max=60):
    out_path = SFX_DIR / out_name
    if out_path.exists():
        log(f"  ↩ {out_name} bereits vorhanden")
        return out_path
    results = freesound_search(query, dur_min, dur_max)
    if not results:
        log(f"  ✗ Kein Ergebnis für: {query}")
        return None
    best = results[0]
    preview_url = best["previews"].get("preview-hq-mp3") or best["previews"].get("preview-lq-mp3")
    if not preview_url:
        log(f"  ✗ Kein Preview für: {best['name']}")
        return None
    req = urllib.request.Request(preview_url, headers={"User-Agent": "WatsonHoerspiel/1.0"})
    with urllib.request.urlopen(req, timeout=30) as r:
        out_path.write_bytes(r.read())
    log(f"  ✓ {out_name}  [{best['name']}]  {best['duration']:.1f}s  ★{best.get('avg_rating',0):.1f}")
    return out_path

# ─── SFX herunterladen ────────────────────────────────────────────────────────
log("=== Freesound SFX Download ===")
sfx_queries = [
    ("fireplace crackling indoor warm", "fireplace.mp3",  10, 120),
    ("paper rustle document single",    "paper_rustle.mp3", 1, 6),
    ("clock ticking old antique",       "clock_ticking.mp3", 3, 30),
    ("quill pen writing paper scratch", "pen_writing.mp3",  2, 10),
]
for query, fname, dmin, dmax in sfx_queries:
    download_sfx(query, fname, dmin, dmax)
    time.sleep(0.3)

# ─── SSML-Filter für Timestamps ──────────────────────────────────────────────
def get_speech_bounds(ts_path, audio_len_ms):
    try:
        alignment = json.loads(ts_path.read_text())
        chars  = alignment.get("characters", [])
        starts = alignment.get("character_start_times_seconds", [])
        ends   = alignment.get("character_end_times_seconds", [])
        if not chars:
            raise ValueError("leer")
        in_tag, real = False, []
        for c, s, e in zip(chars, starts, ends):
            if c == '<': in_tag = True
            if not in_tag: real.append((c, s, e))
            if c == '>': in_tag = False
        if not real:
            raise ValueError("keine Sprachzeichen")
        return int(real[0][1] * 1000), int(real[-1][2] * 1000)
    except Exception:
        return 30, audio_len_ms - 30

# ─── Voice-Track aufbauen + Timeline tracken ─────────────────────────────────
log("\n=== Voice-Track ===")
segments   = []   # [(did, AudioSegment)]
clip_start = {}   # did → start_ms im fertigen Voice-Track

pos = 0
for did, sp in DIALOG:
    mp3 = CLIPS / f"{did}_{sp}.mp3"
    ts  = CLIPS / f"{did}_{sp}.timestamps.json"
    if not mp3.exists():
        log(f"  ✗ {did} fehlt")
        continue
    raw  = AudioSegment.from_mp3(str(mp3))
    norm = effects.normalize(raw, headroom=1.0)
    s_ms, e_ms = get_speech_bounds(ts, len(norm))
    cut_start = max(0, s_ms - LEAD_MS)
    cut_end   = min(len(norm), e_ms + TAIL_MS)
    if cut_end - cut_start < 200:
        cut_start, cut_end = 0, len(norm)
    clipped = norm[cut_start:cut_end]

    clip_start[did] = pos
    segments.append((did, clipped))
    pos += len(clipped)

    pause_ms = PAUSE_AFTER.get(did, 0)
    if pause_ms > 0:
        silence = AudioSegment.silent(duration=pause_ms, frame_rate=44100)
        segments.append((f"{did}_pause", silence))
        pos += pause_ms

voice_dur = pos
log(f"Voice-Track: {voice_dur/1000:.1f}s")

# Voice-Track zusammensetzen
voice_track = segments[0][1]
for _, seg in segments[1:]:
    voice_track = voice_track.append(seg, crossfade=0)

# ─── Titelmusik ──────────────────────────────────────────────────────────────
log("\n=== Titelmusik ===")
title = AudioSegment.from_mp3(str(TITLE_MUSIC))
title = title.set_frame_rate(44100).set_channels(2)

INTRO_SOLO_MS   = 8000   # Titelmusik solo
INTRO_FADEOUT   = 3000   # Fade-Out nach dem Solo
OUTRO_FADEIN    = 4000   # Fade-In am Ende
OUTRO_DURATION  = 9000   # Outro-Länge

# Intro: erste 11s der Titelmusik (8s voll + 3s fade-out)
intro = title[:INTRO_SOLO_MS + INTRO_FADEOUT].fade_out(INTRO_FADEOUT)

# Outro: Titelmusik ab ~10s (um Intro-Wiederholung zu vermeiden), 9s fade-in + voll
outro_src = title[8000:8000 + OUTRO_FADEIN + OUTRO_DURATION]
if len(outro_src) < OUTRO_FADEIN + OUTRO_DURATION:
    outro_src = title[:OUTRO_FADEIN + OUTRO_DURATION]
outro = outro_src.fade_in(OUTRO_FADEIN)

INTRO_OFFSET = INTRO_SOLO_MS   # Dialog startet hier

# ─── Kaminfeuer-Atmo (looping) ───────────────────────────────────────────────
total_dur = INTRO_OFFSET + voice_dur + OUTRO_DURATION + 2000
fire_raw  = SFX_DIR / "fireplace.mp3"
if fire_raw.exists():
    fire = AudioSegment.from_mp3(str(fire_raw)).set_frame_rate(44100).set_channels(2)
    while len(fire) < total_dur + 5000:
        fire = fire + fire
    fire = fire[:total_dur] - 28   # -28dB = dezentes Raumgeräusch
    log(f"Feuer: {len(fire)/1000:.1f}s @ -28dB")
else:
    fire = AudioSegment.silent(duration=total_dur, frame_rate=44100)
    log("  ⚠ Kein Feuer-SFX")

# ─── Gesamt-Canvas aufbauen ───────────────────────────────────────────────────
log("\n=== Mix ===")
total = AudioSegment.silent(duration=total_dur, frame_rate=44100).set_channels(2)

# Feuer-Bett
total = total.overlay(fire, position=0)

# Titelmusik-Intro
title_mono = intro.set_channels(2) if intro.channels == 1 else intro
total = total.overlay(title_mono, position=0)

# Voice-Track
voice_stereo = voice_track.set_channels(2) if voice_track.channels == 1 else voice_track
total = total.overlay(voice_stereo, position=INTRO_OFFSET)

# Titelmusik-Outro
outro_stereo = outro.set_channels(2) if outro.channels == 1 else outro
outro_pos = INTRO_OFFSET + voice_dur + 500
total = total.overlay(outro_stereo, position=outro_pos)

# ─── SFX einbauen ────────────────────────────────────────────────────────────
def load_sfx(fname, gain_db=0):
    p = SFX_DIR / fname
    if not p.exists():
        return None
    seg = AudioSegment.from_mp3(str(p)).set_frame_rate(44100).set_channels(2)
    return seg + gain_db

sfx_paper = load_sfx("paper_rustle.mp3",  -12)
sfx_clock = load_sfx("clock_ticking.mp3", -18)
sfx_pen   = load_sfx("pen_writing.mp3",   -14)

def overlay_sfx(canvas, sfx, clip_id, offset_ms):
    if sfx is None or clip_id not in clip_start:
        return canvas
    pos = INTRO_OFFSET + clip_start[clip_id] + offset_ms
    pos = max(0, min(pos, len(canvas) - len(sfx)))
    return canvas.overlay(sfx, position=pos)

if sfx_paper:
    # Papierrascheln kurz vor dem ersten Wort (Holmes hebt Brief auf)
    total = overlay_sfx(total, sfx_paper[:1200], "d01", -200)
    log("  SFX paper_rustle @ d01-200ms")

if sfx_clock:
    short_tick = sfx_clock[:min(len(sfx_clock), 3500)]
    for cue_clip in ["d10", "d19", "d28"]:
        total = overlay_sfx(total, short_tick, cue_clip, 150)
        log(f"  SFX clock @ {cue_clip}+150ms")

if sfx_pen:
    short_pen = sfx_pen[:min(len(sfx_pen), 2500)]
    for cue_clip in ["d38", "d40"]:
        total = overlay_sfx(total, short_pen, cue_clip, 500)
        log(f"  SFX pen @ {cue_clip}+500ms")

# ─── Export ──────────────────────────────────────────────────────────────────
log(f"\nGesamt: {len(total)/1000:.1f}s — Exportiere…")
total.export(str(OUT), format="mp3", bitrate="320k",
             tags={"title":"Holmes erklärt Watson Mathematik — Final",
                   "artist":"Watson Demo", "album":"Baker Street Hörspiel"})
log(f"Fertig: {OUT.stat().st_size//1024} KB")
print(f"\n✓ {OUT.name}  {OUT.stat().st_size//1024} KB  {len(total)/1000:.1f}s")
