From 4471a4bee81e505f9343e9fbb24e9f77b40280cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:37:06 +0000 Subject: [PATCH] feat(workspace): name the selected part's first-pass take After a named part is selected, the ready rehearsal map names that part's first trusted simplification and tells the player to play that simpler take before adding the rest. Untrusted or missing takes still name the next action instead of hiding it. --- AGENTS.md | 2 +- ARCHITECTURE.md | 1 + CHANGELOG.md | 1 + CLAUDE.md | 2 +- .../workspace/Workspace.first-pass.test.tsx | 78 +++++++ .../src/features/workspace/Workspace.tsx | 23 ++ .../workspace/firstPassSimplification.test.ts | 212 ++++++++++++++++++ .../workspace/firstPassSimplification.ts | 148 ++++++++++++ apps/desktop/src/locales/en/common.json | 3 + apps/desktop/src/locales/ko/common.json | 3 + docs/design-system/component-contract.md | 1 + docs/doctoring/selected-part-first-pass.md | 63 ++++++ 12 files changed, 535 insertions(+), 2 deletions(-) create mode 100644 apps/desktop/src/features/workspace/Workspace.first-pass.test.tsx create mode 100644 apps/desktop/src/features/workspace/firstPassSimplification.test.ts create mode 100644 apps/desktop/src/features/workspace/firstPassSimplification.ts create mode 100644 docs/doctoring/selected-part-first-pass.md diff --git a/AGENTS.md b/AGENTS.md index b9a67ce17..b43b4bce4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # AGENTS.md ## Project overview -- BandScope is a local-first desktop app for rehearsal prep: a practical song view with likely harmony by section and by instrument or vocal role, form and groove cues, stems, playable ranges, simplification guidance, transposition or setup cues, part-overlap cues, visible confidence, and rehearsal priorities. +- BandScope is a local-first desktop app for rehearsal prep: a practical song view with likely harmony by section and by instrument or vocal role, form and groove cues, stems, playable ranges, simplification guidance, transposition or setup cues, part-overlap cues, visible confidence, and rehearsal priorities. After a named part is selected, the ready workspace names that part's first trusted simplification as the next first-pass take. - Authoritative delivery rules live in `ARCHITECTURE.md`, `docs/plans/`, and the root verification scripts. - Brand, tone, UX copy, and prioritization rules live in `docs/brand-story.md` and must be applied to PRDs, TRDs, UI copy, onboarding, empty states, and error messages. - App security rules live in `docs/security/app-security.md` and must be applied to file handling, URL intake, subprocesses, IPC, WebView usage, model loading, updates, logging, cache handling, and export behavior. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ca0df5ac4..ab017e911 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -83,6 +83,7 @@ Last updated: 2026-03-11 - section roadmap with entries, dropouts, pickups, stops, tags, and handoffs - groove and timing cues relevant to locking the band together - playable ranges and density or overlap warnings, with the ready workspace naming tonight's first span and the next instrument check + - the selected part's first trusted simplification as the next first-pass take - simplification, transposition, capo, tuning, or setup cues where applicable - role-specific rehearsal priorities and confidence flags - cue-sheet or chart-style exports that summarize the analysis in rehearsal-friendly form diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6f7e784..2bb4bc004 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- After a named part is selected, name that part's first trusted simplification and tell the player to play that simpler take before adding the rest. - Name tonight's first playable range on the ready rehearsal map and tell the player to check that span on their instrument before the section. - Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace. - 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함. diff --git a/CLAUDE.md b/CLAUDE.md index b5a34c1fa..153a47c3e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,7 @@ BandScope is a local-first desktop app for rehearsal prep: it turns a song into Three layers, decoupled through shared contracts: -- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). The ready workspace names tonight's first playable range and the next instrument check. `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri. +- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). The ready workspace names tonight's first playable range and the next instrument check. After a named part is selected, it also names that part's first trusted simplification as the next first-pass take. `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri. - `apps/desktop/src-tauri/src/main.rs` — the Rust orchestration boundary. Tauri commands (`start_analysis_job`, `get_analysis_job_status`, `select_local_audio_source`, `import_youtube_url`) validate untrusted input (project IDs, file paths, URLs) and spawn the Python engine as a subprocess. There is no loopback HTTP listener and no network path for local analysis. - `services/analysis-engine` — Python package `bandscope_analysis` (librosa/numpy). Entry point `cli.py` reads a JSON job request on stdin and prints a structured job-status JSON envelope on stdout (`--progress-jsonl` streams progress lines). `api.py` orchestrates the pipeline across the `separation`, `sections`, `roles`, `chords`, `ranges`, `temporal`, `transcription`, and `youtube` modules. diff --git a/apps/desktop/src/features/workspace/Workspace.first-pass.test.tsx b/apps/desktop/src/features/workspace/Workspace.first-pass.test.tsx new file mode 100644 index 000000000..8dee9a185 --- /dev/null +++ b/apps/desktop/src/features/workspace/Workspace.first-pass.test.tsx @@ -0,0 +1,78 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { createDemoRehearsalSong } from "@bandscope/shared-types"; +import { afterEach, describe, expect, it } from "vitest"; +import { Workspace } from "./Workspace"; + +const originalLanguage = navigator.language; + +function setNavigatorLanguage(language: string) { + Object.defineProperty(navigator, "language", { + configurable: true, + value: language + }); +} + +describe("Workspace selected-part first-pass take", () => { + afterEach(() => { + setNavigatorLanguage(originalLanguage); + }); + + it("hides the first-pass take until a named part is selected", () => { + setNavigatorLanguage("en-US"); + render(); + + expect(screen.queryByTestId("selected-part-first-pass")).toBeNull(); + }); + + it("names the selected bass part's simpler take as the first pass", () => { + setNavigatorLanguage("en-US"); + render(); + fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" })); + + const callout = screen.getByTestId("selected-part-first-pass"); + expect(callout).toHaveTextContent("Tonight's first-pass take"); + expect(callout).toHaveTextContent( + "First pass for Bass Guitar in verse: Stay on roots if the chorus entrance gets muddy. Play that simpler take before adding the rest." + ); + }); + + it("names the selected vocal part's simpler take as the first pass", () => { + setNavigatorLanguage("en-US"); + render(); + fireEvent.click(screen.getByRole("tab", { name: "Lead Vocal" })); + + expect(screen.getByTestId("selected-part-first-pass")).toHaveTextContent( + "First pass for Lead Vocal in verse: Keep the sustained note centered; skip the ad-lib on the first pass. Play that simpler take before adding the rest." + ); + }); + + it("keeps Korean copy particle-safe for a Latin role name", () => { + setNavigatorLanguage("ko-KR"); + render(); + fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" })); + + const callout = screen.getByTestId("selected-part-first-pass"); + expect(callout).toHaveTextContent("오늘 이 파트의 첫 간소화"); + expect(callout).toHaveTextContent("Bass Guitar 파트"); + expect(callout).not.toHaveTextContent("Bass Guitar으로"); + expect(callout).toHaveTextContent( + "verse에서 Bass Guitar 파트의 첫 패스: Stay on roots if the chorus entrance gets muddy. 나머지를 더하기 전에 그 간소화된 버전으로 연습하세요." + ); + }); + + it("tells the player to confirm a missing first-pass take instead of hiding the next action", () => { + setNavigatorLanguage("en-US"); + const song = createDemoRehearsalSong(); + song.sections[0]!.roles[0] = { + ...song.sections[0]!.roles[0]!, + simplification: "none" + }; + + render(); + fireEvent.click(screen.getByRole("tab", { name: "Bass Guitar" })); + + expect(screen.getByTestId("selected-part-first-pass")).toHaveTextContent( + "This part still needs a trusted first-pass take. Confirm the simpler version before the first run." + ); + }); +}); diff --git a/apps/desktop/src/features/workspace/Workspace.tsx b/apps/desktop/src/features/workspace/Workspace.tsx index d44e20777..515fcd8b8 100644 --- a/apps/desktop/src/features/workspace/Workspace.tsx +++ b/apps/desktop/src/features/workspace/Workspace.tsx @@ -5,6 +5,7 @@ import { SectionRoadmap } from "./SectionRoadmap"; import { GrooveMap } from "./GrooveMap"; import { PracticeProgress } from "./PracticeProgress"; import { fillRangeCopy, firstRangeSqueeze } from "./firstRangeSqueeze"; +import { fillFirstPassCopy, firstPassSimplification } from "./firstPassSimplification"; import { createTranslator, detectPreferredLocale } from "../../i18n"; import { generateCueSheetCsv, generateChartSummaryJson, generateMetadataHandoffJson, sanitizeFilename } from "../../lib/export"; import { Button } from "@/components/ui/button"; @@ -163,6 +164,18 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp } ) : t("workspaceFirstRangeMissing"); + const selectedFirstPass = useMemo( + () => (activeRole ? firstPassSimplification(song, activeRole) : null), + [activeRole, song] + ); + const selectedFirstPassCopy = + selectedFirstPass?.status === "ready" + ? fillFirstPassCopy(t("workspaceSelectedFirstPassReady"), { + roleName: selectedFirstPass.roleName, + sectionLabel: selectedFirstPass.sectionLabel, + value: selectedFirstPass.value + }) + : t("workspaceSelectedFirstPassUnavailable"); /** Handle the practice progress change internally by immutably updating the song state. */ const handlePracticeProgressChange = (newProgress: number) => { @@ -372,6 +385,16 @@ export function Workspace({ song, sourceBootstrap = null, onSongUpdate }: Worksp

Stem Player

{activeRoleDetails?.name ?? activeRole}

+
+

+ {t("workspaceSelectedFirstPassTitle")} +

+

{selectedFirstPassCopy}

+