From 958634a0433db465f649be969a5ab302d1c3195a Mon Sep 17 00:00:00 2001
From: Thales <>
Date: Fri, 26 Jun 2026 09:47:43 +0100
Subject: [PATCH 1/3] chore: drop "karaoke" wording from the MP4 export
The video export is just an MP4 export, not specifically a karaoke
feature. Replace all "karaoke" references in UI strings, the download
filename, comments, docstrings, and docs with neutral MP4/video wording.
No behavior change.
- UI: MP4 "Export Mix" subtitle -> "Export mix with the original video".
- Download filename:
_karaoke.mp4 -> _video.mp4 (frontend
download attr and backend Content-Disposition).
- Comments / docstrings / README updated; no renamed identifiers
(downloadCurrentVideo, /video.mp4, has_video were already neutral).
---
README.md | 2 +-
app/api/stems.py | 6 +++---
app/core/config.py | 2 +-
app/core/models.py | 2 +-
app/pipeline/download.py | 8 ++++----
app/pipeline/runner.py | 4 ++--
static/js/main.js | 6 +++---
static/js/player.js | 4 ++--
tests/test_stems_api.py | 2 +-
9 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index 657935db..fd831d14 100644
--- a/README.md
+++ b/README.md
@@ -309,7 +309,7 @@ Stems land in `./jobs/` on the host. Demucs weights are cached in a named volume
| PATCH | `/api/jobs/{id}/sections` | Save waveform section markers for a job |
| GET | `/api/jobs/{id}/stems/{name}.wav` | Stream a single stem WAV file |
| GET | `/api/jobs/{id}/stems/{name}.mp3` | Transcode and stream a stem as MP3 |
-| GET | `/api/jobs/{id}/video.mp4` | Mux the current mix with the source video (MP4 upload or YouTube) into a karaoke MP4 |
+| GET | `/api/jobs/{id}/video.mp4` | Mux the current mix with the source video (MP4 upload or YouTube) into an MP4 |
| DELETE | `/api/jobs/{id}` | Remove job dir from disk (terminal jobs only) |
---
diff --git a/app/api/stems.py b/app/api/stems.py
index c45d3f73..5447ddb3 100644
--- a/app/api/stems.py
+++ b/app/api/stems.py
@@ -61,7 +61,7 @@ def _validate_stem_path(job_id: str, name: str):
def _parse_lane_gains(stems: str, gains: str) -> tuple[list[str], list[float]]:
"""Parse and validate parallel comma-separated lane names and linear gains.
- Shared by the audio mixdown and the karaoke-video mux. Raises HTTPException
+ Shared by the audio mixdown and the MP4 video mux. Raises HTTPException
on malformed input, unknown lanes, or out-of-range gains."""
names = [s for s in stems.split(",") if s]
raw_gains = [g for g in gains.split(",") if g]
@@ -270,7 +270,7 @@ async def get_video_mixdown(
gains: str = Query(..., description="Comma-separated linear gains, parallel to stems"),
) -> StreamingResponse:
"""Mux a fresh audio mixdown of the current mixer state with the job's preserved
- video into a karaoke MP4 (issue #219). Mirrors get_mixdown's audio graph (encoded
+ video into an MP4 (issue #219). Mirrors get_mixdown's audio graph (encoded
as AAC) and stream-copies video.mp4 -- the silent video kept from an .mp4 upload
or the real video stream downloaded for a YouTube job. 404 when the job has no
video (SoundCloud / plain audio uploads).
@@ -328,7 +328,7 @@ async def get_video_mixdown(
"pipe:1",
]
- filename = f"{_safe_title(job.title)}_karaoke.mp4"
+ filename = f"{_safe_title(job.title)}_video.mp4"
return StreamingResponse(
_stream_ffmpeg(cmd),
media_type="video/mp4",
diff --git a/app/core/config.py b/app/core/config.py
index a685ec4e..7045309f 100644
--- a/app/core/config.py
+++ b/app/core/config.py
@@ -74,7 +74,7 @@ def _detect_device() -> str:
TIMEOUT_FFMPEG = _env_int("STEMDECK_TIMEOUT_FFMPEG", 300)
TIMEOUT_ANALYZE = _env_int("STEMDECK_TIMEOUT_ANALYZE", 120)
TIMEOUT_DEMUCS_STALL = _env_int("STEMDECK_TIMEOUT_DEMUCS_STALL", 1800)
-# Max height for the karaoke-MP4 video stream pulled from YouTube (issue #219).
+# Max height for the MP4 video stream pulled from YouTube (issue #219).
# Capped to keep downloads reasonable; 1080p of a full song is large.
VIDEO_MAX_HEIGHT = max(144, _env_int("STEMDECK_VIDEO_MAX_HEIGHT", 720))
diff --git a/app/core/models.py b/app/core/models.py
index b2a76fe3..26e4dc26 100644
--- a/app/core/models.py
+++ b/app/core/models.py
@@ -53,7 +53,7 @@ class Job:
mix_url: str | None = None # populated when a strict subset was selected
source_url: str | None = None # original URL or "local:" for file uploads
# True when a silent video track (video.mp4) was preserved from an .mp4
- # upload, enabling the "Export Mix (with video)" karaoke export.
+ # upload, enabling the "Export Mix (with video)" MP4 export.
has_video: bool = False
error: str | None = None
# Set by POST /api/jobs/{id}/cancel; consumed by pipeline stages.
diff --git a/app/pipeline/download.py b/app/pipeline/download.py
index 4224a056..4dac6f30 100644
--- a/app/pipeline/download.py
+++ b/app/pipeline/download.py
@@ -150,7 +150,7 @@ def normalize_youtube_url(url: str) -> str:
def _download_video_track(job: Job, url: str, job_dir: Path) -> None:
"""Best-effort: download a video-only H.264/MP4 stream to video.mp4 for the
- karaoke-MP4 export (issue #219). The audio source is downloaded separately as
+ MP4 export (issue #219). The audio source is downloaded separately as
usual; this is a second, additive fetch so the audio pipeline is untouched.
Video-only MP4 needs no ffmpeg merge, so this can't break an audio-only job:
@@ -159,7 +159,7 @@ def _download_video_track(job: Job, url: str, job_dir: Path) -> None:
JobCancelled, which the runner treats like any other cancellation.
Capped at VIDEO_MAX_HEIGHT to keep downloads reasonable -- a full song at
- 1080p is large, and karaoke playback doesn't need it."""
+ 1080p is large, and the MP4 export doesn't need it."""
def vhook(d: dict) -> None:
if job.cancel_requested:
@@ -240,7 +240,7 @@ def hook(d: dict) -> None:
_set(job, progress=1.0, stage="Download complete")
# YouTube jobs additionally fetch the real video stream (below) for the
- # karaoke-MP4 export (issue #219). SoundCloud is audio-only and excluded.
+ # MP4 export (issue #219). SoundCloud is audio-only and excluded.
is_youtube = url.startswith("https://www.youtube.com/")
# No postprocessors -- Demucs reads the raw audio container (webm/m4a/opus/...)
@@ -295,7 +295,7 @@ def hook(d: dict) -> None:
deduped = [t for t in raw_tags if not (t in seen or seen.add(t))] # type: ignore[func-returns-value]
_set(job, tags=deduped[:8] or None)
- # Best-effort: fetch the real video stream for the karaoke-MP4 export.
+ # Best-effort: fetch the real video stream for the MP4 export.
# Non-fatal -- on any failure the job proceeds audio-only.
if is_youtube:
_download_video_track(job, url, job_dir)
diff --git a/app/pipeline/runner.py b/app/pipeline/runner.py
index 240a2249..032eee77 100644
--- a/app/pipeline/runner.py
+++ b/app/pipeline/runner.py
@@ -45,7 +45,7 @@ def _check_cancel(job: Job) -> None:
def _extract_video_track(job: Job, source: Path, job_dir: Path) -> None:
"""For an .mp4 upload, preserve a silent video-only track at
video.mp4 so the studio can later mux it with a custom stem mix
- into a karaoke video (issue #219). Stream-copies the video (no
+ into an MP4 (issue #219). Stream-copies the video (no
re-encode) -- fast and lossless.
Best-effort: an .mp4 with no video stream (audio-only container)
@@ -83,7 +83,7 @@ def _prepare_local_source(job: Job, source: Path, job_dir: Path) -> Path:
would otherwise process silently and output as silence.
For .mp4 uploads, first preserves a silent video.mp4 for later
- karaoke-video export. Deletes the original source file after a
+ MP4 export. Deletes the original source file after a
successful transcode."""
from app.core.config import ffmpeg_executable
diff --git a/static/js/main.js b/static/js/main.js
index bafa4fd5..9c1e3edc 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -176,12 +176,12 @@ function wireFooterControls() {
// MP4 exports the mix muxed with the source video. Stems and region have no
// video equivalent, so they're hidden (via .fmt-mp4) while MP4 is selected,
- // leaving just "Export Mix" relabelled for karaoke.
+ // leaving just "Export Mix".
function applyFormatState() {
const video = format === "mp4";
exportPanel?.classList.toggle("fmt-mp4", video);
if (mixDescEl) {
- mixDescEl.textContent = video ? "Export mix with video for karaoke" : "Export the mixed audio";
+ mixDescEl.textContent = video ? "Export mix with the original video" : "Export the mixed audio";
}
if (!video) updateLoopRegionVisual(); // restores the region item's disabled state
}
@@ -211,7 +211,7 @@ function wireFooterControls() {
panelOpen() ? closePanel() : openPanel();
});
- // Export Mix: MP4 produces the karaoke video; any other format an audio mix.
+ // Export Mix: MP4 produces the video; any other format an audio mix.
itemMix?.addEventListener("click", (e) => {
e.stopPropagation();
if (busy) return;
diff --git a/static/js/player.js b/static/js/player.js
index 2af4e567..9708e9b6 100644
--- a/static/js/player.js
+++ b/static/js/player.js
@@ -1335,7 +1335,7 @@ export function downloadCurrentMix(ext = "wav") {
return true;
}
-// Karaoke video: the preserved source video muxed with the current audio mix.
+// MP4 export: the preserved source video muxed with the current audio mix.
// Only meaningful for mp4-sourced jobs (currentJobHasVideo()); returns false when
// there's no video track or every lane is muted.
export function downloadCurrentVideo() {
@@ -1351,7 +1351,7 @@ export function downloadCurrentVideo() {
.replace(/_{2,}/g, "_")
.slice(0, 80)
.replace(/^_+|_+$/g, "");
- const name = safe ? `${safe}_karaoke.mp4` : "karaoke.mp4";
+ const name = safe ? `${safe}_video.mp4` : "video.mp4";
_triggerDownload(`/api/jobs/${currentJobId}/video.mp4?${q}`, name);
return true;
}
diff --git a/tests/test_stems_api.py b/tests/test_stems_api.py
index ae68c746..fca18104 100644
--- a/tests/test_stems_api.py
+++ b/tests/test_stems_api.py
@@ -386,7 +386,7 @@ def test_all_stems_zip_flac(client, tmp_path):
assert zf.read("vocals.flac")[:4] == b"fLaC"
-# --- karaoke video mux endpoint (#219) ---
+# --- MP4 video mux endpoint (#219) ---
def _make_video_file(tmp_path, job_id: str) -> None:
From 51ef53dae4eb66da3178f3f1974e698c27408c74 Mon Sep 17 00:00:00 2001
From: Thales <>
Date: Fri, 26 Jun 2026 10:17:13 +0100
Subject: [PATCH 2/3] chore: rename "Supporters" UI label to "We Recommend"
Match the README "We Recommend" section. "Supporters" implied a
sponsorship relationship the project explicitly does not have (no money
or funding accepted); these are editorial recommendations of makers and
artists. Updates the rail button (title/aria-label/chip) and the dialog
heading. Internal ids stay friendsBtn/friendsTitle.
---
static/index.html | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/static/index.html b/static/index.html
index ddacfbae..03a2db34 100644
--- a/static/index.html
+++ b/static/index.html
@@ -175,12 +175,12 @@
Settings
-