Fix generation JSON/Lyria errors, add Winamp player, and ship Echoes of the Sahel.
Harden DeepSeek JSON parsing with retry, pre-sanitize Lyria prompts, and instrumental fallback. Add pure HTML Winamp skin at /winamp with playlist export support. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from ozan_radio.dj import parse_llm_json
|
||||
|
||||
|
||||
def test_parse_llm_json_plain():
|
||||
data = parse_llm_json('{"title": "Test", "lyria_prompt": "one line"}')
|
||||
assert data["title"] == "Test"
|
||||
|
||||
|
||||
def test_parse_llm_json_fenced():
|
||||
raw = '```json\n{"title": "Fenced"}\n```'
|
||||
assert parse_llm_json(raw)["title"] == "Fenced"
|
||||
|
||||
|
||||
def test_parse_llm_json_wrapped_text():
|
||||
raw = 'Here is the plan:\n{"title": "Wrapped", "mood": "calm"}\nThanks.'
|
||||
assert parse_llm_json(raw)["title"] == "Wrapped"
|
||||
|
||||
|
||||
def test_parse_llm_json_invalid_raises():
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
parse_llm_json("not json at all")
|
||||
+23
-1
@@ -79,10 +79,32 @@ def test_build_lyria_prompt_hindi_language(sample_plan):
|
||||
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 "griot" not in soft.lower()
|
||||
assert soft != raw
|
||||
|
||||
|
||||
def test_sanitize_for_retry_softens_throat_singing():
|
||||
raw = "Mongolian overtone throat singing over dub bass"
|
||||
soft = _sanitize_for_retry(raw)
|
||||
assert "throat singing" not in soft.lower()
|
||||
assert "wordless" in soft.lower()
|
||||
|
||||
|
||||
def test_sanitize_for_retry_softens_anatolian():
|
||||
raw = "Anatolian saz and ney with dub delay"
|
||||
soft = _sanitize_for_retry(raw)
|
||||
assert "anatolian" not in soft.lower()
|
||||
|
||||
|
||||
def test_instrumental_fallback_strips_vocals():
|
||||
from ozan_radio.lyria import _instrumental_fallback
|
||||
|
||||
raw = "88 BPM dub.\nWordless male baritone hum layer.\nSub bass pulse."
|
||||
out = _instrumental_fallback(raw)
|
||||
assert "baritone" not in out.lower()
|
||||
assert "Instrumental only" in out
|
||||
|
||||
|
||||
def test_response_parts_from_candidates():
|
||||
part = SimpleNamespace(text=None, inline_data=SimpleNamespace(data=b"audio"))
|
||||
content = SimpleNamespace(parts=[part])
|
||||
|
||||
@@ -16,6 +16,7 @@ def client(tmp_path: Path, monkeypatch):
|
||||
gateway = tmp_path / "gateway"
|
||||
gateway.mkdir()
|
||||
(gateway / "index.html").write_text("<html></html>", encoding="utf-8")
|
||||
(gateway / "winamp.html").write_text("<html><title>Winamp</title></html>", encoding="utf-8")
|
||||
(tmp_path / "vocal_cues.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
@@ -88,6 +89,13 @@ def test_root_lists_lyria_endpoint(client: TestClient):
|
||||
r = client.get("/")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["endpoints"]["lyria"] == "/api/lyria"
|
||||
assert r.json()["endpoints"]["winamp"] == "/winamp"
|
||||
|
||||
|
||||
def test_winamp_page(client: TestClient):
|
||||
r = client.get("/winamp")
|
||||
assert r.status_code == 200
|
||||
assert "Winamp" in r.text
|
||||
|
||||
|
||||
def test_lyria_capabilities_without_api_key(client: TestClient):
|
||||
|
||||
@@ -3,7 +3,12 @@ from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from ozan_radio.web_playlist import PLAYLIST_MARKER, build_playlist_payload, export_gateway_playlist
|
||||
from ozan_radio.web_playlist import (
|
||||
PLAYLIST_MARKER,
|
||||
build_playlist_payload,
|
||||
embed_playlist_in_html,
|
||||
export_gateway_playlist,
|
||||
)
|
||||
|
||||
|
||||
def test_build_playlist_payload_excludes_skip(songs_dir: Path):
|
||||
@@ -14,6 +19,13 @@ def test_build_playlist_payload_excludes_skip(songs_dir: Path):
|
||||
assert payload["tracks"][0]["url"].startswith("https://tinqs.com/tinqs/live-radio/media/")
|
||||
|
||||
|
||||
def test_embed_playlist_in_html_replaces_marker():
|
||||
html = f"<script>{PLAYLIST_MARKER}\n{{}}\n{PLAYLIST_MARKER}</script>"
|
||||
out = embed_playlist_in_html(html, {"tracks": [{"id": "abc"}]})
|
||||
assert '"abc"' in out
|
||||
assert PLAYLIST_MARKER in out
|
||||
|
||||
|
||||
def test_export_gateway_playlist_writes_json_and_embeds(tmp_path: Path, songs_dir: Path):
|
||||
gateway = tmp_path / "gateway"
|
||||
gateway.mkdir()
|
||||
@@ -22,6 +34,11 @@ def test_export_gateway_playlist_writes_json_and_embeds(tmp_path: Path, songs_di
|
||||
f"<script>\n{PLAYLIST_MARKER}\n{{}}\n{PLAYLIST_MARKER}\n</script>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
winamp = gateway / "winamp.html"
|
||||
winamp.write_text(
|
||||
f"<script>\n{PLAYLIST_MARKER}\n{{}}\n{PLAYLIST_MARKER}\n</script>\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "songs").mkdir(exist_ok=True)
|
||||
for f in songs_dir.iterdir():
|
||||
dest = tmp_path / "songs" / f.name
|
||||
@@ -34,3 +51,5 @@ def test_export_gateway_playlist_writes_json_and_embeds(tmp_path: Path, songs_di
|
||||
html = index.read_text(encoding="utf-8")
|
||||
assert "11111111" in html
|
||||
assert PLAYLIST_MARKER in html
|
||||
whtml = winamp.read_text(encoding="utf-8")
|
||||
assert "11111111" in whtml
|
||||
|
||||
Reference in New Issue
Block a user