Spaces:
Running on Zero
A newer version of the Gradio SDK is available: 6.20.0
Backstage β adding songs & internals
How songs get built, gated, and shipped behind the AIchael Jackson demo. For using the demo (the webapp, inputs, running locally), see the README.
Build pipeline (build_song.py)
Each song's chorus score in assets/<song>/chorus_target.json is built once and
ships with the repo. Songs are added through one standardized, resumable pipeline,
build_song.py, whose stages are:
fetch yt-dlp by title β sources/<song>/<song>_full.wav (+ validates length)
separate Demucs β vocals / accompaniment
slice cut the chorus WINDOW. Give --window START:END, or --near T and
find_window snaps a phrase+beat-aligned window (won't cut a word/beat)
preproc mel-band-roformer KARAOKE β isolate MJ lead (drop backing choir),
then Whisper align + f0 + ROSVOT notes β grid
grid preproc output β chorus_target.json
prompt resolve the timbre prompt (shared MJ clip; per-song override optional)
accomp vocal-stripped chorus instrumental
verify neutral-'la' render β copyright-safe leakage check (an ear-judgment)
order propose noteβword ORDER by de-inflation (an ear-judgment)
register print the ORDERS/DEMOS snippet for soulx_freelyrics.py
Stages skip if their output exists and are resumable (--from, --only,
--force). Most run automatically; the three ear-judgments β window,
verify, order β stop and ask you to confirm.
The karaoke step matters: some choruses are sung by MJ in unison with a choir, which no separator can cleanly split β the isolated lead comes out smeared and the render is poor. That case is flagged automatically (see Quality gates).
Adding a song, end to end
python backstage/build_song.py --song <key>β runsfetch β β¦ β register, pausing at the ear-judgments (pick the chorus--window, confirm the neutral-laverify, confirm theorder).- Paste the printed
ORDERS/DEMOSsnippet intosoulx_freelyrics.py. python backstage/bake_song.py --song <key>β renders several seeds, keeps the best by timbre, mixes, caches the cover, and runs the quality gates.- A song appears in the demo only when it is registered and
assets/<song>/config.jsonhas"demo": true. New songs default to hidden, so a song can be fully built and baked yet kept out of the demo.
Timbre prompt
Rather than a per-song clip, all songs share one canonical MJ voice prompt,
assets/_shared/mj_prompt.wav β a clean Thriller verse clip. Because it's verse
audio it differs from every song's chorus target, which sidesteps a SoulX
audio-leakage failure mode (the output mimics the prompt when prompt and target
derive from the same clip). singer.prompt_wav() falls back to the shared prompt
unless a song ships its own assets/<song>/prompt.wav override.
Quality gates (demo-eligibility)
Whether a baked cover is good enough for the public demo is decided by two automatic, complementary checks β a render is demo-eligible only if both pass:
- scat gate (
backstage/crappy_fragments.py) β measures the total duration of grid-mismatched audio: sound during a rest (PHANTOM), wrong pitch on a note (OFFPITCH), or silence on a note (DEAD).total_crappy < 5.0s= pass. This catches a render that fails to track an otherwise-good grid. - solo-vs-chorus gate (
backstage/validate_lead.py) β measures how much other-vocal energy surrounded the isolated lead (from the karaoke step's removed-backing output). A high ratio means the chorus is choral (lead in unison with a choir) β the grid itself is corrupt β the scat gate can't see it.
The bake (backstage/bake_song.py) runs both, auto-sets demo: false on failure,
and only recommends demo: true (publishing is a human confirm).
Phoneme-recognition metrics (per-slot, recall/precision/F1, Whisper, timbre-sim)
were all tried and rejected β they don't track perceived quality.
Phonetic learnings (English g2p approximating Spanish)
- Plosives in short slots get swallowed β double the leading consonant
via the override (e.g.
bueβen_B-B-W-EH1). Recipe is auto-applied to slot 1 only. - L coda on high notes drops β
solebecomes "soy". Fix: split the word across two slots so L moves to onset position (soh / lee). SYLLABLE_OVERRIDESinsinger.pypins g2p_en's output for known Spanish syllables it would otherwise mispronounce (e.g.nosβN-OW1-Sinstead of g2p_en'sN-AA1-S).
Repo layout
.
βββ app.py β HF Space entry: bootstrap + launch the demo
βββ soulx_freelyrics_demo.py β Gradio UI (song picker, lyric box, render+cache)
βββ soulx_freelyrics.py β per-song engine: wordβslot mapping (ORDERS/DEMOS)
βββ singer.py β soulx_render(), g2p, mixing, prompt fallback, config
βββ bootstrap_soulx.py β downloads weights + NLTK on first run
βββ assets/
β βββ _shared/mj_prompt.{wav,json} β canonical MJ timbre prompt (shared by all)
β βββ <song>/ β thriller, billie_jean, beat_it, bad, β¦
β βββ chorus_target.json β frozen chorus score (notes + timing)
β βββ accompaniment.wav β chorus instrumental (~16s)
β βββ config.json β per-song knobs incl. the "demo" flag
β βββ prompt.{wav,json} β OPTIONAL per-song timbre-prompt override
β βββ cache/ β rendered covers (incl. pre-baked defaults)
βββ backstage/ β all offline song-building code (not loaded by the app)
β βββ build_song.py β add a song: fetchββ¦βregister (resumable stages)
β βββ find_window.py β propose a phrase+beat-snapped chorus window
β βββ run_preproc_with_whisper.py β SoulX preprocess wrapper (karaoke+Whisper+ROSVOT)
β βββ bake_song.py β seed-best render + auto quality gates
β βββ crappy_fragments.py β scat gate (grid-mismatch duration)
β βββ validate_lead.py β solo-vs-chorus gate
β βββ split_word.py, swap_word.py β per-slot word editing helpers
β βββ verify_neutral.py, validate_grid.py, β¦ β other checks & eval
βββ vendor/SoulX-Singer/ β submodule: muoten fork with --n_steps + --seed
Submodule patches
vendor/SoulX-Singer is pinned to the patch-n-steps-cli branch of
muoten/SoulX-Singer, which adds two
flags to cli/inference.py:
--n_stepsβ overridesconfig.infer.n_stepsfor the CFM solver.--seedβ callstorch.manual_seed(+ numpy + random + cuda) for reproducible renders.
Otherwise it tracks upstream Soul-AILab/SoulX-Singer.