Spaces:
Running on Zero
Running on Zero
| # 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](README.md). | |
| ## 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 | |
| 1. `python backstage/build_song.py --song <key>` β runs `fetch β β¦ β register`, pausing at | |
| the ear-judgments (pick the chorus `--window`, confirm the neutral-`la` | |
| verify, confirm the `order`). | |
| 2. Paste the printed `ORDERS` / `DEMOS` snippet into `soulx_freelyrics.py`. | |
| 3. `python backstage/bake_song.py --song <key>` β renders several seeds, keeps the | |
| best by timbre, mixes, caches the cover, and runs the **quality gates**. | |
| 4. A song appears in the demo only when it is registered **and** | |
| `assets/<song>/config.json` has `"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** β `sole` becomes "soy". Fix: split the word | |
| across two slots so L moves to onset position (`soh / lee`). | |
| - **`SYLLABLE_OVERRIDES`** in `singer.py` pins g2p_en's output for known | |
| Spanish syllables it would otherwise mispronounce (e.g. `nos` β `N-OW1-S` | |
| instead of g2p_en's `N-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](https://github.com/muoten/SoulX-Singer), which adds two | |
| flags to `cli/inference.py`: | |
| - `--n_steps` β overrides `config.infer.n_steps` for the CFM solver. | |
| - `--seed` β calls `torch.manual_seed` (+ numpy + random + cuda) for | |
| reproducible renders. | |
| Otherwise it tracks upstream `Soul-AILab/SoulX-Singer`. | |