diff --git a/BUILD.md b/BUILD.md index 296c37c..d5394c2 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,952 +1,284 @@ -# Stemmy — launchers (BUILD.md) - -The `.bat` launchers make Stemmy a double-click app on Windows. They're **gitignored** -(kept out of the repo), so this file documents what each one does and includes its **full -contents** — paste them back into files of the same name in the project root if you cloned -from GitHub. - -## Install everything at once - -**`install_all.bat`** is the one-shot installer: double-click it and it installs everything in one -self-contained script — it does **not** shell out to the other `.bat` files, so it works even if -they're missing. In order it sets up the `.venv` + app deps + CUDA 12.8 torch, updates yt-dlp, then -installs song ID (shazamio), MIDI/Tab export (Basic Pitch + ONNX), and the Extended 53-stem MSST -model (~2 GB). Every step is skip-if-present and self-healing, the run continues past any failure, -and it prints a summary of anything to re-run at the end. It intentionally **does not** run -`fix_gpu.bat` (that needs admin and changes your Windows power plan — run it yourself if needed). -After it finishes, start Stemmy with `run.bat`. - -## Table of contents - -- [Install everything at once](#install-everything-at-once) -- [One-time setup, then run](#one-time-setup-then-run) -- [What each launcher does](#what-each-launcher-does) -- [Encoding notes (CRLF / SmartScreen)](#encoding-notes-crlf--smartscreen) -- [Full contents](#full-contents) - - [install_all.bat](#install_allbat) - - [setup.bat](#setupbat) - - [run.bat](#runbat) - - [stop.bat](#stopbat) - - [check_gpu.bat](#check_gpubat) - - [check_models.bat](#check_modelsbat) - - [fix_gpu.bat](#fix_gpubat) - - [get_msst.bat](#get_msstbat) - - [get_tabs.bat](#get_tabsbat) - - [get_lyrics.bat](#get_lyricsbat) - - [update_ytdlp.bat](#update_ytdlpbat) - -## One-time setup, then run - -> **Python version matters.** Use **Python 3.10 – 3.13** (3.12 or 3.13 recommended). Python **3.14 is -> too new** — several audio dependencies (`diffq`, `numba`/`llvmlite`, `basic-pitch`) have no wheels for -> it yet and will fail to build. `setup.bat` and `install_all.bat` now auto-pick a supported version via -> the `py` launcher and stop with a clear message if only 3.14+ is found. Get 3.12/3.13 from -> [python.org/downloads](https://www.python.org/downloads/). - -0. **`install_all.bat`** *(shortcut)* — runs steps 1 + 4–7 below in one go (everything except `run.bat` and `fix_gpu.bat`). -1. **`setup.bat`** — creates `.venv`, installs dependencies, then force-installs the CUDA 12.8 - PyTorch build last so nothing overwrites it. Skips anything already installed. -2. **`run.bat`** — starts the server (HIGH priority, to limit laptop GPU throttling) and opens - the studio at `http://127.0.0.1:5002`. -3. *(optional)* **`fix_gpu.bat`** — once, as admin, if the GPU throttles when the window loses focus. -4. *(optional)* **`get_msst.bat`** — once, to enable the experimental **Extended** depth. -5. *(optional)* **`get_tabs.bat`** — once, to enable **MIDI + Tab export** (beta) on each stem. -6. *(optional)* **`get_lyrics.bat`** — once, to enable **song ID** (lyrics work without it via manual entry). -7. *(as needed)* **`update_ytdlp.bat`** — when YouTube imports start returning **403 Forbidden**, to pull the latest yt-dlp. - -## What each launcher does - -| Launcher | What it does | When to run | -|----------|--------------|-------------| -| `install_all.bat` | Self-contained one-shot install: `.venv` + deps + CUDA torch, yt-dlp, song ID, tabs, and MSST — inlined (doesn't call the other bats). Skips `fix_gpu`. | First install, to do everything at once | -| `setup.bat` | Build `.venv`, install deps, force CUDA `cu128` torch last. Idempotent. | First, and after dependency changes | -| `run.bat` | Activate `.venv`, launch the server in a HIGH-priority window, open the browser. | Every time you use Stemmy | -| `stop.bat` | Kill whatever is listening on port 5002. | To shut the server down | -| `check_gpu.bat` | `nvidia-smi`, whether torch sees CUDA + VRAM, and Windows power state. | Diagnosing GPU issues | -| `check_models.bat` | List every model `audio-separator` can auto-download, plus Stemmy's mapping. | Picking / verifying model names | -| `fix_gpu.bat` | (admin) High Performance power plan + EcoQoS opt-out for the model's `python.exe`. | If the GPU throttles on a laptop | -| `get_msst.bat` | Install ZFTurbo MSST + the 53-stem checkpoint for **Extended** (optional, ~2 GB). | Once, to try Extended | -| `get_tabs.bat` | Install Basic Pitch (audio->MIDI, ONNX) for per-stem **MIDI + Tab** export (optional). | Once, to enable tabs | -| `get_lyrics.bat` | Install shazamio for **song identification** (lyrics themselves need no install). | Once, to enable song ID | -| `update_ytdlp.bat` | Update yt-dlp to the latest release. | When YouTube imports start hitting **403 Forbidden** | - -## Encoding notes (CRLF / SmartScreen) - -- All launchers use **Windows CRLF** line endings. If you recreate them, save with CRLF (Notepad, - VS Code "CRLF", or run `unix2dos`), or `cmd.exe` may mis-parse multi-line blocks. -- First launch may trigger **SmartScreen** ("Windows protected your PC"): *More info → Run anyway*. - These are plain scripts you can read in full below. -- `setup.bat` / `get_msst.bat` / `get_tabs.bat` / `get_lyrics.bat` need normal user rights; **`fix_gpu.bat` self-elevates to admin** - (it changes the power plan and per-process power-throttling). - -## Full contents - -### install_all.bat - -One-shot, **self-contained** installer — it inlines every install step instead of calling the -other `.bat` files, so it works even if they were never created. In order: `.venv` + app deps + -CUDA 12.8 torch, then yt-dlp, song ID (shazamio), MIDI/Tab export (Basic Pitch + ONNX), and the -Extended 53-stem MSST model (~2 GB). Every step is skip-if-present and self-healing; the run -continues past any failure and prints a summary at the end. Does **not** run `fix_gpu.bat`. +# Stemmy Windows setup and launch guide -```bat -@echo off -setlocal enabledelayedexpansion -cd /d "%~dp0" -title Stemmy - install everything - -echo ========================================================== -echo Stemmy - install all (self-contained; skips what's done) -echo ========================================================== -echo. -echo Installs, in order: -echo 1. core app + CUDA 12.8 torch (required) -echo 2. yt-dlp (YouTube import, kept current) -echo 3. song ID (shazamio) -echo 4. MIDI + Tab export (Basic Pitch + ONNX) -echo 5. Extended 53-stem depth (ZFTurbo MSST, ~2 GB - optional) -echo. -echo fix_gpu.bat is NOT run here - it needs admin and changes your -echo Windows power plan. Run it separately if the GPU throttles. -echo. -pause - -set "FAILED=" - -REM ====================================================================== -REM 1) CORE: virtual env + app deps + CUDA torch + yt-dlp -REM ====================================================================== -echo. -echo ---------------------------------------------------------- -echo [1] Core app + CUDA 12.8 torch -echo ---------------------------------------------------------- - -REM pick a SUPPORTED Python (3.10 - 3.13); 3.14 has no wheels for the audio -REM stack and 3.13 dropped stdlib audioop, so we prefer 3.13..3.10. -set "PY=" -for %%V in (3.13 3.12 3.11 3.10) do ( - if not defined PY ( - py -%%V -c "import sys" >nul 2>&1 && set "PY=py -%%V" - ) -) -if not defined PY ( - where py >nul 2>&1 && (set "PY=py") || (set "PY=python") - %PY% -c "import sys; raise SystemExit(0 if (3,10)<=sys.version_info[:2]<=(3,13) else 1)" >nul 2>&1 || ( - echo [X] No supported Python found. Stemmy needs Python 3.10 - 3.13. - %PY% --version 2>nul - echo Install Python 3.12 or 3.13 from https://www.python.org/downloads/ - echo then re-run. ^(3.14 is too new; 3.13 dropped a module some libs need.^) - set "FAILED=!FAILED! core:python-version" - goto :after_core - ) -) -%PY% --version >nul 2>&1 || ( - echo [X] Python was not found on PATH. Install Python 3.12 or 3.13 and re-run. - set "FAILED=!FAILED! core:no-python" - goto :after_core -) -for /f "delims=" %%V in ('%PY% --version 2^>^&1') do echo Using %%V - -set "NEWVENV=0" -if not exist ".venv\Scripts\activate.bat" ( - echo Creating virtual environment .venv ... - %PY% -m venv .venv || (set "FAILED=!FAILED! core:venv" & goto :after_core) - set "NEWVENV=1" -) else ( - echo .venv already present - skipping. -) -call ".venv\Scripts\activate.bat" || (set "FAILED=!FAILED! core:activate" & goto :after_core) - -if "!NEWVENV!"=="1" ( - python -m pip install --upgrade pip -) - -REM app dependencies (may pull a CPU-only torch; the CUDA step below corrects it) -python -c "import flask, audio_separator, librosa, soundfile, numpy" 2>nul -if errorlevel 1 ( - echo requirements installing app dependencies ... - pip install -r requirements.txt || (set "FAILED=!FAILED! core:requirements" & goto :after_core) -) else ( - echo requirements already satisfied - skipping. -) - -REM PyTorch with CUDA 12.8 (Blackwell sm_120), asserted LAST so nothing overwrites it -python -c "import torch, sys; sys.exit(0 if torch.cuda.is_available() else 1)" 2>nul -if errorlevel 1 ( - echo torch installing CUDA 12.8 build for Blackwell ^(force-reinstall^) ... - pip install --force-reinstall --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cu128 || (set "FAILED=!FAILED! core:torch" & goto :after_core) -) else ( - echo torch CUDA build already active - skipping. -) - -echo. -echo Verifying the GPU is visible to torch ... -python -c "import torch as T; ok=T.cuda.is_available(); print(' torch :', T.__version__); print(' CUDA :', ok); print(' GPU :', T.cuda.get_device_name(0) if ok else 'CPU only')" -python -c "import torch, sys; sys.exit(0 if torch.cuda.is_available() else 1)" 2>nul || ( - echo [!] Still CPU-only. The cu128 index may not have a wheel for your - echo Python version. Use Python 3.12 or 3.13, or try the cu129 index. -) -:after_core - -REM ====================================================================== -REM 2) yt-dlp (keep current - fixes most YouTube 403 fetch failures) -REM ====================================================================== -echo. -echo ---------------------------------------------------------- -echo [2] yt-dlp (YouTube import) -echo ---------------------------------------------------------- -if exist ".venv\Scripts\activate.bat" ( - call ".venv\Scripts\activate.bat" - echo Updating yt-dlp to the newest version ... - python -m pip install -U yt-dlp || set "FAILED=!FAILED! yt-dlp" -) else ( - echo [!] No .venv - core step must succeed first. Skipping. - set "FAILED=!FAILED! yt-dlp:no-venv" -) - -REM ====================================================================== -REM 3) song ID (shazamio). Lyrics come from LRCLIB (free, no package). -REM ====================================================================== -echo. -echo ---------------------------------------------------------- -echo [3] Song ID (shazamio) -echo ---------------------------------------------------------- -if exist ".venv\Scripts\activate.bat" ( - call ".venv\Scripts\activate.bat" - echo Installing shazamio ^(song identification^) ... - pip install shazamio >nul 2>nul - rem Python 3.13 removed stdlib 'audioop' that pydub (shazamio dep) needs. - python -c "import sys; raise SystemExit(0 if sys.version_info[:2]>=(3,13) else 1)" >nul 2>&1 && pip install audioop-lts >nul 2>nul - python -c "import shazamio; print('Song ID + lyrics ready.')" || ( - echo [!] Song ID install failed - you can still type a title/artist for lyrics. - set "FAILED=!FAILED! song-id" - ) -) else ( - echo [!] No .venv - core step must succeed first. Skipping. - set "FAILED=!FAILED! song-id:no-venv" -) - -REM ====================================================================== -REM 4) MIDI + Tab export (Basic Pitch, ONNX runtime). -REM --no-deps on basic-pitch: its metadata hard-pins TensorFlow, which -REM has no matching wheel on Windows/py3.12. We add the real runtime deps. -REM ====================================================================== -echo. -echo ---------------------------------------------------------- -echo [4] MIDI + Tab export (Basic Pitch) -echo ---------------------------------------------------------- -if exist ".venv\Scripts\activate.bat" ( - call ".venv\Scripts\activate.bat" - echo Installing Basic Pitch ^(audio-to-MIDI^) without its TensorFlow pin ... - pip install "basic-pitch==0.4.0" --no-deps || (set "FAILED=!FAILED! tabs" & goto :after_tabs) - echo Installing the ONNX runtime + light transcription deps ... - pip install onnxruntime "pretty_midi>=0.2.9" "resampy>=0.2.2,<0.4.3" "mir_eval>=0.6" || (set "FAILED=!FAILED! tabs" & goto :after_tabs) - pip install "librosa>=0.10" scikit-learn "setuptools<81" typing-extensions || (set "FAILED=!FAILED! tabs" & goto :after_tabs) - python -c "from basic_pitch.inference import predict; import onnxruntime; print('Tab/MIDI export ready.')" || set "FAILED=!FAILED! tabs" -) else ( - echo [!] No .venv - core step must succeed first. Skipping. - set "FAILED=!FAILED! tabs:no-venv" -) -:after_tabs - -REM ====================================================================== -REM 5) Extended 53-stem depth (ZFTurbo MSST + bs_roformer checkpoint). -REM Big (~2 GB) and VRAM-hungry. We DON'T build the project (its build -REM needs git + setuptools-scm); we drop the code in place and install -REM only the libs inference.py needs. -REM ====================================================================== -echo. -echo ---------------------------------------------------------- -echo [5] Extended 53-stem depth (ZFTurbo MSST, ~2 GB) -echo ---------------------------------------------------------- -set "MODEL_TYPE=bs_roformer" -set "CFG_NAME=mvsep_mega_model_bs_roformer_53_stems.yaml" -set "CKPT_NAME=mvsep_mega_model_bs_roformer_53_stems_v1.ckpt" -set "REL=https://github.com/ZFTurbo/Music-Source-Separation-Training/releases/download/v1.0.21" -set "MSST_DEPS=ml-collections omegaconf PyYAML librosa matplotlib soundfile tqdm numpy beartype einops packaging rotary-embedding-torch PoPE-pytorch" - -if not exist ".venv\Scripts\activate.bat" ( - echo [!] No .venv - core step must succeed first. Skipping. - set "FAILED=!FAILED! msst:no-venv" - goto :after_msst -) -call ".venv\Scripts\activate.bat" -if not exist "models_cache" mkdir "models_cache" -if not exist "models_cache\msst_models" mkdir "models_cache\msst_models" - -echo [5.1] MSST inference code ... -set "MSSTOK=" -if exist "models_cache\msst\inference.py" if exist "models_cache\msst\pyproject.toml" set "MSSTOK=1" -if defined MSSTOK ( - echo present and complete - skipping. -) else ( - echo downloading + extracting ^(via PowerShell^) ... - if exist "models_cache\msst" rmdir /s /q "models_cache\msst" - if exist "models_cache\_msst_tmp" rmdir /s /q "models_cache\_msst_tmp" - curl -fL -o "models_cache\msst.zip" "https://github.com/ZFTurbo/Music-Source-Separation-Training/archive/refs/heads/main.zip" - if errorlevel 1 (set "FAILED=!FAILED! msst:download-code" & goto :after_msst) - rem locate + move the folder containing inference.py inside PowerShell (robust) - powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; try { Expand-Archive -LiteralPath 'models_cache\msst.zip' -DestinationPath 'models_cache\_msst_tmp' -Force; $inf = Get-ChildItem -Path 'models_cache\_msst_tmp' -Recurse -Filter 'inference.py' | Select-Object -First 1; if (-not $inf) { Write-Host '[X] inference.py not found in archive.'; exit 3 }; $src = $inf.Directory.FullName; New-Item -ItemType Directory -Force -Path 'models_cache\msst' | Out-Null; Get-ChildItem -LiteralPath $src -Force | Move-Item -Destination 'models_cache\msst' -Force; exit 0 } catch { Write-Host ('[X] ' + $_.Exception.Message); exit 4 }" - if errorlevel 1 (set "FAILED=!FAILED! msst:extract" & goto :after_msst) - rmdir /s /q "models_cache\_msst_tmp" >nul 2>&1 - del "models_cache\msst.zip" >nul 2>&1 - if not exist "models_cache\msst\inference.py" ( - echo [X] inference.py missing after extract. - set "FAILED=!FAILED! msst:no-inference" - goto :after_msst - ) - if not exist "models_cache\msst\pyproject.toml" ( - echo [X] pyproject.toml missing after extract. - set "FAILED=!FAILED! msst:no-pyproject" - goto :after_msst - ) -) - -echo [5.2] Installing MSST inference dependencies ... -pip install %MSST_DEPS% -if errorlevel 1 (set "FAILED=!FAILED! msst:deps" & goto :after_msst) -python -c "import torch,sys;sys.exit(0 if torch.cuda.is_available() else 1)" 2>nul -if errorlevel 1 ( - echo [!] torch lost CUDA - restoring the cu128 build ... - pip install --force-reinstall --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cu128 -) - -echo [5.3] Downloading model config + checkpoint ... -if exist "models_cache\msst_models\%CFG_NAME%" ( - for %%F in ("models_cache\msst_models\%CFG_NAME%") do if %%~zF LSS 200 del "models_cache\msst_models\%CFG_NAME%" -) -if not exist "models_cache\msst_models\%CFG_NAME%" ( - curl -fL -o "models_cache\msst_models\%CFG_NAME%" "%REL%/%CFG_NAME%" - if errorlevel 1 (set "FAILED=!FAILED! msst:config" & goto :after_msst) -) -if exist "models_cache\msst_models\%CKPT_NAME%" ( - for %%F in ("models_cache\msst_models\%CKPT_NAME%") do if %%~zF LSS 50000000 del "models_cache\msst_models\%CKPT_NAME%" -) -if not exist "models_cache\msst_models\%CKPT_NAME%" ( - echo downloading checkpoint ^(~2 GB, this takes a while^) ... - curl -fL -o "models_cache\msst_models\%CKPT_NAME%" "%REL%/%CKPT_NAME%" - if errorlevel 1 (set "FAILED=!FAILED! msst:checkpoint" & goto :after_msst) -) -set "SZ=" -for %%F in ("models_cache\msst_models\%CKPT_NAME%") do set "SZ=%%~zF" -if not defined SZ (set "FAILED=!FAILED! msst:checkpoint-missing" & goto :after_msst) -if !SZ! LSS 50000000 ( - echo [X] checkpoint only !SZ! bytes - download failed. Deleting. - del "models_cache\msst_models\%CKPT_NAME%" >nul 2>&1 - set "FAILED=!FAILED! msst:checkpoint-small" - goto :after_msst -) - -echo [5.4] Writing manifest ... -> "models_cache\msst_models\manifest.json" echo {"model_type":"%MODEL_TYPE%","config":"%CFG_NAME%","checkpoint":"%CKPT_NAME%"} -echo MSST ready (!SZ! bytes). -:after_msst - -REM ====================================================================== -REM Summary -REM ====================================================================== -echo. -echo ========================================================== -if defined FAILED ( - echo Finished, but these steps reported a problem: - echo !FAILED! - echo Stemmy still runs without the optional ones. Re-run this - echo script - it self-heals - or the matching get_*.bat to retry. -) else ( - echo All installers finished. Start Stemmy with run.bat. -) -echo ========================================================== -echo. -pause -exit /b 0 +This document describes the **current Windows workflow** used by the packaged/cumulative Stemmy build. + +The repository contains the application source. Some Windows convenience launchers are intentionally kept out of normal Git tracking because they are machine-oriented wrappers. They are included in the downloadable Windows package/overlay. + +## Recommended workflow + +### First installation + +Use one of these: + +- `install_all.bat` — installs the core environment plus optional helpers, verifies CUDA last, and automatically creates or refreshes the Stemmy desktop shortcut and icon. +- `setup.bat` — installs the core environment only. + +Use Python **3.10–3.13**; Python 3.12 is the most thoroughly tested. + +After setup completes, run: + +```text +run.bat ``` -### setup.bat +### Applying a source overlay/update -```bat -@echo off -setlocal enabledelayedexpansion -cd /d "%~dp0" - -echo ========================================================== -echo Stemmy - setup (skips anything already installed) -echo ========================================================== -echo. - -REM --- pick a SUPPORTED Python (3.10 - 3.13) ------------------------ -REM The audio stack (diffq, numba/llvmlite, pydub, basic-pitch) has no -REM wheels for Python 3.14 yet, and 3.13 removed the stdlib 'audioop' -REM module pydub needs. So we prefer 3.13 down to 3.10 via the py -REM launcher, and refuse anything outside that range with a clear message. -set "PY=" -for %%V in (3.13 3.12 3.11 3.10) do ( - if not defined PY ( - py -%%V -c "import sys" >nul 2>&1 && set "PY=py -%%V" - ) -) -if not defined PY ( - REM no py launcher match - try bare py / python, then verify the version - where py >nul 2>&1 && (set "PY=py") || (set "PY=python") - %PY% -c "import sys; raise SystemExit(0 if (3,10)<=sys.version_info[:2]<=(3,13) else 1)" >nul 2>&1 || ( - echo [X] No supported Python found. Stemmy needs Python 3.10 - 3.13. - %PY% --version 2>nul - echo Python 3.14 is too new ^(no wheels yet^) and 3.13 dropped a - echo module some audio libs use. Install Python 3.12 or 3.13 from - echo https://www.python.org/downloads/ then re-run this script. - goto :fail - ) -) -%PY% --version >nul 2>&1 || ( - echo [X] Python was not found on PATH. Install Python 3.12 or 3.13 and re-run. - goto :fail -) -for /f "delims=" %%V in ('%PY% --version 2^>^&1') do echo Using %%V - -REM --- virtual environment (.venv is gitignored) ------------------- -set "NEWVENV=0" -if not exist ".venv\Scripts\activate.bat" ( - echo Creating virtual environment .venv ... - %PY% -m venv .venv || goto :fail - set "NEWVENV=1" -) else ( - echo .venv already present - skipping. -) -call ".venv\Scripts\activate.bat" || goto :fail - -REM --- upgrade pip only on a freshly created venv ----------------- -if "!NEWVENV!"=="1" ( - python -m pip install --upgrade pip -) - -REM --- 1) app dependencies ---------------------------------------- -REM audio-separator etc. These can drag a CPU-only torch in from PyPI; -REM the next step corrects that, so it MUST come after this one. -python -c "import flask, audio_separator, librosa, soundfile, numpy" 2>nul -if errorlevel 1 ( - echo requirements installing app dependencies ... - pip install -r requirements.txt || goto :fail -) else ( - echo requirements already satisfied - skipping. -) - -REM --- 2) PyTorch with CUDA, asserted LAST so nothing overwrites it - -REM Your RTX 5060 is Blackwell (compute capability sm_120) and needs a -REM cu128 (CUDA 12.8) build - the default PyPI wheel is CPU-only and will -REM NOT use the GPU. force-reinstall guarantees the CUDA build wins even -REM if the step above pulled a CPU torch. Skipped when CUDA is already live. -python -c "import torch, sys; sys.exit(0 if torch.cuda.is_available() else 1)" 2>nul -if errorlevel 1 ( - echo torch installing CUDA 12.8 build for Blackwell ^(force-reinstall^) ... - pip install --force-reinstall --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cu128 || goto :fail -) else ( - echo torch CUDA build already active - skipping. -) - -REM --- 3) keep yt-dlp current (YouTube link import). YouTube changes often, -REM so re-running setup.bat refreshes it - this is what fixes most fetch fails. -echo yt-dlp updating YouTube import (yt-dlp) ... -pip install -U yt-dlp >nul 2>&1 - -echo. -echo Verifying the GPU is visible to torch ... -python -c "import torch as T; ok=T.cuda.is_available(); print(' torch :', T.__version__); print(' CUDA :', ok); print(' GPU :', T.cuda.get_device_name(0) if ok else 'CPU only')" -python -c "import torch, sys; sys.exit(0 if torch.cuda.is_available() else 1)" 2>nul || ( - echo. - echo [!] Still CPU-only. The cu128 index may not have a wheel for your - echo Python version. Use Python 3.12 or 3.13, or try the cu129 index. -) - -echo. -echo ========================================================== -echo Setup complete. Double-click run.bat to start Stemmy. -echo ========================================================== -echo. -echo NOTE: before your first separation, confirm the model file -echo names in app\models.py against: audio-separator --list_models -echo. -pause -exit /b 0 - -:fail -echo. -echo [X] Setup failed above. Fix the error and run setup.bat again. -echo. -pause -exit /b 1 +When a ZIP contains updated Stemmy source files: + +1. Close Stemmy. +2. Extract the ZIP over the existing Stemmy folder. +3. Choose **Replace files**. +4. Run `run.bat`. + +Do **not** run setup again unless the update explicitly says dependencies changed, the `.venv` was deleted, or Stemmy reports a missing package. + +### Everyday launch + +Run `run.bat` or use the desktop shortcut created automatically by `install_all.bat`. + +The launcher: + +- Activates `.venv`. +- Starts Python without a visible long-running console. +- Redirects output to `logs/`. +- Applies Windows process priority/high-QoS handling to Stemmy Python processes. +- Waits for the local server to respond. +- Opens `http://127.0.0.1:5002` in a dedicated maximized Edge/Chrome app window when possible. +- Explicitly maximizes the newest Stemmy browser window so Chromium cannot restore an older windowed size. +- Monitors that window and stops the server when it closes. + +## Current `install_all.bat` behavior + +The packaged installer is safe to re-run. It: + +1. Creates or reuses `.venv`. +2. Installs/repairs `requirements.txt`. +3. Updates `yt-dlp` and `imageio-ffmpeg`. +4. Installs ShazamIO and MIDI/Tab runtime support where possible. +5. Verifies or reinstalls the CUDA 12.8 PyTorch build **last**, preventing another package from replacing it with CPU-only PyTorch. +6. Creates or refreshes the desktop Stemmy shortcut and assigns `stemmy.ico`. + +The optional Extended/MSST model remains a separate large download when `get_msst.bat` is present. Quick, Standard, and Deep do not depend on it. + +## App-style desktop shortcut + +`install_all.bat` creates or refreshes `Stemmy.lnk` on the desktop with `stemmy.ico` automatically. + +To repair or recreate the shortcut without reinstalling anything, run: + +```text +Create Stemmy Shortcut.bat ``` -### run.bat +The shortcut launches `stemmy_launcher.vbs`, which starts `run.bat` without showing a command window. This is the preferred app-like entry point. -```bat -@echo off -cd /d "%~dp0" - -REM --- use the venv if setup.bat created one ----------------------- -if exist ".venv\Scripts\activate.bat" ( - call ".venv\Scripts\activate.bat" -) else ( - echo [!] No .venv found - run setup.bat first. Falling back to system Python. -) - -echo. -echo Starting Stemmy at http://127.0.0.1:5002 -echo Launching the server in a HIGH priority window to limit laptop -echo GPU throttling when this window loses focus. Keep it open while -echo you work; close it or run stop.bat to shut the server down. -echo. - -REM --- server runs in its own HIGH-priority console --------------- -start "Stemmy" /HIGH python run_stemmy.py - -REM --- give the server a few seconds, then open the studio -------- -timeout /t 4 >nul -start "" http://127.0.0.1:5002 - -exit /b 0 +A `.bat` file cannot embed a Windows icon directly; the `.lnk` shortcut is the clean Windows-native solution without wrapping Stemmy in a third-party executable packager. + +## Closing Stemmy + +Normal shutdown options: + +1. Close the dedicated Stemmy app window with **X**. +2. Choose **Settings -> Close Stemmy**. + +`stop.bat` remains an emergency fallback if the browser or launcher was force-killed and port 5002 is still occupied. + +Your projects and finished Karaoke sessions remain saved on disk. + +## Current launcher files + +| File | Purpose | +|---|---| +| `run.bat` | Main Windows launcher | +| `run_stemmy.py` | Starts the Flask application | +| `stemmy_window.ps1` | Opens, maximizes, and watches the dedicated browser app window | +| `stemmy_windows_performance.py` | Applies priority/high-QoS protection to Stemmy Python processes | +| `sitecustomize.py` | Early Python startup safeguards | +| `stemmy_launcher.vbs` | Hidden entry point used by the desktop shortcut | +| `stemmy.ico` | Multi-resolution Windows icon | +| `Create Stemmy Shortcut.bat` | Manual shortcut repair/refresh tool | +| `create_stemmy_shortcut.ps1` | Creates and configures `Stemmy.lnk` | +| `stop.bat` | Emergency port/process shutdown | + +## Background GPU protection + +Some Windows laptops reduce GPU work when the process owning a console is minimized or loses focus. Earlier launchers attempted only to raise process priority, which was not enough. + +The current launcher instead: + +- Detaches Stemmy from the console. +- Marks Stemmy Python processes as high priority/high QoS. +- Re-applies the policy to relevant child Python processes. +- Disables console-specific pause hazards. +- Sends output to log files rather than relying on an interactive terminal. + +This means stem separation should continue at full speed while Stemmy is in the background or minimized. + +The optional legacy `fix_gpu.bat` power-plan change is generally no longer required for the current packaged launcher, but it can remain useful for unusual OEM/driver power policies. + +## Background update checker + +On startup, Stemmy performs a quiet update-status check. Open: + +```text +Settings -> Updates ``` -### stop.bat +Safe internet-facing helper packages can be updated individually: + +- `yt-dlp` +- `shazamio` +- `imageio-ffmpeg` + +Each update: + +1. Changes only the selected package. +2. Uses `--no-deps` so shared numerical/GPU dependencies are not replaced. +3. Runs a compatibility/import test. +4. Rolls back to the previous version when the compatibility test fails. + +Protected/report-only packages include: + +- PyTorch and CUDA runtime packages +- `audio-separator` +- ONNX runtime/model-stack components +- NumPy and other numerical packages + +Update protected packages manually and deliberately. A newer version is not automatically safer for the installed CUDA/model combination. + +## Manual installation from a raw clone + +The package launchers are optional. Stemmy can run directly from a terminal. ```bat -@echo off -setlocal enabledelayedexpansion -cd /d "%~dp0" - -REM Port Stemmy listens on. Change this if you start it with --port. -set "PORT=5002" - -echo Stopping Stemmy on port %PORT% ... -set "FOUND=0" - -for /f "tokens=5" %%P in ('netstat -ano ^| findstr ":%PORT%" ^| findstr LISTENING') do ( - echo killing PID %%P - taskkill /F /PID %%P >nul 2>&1 - set "FOUND=1" -) - -if "!FOUND!"=="0" ( - echo Nothing was listening on port %PORT%. -) else ( - echo Stemmy stopped. -) - -echo. -timeout /t 2 >nul -exit /b 0 +py -3.12 -m venv .venv +call .venv\Scripts\activate.bat +python -m pip install --upgrade pip +pip install -r requirements.txt + +rem Example for RTX 50-series / CUDA 12.8: +pip install --force-reinstall --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cu128 + +python -c "import torch; print(torch.__version__); print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'CPU')" +python run_stemmy.py ``` -### check_gpu.bat +Then open: -```bat -@echo off -setlocal -cd /d "%~dp0" -title Stemmy - GPU check - -echo ========================================================== -echo Stemmy - GPU check -echo ========================================================== -echo. - -echo [ driver / nvidia-smi ] --------------------------------- -nvidia-smi 2>nul -if errorlevel 1 echo nvidia-smi not found - NVIDIA driver may be missing or not on PATH. -echo. - -echo [ torch sees the GPU ] ---------------------------------- -set "VPY=python" -if exist ".venv\Scripts\python.exe" set "VPY=.venv\Scripts\python.exe" -"%VPY%" -c "import torch as T; ok=T.cuda.is_available(); print(' torch :', T.__version__); print(' built CUDA :', T.version.cuda); print(' CUDA avail :', ok); print(' GPU :', T.cuda.get_device_name(0) if ok else 'CPU only'); print(' VRAM total :', (str(round(T.cuda.get_device_properties(0).total_memory/1073741824,1))+' GB') if ok else 'n/a')" 2>nul -if errorlevel 1 echo torch not importable here - run setup.bat first. -echo. - -echo [ Windows power state ] --------------------------------- -powercfg /getactivescheme -echo. -echo power-throttling opt-out for python: -powercfg /powerthrottling list 2>nul | findstr /i "python" || echo (none set - run fix_gpu.bat to stop Windows throttling Stemmy) -echo. - -pause -exit /b 0 +```text +http://127.0.0.1:5002 ``` -### check_models.bat +Install the PyTorch CUDA build **after** `requirements.txt`. `audio-separator` or another dependency may otherwise replace it with a CPU-only wheel. + +## Optional components + +### Song ID ```bat -@echo off -setlocal -cd /d "%~dp0" -title Stemmy - list separation models - -if exist ".venv\Scripts\activate.bat" ( - call ".venv\Scripts\activate.bat" -) else ( - echo [!] No .venv found - run setup.bat first. - pause & exit /b 1 -) - -echo ========================================================== -echo Models audio-separator can download automatically -echo ========================================================== -echo. -echo Tip: filter the list, e.g. audio-separator -l --list_filter=drums -echo The names below are what you put in app\models.py. -echo. - -audio-separator --list_models - -echo. -echo ---------------------------------------------------------- -echo Stemmy's models (all auto-downloaded by audio-separator): -echo Quick htdemucs_ft.yaml (vocals/drums/bass/other) -echo Standard htdemucs_6s.yaml (+ guitar + piano) -echo Deep htdemucs_6s.yaml + MDX23C-DrumSep-aufr33-jarredou.ckpt -echo (drums split into kick/snare/toms/hat/ride/crash) -echo. -echo No manual model download needed for Quick/Standard/Deep. For the -echo EXTENDED depth (synth/organ/strings/brass/... via ZFTurbo MSST), -echo run get_msst.bat once - see README, search "ZFTurbo MSST install". -echo ---------------------------------------------------------- -echo. -pause +call .venv\Scripts\activate.bat +pip install shazamio ``` -### fix_gpu.bat +Python 3.13 may also require: ```bat -@echo off -setlocal -title Stemmy - fix GPU throttling - -REM --- powercfg power-throttling + scheme changes need admin ------- -net session >nul 2>&1 -if %errorlevel% neq 0 ( - echo Requesting administrator privileges... - powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs" - exit /b -) - -cd /d "%~dp0" - -echo ========================================================== -echo Stemmy - fix GPU throttling (running as admin) -echo ========================================================== -echo. -echo Applies the OS-level counterparts to an in-app speed keep-alive: -echo a High Performance power plan, plus a power-throttling (EcoQoS) -echo opt-out for the Python that runs the models - so Windows stops -echo throttling the GPU when the Stemmy window loses focus on a laptop. -echo. - -echo [1/2] Setting the High Performance power plan ... -powercfg /setactive SCHEME_MIN -if errorlevel 1 (echo could not change the power plan.) else (echo done.) -echo. - -REM --- locate the python.exe Stemmy actually runs ------------------ -set "PYEXE=" -if exist "%~dp0.venv\Scripts\python.exe" set "PYEXE=%~dp0.venv\Scripts\python.exe" -if not defined PYEXE for /f "delims=" %%P in ('where python 2^>nul') do if not defined PYEXE set "PYEXE=%%P" - -echo [2/2] Disabling power throttling for the model process ... -if not defined PYEXE ( - echo no python.exe found - run setup.bat first, then re-run this. - goto :end -) -echo target: %PYEXE% -powercfg /powerthrottling disable /path "%PYEXE%" -if errorlevel 1 ( - echo this Windows build may not support per-app power-throttling - echo control; the High Performance plan above and run.bat's HIGH - echo priority launch still help. -) else ( - echo done - Windows will not EcoQoS-throttle that executable. -) - -:end -echo. -echo ========================================================== -echo Finished. To undo later: -echo powercfg /powerthrottling reset /path "the python.exe above" -echo powercfg /setactive SCHEME_BALANCED -echo ========================================================== -echo. -pause -exit /b 0 +pip install audioop-lts ``` -### get_msst.bat +Lyrics themselves are fetched over HTTPS; ShazamIO is used for audio identification. + +### MIDI/tab export + +Use `get_tabs.bat`, or install Basic Pitch and the ONNX dependencies described by that script. Basic Pitch dependency metadata can attempt to pull an unsuitable TensorFlow build, so the packaged installer intentionally installs only the runtime pieces Stemmy uses. + +### Extended separation + +Use `get_msst.bat` to install the optional ZFTurbo MSST code, configuration, and checkpoint. The model is large and much more demanding on system RAM than Quick, Standard, or Deep. + +## Logs + +| File | Contents | +|---|---| +| `logs/stemmy.log` | Normal server output | +| `logs/stemmy-error.log` | Python/server errors | +| `logs/stemmy-performance.log` | Windows performance-policy activity | +| `logs/stemmy-lyrics.log` | Shazam/LRCLIB lookup paths and failures | +| `logs/stemmy-update.log` | Package update and rollback output | + +When reporting a startup, lyrics, update, or performance problem, include the relevant log rather than only a screenshot. + +## Troubleshooting -> Installs the optional **Extended** depth (ZFTurbo MSST + the 53-stem `bs_roformer` model, ~2 GB). -> Self-healing: re-running only fixes what's broken. To use a different MSST model, edit the -> `MODEL_TYPE` / `CFG_NAME` / `CKPT_NAME` / `REL` variables near the top. +### Stemmy does not open a browser + +Check `logs/stemmy-error.log`. + +A previous launcher could crash before browser startup when Windows selected a legacy `cp1252` output encoding and Python printed a Unicode arrow. The current `run_stemmy.py` configures UTF-8-safe output and uses an ASCII startup line. + +Also check whether another process is already listening on port 5002: ```bat -@echo off -setlocal enableextensions enabledelayedexpansion -cd /d "%~dp0" -title Stemmy - install ZFTurbo MSST + 53-stem instrument model (optional) - -if exist ".venv\Scripts\activate.bat" ( - call ".venv\Scripts\activate.bat" -) else ( - echo [!] No .venv found - run setup.bat first. - pause & exit /b 1 -) -if not exist "models_cache" mkdir "models_cache" -if not exist "models_cache\msst_models" mkdir "models_cache\msst_models" - -rem ==== the model to install (edit these to use a different MSST model) ======== -set "MODEL_TYPE=bs_roformer" -set "CFG_NAME=mvsep_mega_model_bs_roformer_53_stems.yaml" -set "CKPT_NAME=mvsep_mega_model_bs_roformer_53_stems_v1.ckpt" -set "REL=https://github.com/ZFTurbo/Music-Source-Separation-Training/releases/download/v1.0.21" -rem deps the bs_roformer *inference* path needs (torch/torchaudio already in .venv). -rem MSST dropped requirements.txt for pyproject.toml, and its build uses -rem setuptools-scm (needs git) - so we DON'T build the project, we just install -rem these and run inference.py in place. Edit if you switch model architectures. -set "MSST_DEPS=ml-collections omegaconf PyYAML librosa matplotlib soundfile tqdm numpy beartype einops packaging rotary-embedding-torch PoPE-pytorch" -rem ============================================================================ - -echo ========================================================== -echo Stemmy - optional MSST install (extended instrument split) -echo Installs ZFTurbo's MSST + the 53-stem bs_roformer model. -echo Big download (model is ~2 GB) and VRAM-hungry. Optional. -echo (self-healing: re-runs only fix what's broken) -echo ========================================================== -echo. - -echo [1/4] MSST inference code ... -set "MSSTOK=" -if exist "models_cache\msst\inference.py" if exist "models_cache\msst\pyproject.toml" set "MSSTOK=1" -if defined MSSTOK ( - echo present and complete - skipping. -) else ( - echo downloading + extracting ^(via PowerShell, reliable on Win10/11^) ... - if exist "models_cache\msst" rmdir /s /q "models_cache\msst" - if exist "models_cache\_msst_tmp" rmdir /s /q "models_cache\_msst_tmp" - curl -fL -o "models_cache\msst.zip" "https://github.com/ZFTurbo/Music-Source-Separation-Training/archive/refs/heads/main.zip" - if errorlevel 1 goto :fail - rem Extract, then move the folder that actually contains inference.py straight - rem into models_cache\msst. Doing the locate+move inside PowerShell avoids the - rem fragile cmd for-loop / substring / MOVE chain that could leave msst as the - rem parent folder (the "pyproject.toml missing" failure some users hit). - powershell -NoProfile -ExecutionPolicy Bypass -Command "$ErrorActionPreference='Stop'; try { Expand-Archive -LiteralPath 'models_cache\msst.zip' -DestinationPath 'models_cache\_msst_tmp' -Force; $inf = Get-ChildItem -Path 'models_cache\_msst_tmp' -Recurse -Filter 'inference.py' | Select-Object -First 1; if (-not $inf) { Write-Host '[X] inference.py not found in archive.'; exit 3 }; $src = $inf.Directory.FullName; New-Item -ItemType Directory -Force -Path 'models_cache\msst' | Out-Null; Get-ChildItem -LiteralPath $src -Force | Move-Item -Destination 'models_cache\msst' -Force; exit 0 } catch { Write-Host ('[X] ' + $_.Exception.Message); exit 4 }" - if errorlevel 1 goto :fail - rmdir /s /q "models_cache\_msst_tmp" >nul 2>&1 - del "models_cache\msst.zip" >nul 2>&1 - if not exist "models_cache\msst\inference.py" ( - echo [X] inference.py missing after extract. - goto :fail - ) - if not exist "models_cache\msst\pyproject.toml" ( - echo [X] pyproject.toml missing after extract. - goto :fail - ) -) - -echo. -echo [2/4] Installing MSST inference dependencies into .venv ... -echo (not building the project - just the libs inference.py needs) -pip install %MSST_DEPS% -if errorlevel 1 goto :fail -python -c "import torch,sys;sys.exit(0 if torch.cuda.is_available() else 1)" 2>nul -if errorlevel 1 ( - echo [!] torch lost CUDA - restoring the cu128 build ... - pip install --force-reinstall --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cu128 -) - -echo. -echo [3/4] Downloading model config + checkpoint ... -if exist "models_cache\msst_models\%CFG_NAME%" ( - for %%F in ("models_cache\msst_models\%CFG_NAME%") do if %%~zF LSS 200 del "models_cache\msst_models\%CFG_NAME%" -) -if not exist "models_cache\msst_models\%CFG_NAME%" ( - curl -fL -o "models_cache\msst_models\%CFG_NAME%" "%REL%/%CFG_NAME%" - if errorlevel 1 goto :fail -) -if exist "models_cache\msst_models\%CKPT_NAME%" ( - for %%F in ("models_cache\msst_models\%CKPT_NAME%") do if %%~zF LSS 50000000 del "models_cache\msst_models\%CKPT_NAME%" -) -if not exist "models_cache\msst_models\%CKPT_NAME%" ( - echo downloading checkpoint ^(~2 GB, this takes a while^) ... - curl -fL -o "models_cache\msst_models\%CKPT_NAME%" "%REL%/%CKPT_NAME%" - if errorlevel 1 goto :fail -) -set "SZ=" -for %%F in ("models_cache\msst_models\%CKPT_NAME%") do set "SZ=%%~zF" -if not defined SZ goto :fail -if !SZ! LSS 50000000 ( - echo [X] checkpoint only !SZ! bytes - download failed. Deleting. - del "models_cache\msst_models\%CKPT_NAME%" >nul 2>&1 - goto :fail -) - -echo. -echo [4/4] Writing manifest ... -> "models_cache\msst_models\manifest.json" echo {"model_type":"%MODEL_TYPE%","config":"%CFG_NAME%","checkpoint":"%CKPT_NAME%"} - -echo. -echo ========================================================== -echo DONE. Checkpoint OK (!SZ! bytes). -echo In Stemmy, pick the EXTENDED depth to split into many -echo instruments (synth/organ/strings/brass/...). First run is -echo slow and needs lots of VRAM - if it runs out of memory the -echo pass skips and the base stems still complete. -echo ========================================================== -echo. -pause -exit /b 0 - -:fail -echo. -echo [X] MSST setup failed above. Stemmy still works without it -echo (Extended depth will just say it needs MSST). -echo curl ships with Windows 10/11; if missing, update Windows. -echo Re-running this script retries only the broken parts. -echo See README - search "ZFTurbo MSST install" for details. -echo. -pause -exit /b 1 +netstat -ano | findstr :5002 ``` -### get_tabs.bat +Use `stop.bat` to clear an abandoned Stemmy process. + +### GPU is detected but utilization drops in the background + +Confirm you launched with the current `run.bat` or desktop shortcut, not directly with an old PowerShell/Python command. -Installs Spotify's Basic Pitch (audio-to-MIDI) plus a light ONNX runtime so the **TAB** button appears on each stem. Installed with `--no-deps` on purpose: basic-pitch's metadata hard-pins TensorFlow on Python 3.11+, which has no matching wheel on Windows/py3.12 and would fail — Stemmy only needs the bundled `.onnx` model + `onnxruntime`. Run `setup.bat` first. +Review: + +```text +logs/stemmy-performance.log +``` + +Then confirm CUDA: ```bat -@echo off -REM ============================================================ -REM get_tabs.bat - enable MIDI + Tab export (beta) in Stemmy -REM -REM This installs Spotify's Basic Pitch (audio -> MIDI) plus a -REM light ONNX runtime. We install with --no-deps on purpose: -REM basic-pitch's metadata hard-pins TensorFlow on Python 3.11+, -REM which has no matching wheel on Windows/py3.12 and would fail. -REM Stemmy only needs the ONNX runtime (the model ships as .onnx), -REM so we add the handful of real runtime deps ourselves. -REM ============================================================ -setlocal -cd /d "%~dp0" - -if exist ".venv\Scripts\activate.bat" ( - call ".venv\Scripts\activate.bat" -) else ( - echo No .venv found - run setup.bat first. - pause - exit /b 1 -) - -echo. -echo Installing Basic Pitch (audio-to-MIDI) without its TensorFlow pin ... -pip install "basic-pitch==0.4.0" --no-deps || goto :fail - -echo. -echo Installing the ONNX runtime + light transcription deps ... -pip install onnxruntime "pretty_midi>=0.2.9" "resampy>=0.2.2,<0.4.3" "mir_eval>=0.6" || goto :fail -REM basic-pitch imports librosa + scikit-learn at runtime; we install with -REM --no-deps (to skip its TensorFlow pin) so we add these ourselves. They -REM normally come from requirements.txt too, but a failed/partial core install -REM can leave them missing - installing here makes tabs self-sufficient. -REM setuptools provides pkg_resources, which resampy 0.4.2 imports. -pip install "librosa>=0.10" scikit-learn "setuptools<81" typing-extensions || goto :fail - -echo. -echo Verifying ... -python -c "from basic_pitch.inference import predict; import onnxruntime; print('Tab/MIDI export ready.')" || goto :fail - -echo. -echo Done. Restart Stemmy (run.bat) and the TAB button will appear on each stem. -pause -exit /b 0 - -:fail -echo. -echo Install failed. You can retry, or open an issue with the message above. -pause -exit /b 1 +call .venv\Scripts\activate.bat +python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0) if torch.cuda.is_available() else '')" +``` + +### Song identifies but lyrics do not appear + +Review: + +```text +logs/stemmy-lyrics.log ``` -### get_lyrics.bat +The current lyric client records exact lookup, broad search, cleaned metadata variants, provider errors, and fallback attempts. -Installs **shazamio** so Stemmy can identify a track from its audio. Lyrics themselves come from LRCLIB over plain HTTP and need no package, so even without this you can type a title/artist and still get synced lyrics. Run `setup.bat` first. +Confirm ShazamIO imports: ```bat -@echo off -REM ============================================================ -REM get_lyrics.bat - enable song ID + lyrics in Stemmy -REM Installs shazamio (song recognition). Lyrics come from -REM LRCLIB (free, no key) and need no extra package. -REM ============================================================ -setlocal -cd /d "%~dp0" -if exist ".venv\Scripts\activate.bat" ( - call ".venv\Scripts\activate.bat" -) else ( - echo No .venv found - run setup.bat first. - pause - exit /b 1 -) -echo. -echo Installing shazamio (song identification) ... -pip install shazamio || goto :fail -REM Python 3.13 removed the stdlib 'audioop' module that pydub (a shazamio -REM dependency) imports. Install the audioop-lts backport so it works on 3.13. -REM (Harmless on 3.10-3.12, where it simply isn't needed.) -python -c "import sys; raise SystemExit(0 if sys.version_info[:2]>=(3,13) else 1)" >nul 2>&1 && pip install audioop-lts -echo. -python -c "import shazamio; print('Song ID + lyrics ready.')" || goto :fail -echo. -echo Done. Restart Stemmy (run.bat) and use "Show lyrics" in the studio. -pause -exit /b 0 -:fail -echo. -echo Install failed. You can still type a title/artist to fetch lyrics without song ID. -pause -exit /b 1 +call .venv\Scripts\activate.bat +python -c "import shazamio; print('ShazamIO OK')" ``` -### update_ytdlp.bat +Manual title/artist entry remains available when no provider has a matching lyric record. + +### YouTube downloads fail or return 403 -Updates yt-dlp to the latest release. YouTube changes its stream signatures often, which is the -usual cause of **`HTTP Error 403: Forbidden`** on a *subset* of tracks in a karaoke batch (some -download fine, others 403). A stale yt-dlp is the most common reason. Run this, reopen Stemmy, then -use **Retry failed** in the karaoke panel to re-run just the blocked tracks — Stemmy also tries the -`android`/`ios`/`tv` player clients and retries automatically, but the latest yt-dlp fixes the rest. +Update only `yt-dlp` under **Settings -> Updates**, or run: ```bat -@echo off -setlocal -cd /d "%~dp0" -title Stemmy - update yt-dlp - -if exist ".venv\Scripts\activate.bat" ( - call ".venv\Scripts\activate.bat" -) else ( - echo [!] No .venv found - run setup.bat first. - pause ^& exit /b 1 -) - -echo ========================================================== -echo Updating yt-dlp -echo ========================================================== -echo. -echo YouTube changes its streams often, which is the usual cause of -echo "HTTP Error 403: Forbidden" on some tracks. Updating yt-dlp to the -echo latest release fixes the large majority of those failures. -echo. -echo Updating yt-dlp to the newest version ... -python -m pip install -U yt-dlp -echo. -if errorlevel 1 ( - echo [!] Update failed. Check your internet connection and try again. -) else ( - echo [ok] yt-dlp is up to date. - echo Reopen Stemmy, then use "Retry failed" in the karaoke panel - echo to re-run any tracks that hit a 403. -) -echo. -pause +call .venv\Scripts\activate.bat +python -m pip install --upgrade yt-dlp ``` + +Restart Stemmy and use **Retry failed** in Karaoke so successful tracks are not repeated. + +### Closing the browser does not stop Stemmy + +The automatic shutdown behavior depends on the dedicated app window opened by the current launcher. A normal browser tab, a browser crash, or a manually opened URL may not give the launcher a reliable close event. + +Use **Settings -> Close Stemmy** or `stop.bat`. + +## Security and scope + +Stemmy binds to `127.0.0.1` for local single-user use. Do not expose the Flask development server directly to the public internet. + +Audio separation and model inference run locally. YouTube downloads, Shazam identification, lyrics, and dependency checks contact their respective internet services. \ No newline at end of file diff --git a/README.md b/README.md index b823596..f858b67 100644 --- a/README.md +++ b/README.md @@ -1,364 +1,243 @@ # Stemmy -A local, GPU-accelerated **stem-separation studio**. Drop in a song, pull it apart -into deep hierarchical stems: vocals, bass, **drums → kick / snare / toms / hi-hat / -ride / crash**, guitar, piano, keys, other, then solo, mute, level, pan, scrub against -a beat grid, shift pitch/tempo, watch live chord detection, and export stems as WAV or a -zip. Everything runs on your machine; **no audio ever leaves it**. - -![Stemmy studio with the drum split](docs/screenshots/studio.png) - ---- - -## Table of contents - -- [What it does](#what-it-does) -- [Screenshots](#screenshots) -- [Tested on / requirements](#tested-on--requirements) -- [Disk space](#disk-space) -- [Quick start](#quick-start) -- [Separation depths](#separation-depths) -- [The studio](#the-studio) -- [Tab & MIDI export (beta)](#tab--midi-export-beta) -- [Karaoke mode (playlist batch)](#karaoke-mode-playlist-batch) -- [MilkDrop visualizer](#milkdrop-visualizer) -- [Lyrics & song ID](#lyrics--song-id) -- [Extended depth (53-stem, experimental)](#extended-depth-53-stem-experimental) -- [Honest limitations](#honest-limitations) -- [GPU / VRAM tuning (8 GB)](#gpu--vram-tuning-8-gb) -- [Known issues](#known-issues) -- [Project layout](#project-layout) -- [Launchers](#launchers) -- [License](#license) - ---- - -## What it does - -- **Local & private.** All separation and playback happen on your machine. No uploads, no cloud, no account. -- **Drop a file or paste a YouTube link.** Either upload audio (WAV/MP3/FLAC/M4A/OGG/AIFF) or paste a YouTube URL on the Upload screen, and Stemmy fetches the audio locally with yt-dlp and runs the exact same separation flow. -- **A pipeline, not one model.** No single model separates everything well, so Stemmy chains specialized open-source models in sequential passes, the same approach the paid tools use internally. Each pass loads one model, runs, frees VRAM, then hands its output to the next. That keeps peak memory low enough for an 8 GB card. -- **Reliable drum split.** Deep separates the kit into kick / snare / toms / hi-hat / ride / crash, nested under Drums. -- **A real mixing studio.** Per-stem solo / mute / level / pan, real waveforms, a zoomable timeline, a beat-locked metronome with time-signature selector, offline pitch (±12) and tempo shift, live chord readout, color themes, and per-stem or zip export. -- **Resume on interrupt.** Projects save to `projects//`; reopening a half-finished separation picks up from the last completed pass. +A local, GPU-accelerated **stem-separation studio** for Windows. Drop in a song or paste a YouTube link, separate it into vocals, bass, drums and sub-drums, guitar, piano, keys, and other stems, then mix, practice, identify songs, display lyrics, generate chords, tune an instrument, and export the results. + +Everything runs on your own machine. Audio separation, playback, mixing, analysis, tuner processing, and chord generation stay local. + +![Stemmy studio](docs/screenshots/studio.png) + +## Current feature set + +- **GPU stem separation:** Quick, Standard, Deep, and optional Extended workflows. +- **Deep drum breakdown:** kick, snare, toms, hi-hat, ride, and crash nested beneath the drum stem. +- **Full local mixer:** solo, mute, volume, pan, real waveforms, zoom, scrubbing, pitch shift, tempo change, and per-stem downloads. +- **Karaoke mode:** download a playlist, remove vocals, save sessions, retry failures, play tracks full-screen, auto-advance, and show a queue. +- **Automatic song ID and lyrics:** Shazam identification followed by synced or plain lyric lookup, with detailed diagnostics and manual title/artist fallback. +- **MilkDrop visualizer:** Butterchurn/MilkDrop 2 presets plus independent album-cover backgrounds in Studio and Karaoke. +- **Chromatic tuner:** stable local microphone/audio-interface tuner with Standard tuning selected by default and several alternate guitar tunings. +- **Chord Creator:** local multi-genre progression generation with playback, Roman numerals, transposition, variations, diagrams, capo suggestions, and saved favorites. +- **MIDI and ASCII tab export:** optional Basic Pitch/ONNX transcription per stem. +- **Theme support:** green, blue, and red schemes with theme-matched hover states. +- **Export Save As:** whole-project ZIP export can use the browser's Save As picker where supported. +- **Background dependency checks:** safe helper packages can be checked and updated individually from Settings. +- **Windows app-style launcher:** hidden background server, dedicated maximized browser app window, desktop shortcut/icon, and close-window shutdown. ## Screenshots -**The separation passes stream in live**, each model runs one at a time and frees VRAM between passes: +**Live separation passes:** ![Separation passes](docs/screenshots/passes.png) -**Mixing**: solo / mute / level / pan, real waveforms, metronome, pitch & tempo, live chord: +**Mixing studio:** ![Mixing](docs/screenshots/mixing.png) -**Karaoke Mode!** - batch-strip vocals from a whole Youtube playlist into instrumentals with a full-screen player, visualizer & auto-advancing queue +**Karaoke mode:** ![Karaoke Mode](docs/screenshots/Karaoke1.png) -## Tested on / requirements - -Stemmy was built and run on this machine: +## Tested configuration -| | | +| Component | Tested value | |---|---| | OS | Windows 11 | -| GPU | NVIDIA GeForce RTX 5060 Laptop GPU (8 GB VRAM, Blackwell / sm_120) | -| System RAM | 16 GB | +| GPU | NVIDIA GeForce RTX 5060 Laptop GPU, 8 GB VRAM | +| RAM | 16 GB | | Python | 3.12 | -| PyTorch | 2.x + CUDA 12.8 (`cu128`) | +| PyTorch | CUDA 12.8 build | -Requirements: an **NVIDIA GPU** (8 GB VRAM is enough for Quick / Standard / Deep), **Python 3.10 to -3.13**, and a CUDA-matched PyTorch. CPU-only will run but is very slow. The **Extended** depth is the -exception on RAM; see its section below. +Python **3.10-3.13** is supported by the current setup scripts; Python 3.12 is the most thoroughly tested. Python 3.14 remains too new for several optional audio packages. -> **Use Python 3.10 to 3.13 (3.12 or 3.13 recommended).** Python **3.14 is too new**: several audio -> dependencies (`diffq`, `numba` / `llvmlite`, `basic-pitch`) have no wheels for it yet and will try to -> build from source and fail. `setup.bat` and `install_all.bat` auto-pick a supported version through -> the `py` launcher and stop with a clear message if only 3.14+ is found. If you have both 3.14 and a -> supported version installed, the scripts select the supported one; just delete any `.venv` that a -> failed 3.14 run created first, so it rebuilds on the right interpreter. +Budget roughly **15-25 GB free** for the virtual environment, PyTorch, cached models, and uncompressed output stems. Extended separation can require substantially more system RAM than Quick, Standard, or Deep. -## Disk space +## Quick start -Budget roughly **15–25 GB free**, mostly model weights downloaded on first use: +### Current Windows package or cumulative overlay -| Item | Approx size | -|------|-------------| -| `.venv` (torch + audio-separator + deps) | ~6–8 GB | -| Demucs models (`htdemucs_ft`, `htdemucs_6s`) | ~1–2 GB (auto-downloaded) | -| DrumSep model (Deep) | ~0.1 GB (auto-downloaded) | -| ZFTurbo MSST + 53-stem checkpoint (Extended, optional) | ~2–3 GB | -| Per-song output (stems are uncompressed WAV) | ~0.1–0.5 GB each | +The packaged Windows build includes the local launchers and app-style shortcut tools that are intentionally not all stored in Git. -## Quick start +1. Extract the package over the Stemmy folder and allow Windows to replace files. +2. On a first installation, run `install_all.bat` or `setup.bat`. +3. `install_all.bat` automatically creates or refreshes the Stemmy desktop shortcut and icon. +4. For later source-only overlays, **do not run setup again**. +5. Start Stemmy from the desktop shortcut or with `run.bat`. `Create Stemmy Shortcut.bat` remains available as a repair tool. -> Prefer double-clicking? Run **`install_all.bat`** once to install everything in one shot (core app -> plus CUDA torch, then yt-dlp, song ID, tabs, and the optional Extended model), then use `run.bat`. -> See **[BUILD.md](BUILD.md)** for the individual `setup.bat` and `run.bat` flow and what every launcher -> does. The steps below are the manual equivalent. +Stemmy opens maximized in a dedicated Edge or Chrome app window when available. Closing that window with **X** shuts down the local Python server as well. `stop.bat` remains an emergency fallback. -PyTorch must match **your** CUDA, so install it first, otherwise `audio-separator` may pull -a CPU-only torch. +See [BUILD.md](BUILD.md) for the full Windows launcher workflow and troubleshooting. -```bash -# 1) GPU torch for an RTX 5060 / CUDA 12.8 (Blackwell needs cu128) -pip install torch torchaudio --index-url https://download.pytorch.org/whl/cu128 +### Raw Git clone -# 2) everything else +The simplest manual installation is: + +```bat +py -3.12 -m venv .venv +call .venv\Scripts\activate.bat +python -m pip install --upgrade pip pip install -r requirements.txt -# 3) confirm the GPU is visible -python -c "import torch; print(torch.cuda.is_available())" # -> True +rem RTX 50-series / CUDA 12.8 example: +pip install --force-reinstall --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cu128 -# 4) run it -python run_stemmy.py # http://127.0.0.1:5002 -python run_stemmy.py --port 5005 +python run_stemmy.py ``` -Upload a track **or paste a YouTube link** on the Upload screen, pick a depth, watch the passes stream in, then mix in the studio. +Open `http://127.0.0.1:5002`. -### YouTube links +Install the CUDA PyTorch build **after** `requirements.txt`. Some dependency installers can otherwise replace it with CPU-only PyTorch. -The Upload screen has an "or paste a YouTube link" field. Stemmy downloads the audio with **yt-dlp** and extracts it to WAV with **ffmpeg**, then treats it like any uploaded file. Both `yt-dlp` and `imageio-ffmpeg` (a bundled ffmpeg, so you don't have to install one) are pulled in by `setup.bat` / `requirements.txt`; a system ffmpeg on your PATH is used if present. The fetched video's thumbnail is saved and shown next to the track in the Recent Sessions list. The fetch runs on your machine; only download tracks you have the rights to. +## Separation depths -**If a link won't fetch:** re-run `setup.bat` (it updates yt-dlp), then try again. If it still fails, YouTube has likely changed something. Update yt-dlp directly with `pip install -U yt-dlp`, or check [github.com/yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp) for the latest. The Upload screen shows this same hint when a fetch fails. +| Depth | Typical output | Pipeline | +|---|---|---| +| **Quick** | 4 stems | vocals, drums, bass, other | +| **Standard** | 6 stems | adds guitar and piano | +| **Deep** | up to 13 stems | Standard plus the detailed DrumSep pass and analysis | +| **Extended** | many stems | optional ZFTurbo MSST multi-instrument model | -## Separation depths +Models run sequentially and free VRAM between passes. Completed output is persisted under `projects//`, allowing finished projects and interrupted work to be restored. -The depth → pass → model mapping lives in `app/models.py`; tweak it freely. Quick / Standard / -Deep use models that **`audio-separator` downloads for you** the first time; nothing to fetch by hand. +## Studio -| Depth | Stems | Models (passes) | -|-------|-------|-----------------| -| **Quick** | 4 | `htdemucs_ft` → vocals · drums · bass · other | -| **Standard** | 6 | `htdemucs_6s` → + guitar + piano | -| **Deep** | up to 13 | `htdemucs_6s` → **DrumSep** (kick/snare/toms/hi-hat/ride/crash) → keys split (if a model is configured) → analysis | -| **Extended** | many | ZFTurbo **MSST** 53-stem `bs_roformer` (*optional, experimental*) (see below) | +The Studio includes: -**The drum split is built in.** Deep runs the base `htdemucs_6s` separation, then runs -`MDX23C-DrumSep-aufr33-jarredou.ckpt` (in audio-separator's catalog, auto-downloaded) on the -isolated `drums` stem. It's marked optional/experimental: if it can't load or runs out of VRAM, -the Deep run still completes with the base stems and the drum pass reports "skipped"; it can -never break a separation. +- Per-stem solo, mute, pan, level, selection, and download controls. +- Real waveforms and an overview strip with 1x-100x zoom. +- Pitch shifting and playback-tempo controls. +- Detected-track metronome behavior with meter, beat alignment, and feel controls. +- Live chord readout and scrolling chord ribbon. +- A-B loop controls and loop markers. +- Channel sorting and hide-below-peak filtering. +- Recent-session restore and unfinished-project resume. +- Live CPU/GPU status in the lower-left panel. +- Green, blue, and red UI themes. +- Save As behavior for full ZIP export where the browser supports the File System Access API. -Run `check_models.bat` (or `audio-separator --list_models`) to see the full catalog and the exact -names you'd put in `app/models.py`. +## Tuner -## The studio +The **Tuner** button opens a local chromatic tuner using a microphone or connected audio interface. -Once a separation finishes you land in the studio: +- Standard tuning is selected by default. +- Alternate presets include half-step down, Drop D, D Standard, Drop C sharp, Drop C, Open G, and Open D. +- A4 reference is adjustable. +- Input-device selection is supported. +- Pitch smoothing and note-locking reduce unstable note changes. +- Stemmy/Karaoke audio stops before microphone listening begins, and the input is released when the tuner closes. -- **Per-stem controls**: solo, mute, level, pan; download a single stem or export all as a zip. -- **Real waveforms + zoomable timeline** (1×–100×), with a shared sample-locked playback clock. -- **Beat-locked metronome** generated from the detected beats, with a 3/4–7/8 time-signature selector. -- **Pitch & tempo**: offline, non-destructive pitch shift (±12 semitones) and tempo/BPM change. -- **Live chord readout** as the track plays, plus a **scrolling chord ribbon** (Chordify-style: the current chord is highlighted and upcoming chords approach a fixed line, so you can play along). -- **Loop A–B sections** and **tap tempo**, with a **Steady** (fixed-tempo) click mode so the metronome doesn't drift. The loop is marked in **yellow on both the transport scrub bar and the song overview strip**. The A and B boundaries show as labelled yellow lines, and the region between them is washed yellow, so the loop is obvious at a glance. Setting just A (before B) shows a single yellow **A** marker for immediate feedback. -- **Channel sort** (Active / A–Z) and a **hide-below-peak slider** to tuck away empty stems. -- **Color themes**, recent-session history (with a YouTube thumbnail per track), and a **collapsible side panel** (the handle on the panel's edge). +## Chord Creator -You can also open `app/templates/index.html` **directly** in a browser to iterate the UI on mock -data; served through Flask it renders the real project and plays the real stems. - -## Tab & MIDI export (beta) +The **Chord Creator** generates progressions locally without an AI or cloud API. -Each stem gets a **TAB** button that transcribes it to **MIDI** and a **beta ASCII tab**. It runs -[Spotify's Basic Pitch](https://github.com/spotify/basic-pitch) (audio-to-MIDI, ONNX runtime) on the -isolated stem (which is ideal, since Basic Pitch works best on one instrument at a time), then maps -the notes to a fretboard with a small built-in fingering solver. Bass tabs to the 4-string tuning, -everything else to 6-string guitar. - -Enable it once with **`get_tabs.bat`** (the TAB buttons stay hidden until it's installed). First run -loads the model, so give the first stem ~15 seconds. - -Honest scope: **MIDI is the reliable output**: open it in any DAW. The **ASCII tab is a practice -aid, not a Guitar Pro file**: ASCII tabs don't encode rhythm, so timing is only approximated by -column spacing, and polyphonic guitar is imperfect. Monophonic bass is the most accurate. Drum stems -export MIDI but not string tab (they're unpitched). - -`get_tabs.bat` installs Basic Pitch with `--no-deps` on purpose: its metadata hard-pins TensorFlow on -Python 3.11+, which has no Windows/py3.12 wheel and would fail; Stemmy only needs the bundled ONNX -model plus `onnxruntime`, so the installer adds just the light runtime deps. - -## Karaoke mode (playlist batch) - -On the Upload screen, **"Karaoke mode: strip vocals from a whole playlist"** opens a batch panel. -Paste a YouTube **playlist** (or a single link) and Stemmy will, for each track: download the audio, -run a Quick (4-stem) separation, and mix everything except the vocals back down to one -`instrumental.wav`. Progress streams per track, and when it's done you get **all the instrumentals as -a single zip** (each as WAV + MP3, vocals removed). - -Open it any time from the **Karaoke** button in the top bar (next to the song title). It doubles as a **karaoke player**: hit **▶ Play all** and Stemmy plays the instrumentals back to -back, **auto-advancing** to the next track when one ends, with previous/next controls and a -now-playing readout. **Open karaoke player** launches a full-screen performance view with **large, couch-readable synced lyrics** over a MilkDrop backdrop, and a **queue** you can open to see what's next or jump around (the queue has its own close button in a header, kept clear of the player's exit/settings buttons). Each song auto-advances to the next. Songs are **identified per track** (Shazam on the original audio, before vocal removal) so lyrics and album art match what's actually playing; install song ID once with `get_lyrics.bat`; the karaoke panel warns you if it's missing. Without it, lyrics fall back to a lookup from the cleaned video title, which is less reliable. It reuses the same local yt-dlp + separation stack; nothing goes to the cloud. - -**Sessions survive a restart.** Each batch is saved to `karaoke_jobs/.json` as it runs, so if you close Stemmy and come back, finished batches appear under **"Saved sessions"** in the karaoke panel (with name, date/time, and how many tracks are ready). Click **Open** to reopen one and play its instrumentals without re-running anything. The saved-session list shows a **real playable count**: it checks that each track's `instrumental.wav` is actually on disk, so if a project folder was deleted or moved, that track is marked **missing** and skipped rather than silently failing on playback. **Forget** (✕) removes a saved session from the list without touching the separated audio in the project folders. - -**Retry failed tracks.** Downloads can fail transiently (YouTube 403s are common). When a batch finishes with errors, or you restore a session that has errored or interrupted tracks, a **↻ Retry failed** button appears; it re-runs just the failed tracks through the full download → separate → mix pipeline while your finished tracks stay untouched. - -**The player backdrop is always the playing track's art.** The full-screen player forces its album-cover background from the current track's own cover on every track change; it never borrows the artwork of the song loaded in the stem studio. - -## MilkDrop visualizer - -The **Visualizer** button in the top bar (studio only) runs **MilkDrop 2 in your browser** via -[Butterchurn](https://github.com/jberg/butterchurn) (a WebGL port of the classic Winamp MilkDrop -visualizer), bundled locally with **100 presets**, no install, no internet. It fills the whole studio -stage behind the mixer, which turns semi-transparent so the animation shows through. - -The menu has **two fully independent toggles**: **MilkDrop visualizer** and **album-cover background**. -Any combination works (visualizer only, cover only, both layered with MilkDrop drawn over the album art, or neither), plus a preset picker and an opacity slider for the visualizer. **Both toggles are off by -default and start off every time you open Stemmy** (so you always get a clean mixer first); only the -preset and opacity preferences are remembered between sessions. It needs WebGL2, which every modern -browser has. - -The **karaoke player has the same two independent toggles** (gear icon, top-right): full-screen -MilkDrop behind the lyrics and a per-track album-cover background, each on its own switch, with preset -and opacity controls. The album cover comes straight from each track's own art and is independent of -both the lyrics and the visualizer. - -Honest note: **MilkDrop 3 is a native Windows app and can't be embedded in a web UI**. Butterchurn is -the real MilkDrop 2 codebase ported to WebGL, which is the closest thing that can run inside Stemmy. - -## Lyrics & song ID - -In the studio, the bar under the chord ribbon has a **Show lyrics** button. It tries to identify the -track and then fetches **time-synced lyrics** that scroll in step with playback (current line -highlighted, next line previewed). Lyrics come from [LRCLIB](https://lrclib.net): free, no API key, -no extra dependency (plain HTTP). - -Song identification is optional: install it once with **`get_lyrics.bat`** (adds -[shazamio](https://github.com/shazamio/ShazamIO)) and Stemmy can name the track from the audio. -Without it, or if the match fails, you just type the **title/artist** and lyrics still load. Synced -lyrics track the playhead even when you pitch/tempo-shift; if only unsynced lyrics exist, the bar -expands to show the full text. - -Notes: it runs tracks one at a time (each is a full download + separation, so a long playlist takes a -while), jobs live in memory until you download or restart, and, as always, only pull tracks you have -the rights to. - -## Extended depth (53-stem, experimental) - -> **Status: working, but heavy.** Extended is wired end-to-end and produces the full multi-instrument -> split. It is still marked experimental because separation quality on many instruments is model-limited -> and it is far more resource-hungry than the other depths. It now picks its run mode automatically -> (full-length when there is enough RAM, chunked when there is not) and falls back gracefully if a -> full-length pass runs short on memory, so you get stems instead of nothing. - -audio-separator's catalog tops out at the 6 Demucs stems plus the drum split. To pull out **synth, -organ, strings, brass, winds, keys, percussion, kick, snare, toms, hi-hat** and dozens more, Stemmy -can optionally drive **ZFTurbo's MSST** (Music-Source-Separation-Training) with the community -**53-stem `bs_roformer`** model. - -**Install it once** by running **`get_msst.bat`** (documented in [BUILD.md](BUILD.md)). With no git -it uses `curl` + PowerShell's `Expand-Archive` to fetch the MSST repo into `models_cache/msst/`, -installs the inference dependencies into your venv (restoring your cu128 torch if they disturb it), -downloads the model config + checkpoint (~2 GB) into `models_cache/msst_models/`, and writes a -`manifest.json`. Re-running only repairs what's broken. Once installed, the **Extended** card shows -"ready"; otherwise it says *"Needs MSST"* and the pass skips cleanly. - -**RAM is the catch.** These big multi-stem models separate far better with the whole song in view, so -Stemmy prefers to run Extended full-length. The model assembles its entire output as one large array, -so a 4-minute song needs roughly **12 to 16 GB of free RAM** (more for longer tracks). Stemmy now -handles this automatically: - -- **Enough RAM:** it runs full-length for the best separation. -- **Short on RAM:** it slices the song into overlapping segments (chunked), runs the model on each, and - crossfade-stitches them back together. This bounds memory use but separates a little less cleanly. -- **A full-length pass that runs out of memory** is caught and retried in chunked mode automatically, so - you still get stems rather than a silent result. If even that fails, the pass reports a clear - out-of-memory note and the rest of the run is unaffected. -- You can force a mode with an environment variable: `STEMMY_MSST_FULL=1` (always full-length) or - `STEMMY_MSST_FULL=0` (always chunked). **32 GB is comfortable; 64 GB for long tracks.** - -**Two things it cannot do** (asked often enough to spell out): - -- **You can't make it "only do guitars" to save resources.** The model emits all 53 stems in one - pass: the cost is in producing the full array, not in keeping the results, so narrowing the output - saves no RAM or time. -- **There is no rhythm / lead / clean guitar split.** Those are performance *roles*, not instruments; - separation models work on sound sources. The most you get is `electric-guitar` / `acoustic-guitar` / - `guitar` as instrument *types* (unreliably, since a guitar can leak into `other` / `strings`). - -**To swap models:** the model is chosen entirely by `get_msst.bat` (the `MODEL_TYPE`, `CFG_NAME`, -`CKPT_NAME`, `REL` variables at the top) and recorded in `manifest.json`. Pick a different -config+checkpoint from ZFTurbo's model list, update those four variables, and re-run, no code changes. +Genres can be selected individually or combined, including pop, classic rock, alternative rock, post-hardcore, metalcore, punk/pop-punk, indie, blues, folk/country, funk/R&B, jazz, and cinematic/synthwave. -## Honest limitations +Choose a starting chord and length, then generate progression options with: -- **Guitar 1 vs guitar 2 / clean vs distorted** is the hardest case in source separation: same - instrument, same timbre, often the same notes, and is essentially unsolved. Stemmy ships a guitar - split as a best-effort `beta` pass and, with no split model configured, leaves a single clean guitar - stem rather than inventing a bad one. -- **Keys split** (piano / synth / organ / strings) has no model in audio-separator's catalog, so that - sub-split is skipped unless you wire one into the `keys` pass in `app/models.py`. -- **Extended** is experimental and RAM-bound as described above. +- Roman-numeral analysis. +- Genre/key context. +- Preview playback. +- Per-chord replacement. +- Semitone transposition. +- Variation generation. +- Chord diagrams. +- Easy-shape and capo suggestions. +- Copying and local favorites. -For dependable guitar + bass + vocals + multi-drum splits that fit in 16 GB, **use Deep**. +## Karaoke -## GPU / VRAM tuning (8 GB) +Karaoke mode accepts a YouTube playlist or single link, processes one track at a time, removes vocals, and creates instrumental WAV/MP3 files. -Roformer-class models are memory-hungry. If you hit out-of-memory: +The player includes: -- Lower the model's `segment_size` (in the config audio-separator downloads). -- Keep passes sequential (the default), so don't load two big models at once. -- Stems are cached to disk between passes and reused, so a re-run is cheap. -- `fix_gpu.bat` applies a High Performance power plan + an EcoQoS opt-out so Windows doesn't throttle - the GPU when the window loses focus on a laptop (see [BUILD.md](BUILD.md)). +- Full-screen synced lyrics. +- Previous, play/pause, and next controls. +- Auto-advance. +- A navigable queue. +- Per-track album art. +- Independent MilkDrop and cover-art backgrounds. +- Persistent saved sessions. +- Retry Failed for transient YouTube or separation errors. -## Known issues +Only download material you have the right to use. -- **GPU throttles when the console loses focus.** On some laptops (this is a Windows/driver power - behavior, not a Stemmy bug), clicking away from the terminal that's running Stemmy drops GPU - utilization, so separation slows down if you switch windows mid-run. It's most noticeable during - **stem separation**. Workarounds: keep the Stemmy console focused while separating, and/or run - `fix_gpu.bat` (High Performance plan + EcoQoS opt-out). This is the same behavior seen in other - local GPU tools on the same machine. +## Lyrics and song identification -- **Karaoke player and browser autoplay.** The full-screen player tries to start playback the moment - you open it. If your browser's autoplay policy blocks that, the player shows **"Ready, press play"** - with a **▶** instead of getting stuck. Just press play once and it (and auto-advance between tracks) - works for the rest of the session. +Studio and Karaoke automatically attempt to identify each loaded track using ShazamIO, then fetch lyrics. -- **YouTube "403 Forbidden" on some tracks.** During a playlist batch you may see a few tracks fail - with `HTTP Error 403: Forbidden` while others download fine. This is a YouTube-side issue, not a - Stemmy bug: YouTube hands yt-dlp certain stream formats whose signed URLs it then rejects, and it - changes those signatures often. Stemmy already mitigates it by requesting the `android`/`ios`/`tv` - player clients (which usually hand back plain, downloadable audio) and retrying, but the definitive - fix is to **keep yt-dlp current**: run **`update_ytdlp.bat`**, reopen Stemmy, and hit **Retry failed** - in the karaoke panel to re-run only the blocked tracks. It's normal for it to be intermittent; which specific videos 403 depends on what format YouTube served that day. +The current lyric flow: -## Project layout +1. Identifies the song from the original audio. +2. Tries exact lyric metadata. +3. Tries broad and cleaned title/artist searches. +4. Accepts synced or plain lyric results. +5. Preserves previously saved lyrics when a temporary provider request fails. +6. Falls back to manual title/artist entry when no provider has a match. -``` -app/ - server.py Flask routes + SSE separation stream - pipeline.py pass orchestration (load model -> separate -> reorganize -> emit) - models.py depth presets + model registry + stem metadata - msst.py optional ZFTurbo MSST (Extended) orchestrator - analysis.py tempo / beat grid - projects.py project.json store + resume - karaoke.py playlist batch jobs + saved-session persistence - identify.py Shazam song ID + lyrics lookup - templates/index.html the whole studio UI (self-contained; mock data when opened directly) -projects/ per-song output (gitignored) -uploads/ raw uploads (gitignored) -karaoke_jobs/ saved karaoke batch sessions, one JSON per batch (gitignored) -models_cache/ downloaded weights (gitignored) -``` +Detailed lyric activity is written to `logs/stemmy-lyrics.log`. + +## Visualizer + +The Studio and Karaoke player use locally bundled Butterchurn/MilkDrop 2 presets. -## Launchers +MilkDrop and album-cover backgrounds are independent: either can be enabled alone, both can be layered, or both can remain off. Preset and opacity preferences are retained while the visualizer and cover layers start disabled on a new launch. -The Windows `.bat` launchers (`install_all`, `setup`, `run`, `stop`, `check_gpu`, `check_models`, -`fix_gpu`, `get_msst`, `get_tabs`, `get_lyrics`, `update_ytdlp`) are kept local and **gitignored**. -Their full contents and what each does are documented in **[BUILD.md](BUILD.md)** so anyone who clones -the repo can recreate them. **`install_all.bat`** is the one-shot option that runs the whole install in -a single self-contained script. +## Updates -This is a Flask **dev** server, fine for local single-user use. Put a real WSGI server in front if -you ever expose it. +Stemmy performs a quiet dependency-status check in the background. Open **Settings -> Updates** to review it. + +Safe helper packages can be updated individually: + +- `yt-dlp` +- `shazamio` +- `imageio-ffmpeg` + +Each update is isolated with `--no-deps`, followed by a compatibility check and automatic rollback if that check fails. + +GPU/model-stack packages such as PyTorch, CUDA-related packages, `audio-separator`, ONNX components, and NumPy remain protected/report-only. Do not blindly update those packages from the UI. + +## Windows launcher and background GPU behavior + +The current Windows launcher opens Stemmy in a dedicated maximized Edge/Chrome app window and does not depend on a focused PowerShell console: + +- Python runs detached and hidden. +- Output goes to `logs/stemmy.log` and `logs/stemmy-error.log`. +- Stemmy and relevant Python child processes receive high-priority/high-QoS treatment. +- The policy is re-applied to model subprocesses. +- GPU separation continues when the app loses focus or is minimized. +- Closing the dedicated app window requests a safe local server shutdown. + +Performance-policy events are written to `logs/stemmy-performance.log`. + +## Honest limitations + +- Separating two performances of the same instrument, such as rhythm vs lead guitar or clean vs distorted guitar, remains unreliable with current source-separation models. +- The detailed drum split is much more dependable than same-instrument guitar splitting. +- Extended separation is experimental and RAM-heavy. +- Shazam, lyrics, YouTube import, and update checking require internet access even though model inference and audio processing remain local. +- Browser autoplay rules may require pressing Play once in the Karaoke player. +- YouTube changes can temporarily break downloads; update `yt-dlp` individually and use Retry Failed. + +## Project layout + +```text +app/ + server.py Flask routes and separation stream + pipeline.py model-pass orchestration + models.py depth/model configuration + analysis.py tempo and beat analysis + projects.py persistent project state + karaoke.py Karaoke jobs and saved sessions + identify.py Shazam and lyric providers + tools_ui.py isolated Tuner/Chord Creator integration + maintenance.py update checker and shutdown support + static/stemmy-tools/ modular Tuner/Chord Creator/maintenance JS + templates/index.html main application UI +projects/ separated project output, gitignored +uploads/ source audio, gitignored +karaoke_jobs/ saved Karaoke sessions, gitignored +models_cache/ downloaded model files, gitignored +logs/ launcher, performance, update, and lyric diagnostics +``` ## License -[MIT](LICENSE): do what you like, no warranty. Bundled/optional models (Demucs, DrumSep, ZFTurbo -MSST) carry their own licenses from their respective projects. +[MIT](LICENSE). Third-party models and bundled libraries retain their respective licenses. \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py index 8b9a834..65cd232 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,4 +1,12 @@ """Stemmy — local AI stem separation studio.""" -from .server import create_app + +from .server import create_app as _create_app +from .tools_ui import install_tools + + +def create_app(): + """Create Stemmy and attach isolated musician tools.""" + return install_tools(_create_app()) + __all__ = ["create_app"] diff --git a/app/static/stemmy-tools.css b/app/static/stemmy-tools.css new file mode 100644 index 0000000..3b4b5e9 --- /dev/null +++ b/app/static/stemmy-tools.css @@ -0,0 +1,90 @@ +/* Stemmy musician tools: isolated tuner + chord creator UI. */ +.st-tools-btn svg{width:15px;height:15px} +@media (max-width:1280px){.st-tools-btn span{display:none}.st-tools-btn{padding:7px 9px}} + +.st-tool-stage{position:fixed;inset:0;z-index:520;display:none;flex-direction:column;background: + radial-gradient(900px 480px at 50% -10%,rgba(var(--green-rgb),.14),transparent 65%),#060a08;color:var(--text)} +.st-tool-stage.show{display:flex} +.st-tool-head{height:64px;display:flex;align-items:center;gap:14px;padding:0 22px;border-bottom:1px solid var(--line);background:rgba(10,16,12,.94);backdrop-filter:blur(10px)} +.st-tool-head .st-title{font-size:18px;font-weight:700;letter-spacing:-.25px} +.st-tool-head .st-sub{font-size:11px;color:var(--muted);margin-top:2px} +.st-tool-head .st-spacer{flex:1} +.st-tool-close{width:38px;height:38px;border:1px solid var(--line);border-radius:10px;background:rgba(0,0,0,.28);color:var(--muted);font-size:16px} +.st-tool-close:hover{color:var(--text);border-color:var(--green-dim)} +.st-tool-scroll{flex:1;min-height:0;overflow:auto;padding:24px} +.st-tool-grid{display:grid;grid-template-columns:minmax(260px,380px) minmax(420px,1fr);gap:20px;max-width:1400px;margin:0 auto} +.st-panel{border:1px solid var(--line);border-radius:16px;background:linear-gradient(180deg,var(--panel-2),var(--panel));box-shadow:var(--shadow);overflow:hidden} +.st-panel-h{padding:15px 17px;border-bottom:1px solid var(--line-soft);display:flex;align-items:center;gap:10px} +.st-panel-h h3{font-size:14px;margin:0}.st-panel-h small{color:var(--muted-2);font-size:10.5px} +.st-panel-b{padding:17px} +.st-field{display:flex;flex-direction:column;gap:6px;margin-bottom:14px} +.st-field>label,.st-field-label{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted-2)} +.st-row{display:flex;gap:9px;align-items:center;flex-wrap:wrap} +.st-select,.st-input{height:38px;border:1px solid var(--line);border-radius:9px;background:#090e0b;color:var(--text);padding:0 10px;font-family:inherit;outline:none} +.st-select:focus,.st-input:focus{border-color:var(--green-dim)} +.st-input[type=number]{width:84px} +.st-button{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:38px;padding:0 14px;border:1px solid var(--line);border-radius:9px;background:rgba(0,0,0,.25);font-size:12.5px;font-weight:600;color:var(--text)} +.st-button:hover{border-color:var(--green-dim)} +.st-button.primary{background:var(--green);border-color:var(--green);color:#04140b} +.st-button.primary:hover{background:#46ee8c} +.st-button.danger:hover{border-color:var(--danger);color:var(--danger)} +.st-button[disabled]{opacity:.45;cursor:not-allowed} +.st-note{font-size:11px;line-height:1.55;color:var(--muted)} +.st-badge{display:inline-flex;align-items:center;border:1px solid var(--line);border-radius:999px;padding:3px 8px;font-size:10px;color:var(--muted)} +.st-badge.good{border-color:var(--green-dim);color:var(--green)} +.st-switch{display:inline-flex;align-items:center;gap:7px;font-size:12px;color:var(--muted);cursor:pointer} +.st-switch input{accent-color:var(--green)} + +/* Tuner */ +.st-tuner-layout{max-width:1180px;margin:0 auto;display:grid;grid-template-columns:minmax(260px,330px) minmax(440px,1fr);gap:20px} +.st-tuner-face{min-height:520px;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:34px;text-align:center;position:relative} +.st-tuner-status{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--muted-2);min-height:16px} +.st-tuner-note{font-family:var(--mono);font-size:clamp(76px,11vw,150px);line-height:.92;font-weight:700;color:var(--text);letter-spacing:-.08em;text-shadow:0 0 35px rgba(var(--green-rgb),.16);margin:14px 0 8px} +.st-tuner-note.in-tune{color:var(--green);text-shadow:0 0 38px rgba(var(--green-rgb),.42)} +.st-tuner-cents{font-family:var(--mono);font-size:18px;color:var(--muted);min-height:24px} +.st-meter{width:min(620px,92%);height:88px;position:relative;margin:26px 0 14px} +.st-meter-line{position:absolute;left:0;right:0;top:50%;height:2px;background:linear-gradient(90deg,var(--danger),var(--warn) 35%,var(--green) 50%,var(--warn) 65%,var(--danger));border-radius:2px} +.st-meter-center{position:absolute;left:50%;top:12px;bottom:12px;width:2px;background:var(--green);opacity:.75} +.st-meter-needle{position:absolute;left:50%;top:5px;width:3px;height:66px;background:#fff;border-radius:3px;transform-origin:50% 100%;transform:translateX(-50%) rotate(0deg);transition:transform .11s ease-out;box-shadow:0 0 10px rgba(255,255,255,.55)} +.st-meter-labels{display:flex;justify-content:space-between;width:min(620px,92%);font:10px var(--mono);color:var(--muted-2)} +.st-tuner-meta{display:flex;gap:18px;flex-wrap:wrap;justify-content:center;margin-top:18px;font:11px var(--mono);color:var(--muted)} +.st-level{height:7px;border-radius:5px;background:var(--line);overflow:hidden;margin-top:8px}.st-level i{display:block;width:0;height:100%;background:linear-gradient(90deg,var(--teal),var(--green));transition:width .08s} +.st-string-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:8px} +.st-string{border:1px solid var(--line);border-radius:10px;padding:10px 8px;text-align:center;background:rgba(0,0,0,.18)} +.st-string b{display:block;font:18px var(--mono);color:var(--text)}.st-string small{font:9px var(--mono);color:var(--muted-2)} +.st-string.active{border-color:var(--green);background:rgba(var(--green-rgb),.10);box-shadow:0 0 18px -8px var(--green)} +.st-string.active b{color:var(--green)} + +/* Chord creator */ +.st-genre-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px} +.st-genre{display:flex;align-items:center;gap:8px;padding:8px 9px;border:1px solid var(--line-soft);border-radius:9px;background:rgba(0,0,0,.13);font-size:11.5px;color:var(--muted);cursor:pointer} +.st-genre:has(input:checked){border-color:var(--green-dim);color:var(--text);background:rgba(var(--green-rgb),.07)} +.st-genre input{accent-color:var(--green)} +.st-range{width:100%;accent-color:var(--green)} +.st-results{display:flex;flex-direction:column;gap:12px} +.st-empty{min-height:330px;display:grid;place-items:center;text-align:center;color:var(--muted-2);padding:30px} +.st-prog{border:1px solid var(--line);border-radius:13px;background:rgba(0,0,0,.16);overflow:hidden} +.st-prog-top{display:flex;align-items:flex-start;gap:12px;padding:13px 14px;border-bottom:1px solid var(--line-soft)} +.st-prog-num{width:29px;height:29px;border-radius:8px;display:grid;place-items:center;background:var(--green-deep);color:var(--green);font:12px var(--mono);flex:0 0 auto} +.st-prog-info{min-width:0;flex:1}.st-prog-title{font-size:13.5px;font-weight:650}.st-prog-desc{font-size:10.5px;color:var(--muted);margin-top:3px;line-height:1.45} +.st-prog-tags{display:flex;gap:5px;flex-wrap:wrap;margin-top:6px} +.st-prog-body{padding:13px 14px} +.st-chords{display:flex;gap:7px;flex-wrap:wrap;align-items:center} +.st-chord-chip{position:relative;min-width:58px;padding:10px 10px;border:1px solid var(--line);border-radius:9px;background:var(--panel-2);font:14px var(--mono);text-align:center;color:var(--text)} +.st-chord-chip:hover{border-color:var(--green-dim)} +.st-chord-chip .st-cycle{display:block;font:8px var(--sans);color:var(--muted-2);margin-top:3px;text-transform:uppercase;letter-spacing:.05em} +.st-arrow{color:var(--muted-2);font-size:11px} +.st-prog-details{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:12px} +.st-detail{border:1px solid var(--line-soft);border-radius:8px;padding:8px 9px;background:rgba(0,0,0,.11)} +.st-detail small{display:block;font-size:8.5px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted-2);margin-bottom:3px}.st-detail b{font-size:10.5px;font-weight:550;color:var(--muted)} +.st-actions{display:flex;gap:7px;flex-wrap:wrap;margin-top:12px}.st-actions .st-button{min-height:32px;padding:0 10px;font-size:10.5px} +.st-diagrams{display:none;grid-template-columns:repeat(auto-fit,minmax(110px,1fr));gap:8px;margin-top:12px}.st-diagrams.show{display:grid} +.st-diagram{border:1px solid var(--line-soft);border-radius:9px;padding:9px;background:rgba(0,0,0,.14)}.st-diagram h5{margin:0 0 6px;font:12px var(--mono)} +.st-fret{font:10px/1.45 var(--mono);white-space:pre;color:var(--muted)} +.st-faves{margin-top:14px;border-top:1px solid var(--line-soft);padding-top:13px}.st-faves h4{font-size:11px;margin:0 0 8px;color:var(--muted)} +.st-fave-row{display:flex;gap:8px;align-items:center;padding:7px 8px;border:1px solid var(--line-soft);border-radius:8px;margin-bottom:6px;font:10.5px var(--mono);color:var(--muted)}.st-fave-row span{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.st-toast{position:fixed;left:50%;bottom:24px;z-index:600;transform:translate(-50%,20px);opacity:0;pointer-events:none;background:#111a14;border:1px solid var(--green-dim);color:var(--text);border-radius:9px;padding:10px 14px;font-size:12px;transition:.18s}.st-toast.show{opacity:1;transform:translate(-50%,0)} + +@media(max-width:900px){ + .st-tool-scroll{padding:12px}.st-tool-grid,.st-tuner-layout{grid-template-columns:1fr}.st-tuner-face{min-height:430px}.st-prog-details{grid-template-columns:1fr}.st-genre-grid{grid-template-columns:1fr}.st-tool-head{padding:0 12px}.st-tuner-note{font-size:84px} +} diff --git a/app/static/stemmy-tools/part-01.js b/app/static/stemmy-tools/part-01.js new file mode 100644 index 0000000..cd6be92 --- /dev/null +++ b/app/static/stemmy-tools/part-01.js @@ -0,0 +1,116 @@ +(() => { + 'use strict'; + + const $ = (sel, root = document) => root.querySelector(sel); + const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; + const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); + const mod = (n, m) => ((n % m) + m) % m; + const SHARP = ['C','C#','D','D#','E','F','F#','G','G#','A','A#','B']; + const FLAT = ['C','Db','D','Eb','E','F','Gb','G','Ab','A','Bb','B']; + const NOTE_RX = /^([A-G])([#b]?)(-?\d+)?$/; + let toastTimer = 0; + + function toast(message) { + let el = $('#stToast'); + if (!el) { + el = document.createElement('div'); + el.id = 'stToast'; el.className = 'st-toast'; document.body.appendChild(el); + } + el.textContent = message; + el.classList.add('show'); + clearTimeout(toastTimer); + toastTimer = setTimeout(() => el.classList.remove('show'), 1700); + } + + async function copyText(text) { + try { + if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true; } + } catch (_) {} + const ta=document.createElement('textarea'); ta.value=text; ta.style.position='fixed'; ta.style.opacity='0'; + document.body.appendChild(ta); ta.select(); + try { return document.execCommand('copy'); } finally { ta.remove(); } + } + + function svg(path) { + return `${path}`; + } + + function pauseStemmyAudio() { + try { if (typeof window.togglePlay === 'function') window.togglePlay(false); } catch (_) {} + $$('audio').forEach(a => { try { a.pause(); } catch (_) {} }); + } + + function insertTopButton(id, label, title, iconPath, afterId = 'karbtn') { + const anchor = $('#' + afterId) || $('#karbtn'); + const host = anchor && anchor.parentElement; + if (!host || $('#' + id)) return null; + const btn = document.createElement('button'); + btn.id = id; + btn.className = 'topact st-tools-btn'; + btn.title = title; + btn.innerHTML = svg(iconPath) + `${label}`; + anchor.insertAdjacentElement('afterend', btn); + return btn; + } + + function makeStage(id, title, subtitle, body) { + const stage = document.createElement('section'); + stage.id = id; + stage.className = 'st-tool-stage'; + stage.setAttribute('aria-hidden', 'true'); + stage.innerHTML = ` +
+
${svg('')}
+
${title}
${subtitle}
+
+ +
+
${body}
`; + document.body.appendChild(stage); + $('.st-tool-close', stage).addEventListener('click', () => hideStage(stage)); + return stage; + } + + function showStage(stage) { + pauseStemmyAudio(); + stage.classList.add('show'); + stage.setAttribute('aria-hidden', 'false'); + } + + function hideStage(stage) { + stage.classList.remove('show'); + stage.setAttribute('aria-hidden', 'true'); + stage.dispatchEvent(new CustomEvent('stemmy:hide')); + } + + document.addEventListener('keydown', e => { + if (e.key !== 'Escape') return; + const open = $('.st-tool-stage.show'); + if (open) hideStage(open); + }); + + // --------------------------------------------------------------------------- + // Stable chromatic tuner + // --------------------------------------------------------------------------- + const TUNINGS = { + standard: {label:'Standard', notes:['E2','A2','D3','G3','B3','E4']}, + half: {label:'Half-step down', notes:['Eb2','Ab2','Db3','Gb3','Bb3','Eb4']}, + dropd: {label:'Drop D', notes:['D2','A2','D3','G3','B3','E4']}, + dstd: {label:'D standard', notes:['D2','G2','C3','F3','A3','D4']}, + dropcs: {label:'Drop C#', notes:['C#2','G#2','C#3','F#3','A#3','D#4']}, + dropc: {label:'Drop C', notes:['C2','G2','C3','F3','A3','D4']}, + openg: {label:'Open G', notes:['D2','G2','D3','G3','B3','D4']}, + opend: {label:'Open D', notes:['D2','A2','D3','F#3','A3','D4']} + }; + + const tunerBody = ` +
+ +
+
Microphone is off
+
+
Play one string
+
+
−50 flatin tune+50 sharp
+
— Hzconfidence —stable lock —
+
+
`; + + const tunerStage = makeStage('stTunerStage', 'Chromatic tuner', 'stable guitar tuning · alternate tunings · flats and sharps', tunerBody); + const tunerBtn = insertTopButton('tunerbtn', 'Tuner', 'Open stable chromatic tuner', ''); + if (tunerBtn) tunerBtn.addEventListener('click', () => showStage(tunerStage)); + + const tuner = { + ctx:null, stream:null, source:null, analyser:null, raf:0, timer:0, buffer:null, + midiHistory:[], lockedMidi:null, candidateMidi:null, candidateFrames:0, + smoothCents:0, lastGoodAt:0, running:false, devicesLoaded:false + }; + + function noteToMidi(note) { + const m = NOTE_RX.exec(note); + if (!m) return null; + const letter = m[1], accidental = m[2] || '', octave = Number(m[3]); + const base = {C:0,D:2,E:4,F:5,G:7,A:9,B:11}[letter]; + const shift = accidental === '#' ? 1 : accidental === 'b' ? -1 : 0; + return (octave + 1) * 12 + mod(base + shift, 12); + } + + function midiName(midi, flats) { + const rounded = Math.round(midi); + const names = flats ? FLAT : SHARP; + return names[mod(rounded,12)] + (Math.floor(rounded / 12) - 1); + } + + function midiFrequency(midi, a4) { + return a4 * Math.pow(2, (midi - 69) / 12); + } + + function median(values) { + if (!values.length) return 0; + const a = values.slice().sort((x,y)=>x-y); + const mid = Math.floor(a.length/2); + return a.length % 2 ? a[mid] : (a[mid-1]+a[mid])/2; + } + + function yinPitch(input, sampleRate) { + const n = Math.min(input.length, 8192); + const minFreq = 55, maxFreq = 1200; + const minTau = Math.max(2, Math.floor(sampleRate / maxFreq)); + const maxTau = Math.min(Math.floor(sampleRate / minFreq), Math.floor(n / 2) - 1); + const usable = Math.min(3072, n - maxTau - 1); + if (usable < 512) return null; + + let rms = 0; + for (let i=0;i 0.28) return {frequency:null, confidence:1-cmnd[best], rms}; + tau = best; + } + + let betterTau = tau; + if (tau > 1 && tau < maxTau) { + const s0=cmnd[tau-1], s1=cmnd[tau], s2=cmnd[tau+1]; + const denom = 2*(2*s1-s2-s0); + if (Math.abs(denom) > 1e-9) betterTau = tau + (s2-s0)/denom; + } + return {frequency: sampleRate / betterTau, confidence: clamp(1-cmnd[tau],0,1), rms}; + } + + function tunerFlatMode() { return ($('input[name="stAcc"]:checked') || {}).value === 'flat'; } + function tunerA4() { return clamp(parseFloat($('#stA4').value) || 440, 430, 450); } + + function fillTuningSelect() { + const sel = $('#stTuning'); + Object.entries(TUNINGS).forEach(([key,t]) => { + const o=document.createElement('option'); o.value=key; o.textContent=t.label; sel.appendChild(o); + }); + sel.value = 'half'; + renderStrings(); diff --git a/app/static/stemmy-tools/part-03.js b/app/static/stemmy-tools/part-03.js new file mode 100644 index 0000000..a676081 --- /dev/null +++ b/app/static/stemmy-tools/part-03.js @@ -0,0 +1,98 @@ + sel.addEventListener('change', renderStrings); + $$('input[name="stAcc"]').forEach(r => r.addEventListener('change', () => { renderStrings(); if(tuner.lockedMidi != null) renderTuner(tuner.lockedMidi + tuner.smoothCents/100, null, null); })); + } + + function renderStrings(activeMidi = null) { + const t = TUNINGS[$('#stTuning').value] || TUNINGS.standard; + const flats = tunerFlatMode(); + $('#stStrings').innerHTML = t.notes.map((note, i) => { + const midi = noteToMidi(note); + const active = activeMidi != null && Math.abs(activeMidi - midi) < 0.7; + return `
${6-i} string${midiName(midi,flats)}
`; + }).join(''); + } + + async function loadInputDevices() { + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) return; + const devices = (await navigator.mediaDevices.enumerateDevices()).filter(d => d.kind === 'audioinput'); + const sel = $('#stTunerDevice'); + const current = sel.value; + sel.innerHTML = ''; + devices.forEach((d,i) => { + const o=document.createElement('option'); o.value=d.deviceId; o.textContent=d.label || `Audio input ${i+1}`; sel.appendChild(o); + }); + if ([...sel.options].some(o=>o.value===current)) sel.value=current; + tuner.devicesLoaded = true; + } + + function resetTunerDisplay(message='Play one string') { + $('#stTunerNote').textContent='—'; $('#stTunerNote').classList.remove('in-tune'); + $('#stTunerCents').textContent=message; $('#stNeedle').style.transform='translateX(-50%) rotate(0deg)'; + $('#stFreq').textContent='— Hz'; $('#stConfidence').textContent='confidence —'; $('#stHold').textContent='stable lock —'; + renderStrings(null); + } + + function renderTuner(midiFloat, frequency, confidence) { + const flats=tunerFlatMode(); + const nearest=Math.round(midiFloat); + const cents=(midiFloat-nearest)*100; + $('#stTunerNote').textContent=midiName(nearest,flats); + $('#stTunerCents').textContent=(cents>=0?'+':'')+cents.toFixed(1)+' cents'; + $('#stNeedle').style.transform=`translateX(-50%) rotate(${clamp(cents,-50,50)*0.62}deg)`; + $('#stFreq').textContent=frequency ? frequency.toFixed(2)+' Hz' : '— Hz'; + $('#stConfidence').textContent=confidence == null ? 'confidence —' : `confidence ${Math.round(confidence*100)}%`; + $('#stHold').textContent=`stable lock ${midiName(nearest,flats)}`; + $('#stTunerNote').classList.toggle('in-tune', Math.abs(cents)<=3); + $('#stTunerStatus').textContent=Math.abs(cents)<=3?'In tune':cents<0?'Tune up slightly':'Tune down slightly'; + renderStrings(nearest); + } + + function processTuner() { + if (!tuner.running || !tuner.analyser) return; + const now=performance.now(); + if (now - tuner.timer < 78) { tuner.raf=requestAnimationFrame(processTuner); return; } + tuner.timer=now; + tuner.analyser.getFloatTimeDomainData(tuner.buffer); + const result=yinPitch(tuner.buffer,tuner.ctx.sampleRate); + $('#stLevel').style.width=(clamp((result?.rms||0)*900,0,100))+'%'; + + if (!result || !result.frequency || result.confidence < 0.72) { + if (now - tuner.lastGoodAt > 650) { + $('#stTunerStatus').textContent=(result?.rms||0)<0.006?'Waiting for a string':'Listening… hold the note'; + if (now - tuner.lastGoodAt > 1600) resetTunerDisplay('Play one string and let it ring'); + } + tuner.raf=requestAnimationFrame(processTuner); return; + } + + const a4=tunerA4(); + const midiFloat=69+12*Math.log2(result.frequency/a4); + tuner.midiHistory.push(midiFloat); if(tuner.midiHistory.length>7)tuner.midiHistory.shift(); + const stable=median(tuner.midiHistory); + const candidate=Math.round(stable); + + if (tuner.lockedMidi == null) { + if (candidate===tuner.candidateMidi) tuner.candidateFrames++; else { tuner.candidateMidi=candidate; tuner.candidateFrames=1; } + if (tuner.candidateFrames>=3) tuner.lockedMidi=candidate; + } else { + const centsFromLock=(stable-tuner.lockedMidi)*100; + if (Math.abs(centsFromLock)>72) { + if (candidate===tuner.candidateMidi) tuner.candidateFrames++; else { tuner.candidateMidi=candidate; tuner.candidateFrames=1; } + if (tuner.candidateFrames>=4) { tuner.lockedMidi=candidate; tuner.candidateFrames=0; tuner.smoothCents=0; } + } else { tuner.candidateFrames=0; } + } + + if (tuner.lockedMidi != null) { + const rawCents=clamp((stable-tuner.lockedMidi)*100,-55,55); + tuner.smoothCents=tuner.smoothCents*0.76+rawCents*0.24; + const displayMidi=tuner.lockedMidi+tuner.smoothCents/100; + tuner.lastGoodAt=now; + renderTuner(displayMidi,result.frequency,result.confidence); + } + tuner.raf=requestAnimationFrame(processTuner); + } + + async function startTuner() { + if (!navigator.mediaDevices?.getUserMedia) { toast('Microphone access is unavailable in this browser'); return; } + await stopTuner(); + pauseStemmyAudio(); + const deviceId=$('#stTunerDevice').value; diff --git a/app/static/stemmy-tools/part-04.js b/app/static/stemmy-tools/part-04.js new file mode 100644 index 0000000..e6093c5 --- /dev/null +++ b/app/static/stemmy-tools/part-04.js @@ -0,0 +1,83 @@ + const constraints={audio:{deviceId:deviceId?{exact:deviceId}:undefined,echoCancellation:false,noiseSuppression:false,autoGainControl:false,channelCount:1},video:false}; + try { + tuner.stream=await navigator.mediaDevices.getUserMedia(constraints); + const C=window.AudioContext||window.webkitAudioContext; + tuner.ctx=new C({latencyHint:'interactive'}); + tuner.source=tuner.ctx.createMediaStreamSource(tuner.stream); + tuner.analyser=tuner.ctx.createAnalyser(); + tuner.analyser.fftSize=16384; tuner.analyser.smoothingTimeConstant=0; + tuner.buffer=new Float32Array(tuner.analyser.fftSize); + tuner.source.connect(tuner.analyser); + tuner.running=true; tuner.lastGoodAt=performance.now(); tuner.midiHistory=[]; tuner.lockedMidi=null; tuner.candidateMidi=null; tuner.candidateFrames=0; tuner.smoothCents=0; + $('#stTunerStart').disabled=true; $('#stTunerStop').disabled=false; $('#stTunerStatus').textContent='Listening…'; + await loadInputDevices(); + tuner.raf=requestAnimationFrame(processTuner); + } catch (e) { + $('#stTunerStatus').textContent='Microphone permission or device error'; + resetTunerDisplay('Choose an input and try again'); + toast(e?.message || 'Could not start microphone'); + } + } + + async function stopTuner() { + tuner.running=false; + if(tuner.raf)cancelAnimationFrame(tuner.raf); tuner.raf=0; + if(tuner.stream)tuner.stream.getTracks().forEach(t=>t.stop()); + try{if(tuner.source)tuner.source.disconnect();}catch(_){} + try{if(tuner.ctx)await tuner.ctx.close();}catch(_){} + tuner.ctx=tuner.stream=tuner.source=tuner.analyser=null; + $('#stTunerStart').disabled=false; $('#stTunerStop').disabled=true; $('#stTunerStatus').textContent='Microphone is off'; $('#stLevel').style.width='0%'; + } + + fillTuningSelect(); + $('#stTunerStart').addEventListener('click',startTuner); + $('#stTunerStop').addEventListener('click',stopTuner); + $('#stTunerDevice').addEventListener('change',()=>{if(tuner.running)startTuner();}); + tunerStage.addEventListener('stemmy:hide',stopTuner); + + // --------------------------------------------------------------------------- + // Chord creator — local theory engine, no API or AI + // --------------------------------------------------------------------------- + const GENRES = { + pop:{label:'Pop',patterns:[ + ['I','V','vi','IV'],['vi','IV','I','V'],['I','vi','IV','V'],['I','IV','vi','V'],['IV','I','V','vi'] + ],mood:'catchy and resolved',strum:'D D U U D U'}, + rock:{label:'Classic Rock',patterns:[ + ['I','bVII','IV','I'],['I','IV','V','IV'],['I','V','IV','I'],['i','bVII','bVI','bVII'],['I','IV','I','V'] + ],mood:'big, direct movement',strum:'D D D U D U'}, + alt:{label:'Alternative Rock',patterns:[ + ['i','bVI','bIII','bVII'],['I','III','IV','iv'],['i','bVII','bVI','IV'],['vi','IV','I','III'],['i','iv','bVI','bVII'] + ],mood:'moody with contrast',strum:'D D U x U D U'}, + post:{label:'Post-Hardcore',patterns:[ + ['i','bVI','bIII','bVII'],['i','bVII','bVI','bVII'],['i','bIII','bVII','bVI'],['i','iv','bVI','V'],['i','bVI','iv','bVII'] + ],mood:'tense verses and wide choruses',strum:'D D x U D U x U'}, + metal:{label:'Metalcore',patterns:[ + ['i','bVI','bVII','i'],['i','bVI','bIII','bVII'],['i','ii°','bVI','V'],['i','bII','bVI','V'],['i','iv','bII','bVII'] + ],mood:'heavy, dark pull',strum:'Palm-muted 8ths + open accents'}, + punk:{label:'Punk / Pop-Punk',patterns:[ + ['I','IV','V','IV'],['I','V','vi','IV'],['vi','IV','I','V'],['I','bVII','IV','V'],['I','IV','I','V'] + ],mood:'fast and anthemic',strum:'D D D D · driving 8ths'}, + indie:{label:'Indie',patterns:[ + ['I','iii','IV','iv'],['vi','I','V','IV'],['I','V','ii','IV'],['I','IV','iii','vi'],['i','bIII','IV','iv'] + ],mood:'unexpected but melodic',strum:'D D U U D U · light accents'}, + blues:{label:'Blues',patterns:[ + ['I7','IV7','I7','V7','IV7','I7'],['I7','I7','IV7','I7','V7','IV7','I7'],['i7','iv7','i7','V7'] + ],mood:'shuffle and turnaround',strum:'Shuffle: D - da D - da'}, + folk:{label:'Folk / Country',patterns:[ + ['I','IV','I','V'],['I','V','vi','IV'],['I','IV','V','I'],['vi','IV','I','V'],['I','ii','IV','V'] + ],mood:'open and singable',strum:'D D U D U'}, + rnb:{label:'Funk / R&B',patterns:[ + ['i7','IV7','i7','V7'],['ii7','V7','Imaj7','vi7'],['Imaj7','iii7','vi7','IVmaj7'],['i7','bVIImaj7','bVImaj7','V7'] + ],mood:'smooth color and groove',strum:'16ths: x U x U D U x U'}, + jazz:{label:'Jazz',patterns:[ + ['ii7','V7','Imaj7','VI7'],['Imaj7','vi7','ii7','V7'],['iii7','VI7','ii7','V7'],['i7','ii°','V7','i7'] + ],mood:'functional movement',strum:'Comp on 2 and 4'}, + cinematic:{label:'Cinematic / Synthwave',patterns:[ + ['i','bVI','bIII','bVII'],['i','iv','bVI','V'],['I','V','vi','IV'],['i','bVII','bVI','V'],['i','bIII','iv','bVI'] + ],mood:'wide, dramatic lift',strum:'Slow 8ths or pulsing arpeggio'} + }; + + const QUALITY = { + major:{label:'Major',suffix:'',intervals:[0,4,7]}, + minor:{label:'Minor',suffix:'m',intervals:[0,3,7]}, + power:{label:'Power chord',suffix:'5',intervals:[0,7,12]}, diff --git a/app/static/stemmy-tools/part-05.js b/app/static/stemmy-tools/part-05.js new file mode 100644 index 0000000..991160a --- /dev/null +++ b/app/static/stemmy-tools/part-05.js @@ -0,0 +1,68 @@ + dom7:{label:'Dominant 7',suffix:'7',intervals:[0,4,7,10]}, + maj7:{label:'Major 7',suffix:'maj7',intervals:[0,4,7,11]}, + min7:{label:'Minor 7',suffix:'m7',intervals:[0,3,7,10]}, + sus2:{label:'Sus2',suffix:'sus2',intervals:[0,2,7]}, + sus4:{label:'Sus4',suffix:'sus4',intervals:[0,5,7]} + }; + + const ROMAN = {I:0,II:1,III:2,IV:3,V:4,VI:5,VII:6}; + const MAJOR_SCALE=[0,2,4,5,7,9,11], MINOR_SCALE=[0,2,3,5,7,8,10]; + const EASY_SHAPES = new Set(['C','D','Dm','E','Em','G','A','Am','A7','B7','E7','D7','Cmaj7','Fmaj7']); + const FINGERINGS = { + C:['x','3','2','0','1','0'],D:['x','x','0','2','3','2'],Dm:['x','x','0','2','3','1'],E:['0','2','2','1','0','0'],Em:['0','2','2','0','0','0'], + G:['3','2','0','0','0','3'],A:['x','0','2','2','2','0'],Am:['x','0','2','2','1','0'],A7:['x','0','2','0','2','0'],B7:['x','2','1','2','0','2'], + E7:['0','2','0','1','0','0'],D7:['x','x','0','2','1','2'],Cmaj7:['x','3','2','0','0','0'],Fmaj7:['x','x','3','2','1','0'] + }; + + const genreChecks = Object.entries(GENRES).map(([key,g]) => ``).join(''); + const rootOpts = SHARP.map((n,i)=>``).join(''); + const qualOpts = Object.entries(QUALITY).map(([k,q])=>``).join(''); + const chordBody = ` +
+ +
+

Progression ideas

click any chord to swap it
+
Choose your genres and generate.
Each result includes Roman numerals, a genre feel, a strumming idea, playback, transposition, chord diagrams, and an easy-shape/capo suggestion.
+
+
`; + + const chordStage = makeStage('stChordStage','Chord creator','multi-genre progression ideas · beginner-friendly guitar tools',chordBody); + const chordBtn = insertTopButton('chordbtn','Chord Creator','Generate guitar chord progressions','','tunerbtn'); + if(chordBtn)chordBtn.addEventListener('click',()=>{showStage(chordStage);renderFavorites();}); + + $('#stRoot').value='4'; // E + $('#stQuality').value='minor'; + $('#stLength').addEventListener('input',()=>$('#stLengthLabel').textContent=$('#stLength').value); + + let progressions=[]; + let previewCtx=null, previewNodes=[]; + + function selectedGenres(){return $$('#stGenres input:checked').map(x=>x.value);} + function randomOf(arr){return arr[Math.floor(Math.random()*arr.length)];} + function shuffle(arr){const a=arr.slice();for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];}return a;} + + function parseRoman(token) { + let s=token, shift=0; + if(s.startsWith('b')){shift=-1;s=s.slice(1);}else if(s.startsWith('#')){shift=1;s=s.slice(1);} + const m=/^(VII|VI|IV|III|II|V|I)(.*)$/i.exec(s); + if(!m)return null; + const raw=m[1], suffix=m[2]||''; + const degree=ROMAN[raw.toUpperCase()]; + let quality=raw===raw.toLowerCase()?'minor':'major'; + if(suffix.includes('°'))quality='dim'; + else if(suffix==='7')quality=quality==='minor'?'min7':'dom7'; + else if(suffix==='maj7')quality='maj7'; + else if(suffix==='m7')quality='min7'; + else if(suffix==='5')quality='power'; diff --git a/app/static/stemmy-tools/part-06.js b/app/static/stemmy-tools/part-06.js new file mode 100644 index 0000000..7a1c3ea --- /dev/null +++ b/app/static/stemmy-tools/part-06.js @@ -0,0 +1,85 @@ + return {degree,shift,quality,token}; + } + + function qualitySuffix(q){return q==='major'?'':q==='minor'?'m':q==='dim'?'dim':QUALITY[q]?.suffix||'';} + function qualityIntervals(q){return q==='dim'?[0,3,6]:(QUALITY[q]?.intervals||QUALITY.major.intervals);} + function chordName(root,q,flats=true){return (flats?FLAT:SHARP)[mod(root,12)]+qualitySuffix(q);} + + function inferMode(){const v=$('#stMode').value;if(v!=='auto')return v;return ['minor','min7','power'].includes($('#stQuality').value)?'minor':'major';} + + function expandPattern(pattern,length){ + const out=[];while(out.length{ + const p=parseRoman(tok)||{degree:0,shift:0,quality:'major',token:'I'}; + return {root:mod(tonic+scale[p.degree]+p.shift,12),quality:i===0?startQuality:p.quality,roman:tok}; + }); + chords[0].root=startRoot; + return {id:Date.now()+index+Math.random(),genreKey,genreLabel:genre.label,mode,tonic,chords,mood:genre.mood,strum:genre.strum,transpose:0,diagram:false}; + } + + function bestCapo(prog){ + if(!$('#stEasy').checked)return null; + let best={capo:0,score:-1,names:[]}; + for(let capo=0;capo<=7;capo++){ + const names=prog.chords.map(c=>chordName(c.root-capo,c.quality,false)); + let score=names.reduce((s,n)=>s+(EASY_SHAPES.has(n)?3:-1),0)-capo*0.08; + if(capo===0)score+=.2; + if(score>best.score)best={capo,score,names}; + } + return best.score>0?best:null; + } + + function romanLine(prog){return prog.chords.map(c=>c.roman).join(' → ');} + function chordLine(prog,flats=true){return prog.chords.map(c=>chordName(c.root+prog.transpose,c.quality,flats)).join(' → ');} + function keyName(prog){return chordName(prog.tonic+prog.transpose,prog.mode==='minor'?'minor':'major',true);} + + function diagramText(name){ + const f=FINGERINGS[name]; + if(!f)return 'No simple open\nshape stored'; + const strings=['E','A','D','G','B','e']; + return strings.map((s,i)=>`${s}|--${f[i]}--`).join('\n'); + } + + function renderProgressions(){ + const host=$('#stResults'); + host.innerHTML=''; + progressions.forEach((p,idx)=>{ + const capo=bestCapo(p); + const names=p.chords.map(c=>chordName(c.root+p.transpose,c.quality,true)); + const easyText=capo?(capo.capo?`Capo ${capo.capo}: play ${capo.names.join(' – ')}`:`Open-position option: ${capo.names.join(' – ')}`):'No especially easy open-shape version'; + const card=document.createElement('article');card.className='st-prog';card.dataset.id=p.id; + card.innerHTML=` +
${idx+1}
${p.genreLabel} · key of ${keyName(p)}
${p.mood}. Starting from ${names[0]}.
${p.genreLabel}${p.mode}${names.length} chords
+
+
${names.map((n,i)=>`${i?'':''}`).join('')}
+
Roman numerals${romanLine(p)}
Strumming idea${p.strum}
Easy guitar option${easyText}
+
+
${(capo?.names||names).map(n=>`
${n}
${diagramText(n)}
`).join('')}
+
`; + host.appendChild(card); + }); + } + + function generateProgressions(){ + const genres=selectedGenres(); + if(!genres.length){toast('Select at least one genre');return;} + stopPreview(); + const order=shuffle([...Array(6)].map((_,i)=>genres[i%genres.length])); + progressions=order.map((g,i)=>progressionFrom(g,i)); + renderProgressions(); + } + + function replacementChord(p,index){ diff --git a/app/static/stemmy-tools/part-07.js b/app/static/stemmy-tools/part-07.js new file mode 100644 index 0000000..34e627f --- /dev/null +++ b/app/static/stemmy-tools/part-07.js @@ -0,0 +1,70 @@ + const mode=p.mode,scale=mode==='minor'?MINOR_SCALE:MAJOR_SCALE; + const degree=(index+Math.floor(Math.random()*5)+1)%7; + const qualities=mode==='minor'?['minor','dim','major','minor','minor','major','major']:['major','minor','minor','major','major','minor','dim']; + p.chords[index]={root:mod(p.tonic+scale[degree],12),quality:qualities[degree],roman:['I','ii','iii','IV','V','vi','vii°'][degree]}; + } + + async function ensurePreviewCtx(){ + if(!previewCtx){const C=window.AudioContext||window.webkitAudioContext;previewCtx=new C();} + if(previewCtx.state==='suspended')await previewCtx.resume(); + return previewCtx; + } + + function stopPreview(){previewNodes.forEach(n=>{try{n.stop();}catch(_){}});previewNodes=[];} + + async function playProgression(p){ + stopPreview();pauseStemmyAudio(); + const ctx=await ensurePreviewCtx(); + const beat=.72,start=ctx.currentTime+.04; + p.chords.forEach((c,i)=>{ + const rootMidi=48+mod(c.root+p.transpose,12); + const ints=qualityIntervals(c.quality); + ints.forEach((interval,j)=>{ + const osc=ctx.createOscillator(),gain=ctx.createGain(); + osc.type=j===0?'triangle':'sine';osc.frequency.value=440*Math.pow(2,(rootMidi+interval-69)/12); + gain.gain.setValueAtTime(.0001,start+i*beat); + gain.gain.exponentialRampToValueAtTime(j===0?.08:.045,start+i*beat+.035+j*.012); + gain.gain.exponentialRampToValueAtTime(.0001,start+(i+1)*beat-.04); + osc.connect(gain).connect(ctx.destination);osc.start(start+i*beat);osc.stop(start+(i+1)*beat);previewNodes.push(osc); + }); + }); + } + + function favoriteData(){try{return JSON.parse(localStorage.getItem('stemmy.chordFavorites')||'[]');}catch(_){return[];}} + function saveFavorite(p){ + const list=favoriteData(); + const item={id:Date.now(),label:`${p.genreLabel} · ${keyName(p)}`,chords:chordLine(p,true),roman:romanLine(p)}; + list.unshift(item);localStorage.setItem('stemmy.chordFavorites',JSON.stringify(list.slice(0,20)));renderFavorites();toast('Progression saved'); + } + function renderFavorites(){ + const host=$('#stFavorites'),list=favoriteData(); + if(!list.length){host.innerHTML='
No favorites saved yet.
';return;} + host.innerHTML=list.map(x=>`
${x.label}: ${x.chords}
`).join(''); + } + + $('#stGenerate').addEventListener('click',generateProgressions); + $('#stResults').addEventListener('click',async e=>{ + const card=e.target.closest('.st-prog');if(!card)return; + const p=progressions.find(x=>String(x.id)===card.dataset.id);if(!p)return; + const chip=e.target.closest('[data-cycle]'); + if(chip){const i=Number(chip.dataset.cycle);if(i!==0)replacementChord(p,i);else toast('The first chord stays fixed to your choice');renderProgressions();return;} + const act=e.target.closest('[data-act]')?.dataset.act;if(!act)return; + if(act==='play')await playProgression(p); + if(act==='transposeDown'){p.transpose--;renderProgressions();} + if(act==='transposeUp'){p.transpose++;renderProgressions();} + if(act==='variation'){const i=1+Math.floor(Math.random()*Math.max(1,p.chords.length-1));replacementChord(p,i);renderProgressions();} + if(act==='diagram'){p.diagram=!p.diagram;renderProgressions();} + if(act==='copy'){await copyText(`${p.genreLabel} · ${keyName(p)}\n${chordLine(p,true)}\n${romanLine(p)}\nStrumming: ${p.strum}`);toast('Progression copied');} + if(act==='save')saveFavorite(p); + }); + + $('#stFavorites').addEventListener('click',async e=>{ + const id=Number(e.target.dataset.copyfav||e.target.dataset.delfav);if(!id)return; + const list=favoriteData(),item=list.find(x=>x.id===id); + if(e.target.dataset.copyfav&&item){await copyText(`${item.label}\n${item.chords}\n${item.roman}`);toast('Favorite copied');} + if(e.target.dataset.delfav){localStorage.setItem('stemmy.chordFavorites',JSON.stringify(list.filter(x=>x.id!==id)));renderFavorites();} + }); + + chordStage.addEventListener('stemmy:hide',stopPreview); + renderFavorites(); +})(); diff --git a/app/tools_ui.py b/app/tools_ui.py new file mode 100644 index 0000000..d41dde6 --- /dev/null +++ b/app/tools_ui.py @@ -0,0 +1,59 @@ +"""Inject Stemmy's optional musician tools without modifying the main studio template. + +Keeping the tuner and chord creator in isolated static files makes the feature easy +to remove or revise and avoids coupling it to the large, carefully tuned studio UI. +""" + +from __future__ import annotations + +from pathlib import Path + +from flask import Flask, Response + + +_CSS_TAG = ( + '' +) +_JS_TAG = ( + '' +) + + +def install_tools(app: Flask) -> Flask: + """Attach the tuner/chord-creator UI to HTML responses exactly once.""" + + parts_dir = Path(__file__).resolve().parent / "static" / "stemmy-tools" + + @app.get("/stemmy-tools.js") + def _stemmy_tools_bundle(): + parts = sorted(parts_dir.glob("part-*.js")) + source = "\n".join(part.read_text(encoding="utf-8") for part in parts) + return Response( + source, + content_type="application/javascript; charset=utf-8", + headers={"Cache-Control": "no-cache"}, + ) + + @app.after_request + def _inject_musician_tools(response): + if response.status_code != 200 or response.direct_passthrough: + return response + content_type = response.headers.get("Content-Type", "") + if "text/html" not in content_type: + return response + + html = response.get_data(as_text=True) + if 'data-stemmy-tools="js"' in html: + return response + if "" not in html or "" not in html: + return response + + html = html.replace("", f"{_CSS_TAG}", 1) + html = html.replace("", f"{_JS_TAG}", 1) + response.set_data(html) + response.headers["Content-Length"] = str(len(response.get_data())) + return response + + return app