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}

+