diff --git a/gateway/player.html b/gateway/player.html index a92434f..1927765 100644 --- a/gateway/player.html +++ b/gateway/player.html @@ -299,7 +299,30 @@ line-height: 1.4; } .status.generating { color: var(--accent); } + .status.ready-flash { color: #6bcf8e; font-weight: 600; } .status.error { color: #ff5c7a; } + .gen-ready-toast { + position: fixed; + top: 1rem; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + background: linear-gradient(135deg, #1e3a2f, #2a4a3a); + border: 1px solid #6bcf8e; + color: #e8fff0; + padding: 0.85rem 1.25rem; + border-radius: 12px; + font-size: 0.85rem; + font-weight: 600; + box-shadow: 0 8px 32px rgba(0,0,0,0.45); + animation: toast-in 0.35s ease; + max-width: 90vw; + text-align: center; + } + @keyframes toast-in { + from { opacity: 0; transform: translateX(-50%) translateY(-12px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } + } .stack { font-size: 0.7rem; color: #555568; @@ -626,6 +649,7 @@
Connecting…
+ @@ -674,7 +698,10 @@ const moodEl = document.getElementById('mood'); const djEl = document.getElementById('dj'); const statusEl = document.getElementById('status'); + const genReadyToast = document.getElementById('genReadyToast'); const genBtn = document.getElementById('genBtn'); + let pollStartedAt = null; + let lastNotifiedCompletedAt = null; const skipBtn = document.getElementById('skipBtn'); const refreshBtn = document.getElementById('refreshBtn'); const chatLog = document.getElementById('chatLog'); @@ -978,8 +1005,11 @@ const costs = stats.costs || {}; statSpent.textContent = fmtUsd(today.estimated_usd); statPerTrack.textContent = `~${fmtUsd(costs.per_track_estimate_usd)} / track`; - statGenerated.textContent = `${today.generated || 0} / ${today.max_per_day || 10}`; - statRemaining.textContent = `${today.remaining ?? 0} remaining`; + const unlimited = today.unlimited || today.max_per_day === 0; + statGenerated.textContent = unlimited + ? `${today.generated || 0} (unlimited)` + : `${today.generated || 0} / ${today.max_per_day || 10}`; + statRemaining.textContent = unlimited ? '∞' : `${today.remaining ?? 0} remaining`; statMaxBudget.textContent = fmtUsd(stats.projected_daily_max_usd); if (stats.playback) { radioSettings.playback = stats.playback; @@ -1161,22 +1191,63 @@ } let pollTimer = null; + let toastTimer = null; + + function showReadyToast(title) { + genReadyToast.textContent = `Ready Β· ${title}`; + genReadyToast.hidden = false; + clearTimeout(toastTimer); + toastTimer = setTimeout(() => { genReadyToast.hidden = true; }, 8000); + } + + function notifyTrackReady(track, gen) { + const title = track?.title || gen?.last_completed_title || 'New track'; + statusEl.textContent = `Ready Β· playing ${title}`; + statusEl.classList.remove('generating', 'error'); + statusEl.classList.add('ready-flash'); + setTimeout(() => statusEl.classList.remove('ready-flash'), 5000); + showReadyToast(title); + addBubble('dj', `Fresh cut ready: ${title} β€” on air now.`); + if (typeof Notification !== 'undefined' && Notification.permission === 'granted') { + new Notification('Live Ozan Radio', { body: `Ready: ${title}`, tag: 'track-ready' }); + } + lastNotifiedCompletedAt = gen?.last_completed_at || null; + } + function pollForNewTrack() { let n = 0; + pollStartedAt = Date.now(); clearInterval(pollTimer); pollTimer = setInterval(async () => { n += 1; const stats = await loadStats(true); - if (stats?.generation?.busy) return; - if (stats?.generation?.error) { + const gen = stats?.generation; + if (gen?.busy) return; + if (gen?.error) { + clearInterval(pollTimer); + statusEl.textContent = gen.error; + statusEl.classList.add('error'); + addBubble('dj', `Couldn't finish that track: ${gen.error}`); + return; + } + const completedAt = gen?.last_completed_at; + const isNewCompletion = completedAt + && completedAt !== lastNotifiedCompletedAt + && (!pollStartedAt || new Date(completedAt).getTime() >= pollStartedAt - 2000); + await refreshNow(); + const now = await fetch(`${API}/api/now`).then(r => r.json()).catch(() => null); + if (isNewCompletion && now?.track) { + applyTrack(now.track, 'generated'); + notifyTrackReady(now.track, gen); clearInterval(pollTimer); return; } - await refreshNow(); - const now = await fetch(`${API}/api/now`).then(r => r.json()).catch(() => null); if (now?.track?.track_id !== currentTrackId) clearInterval(pollTimer); - if (n > 60) clearInterval(pollTimer); - }, 3000); + if (n > 90) { + clearInterval(pollTimer); + statusEl.textContent = 'Generation taking longer than usual β€” still polling…'; + } + }, 2000); } async function refreshNow() { @@ -1197,22 +1268,35 @@ } async function generate() { + if (typeof Notification !== 'undefined' && Notification.permission === 'default') { + Notification.requestPermission(); + } genBtn.disabled = true; - statusEl.textContent = 'DeepSeek is planning… Lyria is composing…'; + statusEl.textContent = 'Starting… DeepSeek plans, then Lyria composes (~30–90s)'; + statusEl.classList.add('generating'); try { const res = await fetch(`${API}/api/generate`, { method: 'POST' }); const data = await res.json(); if (data.status === 'busy') { statusEl.textContent = data.message; + applyGenerationState(data.generation); + pollForNewTrack(); } else if (data.status === 'limit') { statusEl.textContent = data.message || 'Daily limit reached'; + statusEl.classList.remove('generating'); updateDashboard({ today: data.budget, costs: radioSettings.costs }); } else if (data.status === 'error') { statusEl.textContent = data.message || 'Generation failed'; statusEl.classList.add('error'); applyGenerationState(data.generation || { error: data.message }); + } else if (data.status === 'accepted' || data.generating) { + applyGenerationState(data.generation || { busy: true, phase: 'planning' }); + pollForNewTrack(); } else if (data.status === 'ok') { - if (data.track) applyTrack(data.track, data.source || 'generated'); + if (data.track) { + applyTrack(data.track, data.source || 'generated'); + notifyTrackReady(data.track, data.generation); + } await loadStats(); } else { statusEl.textContent = 'Generation failed β€” check server logs'; diff --git a/settings.json b/settings.json index 542787e..39de4ef 100644 --- a/settings.json +++ b/settings.json @@ -1,71 +1,67 @@ { "listener": "ozan", - "profile_note": "Modern electronica meets ethnic fusion β€” Jon Hopkins Singularity, Karsh Kale, Transglobal Underground. Not Turkish folk. Updated 2026-06-07.", + "profile_note": "Techno-ethnic station β€” Bonobo, Jon Hopkins, Jamaica dub, Sahel, Mongolia overtone vocals, Urdu colour. Beautiful cross-cultural blends. Not Turkish folk. Updated 2026-06-07.", "station": "Live Ozan Radio", "taste": { - "summary": "Modern electronica meets ethnic fusion β€” Jon Hopkins Singularity lush synth builds, Karsh Kale tabla breaks, Transglobal dervish-trance. Electronic production first, world percussion and organic colour woven in. Not Turkish folk.", + "summary": "Techno-ethnic β€” beautiful electronica meets world dub. Bonobo lush downtempo, Jon Hopkins Singularity builds, Jamaica spring-reverb dub, Sahara/Sahel warmth, Mongolian overtone throat textures, Urdu vocal colour, Karsh Kale tabla breaks. Many cultures woven into one gorgeous track β€” not one country alone.", "genres": [ + "techno-ethnic", + "ethnic dub", + "bonobo-style downtempo electronica", + "jamaican dub and reggae-dub", "modern electronica", - "ambient techno meets world", - "ethnic fusion", - "dervish trance", - "tabla electronica", - "asian underground", + "sahel desert electronica", + "mongolian overtone electronica", + "urdu-world fusion", "global dub", "dubtronica", - "world electronica", - "cinematic electronic", - "trip-hop dub" + "trip-hop dub", + "tabla electronica", + "cinematic electronic" ], "mood": [ + "beautiful", "hypnotic", - "trance-like pulse", "warm", "spacious", "late-night", - "cinematic gothic", + "lush and melodic", "meditative but danceable", - "euphoric electronic builds", - "immersive and cinematic", - "cross-cultural blend" + "euphoric builds", + "immersive", + "cross-cultural wonder" ], "instruments": [ - "lush synth pads and arpeggiated electronica (Singularity-style)", - "tabla and Indian percussion driving the groove", - "sitar and synth leads blended", - "organic hand percussion meets digital pulse", - "oud and kora as colour accents", - "sub bass and sidechain warmth", - "dub delay and spring reverb", - "wordless vocal textures", - "Gregorian-adjacent choral layers (Frostbite lane)", - "ceremonial ritual chants (Chac's Dub lane)", - "breakbeat or four-on-floor-lite hypnotic pulse", - "analog warmth and wide stereo immersion" + "lush Bonobo-style synths and melodic electronica", + "Jamaica dub β€” spring reverb, one-drop feel, sub bass culture", + "Sahel kora and desert percussion accents", + "Mongolian overtone throat singing as wordless texture layer", + "Urdu male vocal colour β€” poetic, not Bollywood pop", + "tabla and hand percussion meets digital pulse", + "dub delay and tape echo", + "Jon Hopkins arpeggiated builds", + "organic samples woven through electronic production" ], - "tempo_bpm": [90, 124], - "references": "Jon Hopkins Singularity β€” modern electronica with organic world percussion and euphoric synth builds; Karsh Kale Milan (tabla electronica); Transglobal Underground Dervish Trans; Thievery Corporation lounge; Shye Ben Tzur / Anoushka Shankar Indian fusion; Baaba Maal Sahel accent; Glass Beams psychedelic tint. Electronic production leads β€” world colour is woven in, not folk-first.", + "tempo_bpm": [88, 118], + "references": "Bonobo β€” lush melodic downtempo, beautiful ethnic electronica; Jon Hopkins Singularity; Jamaica dub (Kingston spring reverb, Mungo's Hi Fi); Baaba Maal / Sahara Sahel warmth; Karsh Kale Milan; Transglobal Dervish Trans; Thievery Corporation; Shye Ben Tzur Urdu-Indian fusion without pop. We are inventing techno-ethnic β€” electronic beauty first, world voices as colour.", "avoid": [ - "Turkish-specific tracks β€” Ney Vakti, Anatolian night, Ottoman court, bağlama/saz as the identity", - "Turkish language lead vocals or Turkish folk melody as the hook", - "Anadolu psych or AltΔ±n GΓΌn-style as the main flavour", - "Indian pop or Rajasthani folk dub β€” dholak, kartal, sarangi Bollywood-adjacent feel", - "Japanese enka shamisen slow-burn under 88 BPM β€” Kurai Yoru too slow", - "Armenian duduk hymn dub β€” slow mournful sacred chant, Duduk Zomari too slow", - "single-country homages of any nation", - "lead vocals locked to one national folk tradition", - "West African highlife and palm-wine guitar", - "slow ney-and-bağlama meditation", + "Turkish folk β€” Ney Vakti, Anatolian, Ottoman, bağlama/saz as identity", + "Indian pop / Rajasthani folk dub", + "Japanese enka shamisen slow-burn", + "Armenian duduk hymn slow sacred chant", + "single-country homages", + "slow sluggish tracks under 88 BPM", + "West African highlife palm-wine guitar", "big-room EDM drops", "four-on-the-floor house", - "fuzz electric guitar on griot or lead vocal tracks", - "sluggish tempo under 88 BPM" + "fuzz electric guitar on vocal tracks" ] }, "dj": { "variety": true, "default_length": "1-2 minutes", - "prefer_fusion_over_locale": true + "prefer_fusion_over_locale": true, + "station_mission": "techno-ethnic β€” Bonobo-beautiful blends across Jamaica, Sahara, Mongolia, Urdu, and beyond" }, "playback": { "shuffle": true, diff --git a/songs/genres.json b/songs/genres.json index 8065872..1fdc2c7 100644 --- a/songs/genres.json +++ b/songs/genres.json @@ -1,26 +1,29 @@ { "updated": "2026-06-07", - "description": "Station genre taxonomy β€” tags on each track; categories are broader browse buckets.", + "description": "Techno-ethnic taxonomy β€” Bonobo-beautiful electronica meets world dub and cross-cultural vocals.", + "station_lane": "techno-ethnic", "categories": { - "fusion-electronic": "Jon Hopkins / Karsh Kale β€” modern electronica meets world percussion", + "techno-ethnic": "Bonobo / Hopkins β€” lush electronic beauty meets world colour", + "jamaica-dub": "Kingston spring reverb, reggae-dub bass culture", + "sahara-sahel": "Desert warmth, kora, Sahel pulse", + "mongolia-overtone": "Throat singing as wordless ethereal layer", + "urdu-fusion": "Urdu poetic vocal colour in electronica", "dub-space": "Dub delay, sub bass, spacious mixes", - "ceremonial-world": "Ritual chant, temple/ceremony energy β€” Chac's Dub lane", - "cinematic-gothic": "Cold ether, choral depth, Only Lovers Left Alive mood β€” Frostbite / Gregorian lane", - "desert-dub": "Sahel warmth, caravan pulse, desert blues texture", - "vocal-ethnic": "Wordless or blended ethnic vocals, not folk-pop" + "ceremonial-world": "Ritual chant energy β€” Chac's Dub", + "cinematic-gothic": "Gregorian ether β€” Frostbite Dub" }, "genres": { - "modern-electronica": { "category": "fusion-electronic", "label": "Modern electronica meets world" }, - "tabla-electronica": { "category": "fusion-electronic", "label": "Karsh Kale / Milan-style" }, - "dervish-trance": { "category": "fusion-electronic", "label": "Transglobal hypnotic breaks" }, - "ceremonial-dub": { "category": "ceremonial-world", "label": "Ceremonial chant + dub space" }, - "mesoamerican-electronica": { "category": "ceremonial-world", "label": "Clay flute, ritual pulse" }, - "gregorian-ether": { "category": "cinematic-gothic", "label": "Gregorian-adjacent choral depth" }, - "nordic-dub": { "category": "cinematic-gothic", "label": "Icelandic ether + sub bass" }, - "cinematic-gothic": { "category": "cinematic-gothic", "label": "Cold cinematic gothic dub" }, - "desert-dub": { "category": "desert-dub", "label": "Desert dubtronica" }, - "sahel-blues": { "category": "desert-dub", "label": "Sahel griot warmth" }, - "gnawa-dub": { "category": "dub-space", "label": "Gnawa meets dubtronica" }, + "techno-ethnic": { "category": "techno-ethnic", "label": "Core station lane β€” beautiful blended electronica" }, + "bonobo-downtempo": { "category": "techno-ethnic", "label": "Lush melodic Bonobo-style electronica" }, + "modern-electronica": { "category": "techno-ethnic", "label": "Jon Hopkins Singularity builds" }, + "jamaican-dub": { "category": "jamaica-dub", "label": "Jamaica dub and reggae-dub space" }, + "sahel-electronica": { "category": "sahara-sahel", "label": "Sahara / Sahel desert electronica" }, + "mongolian-overtone": { "category": "mongolia-overtone", "label": "Overtone throat textures in mix" }, + "urdu-electronica": { "category": "urdu-fusion", "label": "Urdu vocal colour, not Bollywood" }, + "tabla-electronica": { "category": "techno-ethnic", "label": "Karsh Kale / Milan-style" }, + "ceremonial-dub": { "category": "ceremonial-world", "label": "Ceremonial chant + dub" }, + "gregorian-ether": { "category": "cinematic-gothic", "label": "Gregorian-adjacent choral dub" }, + "desert-dub": { "category": "sahara-sahel", "label": "Desert dubtronica" }, "world-dub": { "category": "dub-space", "label": "Pan-ethnic dub lounge" } } } diff --git a/songs/library_index.json b/songs/library_index.json index 8ce6700..0557a76 100644 --- a/songs/library_index.json +++ b/songs/library_index.json @@ -1,7 +1,7 @@ { "updated": "2026-06-07", "listener": "ozan", - "summary": "Batch purge 2026-06-07 β€” kept original 5 + Chac's Dub + Frostbite Dub only. Lane: Hopkins/Karsh Kale fusion, not slow country-specific gens.", + "summary": "Techno-ethnic station β€” Bonobo, Jamaica dub, Sahara, Mongolia overtone, Urdu, Hopkins. Seven keepers. Beautiful cross-cultural blends, not single-nation folk.", "genre_taxonomy": "songs/genres.json", "global_avoid": [ "fuzz electric guitar on griot/vocal tracks", @@ -19,14 +19,16 @@ "Armenian duduk hymn dub β€” slow sacred chant, listener hated Duduk Zomari (same problems)" ], "global_love": [ - "Jon Hopkins Singularity β€” modern electronica meets organic world percussion", - "ceremonial dub β€” Chac's Dub temple-step energy", - "Gregorian-adjacent choral ether β€” Frostbite Dub (listener used to love Gregorian)", - "lush arpeggiated synths and euphoric cinematic builds", - "Karsh Kale / Milan-style tabla electronica", - "Transglobal Underground dervish-trance pulse", - "sub bass pulse and warm analog dub space", - "wordless vocal textures" + "Bonobo β€” lush beautiful melodic downtempo ethnic electronica", + "techno-ethnic blends β€” Jamaica dub + Sahara + Mongolia + Urdu in one track", + "Jamaica spring-reverb dub and reggae-dub bass culture", + "Sahara / Sahel desert warmth as electronic colour", + "Mongolian overtone throat singing as wordless ethereal layer", + "Urdu poetic male vocal colour β€” fusion not Bollywood pop", + "Jon Hopkins Singularity builds", + "ceremonial dub β€” Chac's Dub", + "Gregorian ether β€” Frostbite Dub", + "sub bass and warm dub space" ], "gold_standard_id": "1c1d7b8a", "tracks": [ diff --git a/songs/manifest.json b/songs/manifest.json index e0a96d4..8ade0db 100644 --- a/songs/manifest.json +++ b/songs/manifest.json @@ -1,5 +1,5 @@ { - "index": 6, + "index": 0, "count": 7, "tracks": [ { diff --git a/src/ozan_radio/batch_prompts.py b/src/ozan_radio/batch_prompts.py index cd676f0..04f884b 100644 --- a/src/ozan_radio/batch_prompts.py +++ b/src/ozan_radio/batch_prompts.py @@ -1,94 +1,84 @@ -"""Curated batch directions β€” Jon Hopkins Singularity meets ethnic fusion, not Turkish folk.""" +"""Techno-ethnic batch β€” Bonobo-beautiful blends: Jamaica, Sahara, Mongolia, Urdu, and more.""" VOCAL_BATCH: list[str] = [ ( - "Jon Hopkins Singularity-style β€” lush arpeggiated synths, organic tabla and " - "hand percussion, euphoric build, sub bass pulse, wordless vocal textures, 118 BPM. " - "Modern electronica meets world colour." + "Bonobo-style techno-ethnic β€” lush melodic synths, soft breakbeat, " + "Jamaica dub spring reverb on percussion, 96 BPM. Beautiful and spacious." ), ( - "Karsh Kale-style tabla electronica β€” driving tabla breakbeat, sitar riff " - "with synth pad, sub bass, wordless male vocal textures, 100 BPM. Indian-world fusion." + "Jamaica meets Sahara dub β€” Kingston one-drop sub bass, kora accents, " + "wordless Sahel male chant, tape delay, Mungo's Hi Fi warmth, 92 BPM." ), ( - "Transglobal Underground dervish-trance β€” hypnotic ethnic breakbeat, oud and " - "synth swirl, wordless call-and-response vocals, dub delay, 96 BPM pulse." + "Mongolia overtone electronica β€” wordless throat-singing texture layer " + "over Bonobo lush synth pad, tabla pulse, sub bass, 94 BPM ethereal blend." ), ( - "Milan-style fusion dub β€” tabla and sitar over downtempo break, male wordless " - "vocal hums, bass pulse, spring reverb, Thievery Corporation lounge energy." + "Urdu-world Bonobo dub β€” male poetic Urdu vocal colour over downtempo " + "electronica, tabla, dub delay, NOT Bollywood pop, 90 BPM beautiful." ), ( - "Sahel meets Asian underground β€” kora and tabla together, deep sub bass, " - "wordless griot-style chants, hand percussion, 94 BPM. No Turkish instruments lead." + "Jon Hopkins meets Jamaica β€” arpeggiated synth build, reggae-dub bass, " + "spring reverb, hand percussion, euphoric forward pulse 110 BPM." ), ( - "Ethiopian-jazz world breaks β€” krar, breathy wordless vocals, piano, synth pad, " - "tabla accents, sub bass, 90 BPM. Pan-ethnic not national." + "Sahara Sahel techno-ethnic β€” desert kora and talking drum with electronic " + "kick, wordless griot chant, Bonobo melodic warmth, 95 BPM." ), ( - "Persian-Sahel lounge fusion β€” oud and tar with sitar colour, baritone wordless " - "vocal, tabla groove, hand drums, cinematic ether 92 BPM." + "Karsh Kale Milan energy β€” tabla breakbeat, sitar-synth hook, sub bass, " + "Urdu wordless alaap texture blended in, 100 BPM." ), ( - "Balkan-brass world breaks β€” female vocalise, muted trumpet, tabla and darbuka " - "blend, sub bass, 98 BPM dark danceable mix." - ), - ( - "Atlantic-Desert electronica β€” soft wordless vocals, fingerpicked warmth, " - "tabla pulse, oceanic reverb, sub bass fusion lounge 88 BPM." - ), - ( - "Sahel-Asian underground fusion β€” kora and tabla with synth arpeggio, " - "wordless male chants, sub bass 93 BPM. No Rajasthani folk or sarangi pop." - ), - ( - "Desert caravan dub β€” Bambara wordless chants, kora and oud, tabla breakbeat " - "from bar one, sub bass, NO bağlama or ney as lead, 95 BPM." - ), - ( - "Mediterranean dubtronica mix β€” bouzouki colour as accent only, tabla groove, " - "wordless vocals, sub bass, 92 BPM. Not country-specific." - ), - ( - "Kurdish-Sahel storytelling dub β€” tanbur drone as texture, tabla pulse, " - "wordless narrative vocal, synth pad, 91 BPM cross-cultural." - ), - ( - "Nordic-desert ether fusion β€” breathy female wordless vocals, piano, synth, " - "tabla accents, sub bass, cold cinematic dub 90 BPM." - ), - ( - "East-West break fusion β€” sitar hook with tabla breakbeat at 96 BPM, " - "synth arpeggio, wordless vocal hum. No shamisen or enka slow-burn." - ), - ( - "Indo-dub lounge β€” female wordless alaap, tabla and sitar, synth bass, " - "Karsh Kale energy, 97 BPM. Fusion not Bollywood-only." - ), - ( - "Synth-pad world breaks β€” tabla groove, wordless male chant, sub bass, " - "96 BPM forward pulse. No duduk hymn or slow sacred woodwind lead." + "Transglobal dervish-trance β€” hypnotic ethnic breakbeat, oud swirl, " + "wordless vocals, dub space, 98 BPM danceable." ), ( "Chac's Dub successor β€” ceremonial world dub, clay flute ritual chants, " - "baritone chest voice, darbuka, sub bass, temple-step energy 85 BPM." + "baritone chest voice, darbuka, sub bass, Bonobo spacious mix 85 BPM." ), ( - "Frostbite / Gregorian-ether dub β€” choral wordless female vocals, piano, " - "sub bass, cinematic gothic cold warmth, sacred choral through dub filter 90 BPM." + "Frostbite Gregorian-ether β€” choral wordless vocals, piano, sub bass, " + "cinematic gothic through Bonobo-style electronic warmth 90 BPM." ), ( - "Emerald Rush energy β€” driving Jon Hopkins electronica, pulsing synth arpeggio, " - "organic percussion layer, immersive forward momentum, sub bass, 120 BPM. " - "Electronic-first with subtle world accents." + "Jamaica-Mongolia fusion β€” dub bass and spring reverb, overtone throat " + "texture as ethereal pad, organic hand drums, lush synth, 93 BPM." ), ( - "Atlas-Indian dub β€” oud and sitar blended, tabla driving, wordless chant, " - "darbuka accents, sub bass 96 BPM. Multi-region not Turkish." + "Urdu-Sahel Bonobo lounge β€” poetic male Urdu hum, kora sparkle, " + "downtempo electronica, dub echo, beautiful late-night 88 BPM." ), ( - "Afro-Asian highlife-dub fusion β€” wordless call-and-response vocals, clean " - "plucked strings, talking drum with tabla, ney as light accent only, 94 BPM." + "Ethiopian-jazz techno-ethnic β€” krar, breathy vocals, piano, Bonobo synth " + "pad, forward pulse 91 BPM." + ), + ( + "Persian-Sahel Bonobo blend β€” oud and tar with lush electronica, wordless " + "baritone, tabla groove, 94 BPM." + ), + ( + "Balkan-brass world breaks β€” female vocalise, muted trumpet, tabla, " + "sub bass, 98 BPM dark beautiful mix." + ), + ( + "Indo-dub Bonobo β€” female wordless alaap, tabla, sitar colour, " + "lush melodic synth, 97 BPM not pop." + ), + ( + "Ceremonial techno-ethnic β€” ritual wordless chants, clay flute, " + "Bonobo downtempo pulse, deep sub, 92 BPM." + ), + ( + "Emerald Rush energy β€” driving Hopkins electronica, Jamaica dub bass, " + "Sahel percussion layer, 118 BPM immersive." + ), + ( + "Atlas-Indian dub banger β€” oud and sitar with tabla, wordless chant, " + "Bonobo beauty, punchy sub 96 BPM." + ), + ( + "Pan-world Bonobo finale β€” Jamaica dub + Sahara kora + Mongolia overtone " + "texture + Urdu vocal colour + lush synths, 95 BPM gorgeous blend." ), ] diff --git a/src/ozan_radio/dj.py b/src/ozan_radio/dj.py index 59eaf00..f0b4575 100644 --- a/src/ozan_radio/dj.py +++ b/src/ozan_radio/dj.py @@ -45,14 +45,13 @@ Your job: "melodic call-and-response". Avoid "vocal sample" / "griot sample" phrasing. 9. Read listener curation metadata β€” clone what they loved, hard-avoid what they disliked (e.g. fuzz electric guitar on vocal tracks, long guitar-only intros before saz). -10. Positive references: Jon Hopkins Singularity (modern electronica meets organic - world β€” lush synth arpeggios, euphoric builds, immersive pulse); Karsh Kale (Milan); - Transglobal Underground (Dervish Trans). Electronic production leads, ethnic colour woven in. Also clone keepers by genre: - ceremonial-dub (Chac's Dub), gregorian-ether / cinematic-gothic (Frostbite Dub). -11. Listener likes ETHNIC FUSION MIXES but NOT TURKISH β€” no Ney Vakti, no Anatolian - folk, no Ottoman court, no bağlama/saz as the lead identity, no Turkish vocals. - Blend Sahel, Indian fusion, Middle Eastern accents, and dub. Vocals: wordless - textures only, not national folk traditions. +10. Station mission: TECHNO-ETHNIC β€” beautiful electronica meets world dub. Big + inspirations: Bonobo (lush melodic downtempo), Jon Hopkins Singularity, Jamaica + dub (spring reverb, bass culture), Sahara/Sahel warmth, Mongolian overtone throat + textures (wordless ethereal layer), Urdu poetic vocal colour. Blend many cultures + in one gorgeous track β€” Jamaica + Sahara + Mongolia + Urdu + synth beauty. +11. NOT Turkish folk, NOT slow country homages, NOT Indian pop. Clone keepers: + ceremonial-dub (Chac's Dub), gregorian-ether (Frostbite Dub). Forward tempo 88+ BPM. Respond with JSON only: { diff --git a/src/ozan_radio/server.py b/src/ozan_radio/server.py index 3697b3b..70dcc5b 100644 --- a/src/ozan_radio/server.py +++ b/src/ozan_radio/server.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio import json import random +from datetime import datetime, timezone from pathlib import Path from fastapi import BackgroundTasks, FastAPI, HTTPException @@ -35,7 +36,15 @@ app.add_middleware( _config: Config | None = None _queue: RadioQueue | None = None _generating = False -_generation_state: dict = {"busy": False, "phase": None, "error": None, "track_title": None} +_generation_state: dict = { + "busy": False, + "phase": None, + "error": None, + "track_title": None, + "last_completed_at": None, + "last_completed_title": None, + "last_completed_id": None, +} _chat = ChatStore() @@ -101,14 +110,29 @@ def _play_library_entry(q: RadioQueue, cfg: Config, entry: dict) -> dict: return {"status": "ok", "source": "library", "track": np.__dict__ if np else None} -def _set_generation(*, busy: bool, phase: str | None = None, error: str | None = None, title: str | None = None) -> None: +def _set_generation( + *, + busy: bool, + phase: str | None = None, + error: str | None = None, + title: str | None = None, + completed_id: str | None = None, +) -> None: global _generation_state - _generation_state = { + state = { "busy": busy, "phase": phase, "error": error, "track_title": title, + "last_completed_at": _generation_state.get("last_completed_at"), + "last_completed_title": _generation_state.get("last_completed_title"), + "last_completed_id": _generation_state.get("last_completed_id"), } + if completed_id and title: + state["last_completed_at"] = datetime.now(timezone.utc).isoformat() + state["last_completed_title"] = title + state["last_completed_id"] = completed_id + _generation_state = state async def _compose_track(request: str | None = None, *, check_limit: bool = True) -> dict: @@ -138,12 +162,17 @@ async def _compose_track(request: str | None = None, *, check_limit: bool = True _set_generation(busy=True, phase="composing", title=plan.title) track = LyriaEngine(cfg).generate(plan) q.add(track) + q.play_id(track.plan.id) rs = load_radio_settings(lyria_model=cfg.lyria_model) cost = cost_per_track(cfg.lyria_model, rs.costs.__dict__) record_generation(cfg.output_dir, cost, track.plan.id, track.plan.title) np = q.now_playing() _, budget = _can_generate_today(cfg) - _set_generation(busy=False) + _set_generation( + busy=False, + title=track.plan.title, + completed_id=track.plan.id, + ) return { "status": "ok", "source": "generated", @@ -262,8 +291,30 @@ async def patch_settings(body: SettingsPatch) -> dict: @app.post("/api/generate") -async def generate_track() -> dict: - return await _compose_track() +async def generate_track(background: BackgroundTasks) -> dict: + """Start generation in background β€” poll /api/stats for progress and ready state.""" + cfg = _get_config() + if _generating: + return { + "status": "busy", + "message": "Already generating a track", + "generation": _generation_state, + } + ok, budget = _can_generate_today(cfg) + if not ok: + return { + "status": "limit", + "message": f"Daily limit reached ({budget['max_per_day']} new songs)", + "budget": budget, + } + background.add_task(_background_compose, None) + return { + "status": "accepted", + "generating": True, + "message": "Generating β€” planning then Lyria compose (~30–90s)", + "budget": budget, + "generation": _generation_state, + } @app.post("/api/shuffle/next") diff --git a/taste_seeds.json b/taste_seeds.json index d4d1bc5..7bdb95d 100644 --- a/taste_seeds.json +++ b/taste_seeds.json @@ -9,64 +9,61 @@ "Your Top Songs 2025" ], "artists": [ + "Bonobo", + "Jon Hopkins", "Karsh Kale", "Transglobal Underground", "Baaba Maal", "Thievery Corporation", + "Mungo's Hi Fi", "islandman", "Kaya Project", - "Blanco White", "Shye Ben Tzur", "Anoushka Shankar", - "Jon Hopkins", "Glass Beams", "Francis Bebey", - "Mungo's Hi Fi", "Matthew Halsall" ], "albums": [ - { "title": "Garip", "artist": "AltΔ±n GΓΌn", "vibe": "Anadolu psych-folk, bağlama, fuzzy warmth" }, + { "title": "Migration", "artist": "Bonobo", "vibe": "lush melodic downtempo, beautiful ethnic electronica, organic samples" }, + { "title": "Singularity", "artist": "Jon Hopkins", "vibe": "modern electronica meets organic world, euphoric builds" }, { "title": "Estuaire", "artist": "Ablaye Cissoko", "vibe": "kora, West African elegance, spacious" }, { "title": "Mirage", "artist": "Glass Beams", "vibe": "psychedelic instrumental, Middle Eastern tint" }, - { "title": "African Electronic Music 1975-1982", "artist": "Francis Bebey", "vibe": "early African electronic, playful hypnotic" }, - { "title": "Singularity", "artist": "Jon Hopkins", "vibe": "modern electronica meets organic world β€” lush synth builds, immersive pulse, euphoric cinematic" } + { "title": "African Electronic Music 1975-1982", "artist": "Francis Bebey", "vibe": "early African electronic, playful hypnotic" } ], - "preference": "Modern electronica meets ethnic fusion β€” Jon Hopkins Singularity, Karsh Kale, Dervish Trans. Turkish folk no.", + "preference": "Techno-ethnic β€” Bonobo-beautiful electronica meets Jamaica dub, Sahara, Mongolia overtone vocals, Urdu colour. Cross-cultural blends, not single-nation folk.", "reference_tracks": [ - { "title": "Singularity", "artist": "Jon Hopkins", "vibe": "lush arpeggiated synths, organic percussion, euphoric builds, modern electronic meets world" }, - { "title": "Emerald Rush", "artist": "Jon Hopkins", "vibe": "driving hypnotic electronica, pulsing bass, immersive forward momentum" }, + { "title": "Kerala", "artist": "Bonobo", "vibe": "lush downtempo, beautiful world electronica" }, + { "title": "Singularity", "artist": "Jon Hopkins", "vibe": "lush synths, organic percussion, euphoric electronic meets world" }, + { "title": "Milan", "artist": "Karsh Kale", "vibe": "tabla electronica, sitar-synth fusion" }, { "title": "Dervish Trans", "artist": "Transglobal Underground", "vibe": "dervish trance breaks, hypnotic ethnic pulse" }, - { "title": "Milan", "artist": "Karsh Kale", "vibe": "tabla electronica, sitar-synth fusion, driving world breakbeat" } + { "title": "Boboyillo", "artist": "Baaba Maal", "vibe": "Sahara Sahel warmth, rolling desert pulse" } + ], + "cultural_palette": [ + "Jamaica β€” dub spring reverb, reggae-dub bass culture", + "Sahara / Sahel β€” desert warmth, kora, griot colour", + "Mongolia β€” overtone throat singing as ethereal wordless layer", + "Urdu β€” poetic male vocal colour in fusion (not Bollywood pop)", + "and more β€” always blended, always beautiful" ], "genres": [ + "techno-ethnic", + "bonobo-style downtempo", + "ethnic dub", + "jamaican dub", + "sahel electronica", "modern electronica", - "ambient techno meets world", - "ethnic fusion", - "ethnic chill", "dubtronica", "world dub", "trip-hop", - "global fusion jazz", - "afro-dub", - "cinematic electronic", - "psychedelic world" + "global fusion" ], "tracks": [ { "title": "Boboyillo", "artists": ["Baaba Maal", "Rougi"], "album": "Being", - "vibe": "Sahel warmth, rolling desert pulse, call-and-response, spiritual but danceable" - }, - { - "title": "Olalla", - "artists": ["Blanco White"], - "vibe": "melancholic indie folk, Spanish ether, intimate late-night" - }, - { - "title": "Daily Mix 01", - "artists": ["BALTHVS", "Pentagram", "Cem Karaca"], - "vibe": "Turkish rock and psych crossover" + "vibe": "Sahara Sahel warmth, rolling desert pulse, spiritual but danceable" }, { "title": "Daily Mix 03", @@ -76,7 +73,7 @@ { "title": "Daily Mix 04", "artists": ["Shye Ben Tzur", "Anoushka Shankar"], - "vibe": "Indian classical fusion, devotional texture, world spiritual" + "vibe": "Urdu-Indian fusion, devotional texture, world spiritual β€” not pop" } ] }