v0.12.0:1 - hotfix: WhisperX install fails on first scp because ~ doesn't
expand inside shlex.quote() Symptom: "Failed to ship Dockerfile — bash: line 1: ~/whisperx-build/ Dockerfile: No such file or directory" Same bug pattern as v0.8.1:1 (disk probe). shlex.quote() wraps in single quotes, and the remote shell doesn't do tilde expansion inside single quotes — so it tries to write to a literal directory named "~". Fix: use $HOME in double-quoted shell context, which the remote shell expands correctly. The file names (Dockerfile, requirements.txt, etc.) are hardcoded so they're safe to embed unquoted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -163,33 +163,45 @@ class WhisperXInstaller:
|
|||||||
s = self.settings
|
s = self.settings
|
||||||
host = s.whisperx_host
|
host = s.whisperx_host
|
||||||
user = s.whisperx_user
|
user = s.whisperx_user
|
||||||
build_dir = "~/whisperx-build"
|
# NOTE: `~` does not expand inside shlex.quote() single-quotes (bit us
|
||||||
|
# in v0.12.0:0). Use a $HOME-relative path that the REMOTE shell
|
||||||
|
# expands; all path components are hardcoded so injection is moot.
|
||||||
|
build_dir_remote = "\"$HOME\"/whisperx-build"
|
||||||
|
build_dir_display = "~/whisperx-build"
|
||||||
|
|
||||||
# ── Phase 1: stage build context on Spark 2 ──
|
# ── Phase 1: stage build context on Spark 2 ──
|
||||||
job.state = "sending"
|
job.state = "sending"
|
||||||
job.phase = "Sending build context to Spark 2…"
|
job.phase = "Sending build context to Spark 2…"
|
||||||
job.append(f"$ ssh {user}@{host} 'mkdir -p {build_dir}/app'")
|
job.append(f"$ ssh {user}@{host} 'mkdir -p {build_dir_display}/app'")
|
||||||
rc, out, err = await ssh_run(host, user, f"mkdir -p {build_dir}/app && rm -f {build_dir}/Dockerfile {build_dir}/requirements.txt {build_dir}/README.md {build_dir}/app/main.py", s, timeout=10)
|
rc, out, err = await ssh_run(
|
||||||
|
host, user,
|
||||||
|
f"mkdir -p {build_dir_remote}/app && "
|
||||||
|
f"rm -f {build_dir_remote}/Dockerfile {build_dir_remote}/requirements.txt "
|
||||||
|
f"{build_dir_remote}/README.md {build_dir_remote}/app/main.py",
|
||||||
|
s, timeout=10,
|
||||||
|
)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
job.append(f"[mkdir failed] {err.strip()}")
|
job.append(f"[mkdir failed] {err.strip()}")
|
||||||
raise RuntimeError("failed to create build directory")
|
raise RuntimeError("failed to create build directory")
|
||||||
for local_name, remote_rel in BUILD_FILES.items():
|
for local_name, remote_rel in BUILD_FILES.items():
|
||||||
local_path = BUILD_CONTEXT_DIR / local_name
|
local_path = BUILD_CONTEXT_DIR / local_name
|
||||||
body = local_path.read_bytes()
|
body = local_path.read_bytes()
|
||||||
remote_path = f"{build_dir}/{remote_rel}"
|
remote_path_for_shell = f"{build_dir_remote}/{remote_rel}"
|
||||||
cmd = f"cat > {shlex.quote(remote_path)}"
|
# remote_rel is hardcoded ("Dockerfile" / "app/main.py" etc.) — safe
|
||||||
|
# to embed unquoted inside the double-quoted $HOME path.
|
||||||
|
cmd = f"cat > {remote_path_for_shell}"
|
||||||
ok, out, err = await self._ssh_pipe(host, user, cmd, body, timeout=30)
|
ok, out, err = await self._ssh_pipe(host, user, cmd, body, timeout=30)
|
||||||
if not ok:
|
if not ok:
|
||||||
job.append(f"[scp {local_name} failed] {err.strip()[:200]}")
|
job.append(f"[scp {local_name} failed] {err.strip()[:200]}")
|
||||||
raise RuntimeError(f"failed to ship {local_name}")
|
raise RuntimeError(f"failed to ship {local_name}")
|
||||||
job.append(f" → {remote_path} ({len(body)} bytes)")
|
job.append(f" → {build_dir_display}/{remote_rel} ({len(body)} bytes)")
|
||||||
|
|
||||||
# ── Phase 2: docker build ──
|
# ── Phase 2: docker build ──
|
||||||
job.state = "building"
|
job.state = "building"
|
||||||
job.phase = "Building Docker image on Spark 2 (this is the slow part — 5–15 min if base layers aren't cached)…"
|
job.phase = "Building Docker image on Spark 2 (this is the slow part — 5–15 min if base layers aren't cached)…"
|
||||||
build_cmd = (
|
build_cmd = (
|
||||||
f"set -e; "
|
f"set -e; "
|
||||||
f"cd {build_dir}; "
|
f"cd {build_dir_remote}; "
|
||||||
f"echo '=== docker build -t {s.whisperx_container}:latest . ==='; "
|
f"echo '=== docker build -t {s.whisperx_container}:latest . ==='; "
|
||||||
f"docker build -t {s.whisperx_container}:latest ."
|
f"docker build -t {s.whisperx_container}:latest ."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { VersionInfo, IMPOSSIBLE } from '@start9labs/start-sdk'
|
import { VersionInfo, IMPOSSIBLE } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
export const v0_1_0 = VersionInfo.of({
|
export const v0_1_0 = VersionInfo.of({
|
||||||
version: '0.12.0:0',
|
version: '0.12.0:1',
|
||||||
releaseNotes: {
|
releaseNotes: {
|
||||||
en_US:
|
en_US:
|
||||||
'v0.12.0 — WhisperX as a one-click dashboard install. The Audio / Speech tab now shows an "Add WhisperX" banner the first time you open it (when WhisperX isn\'t installed). Clicking it ships the build context to Spark 2 over SSH, runs docker build (~10–15 min first time), runs docker run with a 40 GB memory cap (so a long-audio pathological case gets OOM-killed cleanly instead of swap-thrashing the whole Spark — what bit us with Sortformer on a 90-min file), and polls /health until both Whisper + pyannote 3.1 report loaded. Progress streams live in a build-log dialog with phase + elapsed timer. Once installed, WhisperX auto-appears as a managed service alongside Parakeet and Magpie (Start/Restart/Stop, deep-check, auto-restart on wedge — same lifecycle as the others). The /api/audio/transcribe-with-speakers endpoint now prefers WhisperX when it\'s healthy and falls back to the legacy Parakeet + Sortformer path otherwise — clean cutover, no client-side changes, easy rollback. New endpoints: GET /api/whisperx/status, POST /api/whisperx/install, GET /api/whisperx/install/{job_id}, GET /api/whisperx/install/{job_id}/stream (SSE).',
|
'v0.12.0:1 — hotfix: 0.12.0:0\'s install action used shlex.quote() on the remote build path, which wraps `~/whisperx-build/...` in single quotes — the remote shell then doesn\'t expand the tilde and treats it as a literal directory named `~`. Result: "bash: line 1: ~/whisperx-build/Dockerfile: No such file or directory" on the very first file copy. Same bug pattern we hit before with $HOME in the disk probe. Rewrote to embed $HOME in double-quoted remote shell strings; hardcoded file names (Dockerfile, requirements.txt, README.md, app/main.py) embed unquoted inside that scope. All other 0.12.0 behavior is unchanged.',
|
||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
up: async ({ effects }) => {},
|
up: async ({ effects }) => {},
|
||||||
|
|||||||
Reference in New Issue
Block a user