Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
4e54219
feat(workspace): hear tonight's first lyric cue from the map
seonghobae Aug 18, 2026
193b0d7
fix(workspace): document lyric cue helper
seonghobae Aug 18, 2026
247d144
test(workspace): keep lyric cue placeholders literal
seonghobae Aug 18, 2026
7688cd7
fix(workspace): interpolate lyric cue copy once
seonghobae Aug 18, 2026
270565c
test(workspace): cover lyric cue replacement state
seonghobae Aug 18, 2026
af267b8
fix(workspace): reset heard state for changed lyric cue
seonghobae Aug 18, 2026
66919a2
docs(design): clarify lyric cue unavailable action contract
seonghobae Aug 18, 2026
84ec7fa
test(workspace): assert localized fresh lyric guidance
seonghobae Aug 18, 2026
7d9d7a3
test(player): require executable lyric cue action
seonghobae Aug 18, 2026
5228616
fix(player): require executable lyric cue action
seonghobae Aug 18, 2026
063b629
fix(player): delegate lyric cue playback
seonghobae Aug 18, 2026
156768c
test(workspace): reset returning lyric cue state
seonghobae Aug 18, 2026
744e554
fix(workspace): clear stale returning lyric cue
seonghobae Aug 18, 2026
41d9748
test(workspace): reject unsafe lyric cue metadata
seonghobae Aug 18, 2026
bd135db
fix(workspace): validate lyric cue candidates
seonghobae Aug 18, 2026
778a2ed
test(workspace): cover malformed lyric value
seonghobae Aug 18, 2026
bc588cd
test(workspace): distinguish lyric map navigation from playback
seonghobae Aug 18, 2026
c5637be
fix(workspace): label lyric map navigation honestly
seonghobae Aug 18, 2026
b01dac7
feat(i18n): distinguish lyric navigation from playback
seonghobae Aug 18, 2026
3783bc0
feat(i18n): localize lyric map navigation
seonghobae Aug 18, 2026
431a153
docs(workspace): distinguish lyric navigation from playback
seonghobae Aug 18, 2026
2247c58
docs(workspace): align lyric cue surface authority
seonghobae Aug 18, 2026
3c367b5
test(workspace): decouple lyric navigation from analysis ids
seonghobae Aug 18, 2026
5afe0bb
test(workspace): bind lyric scroll expectations to rendered positions
seonghobae Aug 18, 2026
5c75eec
fix(workspace): use renderer-owned lyric navigation targets
seonghobae Aug 18, 2026
167914e
test(workspace): target rendered lyric map positions directly
seonghobae Aug 18, 2026
d360928
fix(workspace): scroll lyric cues by rendered section position
seonghobae Aug 18, 2026
7bf55bc
fix(workspace): keep analysis ids out of DOM authority
seonghobae Aug 18, 2026
52ee90a
test(workspace): align lyric map contracts with current behavior
seonghobae Aug 18, 2026
bea2bd2
test(a11y): require reduced-motion lyric navigation
seonghobae Aug 18, 2026
5d0e5b2
fix(a11y): honor reduced motion for lyric navigation
seonghobae Aug 18, 2026
18d773e
docs(a11y): record reduced-motion lyric navigation contract
seonghobae Aug 18, 2026
ca849a2
docs(changelog): note reduced-motion lyric scroll
seonghobae Aug 18, 2026
9281b46
test: reject malformed lyric role ids
seonghobae Aug 18, 2026
a92bd8f
fix: reject malformed lyric role ids
seonghobae Aug 18, 2026
548cd1d
test(workspace): lock particle-safe Korean lyric-cue copy
seonghobae Aug 21, 2026
c56a589
fix(workspace): keep Korean lyric-cue role copy particle-safe
seonghobae Aug 21, 2026
8401731
fix(workspace): arm lyric cue after navigation lands and keep Korean …
seonghobae Aug 24, 2026
c17abdc
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae Aug 25, 2026
7bd780a
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +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.
- Name tonight's first lyric cue with the part, words, section, and start time so the singer's next action is obvious.
- 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.

Expand Down
1 change: 1 addition & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Last updated: 2026-03-11

- Product identity, UX tone, copy rules, and prioritization tie-breakers live in `docs/brand-story.md`.
- Future PRDs, TRDs, onboarding copy, empty states, error messages, and marketing copy should use that document as the single brand source of truth.
- Workspace and player copy for tonight's first lyric cue must name the part, words, section, and start time so the singer's next action is obvious.

## Security source

Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Name tonight's first lyric cue on the workspace and player with part, lyric, section, and start time; the workspace action arms the cue, opens the matching map section, and honors the user's reduced-motion preference for that navigation, while the player exposes a Hear action only when its owning playback surface supplies a seek callback.
- 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 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.
Expand Down Expand Up @@ -74,4 +75,5 @@

- `ChordsFeature` (코드 분석) 화면에서 각 파트(Role)의 `transpositionPlan`(이조/조옮김 계획)을 표시하는 기능을 추가했습니다.
- `RangesFeature` (음역대 분석) 화면에서 겹침 경고(Overlap warning) 외에 해당 파트의 채보(Transcription) 가능 노드 수를 요약하여 보여주는 기능을 추가했습니다.
- 신규 UI 요소에 대한 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
- 신규 UI 요소에 대한 100% 테스트 커버리지를 보장하는 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
- 신규 UI 요소에 대한 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
Comment on lines +78 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Duplicate changelog entry for new UI tests

The 0.1.4 changelog line about new UI unit tests is replaced by two lines: one with new '100% 테스트 커버리지' wording and one identical to the original (CHANGELOG.md). The second is a verbatim duplicate describing the same tests.

Suggested change
- 신규 UI 요소에 대한 100% 테스트 커버리지를 보장하는 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
- 신규 UI 요소에 대한 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
- 신규 UI 요소에 대한 100% 테스트 커버리지를 보장하는 단위 테스트를 추가했습니다 (`apps/desktop/src/features/chords/index.test.tsx`, `apps/desktop/src/features/ranges/index.test.tsx`).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). Workspace and player name tonight's first lyric cue with its part, lyric, section, and start time; the workspace action arms the cue and opens the mapped section, while the player renders the Hear action only when its owning playback surface supplies a seek callback. The ready workspace also 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.

Expand All @@ -72,4 +72,4 @@ Supporting packages:
- Documents under `docs/plans/` must include `Security Notes`; `scripts/checks/verify_security_notes.py` enforces this mechanically.
- Lockfiles (`package-lock.json`, `uv.lock`, `Cargo.lock`) are committed and must stay in sync; GitHub Actions are SHA-pinned. Adding a direct dependency requires the admission rationale defined in `AGENTS.md` and `docs/security/dependency-policy.md`.
- CI beyond quickcheck: `gate / ci / rust-check` (Tauri cargo check on macOS) and `build-baseline` Windows/macOS amd64+arm64 native builds are merge gates, alongside CodeQL, dependency-review, sbom, bandit, trivy, secret-scan, and security-audit workflows. Do not weaken or skip them.
- Version metadata lives in `VERSION`, the root `package.json`, and `CHANGELOG.md`; release flow is tag-driven (see `docs/operations/deploy-runbook.md`).
- Version metadata lives in `VERSION`, the root `package.json`, and `CHANGELOG.md`; release flow is tag-driven (see `docs/operations/deploy-runbook.md`).
44 changes: 44 additions & 0 deletions apps/desktop/src/features/player/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { createDemoRehearsalSong } from "@bandscope/shared-types";
import { describe, expect, it, vi } from "vitest";
import { PlayerFeature } from "./index";

describe("PlayerFeature", () => {
it("asks the room to analyze first when no song is loaded", () => {
render(<PlayerFeature title="Player" />);
expect(
screen.getByText("Analyze tonight's song first, then hear the first lyric cue from this player.")
).toBeTruthy();
});

it("keeps the lyric hear action unavailable without a player playback callback", () => {
render(<PlayerFeature title="Player" song={createDemoRehearsalSong()} />);

expect(
screen.queryByRole("button", {
name: "Hear Lead Vocal enter on “city lights” in the verse at 0:10"
})
).toBeNull();
expect(screen.getByText("Lead Vocal enters the verse on “city lights” at 0:10.")).toBeTruthy();
});

it("delegates the lyric hear action to the owning player callback", () => {
const onPlayFromSeconds = vi.fn();
render(
<PlayerFeature
title="Player"
song={createDemoRehearsalSong()}
onPlayFromSeconds={onPlayFromSeconds}
/>
);

fireEvent.click(
screen.getByRole("button", {
name: "Hear Lead Vocal enter on “city lights” in the verse at 0:10"
})
);

expect(onPlayFromSeconds).toHaveBeenCalledTimes(1);
expect(onPlayFromSeconds).toHaveBeenCalledWith(10);
});
});
24 changes: 19 additions & 5 deletions apps/desktop/src/features/player/index.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,41 @@
import type { RehearsalSong } from "@bandscope/shared-types";
import { FirstLyricCueCallout } from "../workspace/FirstLyricCueCallout";
import { createTranslator, detectPreferredLocale } from "../../i18n";

/** Documented. */
export function PlayerFeature(props: { title: string; song?: RehearsalSong | null }) {
const { title, song } = props;
type PlayerFeatureProps = {
title: string;
song?: RehearsalSong | null;
onPlayFromSeconds?: (startSeconds: number) => void;
};

/** Player surface that names tonight's first lyric cue and delegates playback to the owning player. */
export function PlayerFeature({ title, song, onPlayFromSeconds }: PlayerFeatureProps) {
const t = createTranslator(detectPreferredLocale());

if (!song) {
return (
<section style={{ padding: "24px" }}>
<h2>{title}</h2>
<p style={{ color: "#999" }}>No song loaded. Start an analysis to use the player.</p>
<p style={{ color: "#999" }}>{t("firstLyricCueNeedsSong")}</p>
</section>
);
}

return (
<section style={{ padding: "24px" }}>
<h2>{title}</h2>
<FirstLyricCueCallout
song={song}
actionMode="callback-only"
onHearLyricCue={onPlayFromSeconds}
/>
<div
style={{
padding: "16px",
backgroundColor: "#fafafa",
borderRadius: "8px",
border: "1px solid #e8e8e8",
marginTop: "16px",
}}
>
<div style={{ marginBottom: "12px" }}>
Expand Down Expand Up @@ -53,4 +67,4 @@ export function PlayerFeature(props: { title: string; song?: RehearsalSong | nul
</div>
</section>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { createDemoRehearsalSong } from "@bandscope/shared-types";
import { afterEach, describe, expect, it, vi } from "vitest";
import { FirstLyricCueCallout } from "./FirstLyricCueCallout";

afterEach(() => {
vi.unstubAllGlobals();
document.querySelector('[data-testid="song-structure-grid"]')?.remove();
});

/** Mount the renderer-owned song-structure target used by workspace navigation. */
function appendSongStructureTarget() {
const grid = document.createElement("div");
grid.dataset.testid = "song-structure-grid";
const target = document.createElement("div");
const scrollIntoView = vi.fn();
Object.defineProperty(target, "scrollIntoView", {
configurable: true,
value: scrollIntoView
});
grid.appendChild(target);
document.body.appendChild(grid);
return scrollIntoView;
}

describe("FirstLyricCueCallout reduced-motion navigation", () => {
it("avoids smooth scrolling when the user requests reduced motion", () => {
const scrollIntoView = appendSongStructureTarget();
vi.stubGlobal(
"matchMedia",
vi.fn((query: string) => ({
matches: query === "(prefers-reduced-motion: reduce)",
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn()
}))
);

render(<FirstLyricCueCallout song={createDemoRehearsalSong()} />);
fireEvent.click(
screen.getByRole("button", {
name: "Open Lead Vocal lyric cue “city lights” in the verse at 0:10"
})
);

expect(scrollIntoView).toHaveBeenCalledWith({
block: "nearest",
behavior: "auto"
});
});
});
Loading
Loading