From a19c28a36b6eec6f77506cb69d29081de7ec3b33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 09:13:40 +0000 Subject: [PATCH 01/10] feat(workspace): name tonight's first playable range on the map The ready rehearsal board showed chords, cues, and clashes but never named the first playable span, so players still guessed whether a part would fit before the next section. Security Notes: no new filesystem, URL, subprocess, IPC, or model authority. Copy is derived from already-validated rehearsal analysis fields already rendered in the workspace. --- AGENTS.md | 2 +- ARCHITECTURE.md | 2 +- CHANGELOG.md | 1 + CLAUDE.md | 2 +- .../workspace/SectionRoadmap.test.tsx | 17 ++++ .../src/features/workspace/SectionRoadmap.tsx | 13 +++ .../src/features/workspace/Workspace.test.tsx | 56 +++++++++++ .../src/features/workspace/Workspace.tsx | 22 +++++ .../workspace/firstRangeSqueeze.test.ts | 97 +++++++++++++++++++ .../features/workspace/firstRangeSqueeze.ts | 83 ++++++++++++++++ apps/desktop/src/locales/en/common.json | 8 +- apps/desktop/src/locales/ko/common.json | 8 +- 12 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/src/features/workspace/firstRangeSqueeze.test.ts create mode 100644 apps/desktop/src/features/workspace/firstRangeSqueeze.ts diff --git a/AGENTS.md b/AGENTS.md index fca448ce9..b9a67ce17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,7 +83,7 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working - Keep UI and analysis engine decoupled through shared contracts. - Prefer minimal, test-first changes for production code. - Prefer practical, friendly, rehearsal-first wording over academic or authority-heavy language. -- Do not reduce the product to a chord analyzer when form, timing, player coordination, simplification, and setup cues are the real rehearsal blockers. +- Do not reduce the product to a chord analyzer when form, timing, player coordination, playable ranges, simplification, and setup cues are the real rehearsal blockers. - Do not frame usability as a reason to accept weak analysis quality; BandScope should aim for both easy use and high accuracy. ## Safety diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3302a6fc3..ca0df5ac4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -82,7 +82,7 @@ Last updated: 2026-03-11 - likely harmony by section and by role - 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 + - playable ranges and density or overlap warnings, with the ready workspace naming tonight's first span and the next instrument check - 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 eea696893..21de664e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- 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 82c2c704a..b5a34c1fa 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). `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. `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/SectionRoadmap.test.tsx b/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx index 75a199246..2182a41d1 100644 --- a/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx +++ b/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx @@ -31,6 +31,23 @@ describe("SectionRoadmap", () => { expect(screen.getAllByText("큐").length).toBeGreaterThan(0); expect(screen.getAllByTitle("우선순위: high").length).toBeGreaterThan(0); expect(screen.getByText("사용자")).toBeTruthy(); + expect(screen.getAllByText("음역").length).toBeGreaterThan(0); + expect(screen.getByText("C#2 — E3")).toBeTruthy(); + expect(screen.getAllByText("verse 들어가기 전에 이 음역을 악기로 확인해 보세요.").length).toBeGreaterThan(0); + }); + + it("omits the range row when both notes are unnamed", () => { + setNavigatorLanguage("en-US"); + const song = createDemoRehearsalSong(); + song.sections[0]!.roles[0] = { + ...song.sections[0]!.roles[0]!, + range: { lowestNote: " ", highestNote: "none" } + }; + + render(); + + expect(screen.queryByText("Range")).toBeNull(); + expect(screen.queryByText(/Check this span on your instrument/i)).toBeNull(); }); it("uses localized copy for chord edit prompts and control labels", () => { diff --git a/apps/desktop/src/features/workspace/SectionRoadmap.tsx b/apps/desktop/src/features/workspace/SectionRoadmap.tsx index 6f27c2509..29974d024 100644 --- a/apps/desktop/src/features/workspace/SectionRoadmap.tsx +++ b/apps/desktop/src/features/workspace/SectionRoadmap.tsx @@ -2,6 +2,7 @@ import type { RehearsalSong, RehearsalRole } from "@bandscope/shared-types"; import { useId, useMemo } from "react"; import { createTranslator, detectPreferredLocale } from "../../i18n"; import { ConfidenceBadge } from "./ConfidenceBadge"; +import { fillRangeCopy, meaningfulRangeText } from "./firstRangeSqueeze"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; @@ -182,6 +183,18 @@ export function SectionRoadmap({ song, activeRole, onSongUpdate }: SectionRoadma {role.cue.value} + {meaningfulRangeText(role.range.lowestNote) && meaningfulRangeText(role.range.highestNote) ? ( +
+ {t("sectionRangeLabel")} + + {meaningfulRangeText(role.range.lowestNote)} — {meaningfulRangeText(role.range.highestNote)} + +

+ {fillRangeCopy(t("sectionRangeNextAction"), { sectionLabel: section.label })} +

+
+ ) : null} + {role.setupNote && (