Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working
- `docs/operations/deploy-runbook.md`
- `docs/brand-story.md`
- `docs/security/app-security.md`
- `docs/doctoring/audio-resource-policy.md`
- `docs/security/dependency-policy.md`
- `docs/security/cross-platform-build-policy.md`
- `docs/workflow/github-bootstrap-execution-policy.md`
Expand All @@ -90,6 +91,7 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working
- Do not add network-dependent runtime paths for local analysis.
- Treat YouTube import as policy-constrained and fallback-friendly.
- Treat files, URLs, metadata, model artifacts, and project files as untrusted input.
- Apply the canonical audio resource policy in `bandscope_analysis.audio_resource_policy` (15 minutes / 100 MiB / mono-stereo / 44.1 kHz target) before decode or feature DSP. Do not invent a five-minute cap or a silent 120-second transcription window. Rejection copy must name the next rehearsal action and stay payload-free.
- Do not add generic exec/read/write APIs.
- Use `shell=False`-style subprocess invocation with argument arrays only.
- Keep local backend access on allowlisted IPC or `127.0.0.1` only, with strict schema validation.
Expand Down
11 changes: 10 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ARCHITECTURE.md

Last updated: 2026-03-11
Last updated: 2026-08-23

## Brand source

Expand Down Expand Up @@ -55,6 +55,15 @@ Last updated: 2026-03-11
- Split privilege where feasible across UI, analysis workers, subprocesses, model delivery, and updater behavior.
- Fail safely when a link, file, artifact, or boundary cannot be validated.

## Canonical audio resource policy

- Local audio admission is versioned in `services/analysis-engine/src/bandscope_analysis/audio_resource_policy.py` and documented in `docs/doctoring/audio-resource-policy.md`.
- Policy version 1 admits one rehearsal recording up to 15 minutes, 100 MiB encoded, mono or stereo, with source rates from 8 kHz through 192 kHz and a 44.1 kHz analysis target.
- Feature analyzers must not invent a shorter silent cap (including the former 120-second transcription window or a five-minute chord guard). Feature DSP resampling happens after canonical validation.
- Encoded size is checked before decode; decoded layout, sample count, duration, and memory are revalidated because container metadata is untrusted.
- Size conversions use checked arithmetic and fail closed on overflow or non-finite metadata. User-facing copy is payload-free and names the next file-selection action.
- YouTube import uses the same 15-minute / 100 MiB ceiling as local files.

## Repository map

- `apps/desktop` - desktop shell and user-facing React UI
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added

- Canonical local-audio resource policy (15 minutes / 100 MiB / mono-stereo / 44.1 kHz target) shared by temporal analysis, stem separation, YouTube import, bass transcription, and chord recognition. Oversized or malformed songs now name the next file to choose instead of echoing sizes.
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ 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/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.
- `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. `audio_resource_policy.py` is the versioned 15-minute / 100 MiB local-audio budget shared by those modules; feature DSP rates (for example bass pYIN at 22050 Hz) run only after that admission check.

Data flow: React UI → Tauri IPC command → Rust validation + Python subprocess over stdin/stdout → job status and progress events emitted back to the UI.

Expand Down
1 change: 1 addition & 0 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ GitHub is the source of truth for repository governance, PR review, CI/CD, Code
- prefer local processing for audio and analysis
- keep risky capabilities narrow, allowlisted, and explicit
- treat files, URLs, models, caches, and release artifacts as untrusted inputs
- admit local and YouTube audio through one canonical resource policy (15 minutes / 100 MiB / mono-stereo) before decode or feature DSP
- route orchestration through typed Tauri IPC and a narrow Python subprocess bridge before considering any loopback HTTP surface
- bootstrap local audio projects by validating the selected file in Rust, then passing only typed source metadata through the orchestration boundary
- keep project and temp/cache bootstrap roots under Tauri-resolved app-owned directories rather than the shared OS temp namespace
Expand Down
91 changes: 91 additions & 0 deletions docs/doctoring/audio-resource-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Canonical audio resource policy

BandScope admits one rehearsal recording at a time. Every intake path, decoder,
and feature analyzer must apply the same versioned resource budget before
expensive work starts.

## Published policy (version 1)

| Bound | Value | Why this number |
| --- | --- | --- |
| Encoded file bytes | 100 MiB inclusive | Existing temporal intake ceiling; long enough for a 15-minute stereo rehearsal capture without inviting decompression bombs. |
| Duration | 0.05 s through 15 minutes inclusive | Existing temporal and YouTube intake evidence. Not an invented five-minute cap. |
| Source sampling rate | 8 kHz through 192 kHz | Covers phone voice notes through high-rate interface captures. Feature DSP may resample after admission (bass pYIN at 22 050 Hz is allowed). |
| Target sampling rate | 44 100 Hz | Compact-disc PCM rate used by temporal analysis and stem separation. |
| Channel count | Mono or stereo | Rehearsal recordings are not multichannel session stems. |
| Decoded sample count | `15 × 60 × 44100` | Checked product of duration and target rate. |
| Decoded memory | sample count × 2 channels × 4 bytes | Float32 stereo estimate; overflow fails closed. |

## Next action copy

Rejection copy is payload-free. It names the next rehearsal action and never
echoes paths, sizes, durations, or header bytes:

- Choose a shorter or smaller song file to start analysis.
- Choose a song shorter than 15 minutes to start analysis.
- Choose a shorter song file to start analysis.
- Choose a longer song file to start analysis.
- Choose a WAV, MP3, FLAC, or M4A file recorded at a standard sample rate.
- Choose a mono or stereo song file to start analysis.
- Choose another song file. This one could not be measured safely.
- Choose another song file. This one could not be read as audio.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Audit metadata records `policy_version` and `reason` on
`AudioResourcePolicyError`. Those fields stay off the user-facing string.

## Validation order

1. Encoded byte size, before open/decode, where the filesystem size is
trustworthy as an upper bound.
2. YouTube metadata duration ceiling, before download.
3. Decode with the canonical duration bound as a loader safety cap, not as a
silent shorter feature policy.
4. Revalidate decoded arrays because container metadata is untrusted: layout,
sampling rate, sample count, wall-clock duration, and memory estimate.
5. Feature DSP (chromagram hop, pYIN 22 050 Hz, Demucs split) runs only on an
admitted buffer.

## Consumers

- `bandscope_analysis.audio_resource_policy` — versioned policy and validators
- `temporal.analyzer` — local file preflight and decoded revalidation
- `separation.audio_separator` — stem decode preflight and decoded revalidation
- `youtube.download_youtube_audio` — duration ceiling and 100 MiB encoded budget
- `transcription.api` — stem byte budget and 15-minute loader cap (no 120 s silent cap)
- `chords.chord_recognizer` — decoded revalidation at `recognize()`

Desktop Rust currently records `file_size_bytes` at intake but does not yet
enforce this ceiling. The Python engine remains fail-closed if a larger file
reaches analysis.

## Rollback

Revert this slice to restore feature-local limits (YouTube 50 MiB, bass
transcription 120 s, payload-bearing size errors). Do not leave a mix of
canonical validators and the old silent caps on the same branch.

## Security Notes

- Attack surface: untrusted local files, YouTube containers, decoder output,
and caller-supplied NumPy arrays.
- Trust boundary: this policy classifies resources only. It does not open
files, follow paths, or talk to the network.
- Mitigations: checked integer products, fail-closed non-finite metadata,
payload-free copy, decoded revalidation after untrusted headers.
- Test points: inclusive ceilings, next-byte/next-millisecond rejections,
empty and malformed metadata, decoded expansion, overflow, provenance.
- Realistic threats: decompression bombs, huge channel counts, extreme
sampling rates, integer overflow in size conversions, inconsistent
feature-local caps that fail only after expensive work.
- Remaining risk: desktop encoded-byte preflight still records size without
rejecting; duration still requires a decoder; GPU/VRAM budgets are not
part of policy version 1.

## References

International Electrotechnical Commission. (1999). *Compact disc digital audio
system* (IEC 60908). Geneva, Switzerland: IEC.

National Institute of Standards and Technology. (2020). *Security and privacy
controls for information systems and organizations* (NIST Special Publication
800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5
1 change: 1 addition & 0 deletions docs/security/app-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ Every boundary crossing requires validation, scope restriction, minimal logging,
- Cross-check extension, MIME, and actual decode behavior.
- Prefer isolated worker processing for decode and analysis.
- Guard against very large files, abnormal duration, and hostile metadata.
- Apply the canonical audio resource policy (`docs/doctoring/audio-resource-policy.md`): 15 minutes, 100 MiB encoded, mono or stereo, 8–192 kHz source, 44.1 kHz analysis target. Check encoded size before decode and revalidate decoded output. Return payload-free copy that names the next song to choose.
- Do not add arbitrary filesystem scanning just to find media files.
- When bootstrapping a project around local audio, prefer referencing the validated original file plus app-owned temp/cache/project roots over copying the file until persistence requirements justify the extra storage boundary.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Bounded metadata preflight for caller-owned local audio handles.

Security Notes:
- Untrusted input: container headers parsed from caller-owned binary handles.
- Trust boundary: this module inspects metadata only; it never decodes PCM,
follows paths, or opens network resources.
- Safe failure: parser failures and malformed metadata become payload-free
``AudioResourcePolicyError`` values before resampling, downmixing, or
duration truncation can hide the original source characteristics.
- Resource behavior: ``soundfile.info`` reads container metadata without
loading the audio contents into memory, and the handle is rewound for the
downstream decoder.
"""

from __future__ import annotations

from typing import BinaryIO

import soundfile

from bandscope_analysis.audio_resource_policy import (
DEFAULT_AUDIO_RESOURCE_POLICY,
AudioResourcePolicy,
AudioResourcePolicyError,
policy_rejection_message,
validate_channel_count,
validate_duration_seconds,
validate_source_sampling_rate,
)


def _malformed_header_error() -> AudioResourcePolicyError:
"""Build the stable payload-free container-probe failure."""
return AudioResourcePolicyError(
"malformed_header", policy_rejection_message("malformed_header")
)


def preflight_audio_metadata(
fileobj: BinaryIO,
policy: AudioResourcePolicy = DEFAULT_AUDIO_RESOURCE_POLICY,
) -> None:
"""Validate source duration, sample rate, and channel count without decoding PCM."""
try:
fileobj.seek(0)
info = soundfile.info(fileobj)
except Exception as error:
# No decoder runs after a failed metadata probe, so there is no consumer
# that needs the rejected handle rewound. Preserve the parser failure as
# the internal cause instead of masking it with a best-effort seek.
raise _malformed_header_error() from error

try:
fileobj.seek(0)
except Exception as error:
raise _malformed_header_error() from error

validate_source_sampling_rate(info.samplerate, policy)
validate_channel_count(info.channels, policy)
sampling_rate_hz = int(info.samplerate)
duration_seconds = float(info.frames) / float(sampling_rate_hz)
validate_duration_seconds(duration_seconds, policy)
Comment on lines +39 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Metadata preflight module has no production caller

preflight_audio_metadata in audio_metadata.py is new and tested but appears to have no production caller; the documented consumer list omits it. It is currently exercised only by tests.

Open in Devin Review

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

Loading
Loading