Repository navigation
Releases: CharlesMod/infinite-tapedeck
Release list
v0.4 — you can see what it is doing, and steer it
v0.3 made the radio stop repeating itself. This one makes it legible: you can
see which stage of the machine is running, argue with the critic, keep and
export what you like, and trade speed against quality with a slider.
The machine is visible now
A glyph above the tape feed shows which stage is running — a pen while the
LLM writes captions, a waveform while the generator synthesizes, an ear while
the critic scores, a tick or a cross for the verdict, pause bars while it
hands the GPU to another tenant. Verdicts stop pulsing and hold still, because
an accept is a moment rather than a state.
Before this, VRAM and GPU utilisation told you the machine was busy and
nothing told you at what. A six-minute caption residency and a hang looked
identical.
SPEED, replacing PRESS
The old readout divided audio banked by the last hour of clock, so it
collapsed toward zero whenever the spool was full and the daemon was
deliberately idle — measured 0.12x across a window where generation was
running at 0.82x. It reported how busy the radio chose to be, not how fast it
is.
SPEED is audio produced per second of generation, over the last three takes,
with the timing banked per take rather than inferred. Green above 1.0x, the
line where the radio outruns a listener.
Two sliders
Speed / quality — diffusion steps, 20 to 40, one per notch. This is the
only honest speed lever: a regression over 25 takes puts fixed per-take
overhead at effectively zero, so nothing else is there to reclaim. Measured
0.86x realtime at 30 steps and 0.98x at 25.
Song length — 60s to 300s, or leftmost to let each vein use the length of
its own source tracks. Both are read per take, so a move lands on the next
generation.
Shuffle
Three modes. Off is the radio as designed. Generated draws from everything
this station has ever made that is still on disk, so a take banked while you
listen joins the pool on the next pick. All adds the music the station was
built from.
That last mode is the only place the deck serves a file it did not generate,
so it has its own route with its own containment rather than loosening the
filename pattern that guards generated audio — tested against traversal,
absolute paths, URL-encoded traversal, and non-audio files inside the library.
Keepers
Keeping a take worked; everything after it did not. Keepers now list, replay,
rename and export as MP3, WAV or FLAC under their own name. Titles live beside
the file rather than renaming it, because the filename pattern doubles as a
path-traversal guard.
A minute per song, lost to integer division
Reported as "each song is about a minute shorter than asked for", and it was
arithmetic: the caption said "an approximately N-minute piece" where
N = target // 60. A 179-second target asked the model for two minutes. Up to
59 seconds of every request was discarded in the phrasing, before the model
did anything.
The VU meters were not channels
One analyser over the mono sum, with the frequency spectrum split in half —
low bins drove "L", high bins drove "R", each with its own arbitrary scale.
Music always has more energy low, so the left meter sat permanently higher.
Reported as "my left channel always seems higher", and nothing to do with the
audio: takes measure genuinely stereo, correlation 0.16-0.74, balance within
about 2 dB. Now one analyser per channel, RMS in dBFS on a shared scale.
Dead air
- A ComfyUI restart mid-generation cost 160 seconds: the daemon polled for
a prompt that died with the old process, then served a 120s backoff for a
server that was already healthy. Now 16 seconds, measured. - Stopping the service took two minutes, because every long wait was a
plain sleep and shutdown waited out whichever one was running. All of them
wake on SIGTERM now: a 120s backoff returns in 2.0s. - A caption residency blocked generation for six minutes at ~30s per LLM
call. A starving spool now writes three bundles instead of ten and tops up
during the next idle window, and each caption is logged as it is written. - Playing a keeper no longer rolls on into the radio, which sounded like an
old song starting by itself.
Also
Every button, section and deck area explains itself on hover — 25 controls
and 6 sections that previously assumed you had read the source.
v0.3 — the radio stops repeating itself
Most of this release is one bug wearing different clothes: a range that exists
in your music, or in the model, had been replaced somewhere by a single value.
Every song had the same shape
Veins described their energy arc as the average of every track in them.
Averaging eighty tracks cancels out what makes them different and leaves only
the one thing they share — intros are quieter — so all four veins of a
443-track library came out "mid-peak", a label that distinguishes nothing and
then constrains everything. Measured: the library is 62% mid-peak, 25%
builds-to-end, 13% front-loaded, and generation was 100% mid-peak.
Veins now carry the distribution and each take rolls against it. Auto-built
cards also had "energy shape" listed under NEVER LOSE while appearing in
no mutation axis, so the dimension most responsible for one take feeling
unlike the last was the one explicitly forbidden to move.
Every song had the same section plan
The instrumental scaffold was deterministic: intro, instrumental,
instrumental, bridge, padding, outro — the bridge always fourth, five distinct
skeletons across every possible length. With sung takes capped at 25%, that
was one form for three quarters of the radio, and these tags are executable
structure. Now six forms (arch, through-composed, build/drop, rondo, ostinato,
suite) scaled to length: 24 distinct skeletons.
New installs generated with no key at all
Key parsing was gated on a parenthesis that only appears in hand-edited cards,
so every auto-generated card produced no key and the prompt fell back to "your
choice". Both dialects parse now, sharps survive (a trailing \b was turning
F# into F — silently transposing), and a group's mode carries forward over its
bare notes: "G/D/A minor, G major" is three minor keys and one major.
Nothing ever asked the music to do anything
Captions varied instrument, tempo, brightness and length — all surface — with
no vocabulary for motif development, modulation, call-and-response,
displacement or drop-out. 18 concrete techniques, one rolled per take.
The generator's own controls were frozen
top_k and the text encoder's cfg_scale are consumed by the autoregressive
sampler that chooses acoustic codes one at a time — the thing actually
choosing notes, which no caption wording can reach. Both were pinned for every
take ever generated, and one constant named CFG was feeding two unrelated
parameters (diffusion guidance and token-sampling guidance). They are separate
now and roll per take, as do the two seeds.
You can see the critic, and argue with it
Rejected takes are deleted, so the deck could never show that the radio was
working — it generates for minutes, bins the result, and leaves a log line
nobody reads. More than one person read that silence as a broken radio. There
is now a Critic panel with every verdict, its score against the bar, and the
accept rate; near misses are highlighted, because those are the ones a single
nudge would have banked. Stricter / looser buttons move the bar live.
Kept takes are a shelf, not a filename
Keeping worked; everything after it didn't. Keepers now list, replay, rename
and export as MP3, WAV or FLAC under their own name. Titles live in a sidecar
rather than renaming files, because the filename pattern doubles as the
path-traversal guard on the audio route.
Fixed
- A silent LLM outage. llama-swap could not start its model because a
configured projector file was missing; llama-server aborts before serving
and the proxy reports only "upstream command exited prematurely". Every
caption fell back to static card text, which also meant none of the
technique, key or arc work above reached generation. The fallback is
correct in the moment, but it was being banked — a full batch of them
filled the queue and the radio spent forty takes on it. It now writes two. - Captures survive a full card. An allocation that does not fit retries
after releasing cached memory, then describes a shorter excerpt rather than
dropping the track. The listening pass sizes its clip from free VRAM at
start — weights are a fixed ~7.7 GB but activations were ~5.2 GB and nobody
was choosing them. - The three tenants hand the GPU over properly. The listening pass had no
idea the lyricist existed, and the music server's boot could ask the pass
for room but not the lyricist. - Steps are 30 always; the catch-up lever that dropped a starved spool to 20
cheapened exactly the takes someone was waiting for. analysis/capture.logis flushed per line — it exists for the moments a
capture looks stuck, and was empty precisely then.
Known, and honest about it
Requested energy shapes arrive about 40% of the time, and builds-to-end
essentially never. The cause is upstream: takes end at a median 46% of the
length their caption plans for, so the back half of a piece — where its
shape is decided — is never generated. Steering the caption is not the same as
steering the audio, and the code says so where it matters.
v0.2.1 — a visible critic, and a deck that survives a dub
Two things you could not see before, and one that stopped working when you
looked away.
You can see the critic now, and argue with it
The radio throws away most of what it generates — that is the whole point of
having a critic — but rejected takes are deleted, so the deck had no way to
show it was working. It generated for minutes, binned the result, and left a
log line nobody reads. More than one person read that silence as a broken
radio.
The deck now has a Critic panel: every verdict with its score against the
bar, the accept rate, and why that bar is where it is. Takes that missed by a
hair (within 0.03) are highlighted, because those are the ones a single nudge
would have banked.
And the bar is yours to move — stricter / looser buttons, applied without
a restart, clamped to +/-0.15. The panel suggests a direction: nothing
passing, several near misses, or everything passing.
The deck could not come back while a dub was running
Music Flamingo legitimately fills a 16 GB card. ComfyUI cannot create a CUDA
context when it is full, so it exited before serving anything and systemd
crash-looped it — 19 restarts in the case that prompted this, each one loading
torch and stealing CPU from the dub it was waiting on, with the deck down
throughout.
The music server now waits for the card instead of fighting it, and asks for
it rather than waiting on luck: it raises a flag, and the listening pass
releases its allocator cache at the next track boundary. Measured 1.6 GB free
before, 3.1 GB after — so the deck returns within one track instead of at the
end of the dub.
Capping the captioner's VRAM was tried first and reverted: on a 16 GB card any
budget that leaves real headroom starves the model, and a 12 GB cap failed
every single track on a 334 MB allocation. The comment in caption_pass.py
records why, so nobody re-derives it.
Also
- The take counters sit on the floor of the tape window instead of floating in
the middle of it. analysis/capture.logis flushed per line. It exists for the moments a
capture looks stuck, and it was empty exactly then.- A liveness probe that would have terminated the process it was checking on
Windows.
v0.2 — installable
This release exists because two people tried to install v0.1 and told us what
happened. Both were right, and both problems are fixed.
The install actually works now
The old instructions interleaved ComfyUI's setup with this repo's and then told
you to run ./venv/bin/python from a repo that has no venv — tapedeck runs
inside ComfyUI's interpreter and never had one of its own. They also never
mentioned scikit-learn, without which every library of 24+ tracks failed to
cluster.
docs/INSTALL.md is rewritten: it names that one interpreter once, uses it
everywhere, and is in dependency order. It was verified by cloning this repo
and following it start to finish on a clean checkout.
analysis/preflight.py— checks your interpreter, every import, audio
decoders for the formats actually in your library, VRAM, disk, the three
model files, and whether ComfyUI is reachable. Run it before you spend an
hour on a capture.requirements.txt— including the caption dependencies, which were
previously commented out.library/now survives a clone. It was gitignored, so the docs pointed at a
folder that did not exist.
The radio no longer rejects everything
The critic calibrated its bar from how similar your library is to itself.
That is the right anchor for a broad library and impossible for a tight one: a
single-artist corpus calibrates around 0.935, generated audio scores 0.63–0.83,
and nothing is ever accepted. One report showed exactly this — zero takes
banked, ever.
The bar is now also ranked against what each vein actually generates, keeping
the better half, and the lower of the two wins. Replaying that reported log
takes it from 0/16 accepted to 6/16. A library whose bar is already reachable
behaves exactly as before. Tune with critic_keep_frac; 0.0 restores the old
behavior.
The AI listening pass runs by default
Without it, a station is described to the generator only by numbers — tempo,
brightness, energy shape. Nothing says which genre or which instruments, so
takes drift off-style and then fail the critic. It is the single biggest
quality difference in the system, so it is now the default (--no-captions
opts out). If you skip it, --describe "dark melodic techno, analog hardware, no vocals" gives the generator something to aim at.
Fixed
- A first capture used to hang forever. The captioner yields the GPU when
the radio's tank runs low; on a first capture the tank is empty, so it
yielded immediately and then waited for a tank nothing was filling. - Captures died with
ValueError: need at least one array to stackwhen
every track failed to embed. Each stage now stops where the failure is and
says what to fix. - Captures died with CUDA OOM when ComfyUI held model weights with an idle
queue. Embedding now checks free VRAM and falls back to CPU. - Capture logs reached hundreds of megabytes — bitsandbytes logs one line
per matmul (2.9M lines measured), which also flooded the deck's readout.
Console output is now one line per stage with timings; full detail goes to
analysis/capture.log. - A cancelled capture no longer marks the track it was working on as bad.
- A typo'd
--stationno longer captures your current directory. - A liveness check that would terminate the process it was checking on Windows.
Platform
Linux. Windows via WSL2, which is now documented properly. Native Windows is
not supported — the radio reads /proc for GPU attribution and uses POSIX
signals — and neither is macOS, since Music 3 needs CUDA.
∞ TAPEDECK v0.1.1 — first-night fixes
Fixes from the first evening of real listening:
- Captures no longer die at their first duty-cycle yield — an uninitialized variable in the caption pipeline's wait loop killed every long capture the moment it politely handed the GPU back to the radio.
- The 25% lyric cap holds against bundle reuse — one caption composing two songs was smuggling sung takes past the ratio; the cap is now enforced both in the refill simulation and at generation time against the live spool.
- Deck polish: setlist count off-by-one, cassette label honesty.
Every one of these was caught by a human listening to (and looking at) the deck, not by a log. Ship early, listen closely.
∞ TAPEDECK v0.1 — the radio that never runs out
Turn a folder of songs into a radio station that never ends — fully local AI music generation on one consumer GPU, taste-modeled from your own library.
What v0.1 does
- Stations = folders. Your whole library, or twelve songs that define a 2 a.m. mood — each becomes an infinite Pandora-style station (under 24 tracks skips clustering: the folder is the mood).
- Learns your taste, rejects its own misses. librosa features + CLAP embeddings distill your collection into taste veins; every generated take is scored against your corpus before you hear it, with self-calibrating thresholds, an adaptive bar that eases on rejects and snaps back on success, and essence cards that rewrite themselves when a vein keeps failing — no genre ever sits out.
- The radio is never silent. Empty spool serves labeled archive replays while an emergency take cuts; the first two takes into an empty spool always run full quality — the demo moment is protected.
- One GPU, many tenants, zero babysitting. Generation, an optional local LLM (captions + sung lyrics with real structure), and the optional Music Flamingo captioner time-slice a single 16 GB card via duty cycles, watchdogs, and a persistent pre-written bundle queue. A hard cap keeps sung takes ≤ 25% of the spool (configurable).
- A cassette deck that tells the truth. Spinning reels wind black tape from real ledgers (unplayed left, played right), VU meters ride the actual signal, a press-speed gauge shows measured ×realtime, and a card-ownership readout names who is spending the GPU right now.
Requirements
Linux · Python 3.12 · 16 GB NVIDIA GPU (RTX 4080/5080 class) · ComfyUI 0.33+ · MiniMax Music 3 weights (~14 GB). Local LLM endpoint and AI captioning are optional — the radio degrades gracefully without them.
See docs/INSTALL.md.
Honest numbers (reference: RTX 5080)
Generation runs slower than realtime (~0.6–0.8× per unique song, int8 DiT): the radio banks a spool while your GPU idles and spends it while you listen. Expect ~2 hours of fresh music per idle overnight. Quality-first defaults; every speed knob (dit_model, steps, starved_steps, …) is config.
Known gaps
- README screenshot pending; packaged install path not yet walked on a clean machine — extracted from a working daily-driver install. Issues welcome.
- Model licenses are yours to respect: Music 3 (MiniMax Community License), Music Flamingo (NVIDIA non-commercial).