Merge pull request 'Rename project from karaoke-maker to songtrace' (#3) from songtrace-rename into main

Reviewed-on: branderson/karaoke-maker#3
This commit is contained in:
2026-07-27 18:42:57 -07:00
52 changed files with 104 additions and 89 deletions
+2 -2
View File
@@ -11,9 +11,9 @@ COPY pyproject.toml ./
COPY src ./src
RUN pip install --no-cache-dir .
ENV KARAOKE_MAKER_CONFIG=/config/config.toml \
ENV SONGTRACE_CONFIG=/config/config.toml \
TORCH_HOME=/data/models \
XDG_CACHE_HOME=/data/models/cache
EXPOSE 8000
CMD ["karaoke-maker", "serve"]
CMD ["songtrace", "serve"]
+23 -8
View File
@@ -1,4 +1,6 @@
# karaoke-maker
# songtrace
Turn a song and its lyrics into a playable UltraStar chart.
A CLI and minimal web UI that turns a song — title, artist, lyrics you supply,
and a YouTube URL or local video file — into a playable
@@ -35,8 +37,21 @@ editor such as [Yass](http://www.yass-hq.de/) or
Config is TOML. Resolution order:
1. `--config <path>` passed explicitly on the command line
2. the `KARAOKE_MAKER_CONFIG` environment variable
3. `~/.config/karaoke-maker/config.toml`
2. the `SONGTRACE_CONFIG` environment variable
3. `~/.config/songtrace/config.toml`
If you used this project under its former name, move your existing config once:
```sh
pip uninstall karaoke-maker # removes the old console script
mv ~/.config/karaoke-maker ~/.config/songtrace
```
The Docker deployment needs no config migration — the image sets
`SONGTRACE_CONFIG` itself and the config is bind-mounted to a fixed path.
Run `docker compose down` before upgrading (or `docker compose up -d
--remove-orphans`) since the service key changed and Compose won't replace
the old container in place.
Copy the example and fill in your Whisper server:
@@ -73,7 +88,7 @@ container recreation.
Run the CLI inside the container instead of the web UI:
```sh
docker compose run --rm karaoke-maker karaoke-maker make \
docker compose run --rm songtrace songtrace make \
--title "Song Title" --artist "Artist Name" \
--lyrics /data/lyrics.txt --youtube https://youtu.be/...
```
@@ -87,17 +102,17 @@ installed on the host; `docker-compose.yml` requests one GPU via the
```sh
pip install -e .
karaoke-maker make --help
songtrace make --help
```
```
karaoke-maker make --title TITLE --artist ARTIST --lyrics LYRICS_FILE \
songtrace make --title TITLE --artist ARTIST --lyrics LYRICS_FILE \
(--youtube URL | --video PATH) \
[--genre GENRE] [--year YEAR] [--language LANGUAGE] [--cover COVER_IMAGE] \
[--bpm BPM] [--force-from STAGE] [--config CONFIG]
```
Run the web UI instead with `karaoke-maker serve [--config CONFIG]`.
Run the web UI instead with `songtrace serve [--config CONFIG]`.
## Re-running a stage
@@ -108,7 +123,7 @@ stage and everything after it, reusing the cached output of everything
before it:
```sh
karaoke-maker make --title "..." --artist "..." --lyrics lyrics.txt \
songtrace make --title "..." --artist "..." --lyrics lyrics.txt \
--youtube https://youtu.be/... --force-from align
```
+1 -1
View File
@@ -1,5 +1,5 @@
services:
karaoke-maker:
songtrace:
build: .
ports:
- "8000:8000"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

+3 -3
View File
@@ -1,5 +1,5 @@
[project]
name = "karaoke-maker"
name = "songtrace"
version = "0.1.0"
description = "Generate UltraStar/Melody Mania karaoke songs from a video and reference lyrics"
requires-python = ">=3.11"
@@ -26,7 +26,7 @@ dependencies = [
dev = ["pytest", "playwright"]
[project.scripts]
karaoke-maker = "karaoke_maker.cli:main"
songtrace = "songtrace.cli:main"
[build-system]
requires = ["setuptools>=68"]
@@ -36,7 +36,7 @@ build-backend = "setuptools.build_meta"
where = ["src"]
[tool.setuptools.package-data]
karaoke_maker = ["static/*", "static/fonts/*"]
songtrace = ["static/*", "static/fonts/*"]
[tool.pytest.ini_options]
testpaths = ["tests"]
+3 -3
View File
@@ -15,9 +15,9 @@ import httpx
import uvicorn
from playwright.sync_api import sync_playwright
from karaoke_maker.config import Config
from karaoke_maker.pipeline import PipelineError, PipelineResult
from karaoke_maker.web import create_app
from songtrace.config import Config
from songtrace.pipeline import PipelineError, PipelineResult
from songtrace.web import create_app
REPO_ROOT = Path(__file__).resolve().parent.parent
OUT_IMAGE = REPO_ROOT / "docs" / "images" / "webui.png"
@@ -16,7 +16,7 @@ def _positive_float(value: str) -> float:
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="karaoke-maker")
parser = argparse.ArgumentParser(prog="songtrace")
parser.add_argument("--version", action="store_true")
sub = parser.add_subparsers(dest="command")
@@ -72,7 +72,7 @@ def _cmd_make(args) -> int:
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
if args.version:
print(f"karaoke-maker {version('karaoke-maker')}")
print(f"songtrace {version('songtrace')}")
return 0
if args.command == "make":
return _cmd_make(args)
@@ -12,7 +12,7 @@ class Config:
whisper_api: str = "openai"
output_dir: Path = Path("output")
work_dir: Path = Path("work")
creator: str = "karaoke-maker"
creator: str = "songtrace"
demucs_model: str = "htdemucs_ft"
crepe_model: str = "full"
device: str = "cuda"
@@ -25,8 +25,8 @@ _PATH_FIELDS = {"output_dir", "work_dir"}
def load_config(path: Path | None = None) -> Config:
if path is None:
env = os.environ.get("KARAOKE_MAKER_CONFIG")
path = Path(env) if env else Path.home() / ".config/karaoke-maker/config.toml"
env = os.environ.get("SONGTRACE_CONFIG")
path = Path(env) if env else Path.home() / ".config/songtrace/config.toml"
data = tomllib.loads(Path(path).read_text())
known = {f.name for f in fields(Config)}
unknown = set(data) - known
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>karaoke-maker</title>
<title>songtrace</title>
<style>
@font-face {
font-family: "Chakra Petch";
@@ -473,7 +473,7 @@ button { cursor: pointer; }
<aside id="sidebar">
<div class="brand">
<h1 class="wordmark"><span>KARAOKE</span> <span class="second">MAKER</span></h1>
<h1 class="wordmark"><span>SONG</span> <span class="second">TRACE</span></h1>
<div class="kana" aria-hidden="true">カラオケ</div>
</div>
<div>
+5 -5
View File
@@ -1,8 +1,8 @@
import json
from pathlib import Path
from karaoke_maker.config import Config
from karaoke_maker.pipeline import Ctx, SongInput
from karaoke_maker.stages import acquire
from songtrace.config import Config
from songtrace.pipeline import Ctx, SongInput
from songtrace.stages import acquire
def make_ctx(tmp_path, **kw):
inp = SongInput(title="T", artist="A", lyrics_text="x", **kw)
@@ -46,7 +46,7 @@ def test_outputs_are_meta_and_audio(tmp_path):
def test_requires_exactly_one_source(tmp_path):
import pytest
from karaoke_maker.pipeline import PipelineError
from songtrace.pipeline import PipelineError
ctx = make_ctx(tmp_path)
with pytest.raises(PipelineError):
acquire.acquire_stage().run(ctx)
@@ -73,7 +73,7 @@ def test_classify_no_thumbnail(tmp_path):
def test_classify_no_video_raises(tmp_path):
import pytest
from karaoke_maker.pipeline import PipelineError
from songtrace.pipeline import PipelineError
(tmp_path / "source_video.jpg").write_bytes(b"t")
with pytest.raises(PipelineError):
acquire._classify_downloads(tmp_path)
+4 -4
View File
@@ -2,10 +2,10 @@ import json
import pytest
from karaoke_maker.config import Config
from karaoke_maker.lyrics import normalize_lyrics
from karaoke_maker.pipeline import Ctx, PipelineError, SongInput
from karaoke_maker.stages import align
from songtrace.config import Config
from songtrace.lyrics import normalize_lyrics
from songtrace.pipeline import Ctx, PipelineError, SongInput
from songtrace.stages import align
LYRICS = "it's one two three\nfour five six seven eight"
+1 -1
View File
@@ -1,7 +1,7 @@
import numpy as np
import soundfile as sf
from karaoke_maker.audio_io import load_wav
from songtrace.audio_io import load_wav
def test_load_wav_mono(tmp_path):
+2 -2
View File
@@ -1,5 +1,5 @@
from karaoke_maker.cli import main
from songtrace.cli import main
def test_version(capsys):
assert main(["--version"]) == 0
assert "karaoke-maker" in capsys.readouterr().out
assert "songtrace" in capsys.readouterr().out
+3 -3
View File
@@ -2,9 +2,9 @@ import os
import pytest
import karaoke_maker.cli as cli
from karaoke_maker.config import Config
from karaoke_maker.pipeline import PipelineError, PipelineResult
import songtrace.cli as cli
from songtrace.config import Config
from songtrace.pipeline import PipelineError, PipelineResult
def setup_cfg(monkeypatch, tmp_path):
+2 -2
View File
@@ -1,6 +1,6 @@
import pytest
from pathlib import Path
from karaoke_maker.config import Config, load_config
from songtrace.config import Config, load_config
def test_load_minimal(tmp_path):
@@ -24,7 +24,7 @@ def test_paths_and_overrides(tmp_path):
def test_env_var(tmp_path, monkeypatch):
cfg_file = tmp_path / "c.toml"
cfg_file.write_text('whisper_url = "http://env/v1"\n')
monkeypatch.setenv("KARAOKE_MAKER_CONFIG", str(cfg_file))
monkeypatch.setenv("SONGTRACE_CONFIG", str(cfg_file))
assert load_config().whisper_url == "http://env/v1"
+9 -9
View File
@@ -4,27 +4,27 @@ import shutil
import pytest
requires_full = pytest.mark.skipif(
not os.environ.get("KARAOKE_E2E"),
reason="set KARAOKE_E2E=1 (needs GPU, ffmpeg, whisper server, network)")
not os.environ.get("SONGTRACE_E2E"),
reason="set SONGTRACE_E2E=1 (needs GPU, ffmpeg, whisper server, network)")
@requires_full
def test_full_pipeline(tmp_path):
from karaoke_maker.config import load_config
from karaoke_maker.pipeline import SongInput, run_pipeline
from songtrace.config import load_config
from songtrace.pipeline import SongInput, run_pipeline
assert shutil.which("ffmpeg"), "ffmpeg required"
cfg = load_config()
cfg.output_dir = tmp_path / "out"
cfg.work_dir = tmp_path / "work"
inp = SongInput(
title=os.environ["KARAOKE_E2E_TITLE"],
artist=os.environ["KARAOKE_E2E_ARTIST"],
lyrics_text=open(os.environ["KARAOKE_E2E_LYRICS"]).read(),
youtube_url=os.environ.get("KARAOKE_E2E_URL"),
title=os.environ["SONGTRACE_E2E_TITLE"],
artist=os.environ["SONGTRACE_E2E_ARTIST"],
lyrics_text=open(os.environ["SONGTRACE_E2E_LYRICS"]).read(),
youtube_url=os.environ.get("SONGTRACE_E2E_URL"),
)
result = run_pipeline(inp, cfg)
txts = list(result.output_dir.glob("*.txt"))
assert len(txts) == 1
from karaoke_maker.ultrastar import get_header, parse_ultrastar
from songtrace.ultrastar import get_header, parse_ultrastar
song = parse_ultrastar(txts[0].read_text(encoding="utf-8"))
assert get_header(song, "BPM") and get_header(song, "GAP")
assert len(song.body) > 20
+5 -5
View File
@@ -2,11 +2,11 @@ import json
import numpy as np
import pytest
from karaoke_maker.config import Config
from karaoke_maker.grid import scale_bpm, to_ultrastar
from karaoke_maker.pipeline import Ctx, PipelineError, SongInput
from karaoke_maker.stages import all_stages, grid as grid_stage_mod
from karaoke_maker.ultrastar import (
from songtrace.config import Config
from songtrace.grid import scale_bpm, to_ultrastar
from songtrace.pipeline import Ctx, PipelineError, SongInput
from songtrace.stages import all_stages, grid as grid_stage_mod
from songtrace.ultrastar import (
LineBreak, Note, get_header, parse_ultrastar,
)
+1 -1
View File
@@ -1,4 +1,4 @@
from karaoke_maker.lyrics import normalize_lyrics, alignment_token
from songtrace.lyrics import normalize_lyrics, alignment_token
RAW = """[Verse 1]
It's one two three
+4 -4
View File
@@ -2,13 +2,13 @@ import json
import numpy as np
import pytest
from karaoke_maker.config import Config
from karaoke_maker.notes import (
from songtrace.config import Config
from songtrace.notes import (
MIN_NOTE_SEC, TimedNote, build_notes, hz_to_ultrastar, segment_span,
syllabify,
)
from karaoke_maker.pipeline import Ctx, SongInput
from karaoke_maker.stages import all_stages, notes as notes_stage_mod
from songtrace.pipeline import Ctx, SongInput
from songtrace.stages import all_stages, notes as notes_stage_mod
def flat_pitch(duration=10.0, hz=261.63): # C4
+4 -4
View File
@@ -4,12 +4,12 @@ from pathlib import Path
import pytest
from PIL import Image
from karaoke_maker.config import Config
from karaoke_maker.pipeline import (
from songtrace.config import Config
from songtrace.pipeline import (
STAGE_ORDER, Ctx, PipelineError, SongInput, run_pipeline, song_slug,
)
from karaoke_maker.stages import all_stages, package
from karaoke_maker.ultrastar import get_header, parse_ultrastar
from songtrace.stages import all_stages, package
from songtrace.ultrastar import get_header, parse_ultrastar
SONG_TXT = ("#TITLE:T\n#ARTIST:A\n#MP3:A - T.mp3\n#COVER:A - T.jpg\n"
"#VIDEO:A - T.webm\n#BPM:200\n#GAP:0\n: 0 1 0 la\nE\n")
+2 -2
View File
@@ -1,7 +1,7 @@
import json
from pathlib import Path
from karaoke_maker.config import Config
from karaoke_maker.pipeline import SongInput, Stage, run_pipeline, song_slug
from songtrace.config import Config
from songtrace.pipeline import SongInput, Stage, run_pipeline, song_slug
def make_cfg(tmp_path):
return Config(whisper_url="http://x/v1", output_dir=tmp_path / "out",
+3 -3
View File
@@ -1,7 +1,7 @@
import numpy as np
from karaoke_maker.config import Config
from karaoke_maker.pipeline import Ctx, SongInput
from karaoke_maker.stages import pitch
from songtrace.config import Config
from songtrace.pipeline import Ctx, SongInput
from songtrace.stages import pitch
def test_pitch_stage_writes_npz(tmp_path, monkeypatch):
+3 -3
View File
@@ -3,9 +3,9 @@ from pathlib import Path
import pytest
from karaoke_maker.config import Config
from karaoke_maker.pipeline import STAGE_ORDER, PipelineError, SongInput
from karaoke_maker.rebuild import (FIELD_STAGE, changed_fields, plan,
from songtrace.config import Config
from songtrace.pipeline import STAGE_ORDER, PipelineError, SongInput
from songtrace.rebuild import (FIELD_STAGE, changed_fields, plan,
relocate_work, superseded_output)
+3 -3
View File
@@ -1,9 +1,9 @@
import numpy as np
import soundfile as sf
import torch
from karaoke_maker.config import Config
from karaoke_maker.pipeline import Ctx, SongInput
from karaoke_maker.stages import separate
from songtrace.config import Config
from songtrace.pipeline import Ctx, SongInput
from songtrace.stages import separate
def test_separate_writes_stems(tmp_path, monkeypatch):
+3 -3
View File
@@ -1,6 +1,6 @@
from karaoke_maker.lyrics import normalize_lyrics
from karaoke_maker.whisper_client import TranscriptSegment
from karaoke_maker.stages.transcribe import lyrics_similarity, vocal_sections
from songtrace.lyrics import normalize_lyrics
from songtrace.whisper_client import TranscriptSegment
from songtrace.stages.transcribe import lyrics_similarity, vocal_sections
def seg(s, e, t=""):
return TranscriptSegment(start=s, end=e, text=t)
+1 -1
View File
@@ -1,5 +1,5 @@
from pathlib import Path
from karaoke_maker.ultrastar import (
from songtrace.ultrastar import (
Note, LineBreak, parse_ultrastar, serialize_ultrastar,
get_header, format_bpm, parse_bpm,
)
+8 -8
View File
@@ -6,9 +6,9 @@ import time
import pytest
from fastapi.testclient import TestClient
from karaoke_maker.config import Config
from karaoke_maker.pipeline import STAGE_ORDER, PipelineError, PipelineResult, song_slug
from karaoke_maker.web import STAGE_LINE, create_app
from songtrace.config import Config
from songtrace.pipeline import STAGE_ORDER, PipelineError, PipelineResult, song_slug
from songtrace.web import STAGE_LINE, create_app
def make_client(tmp_path, runner):
@@ -190,7 +190,7 @@ def test_cover_upload_kept_after_success(tmp_path):
def test_cleanup_uploads_removes_an_inherited_upload_dir(tmp_path):
"""A rebuild's video may live under its parent's id, not its own."""
from karaoke_maker.web import _cleanup_uploads
from songtrace.web import _cleanup_uploads
cfg = Config(whisper_url="u", work_dir=tmp_path / "work",
output_dir=tmp_path / "out")
parent = cfg.work_dir / "uploads" / "parent"
@@ -201,7 +201,7 @@ def test_cleanup_uploads_removes_an_inherited_upload_dir(tmp_path):
def test_cleanup_uploads_ignores_paths_outside_the_upload_root(tmp_path):
from karaoke_maker.web import _cleanup_uploads
from songtrace.web import _cleanup_uploads
cfg = Config(whisper_url="u", work_dir=tmp_path / "work",
output_dir=tmp_path / "out")
outside = tmp_path / "somewhere" / "keep.mp4"
@@ -212,7 +212,7 @@ def test_cleanup_uploads_ignores_paths_outside_the_upload_root(tmp_path):
def test_cleanup_uploads_tolerates_a_missing_file(tmp_path):
from karaoke_maker.web import _cleanup_uploads
from songtrace.web import _cleanup_uploads
cfg = Config(whisper_url="u", work_dir=tmp_path / "work",
output_dir=tmp_path / "out")
_cleanup_uploads(cfg, "child", cfg.work_dir / "uploads" / "child" / "gone.mp4")
@@ -340,13 +340,13 @@ def test_fonts_are_served(tmp_path):
def test_font_licence_is_bundled():
from karaoke_maker.web import STATIC
from songtrace.web import STATIC
licence = (STATIC / "fonts" / "OFL.txt").read_text(encoding="utf-8", errors="replace")
assert "SIL OPEN FONT LICENSE" in licence.upper()
def _index_html():
from karaoke_maker.web import STATIC
from songtrace.web import STATIC
return (STATIC / "index.html").read_text(encoding="utf-8")
+2 -2
View File
@@ -1,7 +1,7 @@
import httpx
from pathlib import Path
from karaoke_maker.config import Config
from karaoke_maker.whisper_client import TranscriptSegment, transcribe
from songtrace.config import Config
from songtrace.whisper_client import TranscriptSegment, transcribe
def test_transcribe_parses_segments(tmp_path):
audio = tmp_path / "v.wav"