From cacbe2e20294d65052a899df144777e92b1424ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:56:04 +0000 Subject: [PATCH 1/4] Initial plan From e405801638c8dbf6e6cca68cbd316bdecd66eeb7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:10:32 +0000 Subject: [PATCH 2/4] chore: outline ML orchestration implementation plan --- package-lock.json | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index fcf8dcd5..7771fcf4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -947,9 +947,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -967,9 +964,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -987,9 +981,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1007,9 +998,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1027,9 +1015,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1047,9 +1032,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ From 89fd635d610b16ed31ef0e4dbe6297701e992fa3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:21:42 +0000 Subject: [PATCH 3/4] Fix failing test, add progressMessage fallback test, and add Python RuntimeError graceful-degradation test --- apps/desktop/src-tauri/src/main.rs | 2 +- apps/desktop/src/App.test.tsx | 25 ++++++++++++- apps/desktop/src/App.tsx | 2 +- .../features/workspace/WorkspaceStates.tsx | 23 ++++++++++-- .../src/bandscope_analysis/api.py | 2 +- services/analysis-engine/tests/test_api.py | 36 +++++++++++++++++++ 6 files changed, 84 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 9b8d75fb..0a8e134c 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -30,7 +30,7 @@ struct AppStateInner { } const MAX_IN_FLIGHT_JOBS: usize = 2; -const ANALYSIS_PROCESS_TIMEOUT: Duration = Duration::from_secs(30); +const ANALYSIS_PROCESS_TIMEOUT: Duration = Duration::from_secs(600); const ANALYSIS_WAIT_POLL: Duration = Duration::from_millis(50); const AUDIO_EXTENSIONS: [&str; 4] = ["wav", "mp3", "flac", "m4a"]; const MISSING_ANALYSIS_PYTHON: &str = "__bandscope_missing_analysis_python__"; diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index fa924ece..80014535 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -359,7 +359,7 @@ describe("App", () => { fireEvent.click(screen.getByRole("button", { name: /start analysis/i })); await waitFor(() => { - expect(screen.getByText(/queued for analysis/i)).toBeTruthy(); + expect(screen.getAllByText(/queued for analysis/i).length).toBeGreaterThan(0); }); expect(screen.getAllByRole("status").some((status) => /queued for analysis/i.test(status.textContent ?? ""))).toBe(true); await waitFor(() => { @@ -407,6 +407,29 @@ describe("App", () => { ); }); + it("falls back to a state-derived message in the status badge when progressLabel is absent", async () => { + tauriInvoke + .mockResolvedValueOnce(bootstrapResponse()) + .mockResolvedValueOnce(jobStatusResponse({ + jobId: "job-fallback-1", + state: "running", + progressLabel: undefined + })); + + render(); + + fireEvent.click(screen.getByRole("button", { name: /choose local audio/i })); + await waitFor(() => expect(screen.getByText(/late-night-set\.wav/i)).toBeTruthy()); + + fireEvent.click(screen.getByRole("button", { name: /start analysis/i })); + + await waitFor(() => { + expect( + screen.getAllByRole("status").some((el) => /running analysis/i.test(el.textContent ?? "")) + ).toBe(true); + }); + }); + it("keeps handoff metadata tied to the source that produced the current result", async () => { const originalCreateObjectUrl = URL.createObjectURL; const originalRevokeObjectUrl = URL.revokeObjectURL; diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 9486ae12..b4186353 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -361,7 +361,7 @@ export function App() { return ; } if (analysisInFlight || isStarting) { - return ; + return ; } if (jobResult) { return ; diff --git a/apps/desktop/src/features/workspace/WorkspaceStates.tsx b/apps/desktop/src/features/workspace/WorkspaceStates.tsx index 8f9aba1b..77ff9cc9 100644 --- a/apps/desktop/src/features/workspace/WorkspaceStates.tsx +++ b/apps/desktop/src/features/workspace/WorkspaceStates.tsx @@ -1,5 +1,6 @@ import { createTranslator, detectPreferredLocale } from "../../i18n"; import { Card, CardContent } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; import { Loader2, Music, AlertCircle } from "lucide-react"; /** Documented. */ @@ -19,7 +20,13 @@ export function EmptyState() { } /** Documented. */ -export function LoadingState() { +export function LoadingState({ + progressLabel, + progressPercent +}: { + progressLabel?: string; + progressPercent?: number; +}) { const t = createTranslator(detectPreferredLocale()); return ( ); diff --git a/services/analysis-engine/src/bandscope_analysis/api.py b/services/analysis-engine/src/bandscope_analysis/api.py index c08ca99f..63f6bfad 100644 --- a/services/analysis-engine/src/bandscope_analysis/api.py +++ b/services/analysis-engine/src/bandscope_analysis/api.py @@ -523,7 +523,7 @@ def run_analysis_job_updates( try: audio_features = _build_local_audio_features(request) - except (FileNotFoundError, ValueError) as error: + except Exception as error: updates.append( _build_job_status( job_id=job_id, diff --git a/services/analysis-engine/tests/test_api.py b/services/analysis-engine/tests/test_api.py index bb155a6e..526554d4 100644 --- a/services/analysis-engine/tests/test_api.py +++ b/services/analysis-engine/tests/test_api.py @@ -480,6 +480,42 @@ def test_run_analysis_job_updates_fail_safely_when_local_separation_fails() -> N } +def test_run_analysis_job_updates_fail_safely_on_unexpected_exception() -> None: + """Ensure unexpected runtime errors (not just ValueError) return a typed failure envelope.""" + with patch("bandscope_analysis.api.AudioStemSeparator") as separator_class: + separator_class.return_value.separate.side_effect = RuntimeError( + "Unexpected GPU out-of-memory error" + ) + + updates = list( + run_analysis_job_updates( + "job-unexpected-err", + { + "sourceKind": "local_audio", + "projectId": "project-1", + "sourceLabel": "late-night-set.wav", + "roleFocus": ["bass-guitar"], + "localSource": { + "sourcePath": "/Users/test/Music/late-night-set.wav", + "fileName": "late-night-set.wav", + "extension": "wav", + "fileSizeBytes": 1024000, + }, + }, + "2026-03-12T00:00:00Z", + ) + ) + + assert [(update["state"], update.get("progressStage")) for update in updates] == [ + ("running", "decode"), + ("running", "separate"), + ("failed", "separate"), + ] + assert updates[-1]["progressPercent"] == 45 + assert updates[-1]["error"]["code"] == "engine_unavailable" + assert "Unexpected GPU out-of-memory error" in updates[-1]["error"]["message"] + + def test_cached_analysis_helpers_treat_invalid_cache_as_miss(tmp_path) -> None: """Ensure malformed cache files degrade to cache misses without failing analysis.""" cache_path = tmp_path / "analysis-cache.json" From 67e34a76e1509e3e3f5d413b0916c4b2aaac73dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:26:12 +0000 Subject: [PATCH 4/4] Add logger.exception to except Exception handler in run_analysis_job_updates --- services/analysis-engine/src/bandscope_analysis/api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/analysis-engine/src/bandscope_analysis/api.py b/services/analysis-engine/src/bandscope_analysis/api.py index 63f6bfad..28b1cc78 100644 --- a/services/analysis-engine/src/bandscope_analysis/api.py +++ b/services/analysis-engine/src/bandscope_analysis/api.py @@ -4,6 +4,7 @@ import hashlib import json +import logging from pathlib import Path from typing import Any, Literal, NotRequired, TypedDict, cast @@ -12,6 +13,8 @@ from bandscope_analysis.sections import extract_sections from bandscope_analysis.separation import AudioStemSeparator +logger = logging.getLogger(__name__) + MAX_SECTION_TIME_SECONDS = 4_294_967_295 ANALYSIS_CACHE_SCHEMA_VERSION = 1 @@ -524,6 +527,7 @@ def run_analysis_job_updates( try: audio_features = _build_local_audio_features(request) except Exception as error: + logger.exception("Stem separation failed for job %s: %s", job_id, error) updates.append( _build_job_status( job_id=job_id,