from __future__ import annotations from types import SimpleNamespace import pytest from google.genai import errors as genai_errors from ozan_radio.dj import TrackPlan from ozan_radio.lyria import ( _block_reason, _build_lyria_prompt, _extract_audio, _extract_lyrics, _response_parts, _sanitize_for_retry, _wants_vocals, friendly_lyria_error, ) from ozan_radio.settings import LyriaSettings def test_wants_vocals_explicit_no(): assert _wants_vocals("Instrumental. No vocals. Build slowly.") is False def test_wants_vocals_whispered_texture(): assert _wants_vocals("whispered vocal textures drift in and out") is True def test_wants_vocals_griot_request(): assert _wants_vocals("West African griot vocals over desert dub") is True def test_build_lyria_prompt_instrumental(sample_plan, lyria_instrumental): out = _build_lyria_prompt(sample_plan, lyria_instrumental) assert "Instrumental only, no vocals" in out assert sample_plan.lyria_prompt in out def test_build_lyria_prompt_mix(sample_plan, lyria_mix): out = _build_lyria_prompt(sample_plan, lyria_mix) assert "Wordless vocal textures" in out assert "Instrumental only" not in out def test_build_lyria_prompt_vocals_override_mode(sample_plan, lyria_instrumental): plan = TrackPlan( id="v1", title="Griot Night", mood="", dj_line="", lyria_prompt="Sahel griot male vocal chants over dub.", ) out = _build_lyria_prompt(plan, lyria_instrumental) assert "Include lead vocals" in out def test_build_lyria_prompt_hindi_language(sample_plan): cfg = LyriaSettings( model="lyria-3-pro-preview", vocal_mode="vocals", language="hi", singer_profile="male_baritone", output_format="mp3", prefer_instrumental=False, ) plan = TrackPlan( id="h1", title="Devotional", mood="", dj_line="", lyria_prompt="Hindi devotional desert dub.", ) out = _build_lyria_prompt(plan, cfg) assert "Hindi" in out assert "Male Baritone" in out def test_sanitize_for_retry_softens_griot(): raw = "West African griot vocal sample over dub" soft = _sanitize_for_retry(raw) assert "griot" not in soft.lower() or "Sahel" in soft assert soft != raw def test_response_parts_from_candidates(): part = SimpleNamespace(text=None, inline_data=SimpleNamespace(data=b"audio")) content = SimpleNamespace(parts=[part]) candidate = SimpleNamespace(content=content, finish_reason="STOP") response = SimpleNamespace(parts=None, candidates=[candidate]) parts = _response_parts(response) assert len(parts) == 1 assert _extract_audio(parts) == b"audio" def test_response_parts_empty_candidates(): response = SimpleNamespace(parts=None, candidates=[]) assert _response_parts(response) == [] def test_extract_lyrics_joins_text(): parts = [ SimpleNamespace(text="[[A0]]", inline_data=None), SimpleNamespace(text="line two", inline_data=None), ] assert "line two" in _extract_lyrics(parts) def test_block_reason_strips_enum_prefix(): feedback = SimpleNamespace(block_reason="BlockedReason.PROHIBITED_CONTENT") response = SimpleNamespace(prompt_feedback=feedback) assert _block_reason(response) == "PROHIBITED_CONTENT" def test_friendly_lyria_error_quota(): exc = genai_errors.ClientError( 429, {"error": {"message": "resource_exhausted", "status": "RESOURCE_EXHAUSTED"}}, None, ) msg = friendly_lyria_error(exc) assert "quota" in msg.lower() or "resource" in msg.lower() def test_friendly_lyria_error_runtime_passthrough(): assert friendly_lyria_error(RuntimeError("custom")) == "custom"