Add Live Ozan Radio post with workspace screenshot.
Personal AI station post: DeepSeek DJ, Lyria 3, curation metadata, and Gitea auto-play gateway. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
@@ -167,6 +167,13 @@
|
||||
<span class="blog-card__read">Read →</span>
|
||||
</a>
|
||||
|
||||
<a href="live-ozan-radio" class="blog-card">
|
||||
<span class="blog-card__date">7 June 2026</span>
|
||||
<h2 class="blog-card__title">Live Ozan Radio: A Personal AI Station in Cursor</h2>
|
||||
<p class="blog-card__excerpt">I wanted a radio that never plays catalog music — only fresh AI compositions shaped by my taste. Here's the stack, the player, and how I DJ every generated track with metadata.</p>
|
||||
<span class="blog-card__read">Read →</span>
|
||||
</a>
|
||||
|
||||
<a href="blog-visual-upgrade" class="blog-card">
|
||||
<span class="blog-card__date">3 June 2026</span>
|
||||
<h2 class="blog-card__title">How We Restyled Our Blog with Two Template Files and Zero Dependencies</h2>
|
||||
|
||||
|
Before
After
|
@@ -0,0 +1,330 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Live Ozan Radio: A Personal AI Station in Cursor — Tinqs Blog</title>
|
||||
<meta name="description" content="No Spotify playback — DeepSeek DJs, Lyria composes, and every track gets curated metadata. How we built a personal radio that auto-plays from our own Gitea.">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://www.tinqs.com/blog/live-ozan-radio">
|
||||
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://www.tinqs.com/blog/live-ozan-radio">
|
||||
<meta property="og:title" content="Live Ozan Radio: A Personal AI Station in Cursor">
|
||||
<meta property="og:description" content="Personal AI radio: DeepSeek DJ + Lyria 3, taste from screenshots, auto-play on tinqs.com.">
|
||||
<meta property="og:image" content="https://tinqs.com/tinqs/blog/media/branch/main/img/live-ozan-radio-workspace.png">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Live Ozan Radio: A Personal AI Station in Cursor">
|
||||
<meta name="twitter:description" content="Personal AI radio: DeepSeek DJ + Lyria 3, taste from screenshots, auto-play on tinqs.com.">
|
||||
<meta name="twitter:image" content="https://tinqs.com/tinqs/blog/media/branch/main/img/live-ozan-radio-workspace.png">
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
"headline": "Live Ozan Radio: A Personal AI Station in Cursor",
|
||||
"datePublished": "2026-06-07",
|
||||
"author": {
|
||||
"@type": "Person",
|
||||
"name": "Ozan Bozkurt"
|
||||
},
|
||||
"publisher": {
|
||||
"@type": "Organization",
|
||||
"name": "Tinqs Limited",
|
||||
"url": "https://www.tinqs.com"
|
||||
},
|
||||
"description": "No Spotify playback — DeepSeek DJs, Lyria composes, and every track gets curated metadata. How we built a personal radio that auto-plays from our own Gitea."
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* ── Self-contained post styles (Studio provides site chrome) ── */
|
||||
|
||||
:root {
|
||||
--c-accent: #c9935a;
|
||||
--c-accent-l: #d4a87c;
|
||||
--c-bg: #0d1117;
|
||||
--c-text: #e6edf3;
|
||||
--c-muted: #9aa7b4;
|
||||
--c-border: #2a3340;
|
||||
--c-blue: #38bdf8;
|
||||
--c-purple: #a855f7;
|
||||
--c-gold: #f59e0b;
|
||||
--c-code-bg: #1c2230;
|
||||
--c-pre-bg: #0a0e14;
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--c-text);
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ── Post container ── */
|
||||
.post {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 24px 48px;
|
||||
}
|
||||
|
||||
/* ── Back link ── */
|
||||
.post__back {
|
||||
color: var(--c-blue);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
display: inline-block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.post__back:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Gradient title ── */
|
||||
.post__title {
|
||||
background: linear-gradient(90deg, #c9935a, #f59e0b 40%, #38bdf8);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
font-weight: 800;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* ── Date pill ── */
|
||||
.post__date {
|
||||
display: inline-block;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--c-blue);
|
||||
border: 1px solid rgba(147, 140, 129, 0.25);
|
||||
border-radius: 999px;
|
||||
padding: 4px 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ── Lead ── */
|
||||
.post__lead {
|
||||
color: var(--c-muted);
|
||||
font-size: 1.08rem;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* ── Body ── */
|
||||
.post__body { font-size: 1rem; line-height: 1.7; }
|
||||
|
||||
.post__body p { margin: 14px 0; }
|
||||
|
||||
.post__body h2 {
|
||||
font-size: 1.7rem;
|
||||
margin: 54px 0 6px;
|
||||
padding-left: 16px;
|
||||
border-left: 4px solid var(--c-accent);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.post__body h3 {
|
||||
color: var(--c-purple);
|
||||
font-size: 1.18rem;
|
||||
margin: 30px 0 4px;
|
||||
}
|
||||
|
||||
.post__body h4, .post__body h5, .post__body h6 {
|
||||
margin: 20px 0 4px;
|
||||
}
|
||||
|
||||
/* ── Inline code ── */
|
||||
.post__body code {
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.86em;
|
||||
background: var(--c-code-bg);
|
||||
color: #9fe6c0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
/* ── Code blocks ── */
|
||||
.post__body pre {
|
||||
background: var(--c-pre-bg);
|
||||
border: 1px solid var(--c-border);
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
overflow-x: auto;
|
||||
margin: 14px 0;
|
||||
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.55;
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.post__body pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* ── Blockquote ── */
|
||||
.post__body blockquote {
|
||||
background: rgba(245, 158, 11, 0.08);
|
||||
border: 1px solid rgba(245, 158, 11, 0.25);
|
||||
border-left: 4px solid var(--c-gold);
|
||||
border-radius: 0 12px 12px 0;
|
||||
padding: 16px 18px;
|
||||
margin: 18px 0;
|
||||
color: #f4e3c4;
|
||||
font-size: 0.94rem;
|
||||
}
|
||||
|
||||
/* ── Links ── */
|
||||
.post__body a { color: var(--c-blue); }
|
||||
.post__body a:hover { color: var(--c-purple); }
|
||||
|
||||
/* ── Strong ── */
|
||||
.post__body strong { color: var(--c-gold); }
|
||||
|
||||
/* ── HR ── */
|
||||
.post__body hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--c-border);
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
/* ── Figures ── */
|
||||
.post__body figure { margin: 20px 0; }
|
||||
.post__body figure img {
|
||||
max-width: 100%;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.post__body figcaption {
|
||||
color: var(--c-muted);
|
||||
font-size: 0.85rem;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* ── Lists ── */
|
||||
.post__body ul, .post__body ol { padding-left: 1.5em; margin: 10px 0; }
|
||||
.post__body li { margin: 4px 0; }
|
||||
|
||||
/* ── Author ── */
|
||||
.post__author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-top: 48px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.post__author-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--c-accent);
|
||||
color: var(--c-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.post__author-info {
|
||||
font-size: 0.85rem;
|
||||
color: var(--c-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.post__author-name {
|
||||
color: var(--c-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- POST -->
|
||||
<article class="post">
|
||||
<a href="/blog/" class="post__back">← All Posts</a>
|
||||
<span class="post__date">7 June 2026</span>
|
||||
<h1 class="post__title">Live Ozan Radio: A Personal AI Station in Cursor</h1>
|
||||
<p class="post__lead">I do not want a playlist. I want a station — something that feels like late-night desert dub and Anadolu psych drifting out of a speaker, but every track is composed fresh, never pulled from Spotify or Apple Music. So we built <strong>Live Ozan Radio</strong>: DeepSeek as the on-air DJ, Google Lyria 3 as the music engine, and our own Gitea instance as the host.
|
||||
|
||||
!<a href="https://tinqs.com/tinqs/blog/media/branch/main/img/live-ozan-radio-workspace.png" style="color: var(–c-accent-l);">Live Ozan Radio in Cursor — player dashboard, saved songs, and DJ chat beside the editor</a>
|
||||
|
||||
That screenshot is how I actually use it: Cursor on the left with taste notes and vocal cues, the local player on <code>:8787</code> on the right, saved songs in a scrollable library, and a chat box to steer the next generation. It is dogfooding in the truest sense — we run our game studio on the same Gitea fork we sell as Tinqs Studio, and the radio lives in that repo too.
|
||||
|
||||
## No catalog, ever
|
||||
|
||||
The rule is simple: <strong>nothing pre-recorded from the outside world</strong>. Every MP3 is generated by Lyria from a prompt that DeepSeek writes using <code>settings.json</code> (taste profile) and <code>taste_seeds.json</code> (built from Spotify screenshots in Cursor — no Spotify API). Shuffle mode mixes saved tracks with new compositions until the daily cap hits.
|
||||
|
||||
The stack:
|
||||
|
||||
| Layer | What it does |
|
||||
|——-|—————-|
|
||||
| <strong>DeepSeek</strong> | DJ brain — mood, Lyria prompts, chat |
|
||||
| <strong>Lyria 3 Pro</strong> | Full songs (~1–2 min), vocals optional |
|
||||
| <strong>FastAPI player</strong> | Stream, library, cost dashboard, vocal booth |
|
||||
| <strong>Git LFS</strong> | Committed songs + rich metadata |
|
||||
|
||||
## Curating every generation like a real DJ
|
||||
|
||||
Raw MP3s are not enough. Each track gets an extensive <code>*.meta.json</code>: rating (<code>love</code> / <code>keeper</code> / <code>skip</code>), what I loved, what I hated, <code>clone_prompt_hints</code> for successors, BPM, skip-intro timestamps, and instrument tags. A <code>library_index.json</code> rolls that up so the DJ knows, for example, that <strong>Sahara's Saz</strong> is the gold standard (saz + ney + sub bass by twenty seconds) and that <strong>fuzz electric guitar on vocal tracks</strong> is a hard avoid.
|
||||
|
||||
That metadata feeds back into every <code>plan_next</code> call. When I said Caravan of the Night was "not bad" but I hated the electric guitar bit, that went into <code>disliked</code> and <code>avoid_in_successors</code> — the next griot-vocal generation should keep the Sahel chants and drop the Tinariwen-style guitar.
|
||||
|
||||
## Public auto-play on tinqs.com
|
||||
|
||||
The full dashboard (<code>python -m ozan_radio serve</code> → <code>http://127.0.0.1:8787/player</code>) needs the API for Lyria compose, settings, and chat. But listening-only is static: <strong><code>gateway/index.html</code></strong> in the repo embeds the playlist and auto-plays from Git LFS media URLs when you open it on Git Studio:
|
||||
|
||||
<strong>https://tinqs.com/tinqs/live-radio/src/branch/main/gateway/index.html</strong>
|
||||
|
||||
Commit, push, share the link — no server required for listeners. New tracks run <code>export-web</code> (or auto-export on save) to refresh the embedded playlist.
|
||||
|
||||
## Vocal booth in the browser
|
||||
|
||||
For instrumentals where I want my own layer (deep chest / throat-sync over Nomad's Saz when the saz kicks in at ~0:30), the player includes a <strong>Chrome vocal booth</strong>: cue sheet with timestamps, skip-to-saz, <code>MediaRecorder</code> for takes, download as WebM. Lyria cannot remix an existing MP3 — but I can record over it locally while the track plays in headphones.
|
||||
|
||||
## Lyria settings in the web UI
|
||||
|
||||
The player settings panel talks to <code>/api/lyria</code> and adapts from the real Gemini API: model (Pro vs 30s Clip), vocal mode (instrumental / mix / full vocals), lyric language, singer profile, WAV vs MP3. Changes persist to <code>settings.json</code> and shape both the Lyria suffix and the DJ system prompt.
|
||||
|
||||
## Repo
|
||||
|
||||
Open source on our Gitea: <strong>https://tinqs.com/tinqs/live-radio</strong>
|
||||
|
||||
Clone, add your taste via Cursor + screenshots, run the server, or just hit the gateway link and let it shuffle. If you are building on Tinqs Studio, this is the kind of small, weird, personal tool that belongs in the same forge as your docs and your game — not on someone else's CDN.
|
||||
|
||||
—
|
||||
|
||||
<em>Inspired by Google's Magenta RealTime 2 segment on AI Search — we wanted the Lyria full-song path first; optional live MRT2 layer on Apple Silicon is on the roadmap.</em></p>
|
||||
|
||||
<div class="post__body">
|
||||
|
||||
</div>
|
||||
|
||||
<div class="post__author">
|
||||
<div class="post__author-avatar">OB</div>
|
||||
<div class="post__author-info">
|
||||
<span class="post__author-name">Ozan Bozkurt</span><br>
|
||||
CTO & Developer, Tinqs
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
After
|
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: "Live Ozan Radio: A Personal AI Station in Cursor"
|
||||
slug: live-ozan-radio
|
||||
date: "2026-06-07"
|
||||
description: "No Spotify playback — DeepSeek DJs, Lyria composes, and every track gets curated metadata. How we built a personal radio that auto-plays from our own Gitea."
|
||||
og_description: "Personal AI radio: DeepSeek DJ + Lyria 3, taste from screenshots, auto-play on tinqs.com."
|
||||
og_image: "https://tinqs.com/tinqs/blog/media/branch/main/img/live-ozan-radio-workspace.png"
|
||||
excerpt: "I wanted a radio that never plays catalog music — only fresh AI compositions shaped by my taste. Here's the stack, the player, and how I DJ every generated track with metadata."
|
||||
author: "Ozan Bozkurt"
|
||||
author_initials: "OB"
|
||||
author_role: "CTO & Developer, Tinqs"
|
||||
---
|
||||
|
||||
I do not want a playlist. I want a station — something that feels like late-night desert dub and Anadolu psych drifting out of a speaker, but every track is composed fresh, never pulled from Spotify or Apple Music. So we built **Live Ozan Radio**: DeepSeek as the on-air DJ, Google Lyria 3 as the music engine, and our own Gitea instance as the host.
|
||||
|
||||

|
||||
|
||||
That screenshot is how I actually use it: Cursor on the left with taste notes and vocal cues, the local player on `:8787` on the right, saved songs in a scrollable library, and a chat box to steer the next generation. It is dogfooding in the truest sense — we run our game studio on the same Gitea fork we sell as Tinqs Studio, and the radio lives in that repo too.
|
||||
|
||||
## No catalog, ever
|
||||
|
||||
The rule is simple: **nothing pre-recorded from the outside world**. Every MP3 is generated by Lyria from a prompt that DeepSeek writes using `settings.json` (taste profile) and `taste_seeds.json` (built from Spotify screenshots in Cursor — no Spotify API). Shuffle mode mixes saved tracks with new compositions until the daily cap hits.
|
||||
|
||||
The stack:
|
||||
|
||||
| Layer | What it does |
|
||||
|-------|----------------|
|
||||
| **DeepSeek** | DJ brain — mood, Lyria prompts, chat |
|
||||
| **Lyria 3 Pro** | Full songs (~1–2 min), vocals optional |
|
||||
| **FastAPI player** | Stream, library, cost dashboard, vocal booth |
|
||||
| **Git LFS** | Committed songs + rich metadata |
|
||||
|
||||
## Curating every generation like a real DJ
|
||||
|
||||
Raw MP3s are not enough. Each track gets an extensive `*.meta.json`: rating (`love` / `keeper` / `skip`), what I loved, what I hated, `clone_prompt_hints` for successors, BPM, skip-intro timestamps, and instrument tags. A `library_index.json` rolls that up so the DJ knows, for example, that **Sahara's Saz** is the gold standard (saz + ney + sub bass by twenty seconds) and that **fuzz electric guitar on vocal tracks** is a hard avoid.
|
||||
|
||||
That metadata feeds back into every `plan_next` call. When I said Caravan of the Night was "not bad" but I hated the electric guitar bit, that went into `disliked` and `avoid_in_successors` — the next griot-vocal generation should keep the Sahel chants and drop the Tinariwen-style guitar.
|
||||
|
||||
## Public auto-play on tinqs.com
|
||||
|
||||
The full dashboard (`python -m ozan_radio serve` → `http://127.0.0.1:8787/player`) needs the API for Lyria compose, settings, and chat. But listening-only is static: **`gateway/index.html`** in the repo embeds the playlist and auto-plays from Git LFS media URLs when you open it on Git Studio:
|
||||
|
||||
**https://tinqs.com/tinqs/live-radio/src/branch/main/gateway/index.html**
|
||||
|
||||
Commit, push, share the link — no server required for listeners. New tracks run `export-web` (or auto-export on save) to refresh the embedded playlist.
|
||||
|
||||
## Vocal booth in the browser
|
||||
|
||||
For instrumentals where I want my own layer (deep chest / throat-sync over Nomad's Saz when the saz kicks in at ~0:30), the player includes a **Chrome vocal booth**: cue sheet with timestamps, skip-to-saz, `MediaRecorder` for takes, download as WebM. Lyria cannot remix an existing MP3 — but I can record over it locally while the track plays in headphones.
|
||||
|
||||
## Lyria settings in the web UI
|
||||
|
||||
The player settings panel talks to `/api/lyria` and adapts from the real Gemini API: model (Pro vs 30s Clip), vocal mode (instrumental / mix / full vocals), lyric language, singer profile, WAV vs MP3. Changes persist to `settings.json` and shape both the Lyria suffix and the DJ system prompt.
|
||||
|
||||
## Repo
|
||||
|
||||
Open source on our Gitea: **https://tinqs.com/tinqs/live-radio**
|
||||
|
||||
Clone, add your taste via Cursor + screenshots, run the server, or just hit the gateway link and let it shuffle. If you are building on Tinqs Studio, this is the kind of small, weird, personal tool that belongs in the same forge as your docs and your game — not on someone else's CDN.
|
||||
|
||||
---
|
||||
|
||||
*Inspired by Google's Magenta RealTime 2 segment on AI Search — we wanted the Lyria full-song path first; optional live MRT2 layer on Apple Silicon is on the roadmap.*
|
||||
Reference in New Issue
Block a user