<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MadEnvel</title>
    <description>The latest articles on DEV Community by MadEnvel (@madenvel).</description>
    <link>https://dev.to/madenvel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4038437%2Ff58da7f6-adb1-4a10-9381-608955cb7ad6.png</url>
      <title>DEV Community: MadEnvel</title>
      <link>https://dev.to/madenvel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/madenvel"/>
    <language>en</language>
    <item>
      <title>"Something melancholic for tonight": Building semantic music search that runs on a Raspberry Pi</title>
      <dc:creator>MadEnvel</dc:creator>
      <pubDate>Mon, 20 Jul 2026 16:22:30 +0000</pubDate>
      <link>https://dev.to/madenvel/something-melancholic-for-tonight-building-semantic-music-search-that-runs-on-a-raspberry-pi-3kko</link>
      <guid>https://dev.to/madenvel/something-melancholic-for-tonight-building-semantic-music-search-that-runs-on-a-raspberry-pi-3kko</guid>
      <description>&lt;p&gt;I wanted to type &lt;strong&gt;"something melancholic for tonight"&lt;/strong&gt; and get a playlist from my own FLAC collection — matched by how the music &lt;em&gt;sounds&lt;/em&gt;, not by tags. And I wanted it to run entirely locally, on the same Raspberry Pi 4 with 4 GB of RAM that plays the music. No cloud, no external APIs.&lt;/p&gt;

&lt;p&gt;This post is about getting there: two failed approaches, a benchmark that told me exactly what my model couldn't do, a tiny 66k-parameter network that fixed it, and the memory tricks needed to make everything fit next to a running audio server.&lt;/p&gt;

&lt;p&gt;Some context first. For the last three years I've been building &lt;a href="https://kalinkaplayer.com" rel="noopener noreferrer"&gt;Kalinka&lt;/a&gt; (&lt;a href="//[github.com/madenvel/KalinkaPlayer]"&gt;GitHub&lt;/a&gt;), an open-source Hi-Fi music streamer: a server that runs on a Raspberry Pi connected to a DAC and plays audio bit-perfect and gapless through ALSA, with desktop/mobile apps acting as remote controls. It grew out of frustration with my receiver's built-in streaming (every 192 kHz track started with an audible stutter — I only noticed after two years, when I heard the same song open cleanly on YouTube), and with the alternatives: Roon wanted a subscription, Volumio put Qobuz behind an extra paywall on top of the Qobuz subscription I already paid for. So I did the reasonable thing and wrote my own.&lt;/p&gt;

&lt;p&gt;The C++ audio graph, gapless playback against CDN links that expire mid-queue, and the real-time client sync each deserve their own write-up. Today: search.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with tags
&lt;/h2&gt;

&lt;p&gt;By the time Kalinka's local library became central, I had a collection big enough to hit a familiar wall: lots of music, no way to pick something &lt;em&gt;for right now&lt;/em&gt;. Genre, year, album — none of it describes how a track sounds. Text search assumes you already know what you're looking for. I wanted the opposite: describe a mood, get tracks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1: pre-trained taggers (it went badly)
&lt;/h2&gt;

&lt;p&gt;The obvious approach: auto-tag the whole library, then search the tags. The &lt;a href="https://essentia.upf.edu/" rel="noopener noreferrer"&gt;Essentia&lt;/a&gt; project ships pre-trained TensorFlow models for exactly this, so I wired up three:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Predicts&lt;/th&gt;
&lt;th&gt;Model (backbone → head)&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Genre&lt;/td&gt;
&lt;td&gt;EffNet-Discogs → Discogs-400&lt;/td&gt;
&lt;td&gt;top-N genre labels + scores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mood&lt;/td&gt;
&lt;td&gt;VGGish → MIREX mood&lt;/td&gt;
&lt;td&gt;one of 5 mood clusters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Danceability&lt;/td&gt;
&lt;td&gt;VGGish → danceability&lt;/td&gt;
&lt;td&gt;a single float&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It worked… disappointingly. The genre classifier was noisy — a suspicious number of tracks came back as &lt;code&gt;electronic---ambient&lt;/code&gt; regardless of what they actually were — and produced usable genres for only about a third of my library. The mood model collapsed the entire collection into three or four clusters. The published numbers explain why: PR-AUC below 0.2. Multi-label music tagging is just a hard problem.&lt;/p&gt;

&lt;p&gt;And even ignoring quality, the stack was too heavy. TensorFlow plus its dependency tree is not something you want to install on a Raspberry Pi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 2: betting on CLAP
&lt;/h2&gt;

&lt;p&gt;In parallel I was already computing an embedding for every track with &lt;a href="https://huggingface.co/docs/transformers/en/model_doc/clap" rel="noopener noreferrer"&gt;CLAP&lt;/a&gt; (Contrastive Language-Audio Pretraining). CLAP's core idea is what I actually needed: audio and text map into the &lt;em&gt;same&lt;/em&gt; vector space, so a text query becomes an embedding and search becomes nearest-neighbour lookup — no intermediate tags at all.&lt;/p&gt;

&lt;p&gt;It quickly became clear this was the real solution and the tagging pipeline had been a temporary crutch.&lt;/p&gt;

&lt;p&gt;Making CLAP behave on a Pi took a few iterations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dropped PyTorch + &lt;code&gt;laion-clap&lt;/code&gt; for ONNX Runtime&lt;/strong&gt; — far easier to distribute, much lighter at startup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Swapped the general audio model for an &lt;a href="https://arxiv.org/abs/2202.00874" rel="noopener noreferrer"&gt;HTSAT&lt;/a&gt;-based CLAP trained specifically on music.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Since the model only "hears" ~10 seconds at a time, I &lt;strong&gt;sample several fragments per track and average the embeddings.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Measure before you optimize
&lt;/h2&gt;

&lt;p&gt;"Vibe" is too subjective to develop against, so before tuning anything I built a proper benchmark on &lt;a href="https://mtg.github.io/mtg-jamendo-dataset/" rel="noopener noreferrer"&gt;MTG-Jamendo&lt;/a&gt;, where tracks are labelled by humans. The results were clarifying - and a little sobering:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query category&lt;/th&gt;
&lt;th&gt;P@10&lt;/th&gt;
&lt;th&gt;Lift over random&lt;/th&gt;
&lt;th&gt;MRR&lt;/th&gt;
&lt;th&gt;Hit-rate@10&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Genre&lt;/td&gt;
&lt;td&gt;0.296&lt;/td&gt;
&lt;td&gt;+0.212&lt;/td&gt;
&lt;td&gt;0.670&lt;/td&gt;
&lt;td&gt;0.880&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instrument&lt;/td&gt;
&lt;td&gt;0.273&lt;/td&gt;
&lt;td&gt;+0.157&lt;/td&gt;
&lt;td&gt;0.476&lt;/td&gt;
&lt;td&gt;0.933&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mood / theme&lt;/td&gt;
&lt;td&gt;0.140&lt;/td&gt;
&lt;td&gt;+0.050&lt;/td&gt;
&lt;td&gt;0.315&lt;/td&gt;
&lt;td&gt;0.733&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overall&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.247&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.153&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.520&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.855&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CLAP is genuinely good at concrete sonic properties. Instrument queries shine — &lt;code&gt;piano&lt;/code&gt; hits P@10 = 0.80, meaning eight of the top ten results are relevant. Guitar, strings, violin all do well; among genres, hip-hop and jazz stand out.&lt;/p&gt;

&lt;p&gt;Abstract emotional concepts are another story. &lt;code&gt;uplifting&lt;/code&gt;, &lt;code&gt;inspiring&lt;/code&gt;, &lt;code&gt;love&lt;/code&gt; — effectively zero. Worse than picking tracks at random:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Strong (P@10)&lt;/th&gt;
&lt;th&gt;Weak (P@10)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instruments&lt;/td&gt;
&lt;td&gt;piano 0.80, guitar 0.50, acoustic guitar / strings / cello 0.40&lt;/td&gt;
&lt;td&gt;synthesizer 0.30, voice 0.10, "electronic production" 0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Genres&lt;/td&gt;
&lt;td&gt;hip-hop 0.80, jazz / rock / electronic 0.60&lt;/td&gt;
&lt;td&gt;pop 0.20, techno / world 0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mood / theme&lt;/td&gt;
&lt;td&gt;epic 0.50, film / meditative 0.30&lt;/td&gt;
&lt;td&gt;uplifting / inspiring / love 0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This turns out to be a well-known property of CLAP. But reproducing it on my own benchmark was oddly satisfying: instead of a vague feeling that mood search was weak, I had a number — and a direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing mood: a small model on top of CLAP
&lt;/h2&gt;

&lt;p&gt;My hypothesis: if CLAP embeddings describe the &lt;em&gt;sound&lt;/em&gt; well, maybe a small extra model can learn to extract the &lt;em&gt;emotional&lt;/em&gt; component from them.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://cvml.unige.ch/databases/DEAM/" rel="noopener noreferrer"&gt;DEAM&lt;/a&gt; dataset fit perfectly — ~1,800 tracks hand-annotated on two axes: &lt;strong&gt;valence&lt;/strong&gt; (how positive the music feels) and &lt;strong&gt;arousal&lt;/strong&gt; (how energetic it is). I computed CLAP embeddings for DEAM and trained a two-layer MLP with just &lt;strong&gt;66k parameters&lt;/strong&gt; to map an embedding to a &lt;code&gt;(valence, arousal)&lt;/code&gt; point.&lt;/p&gt;

&lt;p&gt;(Training ran on my Lenovo ThinkBook 13x, which got hot enough to burn my fingers twice. The eventual fix was parking it under the air conditioner. A cooling pad is probably in my future.)&lt;/p&gt;

&lt;h3&gt;
  
  
  The part where I got a negative R²
&lt;/h3&gt;

&lt;p&gt;My first attempt was a textbook lesson in &lt;strong&gt;train/serve skew&lt;/strong&gt;. I had computed DEAM's embeddings with the &lt;em&gt;general-audio&lt;/em&gt; CLAP model — while production had already moved to the &lt;em&gt;music-trained&lt;/em&gt; one. On real production embeddings the results were catastrophic: &lt;strong&gt;R² = −0.42&lt;/strong&gt;. The model performed worse than a baseline that assigns every track the same average value.&lt;/p&gt;

&lt;p&gt;After finding the mismatch and retraining on the exact embeddings used in production:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Axis&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;R²&lt;/th&gt;
&lt;th&gt;CCC&lt;/th&gt;
&lt;th&gt;RMSE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Valence&lt;/td&gt;
&lt;td&gt;MLP head&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.594&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.761&lt;/td&gt;
&lt;td&gt;0.755&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arousal&lt;/td&gt;
&lt;td&gt;MLP head&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.711&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.827&lt;/td&gt;
&lt;td&gt;0.683&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valence&lt;/td&gt;
&lt;td&gt;ridge baseline&lt;/td&gt;
&lt;td&gt;0.480&lt;/td&gt;
&lt;td&gt;0.626&lt;/td&gt;
&lt;td&gt;0.855&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arousal&lt;/td&gt;
&lt;td&gt;ridge baseline&lt;/td&gt;
&lt;td&gt;0.631&lt;/td&gt;
&lt;td&gt;0.742&lt;/td&gt;
&lt;td&gt;0.773&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I plug this mood signal into ranking through a confidence gate, so it only influences queries that are actually about emotion — it stays out of the way when you search for &lt;code&gt;piano&lt;/code&gt; or &lt;code&gt;hip-hop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Re-running the benchmark: &lt;strong&gt;mood search improved by ~80%&lt;/strong&gt; (lift over random went from +0.05 to +0.09), and P@10 for &lt;code&gt;uplifting&lt;/code&gt; went from 0.00 to 0.30. Genre and instrument search degraded by an amount small enough to ignore. A trade I'll take:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query category&lt;/th&gt;
&lt;th&gt;P@10&lt;/th&gt;
&lt;th&gt;Random baseline&lt;/th&gt;
&lt;th&gt;vs random&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Genre&lt;/td&gt;
&lt;td&gt;0.296&lt;/td&gt;
&lt;td&gt;0.084&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.5×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instrument&lt;/td&gt;
&lt;td&gt;0.273&lt;/td&gt;
&lt;td&gt;0.116&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.4×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mood / theme&lt;/td&gt;
&lt;td&gt;0.140&lt;/td&gt;
&lt;td&gt;0.090&lt;/td&gt;
&lt;td&gt;1.6×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overall&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.247&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.094&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.6×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;P@10&lt;/th&gt;
&lt;th&gt;Random baseline&lt;/th&gt;
&lt;th&gt;vs random&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;hip-hop&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;0.063&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.7×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;trance&lt;/td&gt;
&lt;td&gt;0.40&lt;/td&gt;
&lt;td&gt;0.032&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12.5×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jazz&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;0.067&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9.0×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rock&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;0.083&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7.2×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;piano&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;0.277&lt;/td&gt;
&lt;td&gt;2.9×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Making it fit in 4 GB
&lt;/h2&gt;

&lt;p&gt;On the Pi, the bottleneck was memory, not compute. Indexing several tracks in a row kept growing RSS until the OOM killer took the process down; forcing Python's garbage collector after every iteration stopped the bleed. Beyond that, two optimizations did the heavy lifting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Quantized stored embeddings to &lt;code&gt;int8&lt;/code&gt;.&lt;/strong&gt; Vectors shrank 4×, and ~99% of top-10 results stayed identical. Practically free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split CLAP in two.&lt;/strong&gt; The text encoder (needed for every user query) stays resident. The much heavier audio encoder is only needed for indexing — so it unloads automatically once indexing finishes and comes back when new tracks arrive.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Deleting the scaffolding
&lt;/h2&gt;

&lt;p&gt;At this point CLAP embeddings fully covered semantic search, and the little valence/arousal head covered mood — both jobs the Essentia taggers were originally there for, done better and cheaper. Meanwhile the taggers had turned into pure liability: &lt;code&gt;essentia-tensorflow&lt;/code&gt; ships ARM wheels only for Python 3.11, which had silently pinned my entire environment to 3.11 right when I needed 3.13.&lt;/p&gt;

&lt;p&gt;So the whole auto-tagging pipeline went in the bin — TensorFlow (a multi-hundred-megabyte dependency), the Python version pin, the &lt;code&gt;tags_predicted&lt;/code&gt; column, and all the tag-based ranking logic. What replaced genre and danceability detection? Nothing separate — CLAP already does it. What replaced the mood model? The one I trained myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;Kalinka's smart search today is exactly two components: CLAP audio embeddings and a 66k-parameter valence/arousal model, both running locally through ONNX Runtime. No TensorFlow, no external taggers, no cloud.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4roe5ft0wzbl94d9bh4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4roe5ft0wzbl94d9bh4.png" alt="Kalinka search results for " width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A nice side effect: a Russian song and a Pink Floyd track can land next to each other in the results if they share a mood. The system ranks by how music sounds, not what it's called. And all of it fits on a Raspberry Pi 4 with 4 GB of RAM.&lt;/p&gt;

&lt;p&gt;(For completeness: semantic search is one layer of a larger query system — SQLite FTS for full-text, streaming-service search, and semantic catalog matching, with &lt;a href="https://rapidfuzz.com/" rel="noopener noreferrer"&gt;RapidFuzz&lt;/a&gt;-based ranking and smart-search results surfacing only when there's no exact match. One search box, several engines behind it.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Recent work went into first-run setup and easier installation, plus a second free streaming source (Jamendo) with semantic search built the same fully-local way. The model still can't search by origin or era — &lt;code&gt;italian 80s&lt;/code&gt; won't find my Toto Cutugno records — so there's plenty left to explore.&lt;/p&gt;

&lt;p&gt;Kalinka is open source, and I'd genuinely welcome testers and contributors — not just code: trying it on different hardware, bug reports, UI feedback, docs, new plugins. AI-assisted PRs are fine by me; what matters is that the author understands the solution, can explain and maintain it, and the code is tested and fits the architecture.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kalinka Player GitHub:&lt;/strong&gt; &lt;a href="https://github.com/madenvel/KalinkaPlayer" rel="noopener noreferrer"&gt;Kalinka Player&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Kalinka Player Client:&lt;/strong&gt; &lt;a href="https://github.com/madenvel/KalinkaAI" rel="noopener noreferrer"&gt;Kalinka AI App&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://kalinkaplayer.com" rel="noopener noreferrer"&gt;https://kalinkaplayer.com&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>raspberrypi</category>
      <category>machinelearning</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
