rename: slopsmith → feedBack, byron → got-feedBack (#537)

* Update GitHub repo references from feedback* to feedBack*

* rename: slopsmith -> feedBack, byron -> got-feedBack

Renames across the entire codebase:
- slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack
- byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack
- /home/byron/ -> /opt/got-feedBack/
- byron@ougsoft.com -> hi@got-feedBack.org
- github.com/byrongamatos/ -> github.com/got-feedback/
- com.byron. -> com.got-feedback.
- SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback
- Protocol/storage strings migrated with read-old/write-new pattern
- window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias

Refs: #rename-slopsmith

* rename: complete regen against current main + fix backward-compat alias

Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of
current main (3 commits had landed since the branch: #572/#554/#574),
resolving the four content conflicts in favour of main's newer content
(autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge).

Completion fixes on top of the mechanical rename:
- Re-apply rename to post-branch content the original rename never saw:
  window.slopsmith(.Tour) consumers in lessons.js / notifications.js /
  onboarding-tour.js, and the matching JS + python tests (autoplay_exit,
  progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env
  vars now match server.py (which reads FEEDBACK_SYNC_STARTUP /
  FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real
  path again.
- Restore the window.slopsmith backward-compat alias dropped during conflict
  resolution, and move the bus aliases to AFTER the _feedBackExisting merge
  block so they reference the fully-assembled object (also fixes the
  loop_api.test.js API-surface regex, which the original PR latently broke).
- Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR)
  and gitignore it.
- Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped
  source labels.

Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669
passed / 1683 collected with 0 import errors; zero residual slopsmith/byron
except the two intentional window.slopsmith aliases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* rename: implement advertised backward-compat + prune dead community plugins

Address gaps where PR #537's "Backward compatibility" section was advertised
but not implemented, and clean up the community plugin list.

Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured):
- New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py
  (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and
  tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI /
  SLOPSMITH_PLUGINS_DIR / etc. deployments keep working.
- Fix the rename collapsing plugins/__init__.py and minigames/routes.py from
  `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant
  `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost).

Storage (app.js update-channel):
- Read feedBack-update-channel, fall back to legacy slopsmith-update-channel,
  and clear the legacy key on write — so a user's update-channel preference
  survives the rename instead of resetting to "stable".

Community plugin list (README): the rename rewrote third-party repo URLs we
don't own. Probed every one; their owners never renamed, so:
- Restore the 13 live community plugins to their real slopsmith-* names.
- Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex
  tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle).
- Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory).

Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors,
rename-sensitive + startup suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: byrongamatos <xasiklas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bret Mogilefsky
2026-06-23 11:03:01 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 byrongamatos
parent a8ad02739a
commit af2949677a
257 changed files with 2389 additions and 2293 deletions
+2 -2
View File
@@ -39,7 +39,7 @@ jobs:
first=$(printf '%s\n' "$hits" | head -n1) first=$(printf '%s\n' "$hits" | head -n1)
file=$(printf '%s' "$first" | cut -d: -f1) file=$(printf '%s' "$first" | cut -d: -f1)
line=$(printf '%s' "$first" | cut -d: -f2) line=$(printf '%s' "$first" | cut -d: -f2)
echo "::error file=${file},line=${line}::print() or traceback.print_exc() found in server.py, lib/, or a bundled plugin routes.py. Use the slopsmith logger (lib/logging_setup.py) — see issues #155 / #242." echo "::error file=${file},line=${line}::print() or traceback.print_exc() found in server.py, lib/, or a bundled plugin routes.py. Use the feedBack logger (lib/logging_setup.py) — see issues #155 / #242."
exit 1 exit 1
fi fi
@@ -56,7 +56,7 @@ jobs:
tailwind-fresh: tailwind-fresh:
# Guard that the committed static/tailwind.min.css is in sync with source. # Guard that the committed static/tailwind.min.css is in sync with source.
# The Play CDN's runtime JIT was removed (slopsmith-desktop#110); a prebuilt # The Play CDN's runtime JIT was removed (feedBack-desktop#110); a prebuilt
# stylesheet only contains classes the scanner saw at build time, so stale # stylesheet only contains classes the scanner saw at build time, so stale
# CSS silently ships unstyled elements. Rebuild and fail on any diff. # CSS silently ships unstyled elements. Rebuild and fail on any diff.
name: tailwind-fresh name: tailwind-fresh
+2 -2
View File
@@ -1,7 +1,7 @@
name: Sync VERSION from desktop release name: Sync VERSION from desktop release
# Updates the VERSION file in this repo whenever slopsmith-desktop # Updates the VERSION file in this repo whenever feedBack-desktop
# publishes a new tagged release. slopsmith-desktop's build.yml # publishes a new tagged release. feedBack-desktop's build.yml
# dispatches the `desktop-released` event at the end of a successful # dispatches the `desktop-released` event at the end of a successful
# tag build (see docs in CLAUDE.md). A `workflow_dispatch` trigger is # tag build (see docs in CLAUDE.md). A `workflow_dispatch` trigger is
# kept for manual testing / recovery. # kept for manual testing / recovery.
+2
View File
@@ -9,6 +9,7 @@ build/
.env* .env*
.DS_Store .DS_Store
.vscode/ .vscode/
data/web_library.db
static/*.ogg static/*.ogg
static/*.mp3 static/*.mp3
static/*.wav static/*.wav
@@ -48,3 +49,4 @@ Thumbs.db
*.tmp *.tmp
*.swp *.swp
.idea/ .idea/
plugins/support_creators
+11 -11
View File
@@ -1,6 +1,6 @@
# Slopsmith Constitution # FeedBack Constitution
> Slopsmith is a self-hosted, single-user web app for browsing, playing, and > FeedBack is a self-hosted, single-user web app for browsing, playing, and
> practicing interactive music notation, built around its own open `.sloppak` > practicing interactive music notation, built around its own open `.sloppak`
> chart format (charts imported from Guitar Pro / MusicXML or authored in the > chart format (charts imported from Guitar Pro / MusicXML or authored in the
> built-in editor). This constitution captures the non-negotiable principles > built-in editor). This constitution captures the non-negotiable principles
@@ -13,7 +13,7 @@
### I. Self-Hosted, Single-User, Docker-First ### I. Self-Hosted, Single-User, Docker-First
Slopsmith targets one user running one container against a personal FeedBack targets one user running one container against a personal
song library folder. There is no multi-tenant model, no song library folder. There is no multi-tenant model, no
authentication, no rate limiting, and no shared backend. Deployment is authentication, no rate limiting, and no shared backend. Deployment is
expressed as a single `docker compose up -d` against the bundled expressed as a single `docker compose up -d` against the bundled
@@ -41,12 +41,12 @@ is Tailwind CSS, served as a prebuilt static stylesheet
(`static/tailwind.min.css`, regenerated by `scripts/build-tailwind.sh`) (`static/tailwind.min.css`, regenerated by `scripts/build-tailwind.sh`)
— never the runtime Play CDN, whose on-the-fly JIT rescans the DOM on — never the runtime Play CDN, whose on-the-fly JIT rescans the DOM on
the main thread and caused sustained frame drops with the 3D highway the main thread and caused sustained frame drops with the 3D highway
(slopsmith-desktop#110). No React, Vue, Svelte, bundler, transpiler, or (feedBack-desktop#110). No React, Vue, Svelte, bundler, transpiler, or
TypeScript appears in the core static tree, and no build step runs on TypeScript appears in the core static tree, and no build step runs on
the serve path: the Tailwind build is a maintainer-only one-shot whose the serve path: the Tailwind build is a maintainer-only one-shot whose
output is committed, so Docker / desktop / end users never build. New output is committed, so Docker / desktop / end users never build. New
features extend `app.js` and the existing globals (`window.playSong`, features extend `app.js` and the existing globals (`window.playSong`,
`window.showScreen`, `window.createHighway`, `window.slopsmith`). `window.showScreen`, `window.createHighway`, `window.feedBack`).
**Non-negotiable rules** **Non-negotiable rules**
@@ -89,7 +89,7 @@ do not collide in `sys.modules`.
sibling imports. Bare `import sibling` works during transition but sibling imports. Bare `import sibling` works during transition but
triggers a startup warning when a name collides. triggers a startup warning when a name collides.
- Plugins MUST register routes under `/api/plugins/<plugin_id>/...`, - Plugins MUST register routes under `/api/plugins/<plugin_id>/...`,
use `window.slopsmith.emit/on` for cross-plugin communication, and use `window.feedBack.emit/on` for cross-plugin communication, and
prefix their `localStorage` keys with their plugin id. prefix their `localStorage` keys with their plugin id.
- Plugins inherit this constitution and may layer additional rules in - Plugins inherit this constitution and may layer additional rules in
their own `CLAUDE.md`, but MUST NOT relax core principles (e.g. a their own `CLAUDE.md`, but MUST NOT relax core principles (e.g. a
@@ -97,7 +97,7 @@ do not collide in `sys.modules`.
### IV. Backwards-Compatible Chart Library ### IV. Backwards-Compatible Chart Library
The whole point of Slopsmith is that a user points it at an existing The whole point of FeedBack is that a user points it at an existing
song library folder and it Just Works. The library is scanned and song library folder and it Just Works. The library is scanned and
indexed in `meta.db` (SQLite via `MetadataDB`). The open Sloppak indexed in `meta.db` (SQLite via `MetadataDB`). The open Sloppak
format (`lib/sloppak.py`; specified at format (`lib/sloppak.py`; specified at
@@ -145,7 +145,7 @@ push and PR to `main` against Python 3.12.
All backend output goes through the stdlib `logging` pipeline configured All backend output goes through the stdlib `logging` pipeline configured
by `lib/logging_setup.py`, controlled by `LOG_LEVEL` / `LOG_FORMAT` / by `lib/logging_setup.py`, controlled by `LOG_LEVEL` / `LOG_FORMAT` /
`LOG_FILE`. Plugins receive a pre-configured `context["log"]` namespaced `LOG_FILE`. Plugins receive a pre-configured `context["log"]` namespaced
to `slopsmith.plugin.<id>` and MUST use it instead of `print`. HTTP to `feedBack.plugin.<id>` and MUST use it instead of `print`. HTTP
responses carry a `X-Request-ID` header from `CorrelationIdMiddleware` responses carry a `X-Request-ID` header from `CorrelationIdMiddleware`
and the same id appears as `request_id` in JSON log lines. The and the same id appears as `request_id` in JSON log lines. The
"Settings → Export Diagnostics" bundle (`lib/diagnostics_bundle.py`) "Settings → Export Diagnostics" bundle (`lib/diagnostics_bundle.py`)
@@ -171,7 +171,7 @@ User configuration lives in two places: server-side under `CONFIG_DIR`
(SQLite `meta.db`, `config.yaml`, plugin opted-in files) and client- (SQLite `meta.db`, `config.yaml`, plugin opted-in files) and client-
side in browser `localStorage`. Both can be exported and re-imported side in browser `localStorage`. Both can be exported and re-imported
as a single bundle (`POST /api/settings/import`, as a single bundle (`POST /api/settings/import`,
`GET /api/settings/export`, slopsmith#113). Import is two-phase: `GET /api/settings/export`, feedBack#113). Import is two-phase:
phase-1 validates the entire bundle (schema, paths, encoding) and phase-1 validates the entire bundle (schema, paths, encoding) and
phase-2 commits each file atomically via temp+rename. Plugins opt phase-2 commits each file atomically via temp+rename. Plugins opt
their server-side files into the bundle via their server-side files into the bundle via
@@ -188,7 +188,7 @@ no `..`, no absolute paths).
Importing a bundle whose schema predates the running plugin's code Importing a bundle whose schema predates the running plugin's code
MUST restore bytes verbatim — the plugin copes at next load. MUST restore bytes verbatim — the plugin copes at next load.
- The `VERSION` file is the single source of truth for the running - The `VERSION` file is the single source of truth for the running
release; it is auto-bumped from `slopsmith-desktop` releases via release; it is auto-bumped from `feedBack-desktop` releases via
`.github/workflows/sync-version.yml`. Manual edits are reserved for `.github/workflows/sync-version.yml`. Manual edits are reserved for
out-of-band recovery only. out-of-band recovery only.
@@ -219,7 +219,7 @@ no `..`, no absolute paths).
- **Branching**: never push directly to `main`. Always feature branch - **Branching**: never push directly to `main`. Always feature branch
+ PR. Exception: the automated `VERSION` bump from + PR. Exception: the automated `VERSION` bump from
`slopsmith-desktop`'s release job, which commits to `main` as `feedBack-desktop`'s release job, which commits to `main` as
`github-actions[bot]`. `github-actions[bot]`.
- **Reviews**: PRs run the local Codex review loop - **Reviews**: PRs run the local Codex review loop
(`feedback_codex_preflight.md`) and the GitHub Copilot review pass (`feedback_codex_preflight.md`) and the GitHub Copilot review pass
+33 -33
View File
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- **Autoplay & auto-exit — a global "click it, it plays; finish, you're back at the menu" option (default ON).** New single Settings toggle (`autoplayExit` in `localStorage`, surfaced in both the v3 and classic settings screens; absence of the key = enabled) that closes the friction at both ends of the play loop. **Autoplay:** `playSong()` previously loaded a chart paused, requiring a Play press; a one-shot flag armed per fresh load is now consumed by the next `song:ready` (highway.js) to auto-start via the existing `togglePlay()` path (HTML5 + `_juceMode` + count-in). Arrangement switches / seeks reuse the same `song:ready` event but never arm the flag, so they don't auto-restart. **Auto-exit:** on `song:ended`, core returns to the launching menu after a short grace delay — unless a visible full-screen results/dialog overlay is on top (detected via `[role=dialog][aria-modal]` / `.fixed.inset-0` with a `getClientRects()` visibility test that works for `position:fixed`), in which case the return is deferred so that score screen's own Close button (calling `window.closeCurrentSong()`) drives the exit. A plugin can also defer explicitly via the new `window.slopsmith.holdAutoExit()` (called synchronously from its own `song:ended` handler — core's listener runs first). Both paths mean **no external plugin PR is required** for a results screen to be respected. **Context-aware destination:** the player's remembered origin (`_playerOriginScreen`) now honours any real launch screen instead of clamping to library/home/favorites, and a one-shot `window.slopsmith.setReturnScreen(id)` override lets the lessons catalog (`static/v3/lessons.js`) send a finished lesson back to the lessons screen — not the song library — even though the external tutorials plugin owns the `playSong` call. Also exposes a read-only `window.slopsmith.autoplayExit` getter for plugins. Songs and lessons share the same `playSong` → highway path, so both inherit the behaviour. Core-only (`static/app.js`, `static/v3/lessons.js`, both `index.html`s); the end-of-song score screen itself remains a plugin. Optional polish (not required — the overlay heuristic already covers it): external scoring/note-detection plugins (e.g. SlopScale) may call `holdAutoExit()` + `closeCurrentSong()` for an exact, heuristic-free handoff. - **Autoplay & auto-exit — a global "click it, it plays; finish, you're back at the menu" option (default ON).** New single Settings toggle (`autoplayExit` in `localStorage`, surfaced in both the v3 and classic settings screens; absence of the key = enabled) that closes the friction at both ends of the play loop. **Autoplay:** `playSong()` previously loaded a chart paused, requiring a Play press; a one-shot flag armed per fresh load is now consumed by the next `song:ready` (highway.js) to auto-start via the existing `togglePlay()` path (HTML5 + `_juceMode` + count-in). Arrangement switches / seeks reuse the same `song:ready` event but never arm the flag, so they don't auto-restart. **Auto-exit:** on `song:ended`, core returns to the launching menu after a short grace delay — unless a visible full-screen results/dialog overlay is on top (detected via `[role=dialog][aria-modal]` / `.fixed.inset-0` with a `getClientRects()` visibility test that works for `position:fixed`), in which case the return is deferred so that score screen's own Close button (calling `window.closeCurrentSong()`) drives the exit. A plugin can also defer explicitly via the new `window.feedBack.holdAutoExit()` (called synchronously from its own `song:ended` handler — core's listener runs first). Both paths mean **no external plugin PR is required** for a results screen to be respected. **Context-aware destination:** the player's remembered origin (`_playerOriginScreen`) now honours any real launch screen instead of clamping to library/home/favorites, and a one-shot `window.feedBack.setReturnScreen(id)` override lets the lessons catalog (`static/v3/lessons.js`) send a finished lesson back to the lessons screen — not the song library — even though the external tutorials plugin owns the `playSong` call. Also exposes a read-only `window.feedBack.autoplayExit` getter for plugins. Songs and lessons share the same `playSong` → highway path, so both inherit the behaviour. Core-only (`static/app.js`, `static/v3/lessons.js`, both `index.html`s); the end-of-song score screen itself remains a plugin. Optional polish (not required — the overlay heuristic already covers it): external scoring/note-detection plugins (e.g. SlopScale) may call `holdAutoExit()` + `closeCurrentSong()` for an exact, heuristic-free handoff.
- **"Song Editor" promoted to a first-class v3 sidebar item.** The editor - **"Song Editor" promoted to a first-class v3 sidebar item.** The editor
plugin (`id: editor`) now gets its own dedicated sidebar entry — under the plugin (`id: editor`) now gets its own dedicated sidebar entry — under the
Library group, just below Songs — via the existing `PROMOTED_PLUGINS` Library group, just below Songs — via the existing `PROMOTED_PLUGINS`
@@ -17,48 +17,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(`renderPromotedNav` checks `/api/plugins`), so it appears only when the (`renderPromotedNav` checks `/api/plugins`), so it appears only when the
editor is loaded. The displayed label comes from the plugin's manifest editor is loaded. The displayed label comes from the plugin's manifest
`nav.label`. `nav.label`.
- **Guitar Pro → notation importer (`lib/gp2notation.py`)** (slopsmith#825 WS4b, epic #828). Piano/keys tracks imported from Guitar Pro (GPIF: `.gpx` GP6 / `.gp` GP7-8) now produce real Sloppak Notation Format data (sloppak-spec §5.3) alongside the `midi = string*24 + fret` guitar wire encoding. `gp2rs_gpx.convert_file` writes a `<stem>.notation.json` sidecar next to each keys arrangement XML (best-effort — a notation bug never breaks the RS-XML conversion), and `gp2notation.attach_notation_to_sloppak()` is the assembly-side helper that renames it into `notation_<id>.json` + adds the per-arrangement `notation:` manifest sub-key. Voice→staff routing salvages the logic from PR #703 (whose `stf` wire-field approach this supersedes): GP voice position 0 → `rh` staff (`G2`), positions ≥ 1 → `lh` (`F4`); a forced-LH track (the merged `Piano LH` partner from `_find_piano_pairs`, or a standalone track named `… LH`) routes everything to `lh` — preserving authored hand crossings instead of inferring hands from pitch. Emits measures with absolute `t` from the bar-indexed tempo map, change-only `ts`/`tempo`/`ks`, and `beat_groups` for compound/irregular meters (6/8 → `[3,3]`, 9/8 → `[3,3,3]`, 5/8 → `[2,3]`, 7/8 → `[2,2,3]` — cf. the slopsmith#261 denominator pitfalls); beats carry `dur`/`dot`/`tu`/`rest` from GP rhythms and notes carry absolute `midi` (String+Fret resolves via the string template's concert pitches, Tone+Octave via `(octave+1)*12 + step`) with `tied` continuations kept as real beats (engraving needs the tied notehead — unlike the RS-XML walk, which drops them and extends sustain). Timing reuses the `gp2rs_gpx` machinery (bar-indexed tempo map, per-beat rhythm durations, `_note_midi`) so notation lines up with the RS XML the highway plays — with one deliberate divergence: double dots advance time ×1.75 (vs. the RS-XML walk's single-dot ×1.5 approximation) so a written `dot: 2` agrees with the emitted beat times; sharing the walk itself stays tracked in slopsmith#618. Tests: `tests/test_gp2notation.py`. - **Guitar Pro → notation importer (`lib/gp2notation.py`)** (feedBack#825 WS4b, epic #828). Piano/keys tracks imported from Guitar Pro (GPIF: `.gpx` GP6 / `.gp` GP7-8) now produce real Sloppak Notation Format data (sloppak-spec §5.3) alongside the `midi = string*24 + fret` guitar wire encoding. `gp2rs_gpx.convert_file` writes a `<stem>.notation.json` sidecar next to each keys arrangement XML (best-effort — a notation bug never breaks the RS-XML conversion), and `gp2notation.attach_notation_to_sloppak()` is the assembly-side helper that renames it into `notation_<id>.json` + adds the per-arrangement `notation:` manifest sub-key. Voice→staff routing salvages the logic from PR #703 (whose `stf` wire-field approach this supersedes): GP voice position 0 → `rh` staff (`G2`), positions ≥ 1 → `lh` (`F4`); a forced-LH track (the merged `Piano LH` partner from `_find_piano_pairs`, or a standalone track named `… LH`) routes everything to `lh` — preserving authored hand crossings instead of inferring hands from pitch. Emits measures with absolute `t` from the bar-indexed tempo map, change-only `ts`/`tempo`/`ks`, and `beat_groups` for compound/irregular meters (6/8 → `[3,3]`, 9/8 → `[3,3,3]`, 5/8 → `[2,3]`, 7/8 → `[2,2,3]` — cf. the feedBack#261 denominator pitfalls); beats carry `dur`/`dot`/`tu`/`rest` from GP rhythms and notes carry absolute `midi` (String+Fret resolves via the string template's concert pitches, Tone+Octave via `(octave+1)*12 + step`) with `tied` continuations kept as real beats (engraving needs the tied notehead — unlike the RS-XML walk, which drops them and extends sustain). Timing reuses the `gp2rs_gpx` machinery (bar-indexed tempo map, per-beat rhythm durations, `_note_midi`) so notation lines up with the RS XML the highway plays — with one deliberate divergence: double dots advance time ×1.75 (vs. the RS-XML walk's single-dot ×1.5 approximation) so a written `dot: 2` agrees with the emitted beat times; sharing the walk itself stays tracked in feedBack#618. Tests: `tests/test_gp2notation.py`.
- **Legacy keys → notation lifter (`scripts/lift_keys_notation.py`)** (slopsmith#825 WS4c, epic #828). One-time batch converter that lifts existing **directory-form** piano/keys sloppaks from the legacy guitar wire encoding (`midi = s*24 + f`) into real Sloppak Notation Format files (sloppak-spec §5.3). Candidates are arrangements whose name matches `\b(keys|piano|keyboard|synth)\b` (case-insensitive); each gets a `notation_<id>.json` plus the per-arrangement `notation:` manifest sub-key. Measures derive from the song-level `beats` downbeats (`measure >= 0`; `song_timeline.json` preferred, first-arrangement fallback), with per-measure tempo from downbeat spacing (emitted only on a > 1 BPM change). Durations come from the wire sustain (`sus`, legacy `l` alias) when present, else the gap to the next onset in the same hand — quantized to the nearest plain/single-dotted `{1,2,4,8,16,32}` denominator at the local tempo, floored at a 32nd. Hands are split heuristically: onsets within 10 ms form a group; a group spanning > 12 semitones splits at its largest internal interval gap (low side → `lh`), otherwise the whole group goes by mean pitch vs middle C — single-staff output when everything lands on one hand. Idempotent (arrangements already carrying `notation:` are skipped; an orphan `notation_<id>.json` without the manifest key is refused, not overwritten) with `--dry-run` support; every payload is checked via `notation.validate_notation` before write. Honest caveat: the manifest is round-tripped through PyYAML (`safe_load` + `safe_dump(sort_keys=False)`) — key order survives, YAML comments/custom formatting do not (the script warns when comments are present). Zip-form `.sloppak` files are reported and skipped. Tests: `tests/test_lift_keys_notation.py`. - **Legacy keys → notation lifter (`scripts/lift_keys_notation.py`)** (feedBack#825 WS4c, epic #828). One-time batch converter that lifts existing **directory-form** piano/keys sloppaks from the legacy guitar wire encoding (`midi = s*24 + f`) into real Sloppak Notation Format files (sloppak-spec §5.3). Candidates are arrangements whose name matches `\b(keys|piano|keyboard|synth)\b` (case-insensitive); each gets a `notation_<id>.json` plus the per-arrangement `notation:` manifest sub-key. Measures derive from the song-level `beats` downbeats (`measure >= 0`; `song_timeline.json` preferred, first-arrangement fallback), with per-measure tempo from downbeat spacing (emitted only on a > 1 BPM change). Durations come from the wire sustain (`sus`, legacy `l` alias) when present, else the gap to the next onset in the same hand — quantized to the nearest plain/single-dotted `{1,2,4,8,16,32}` denominator at the local tempo, floored at a 32nd. Hands are split heuristically: onsets within 10 ms form a group; a group spanning > 12 semitones splits at its largest internal interval gap (low side → `lh`), otherwise the whole group goes by mean pitch vs middle C — single-staff output when everything lands on one hand. Idempotent (arrangements already carrying `notation:` are skipped; an orphan `notation_<id>.json` without the manifest key is refused, not overwritten) with `--dry-run` support; every payload is checked via `notation.validate_notation` before write. Honest caveat: the manifest is round-tripped through PyYAML (`safe_load` + `safe_dump(sort_keys=False)`) — key order survives, YAML comments/custom formatting do not (the script warns when comments are present). Zip-form `.sloppak` files are reported and skipped. Tests: `tests/test_lift_keys_notation.py`.
- **Notation schema v1 freeze — completeness batch** (slopsmith#822, epic #828). Adds the low-hanging-fruit fields ahead of content production: top-level credits `rights`/`lyricist`/`arranger`; measure `pickup` (anacrusis); beat `arp` (arpeggiate), `ferm` (fermata), and **typed grace notes**`grace: "a"` (acciaccatura, MusicXML `grace/@slash=yes`) / `"p"` (appoggiatura); note `stem` (`"up"`/`"down"` force). Pedal is settled as the existing `spd`/`sph`/`spu` trio with a documented MusicXML `<pedal start|change|stop>` mapping — no separate `ped` field. A new "v1 non-features" spec subsection pins the accepted limitations (microtonal, figured bass, mid-measure key/time/clef changes, `ott`/`barline`/ornaments/`trem`/`glis`) as additive-v1.x territory. `lib/notation.py` gains the `GRACE_TYPES`, `STEM_DIRECTIONS`, and `DYNAMICS` vocabularies; the validator stays permissive by design. - **Notation schema v1 freeze — completeness batch** (feedBack#822, epic #828). Adds the low-hanging-fruit fields ahead of content production: top-level credits `rights`/`lyricist`/`arranger`; measure `pickup` (anacrusis); beat `arp` (arpeggiate), `ferm` (fermata), and **typed grace notes**`grace: "a"` (acciaccatura, MusicXML `grace/@slash=yes`) / `"p"` (appoggiatura); note `stem` (`"up"`/`"down"` force). Pedal is settled as the existing `spd`/`sph`/`spu` trio with a documented MusicXML `<pedal start|change|stop>` mapping — no separate `ped` field. A new "v1 non-features" spec subsection pins the accepted limitations (microtonal, figured bass, mid-measure key/time/clef changes, `ott`/`barline`/ornaments/`trem`/`glis`) as additive-v1.x territory. `lib/notation.py` gains the `GRACE_TYPES`, `STEM_DIRECTIONS`, and `DYNAMICS` vocabularies; the validator stays permissive by design.
- **Notation format — standard musical notation as a first-class sloppak type.** Promotes keys, piano, violin, and any other staff-notation instrument out of the guitar wire format and into their own data structure, following the same promotion path used for drums (slopsmith#344). New `lib/notation.py` defines the canonical vocabulary (`CLEFS`, `DURATIONS`, `SCHEMA_VERSION`), a permissive `validate_notation()` check, and `measures_to_wire()` / `measure_to_wire()` wire helpers. `lib/sloppak.py::load_song` reads a new per-arrangement `notation:` sub-key from each arrangement entry in the manifest (Option B: per-arrangement, not song-wide), applies path-traversal guards, validates the parsed JSON via `validate_notation()`, and surfaces all notation payloads on `LoadedSloppak.notation_by_id` (a `dict[str, dict]` keyed by arrangement id). A failed or missing notation file for one arrangement does not abort or skip the arrangement itself — partial-failure isolation mirrors the drum tab loader. `file:` is now optional when `notation:` is present: the loader creates a minimal stub arrangement so a notation-only arrangement entry does not require a guitar wire format JSON. `/ws/highway/{filename}` gains two new message types — `notation_info` (staves, instrument, total measure count) and chunked `notation_measures` (32 measures per chunk) — streamed after `sections` and before `anchors`; `song_info` carries a new `has_notation: bool` flag so viz pickers can auto-activate the notation plugin regardless of arrangement name. The notation file schema is measure-structured (`measure → staff → voice → beat → note`), uses MIDI for pitch (no string/fret/tuning indirection), and carries the full set of effects that alphaTab can render. See `docs/sloppak-spec.md` §5.3 for the full schema. Open questions resolved per the piano/keys epic (slopsmith#828 / #822): Option B (per-arrangement `notation:` sub-key) and `file:`-optional-when-`notation:`-present are the endorsed design. - **Notation format — standard musical notation as a first-class sloppak type.** Promotes keys, piano, violin, and any other staff-notation instrument out of the guitar wire format and into their own data structure, following the same promotion path used for drums (feedBack#344). New `lib/notation.py` defines the canonical vocabulary (`CLEFS`, `DURATIONS`, `SCHEMA_VERSION`), a permissive `validate_notation()` check, and `measures_to_wire()` / `measure_to_wire()` wire helpers. `lib/sloppak.py::load_song` reads a new per-arrangement `notation:` sub-key from each arrangement entry in the manifest (Option B: per-arrangement, not song-wide), applies path-traversal guards, validates the parsed JSON via `validate_notation()`, and surfaces all notation payloads on `LoadedSloppak.notation_by_id` (a `dict[str, dict]` keyed by arrangement id). A failed or missing notation file for one arrangement does not abort or skip the arrangement itself — partial-failure isolation mirrors the drum tab loader. `file:` is now optional when `notation:` is present: the loader creates a minimal stub arrangement so a notation-only arrangement entry does not require a guitar wire format JSON. `/ws/highway/{filename}` gains two new message types — `notation_info` (staves, instrument, total measure count) and chunked `notation_measures` (32 measures per chunk) — streamed after `sections` and before `anchors`; `song_info` carries a new `has_notation: bool` flag so viz pickers can auto-activate the notation plugin regardless of arrangement name. The notation file schema is measure-structured (`measure → staff → voice → beat → note`), uses MIDI for pitch (no string/fret/tuning indirection), and carries the full set of effects that alphaTab can render. See `docs/sloppak-spec.md` §5.3 for the full schema. Open questions resolved per the piano/keys epic (feedBack#828 / #822): Option B (per-arrangement `notation:` sub-key) and `file:`-optional-when-`notation:`-present are the endorsed design.
- **`song_timeline.json` — beats and sections as a top-level file.** A new optional top-level file pointed at by a new manifest key (`song_timeline: song_timeline.json`) provides the correct home for song-wide beats and sections, replacing the legacy convention of embedding them in the first arrangement JSON. The loader in `lib/sloppak.py` reads and validates the file (must be a dict with `beats` and `sections` as lists), clears and repopulates `Song.beats` / `Song.sections` from it when present, and stores the raw dict on `LoadedSloppak.song_timeline`. The existing arrangement-JSON fallback is fully preserved: all existing sloppaks that omit `song_timeline:` continue to load without any change. This is a prerequisite for notation-only sloppaks, which may have no arrangement JSON at all and therefore no carrier for beats/sections data. New sloppaks should put beats/sections in `song_timeline.json` only. See `docs/sloppak-spec.md` §2 and §5.3. - **`song_timeline.json` — beats and sections as a top-level file.** A new optional top-level file pointed at by a new manifest key (`song_timeline: song_timeline.json`) provides the correct home for song-wide beats and sections, replacing the legacy convention of embedding them in the first arrangement JSON. The loader in `lib/sloppak.py` reads and validates the file (must be a dict with `beats` and `sections` as lists), clears and repopulates `Song.beats` / `Song.sections` from it when present, and stores the raw dict on `LoadedSloppak.song_timeline`. The existing arrangement-JSON fallback is fully preserved: all existing sloppaks that omit `song_timeline:` continue to load without any change. This is a prerequisite for notation-only sloppaks, which may have no arrangement JSON at all and therefore no carrier for beats/sections data. New sloppaks should put beats/sections in `song_timeline.json` only. See `docs/sloppak-spec.md` §2 and §5.3.
- **`note-detection` capability domain promoted — control plane (spec 009)** (slopsmith#727/#728, epic #828). New core host `static/capabilities/note-detection.js`: provider registry (kinds `midi`/`engine`/`js`, primitives `pitch.estimate`/`verify.target`), requester-owned context-scoped detection bindings (`open-binding`/`close-binding`/`set-target`/`clear-target` — each binding carries its own redacted tuning context, independent of the host's loaded song, per spec-009 FR-003), and hit/miss/verdict observability events (consumers own judgment). The legacy chart-coupled `highway.setNoteStateProvider` surface keeps working and is wrapped for compatibility-shim hit accounting. Diagnostics (`slopsmith.note_detection_capability.v1`) carry provider/binding summaries and bounded outcomes — no raw audio, device labels, or song identity. Migrating the chart path, Step Mode verify, minigames YIN, and the engine verifier onto bindings is the remainder of the spec-009 slice. - **`note-detection` capability domain promoted — control plane (spec 009)** (feedBack#727/#728, epic #828). New core host `static/capabilities/note-detection.js`: provider registry (kinds `midi`/`engine`/`js`, primitives `pitch.estimate`/`verify.target`), requester-owned context-scoped detection bindings (`open-binding`/`close-binding`/`set-target`/`clear-target` — each binding carries its own redacted tuning context, independent of the host's loaded song, per spec-009 FR-003), and hit/miss/verdict observability events (consumers own judgment). The legacy chart-coupled `highway.setNoteStateProvider` surface keeps working and is wrapped for compatibility-shim hit accounting. Diagnostics (`feedBack.note_detection_capability.v1`) carry provider/binding summaries and bounded outcomes — no raw audio, device labels, or song identity. Migrating the chart path, Step Mode verify, minigames YIN, and the engine verifier onto bindings is the remainder of the spec-009 slice.
- **`visualization` capability domain promoted (cap:6)** (slopsmith#828). New core host `static/capabilities/visualization.js` registers a provider-coordinator owning the highway renderer surface: commands `inspect` / `list-providers` / `select-renderer` / `clear-renderer` (selection delegates to the existing picker so persistence, WebGL2 gating, and fallback stay single-sourced), events `providers-refreshed` / `renderer-changed` / `renderer-ready` / `renderer-failed`. Legacy discovery (`type: "visualization"` manifests, `window.slopsmithViz_*` globals) keeps working unchanged and is accounted as compatibility shims with hit counts. `static/app.js` attributes every renderer change (auto-match / user-select / fallback) and auto-match outcomes into the domain. Diagnostics (`slopsmith.visualization_capability.v1`) carry provider ids/labels/context types, active renderer + selection source, last auto-match outcome, and last failure — no song filenames/titles. Per-panel (splitscreen) selection is a tracked follow-up. - **`visualization` capability domain promoted (cap:6)** (feedBack#828). New core host `static/capabilities/visualization.js` registers a provider-coordinator owning the highway renderer surface: commands `inspect` / `list-providers` / `select-renderer` / `clear-renderer` (selection delegates to the existing picker so persistence, WebGL2 gating, and fallback stay single-sourced), events `providers-refreshed` / `renderer-changed` / `renderer-ready` / `renderer-failed`. Legacy discovery (`type: "visualization"` manifests, `window.feedBackViz_*` globals) keeps working unchanged and is accounted as compatibility shims with hit counts. `static/app.js` attributes every renderer change (auto-match / user-select / fallback) and auto-match outcomes into the domain. Diagnostics (`feedBack.visualization_capability.v1`) carry provider ids/labels/context types, active renderer + selection source, last auto-match outcome, and last failure — no song filenames/titles. Per-panel (splitscreen) selection is a tracked follow-up.
- **Viz picker routes notation arrangements** (slopsmith#826, epic #828). `window.slopsmith.currentSong` gains `hasNotation` (sibling of `hasDrumTab`) from the `song_info` frame's `has_notation` flag, so notation viz plugins (Staff View, Keys Highway 3D) can gate `matchesArrangement` on data presence instead of arrangement-name heuristics. When a notation-only arrangement (no wire notes — `file:` omitted per sloppak-spec §5.3) falls through Auto with no notation plugin installed, the built-in highway still takes the canvas but the Auto label reads "no notation view installed" and a one-shot dismissable hint points at the visualization picker — never a silently blank board. - **Viz picker routes notation arrangements** (feedBack#826, epic #828). `window.feedBack.currentSong` gains `hasNotation` (sibling of `hasDrumTab`) from the `song_info` frame's `has_notation` flag, so notation viz plugins (Staff View, Keys Highway 3D) can gate `matchesArrangement` on data presence instead of arrangement-name heuristics. When a notation-only arrangement (no wire notes — `file:` omitted per sloppak-spec §5.3) falls through Auto with no notation plugin installed, the built-in highway still takes the canvas but the Auto label reads "no notation view installed" and a one-shot dismissable hint points at the visualization picker — never a silently blank board.
- **Keys instrument path in progression** (slopsmith#828). New `data/progression/paths/keys.json` (5 levels / 15 challenges at parity with the guitar path) plus keys-flavoured daily/weekly quest pool entries (`d.keys-one` "Ivory Tower", `w.keys-three` "Grand Recital"). `lib/progression.py::instrument_for_arrangement()` now attributes `type: piano|keys` arrangements — and names matching `keys`/`piano`/`keyboard`/`synth` on a word boundary — to the new `keys` instrument, so scored keys runs advance the path automatically. Purely content + attribution: no schema or API changes. - **Keys instrument path in progression** (feedBack#828). New `data/progression/paths/keys.json` (5 levels / 15 challenges at parity with the guitar path) plus keys-flavoured daily/weekly quest pool entries (`d.keys-one` "Ivory Tower", `w.keys-three` "Grand Recital"). `lib/progression.py::instrument_for_arrangement()` now attributes `type: piano|keys` arrangements — and names matching `keys`/`piano`/`keyboard`/`synth` on a word boundary — to the new `keys` instrument, so scored keys runs advance the path automatically. Purely content + attribution: no schema or API changes.
- **v3 library: exact artist/album filters + scroll/page-depth restore** (slopsmith#857). The v3 Songs toolbar gains Artist and Album dropdowns (Album populates from the selected artist and stays disabled until one is chosen), backed by new exact, case-insensitive (`COLLATE NOCASE`) `artist` / `album` query params threaded through `MetadataDB._build_where``query_page` / `query_artists` / `query_stats` and the `/api/library`, `/api/library/artists`, `/api/library/stats` endpoints (the free-text `q` search stays fuzzy and composes with the exact filters). The artist/album catalog is fetched independently of the active artist/album selection so the dropdowns always list the full set for the current provider/search. The toolbar is now sticky so filter controls stay reachable when browsing deep libraries, and returning from the player restores the previous scroll position **and** the loaded infinite-scroll page depth via a `sessionStorage` snapshot keyed by a filter/sort/view state hash (invalidated whenever those change, so a filter change still resets to the top). Tests: `tests/test_library_filters.py` (backend artist/album filters), `tests/js/v3_songs_scroll.test.js` (state-hash + snapshot helpers). - **v3 library: exact artist/album filters + scroll/page-depth restore** (feedBack#857). The v3 Songs toolbar gains Artist and Album dropdowns (Album populates from the selected artist and stays disabled until one is chosen), backed by new exact, case-insensitive (`COLLATE NOCASE`) `artist` / `album` query params threaded through `MetadataDB._build_where``query_page` / `query_artists` / `query_stats` and the `/api/library`, `/api/library/artists`, `/api/library/stats` endpoints (the free-text `q` search stays fuzzy and composes with the exact filters). The artist/album catalog is fetched independently of the active artist/album selection so the dropdowns always list the full set for the current provider/search. The toolbar is now sticky so filter controls stay reachable when browsing deep libraries, and returning from the player restores the previous scroll position **and** the loaded infinite-scroll page depth via a `sessionStorage` snapshot keyed by a filter/sort/view state hash (invalidated whenever those change, so a filter change still resets to the top). Tests: `tests/test_library_filters.py` (backend artist/album filters), `tests/js/v3_songs_scroll.test.js` (state-hash + snapshot helpers).
### Fixed ### Fixed
- **A song's accuracy badge now updates on its library card right after you play it — no restart needed.** The v3 library (`static/v3/songs.js`) loaded the best-accuracy map (`/api/stats/best`) once into `state.accuracy` at render time and only ever refreshed it on a full re-render; the play→return flow takes the screen-entry fast-path that restores the cached grid DOM without re-fetching, so a just-earned score stayed invisible until the next restart re-ran `render()`. The `stats-recorder` now emits a `stats:recorded` event (carrying `filename`/`arrangement`) once the scored `POST /api/stats` resolves on the server — the correct moment, since `song:stop` fires before the POST completes. `songs.js` listens: if the library is the active screen it re-fetches `/api/stats/best` and patches the affected card/row badge in place; otherwise it marks the filename dirty and `onV3SongsScreenEnter` applies it on return (a failed fetch keeps the entry dirty to retry). Badge markup was factored into a shared `accuracyBadge(filename, variant)` (grid pill + tree-row percentage, both tagged `.fb-acc-badge`) so the in-place `repaintAccuracy` can find and replace them without a full list re-render (scroll/pagination preserved). The old empty `song:stop` "refresh lazily next render" placeholder is replaced. - **A song's accuracy badge now updates on its library card right after you play it — no restart needed.** The v3 library (`static/v3/songs.js`) loaded the best-accuracy map (`/api/stats/best`) once into `state.accuracy` at render time and only ever refreshed it on a full re-render; the play→return flow takes the screen-entry fast-path that restores the cached grid DOM without re-fetching, so a just-earned score stayed invisible until the next restart re-ran `render()`. The `stats-recorder` now emits a `stats:recorded` event (carrying `filename`/`arrangement`) once the scored `POST /api/stats` resolves on the server — the correct moment, since `song:stop` fires before the POST completes. `songs.js` listens: if the library is the active screen it re-fetches `/api/stats/best` and patches the affected card/row badge in place; otherwise it marks the filename dirty and `onV3SongsScreenEnter` applies it on return (a failed fetch keeps the entry dirty to retry). Badge markup was factored into a shared `accuracyBadge(filename, variant)` (grid pill + tree-row percentage, both tagged `.fb-acc-badge`) so the in-place `repaintAccuracy` can find and replace them without a full list re-render (scroll/pagination preserved). The old empty `song:stop` "refresh lazily next render" placeholder is replaced.
- **Changing Settings → 3D Highway → Fret spacing no longer ejects you to the home screen.** The `highway_3d` plugin's `h3dSetFretSpacing` was the lone 3D-highway setting that called `location.reload()` to apply — and since the SPA boots with `#home` as the active screen (`index.html` `.screen.active`), the reload dropped the user out of Settings onto the homescreen. It now applies live like every other 3D-highway setting: it rebinds the module-scope `_h3dFretUniform` flag (so panels mounted later this session pick up the new mode), recomputes the two `fretX`-derived scalars that were baked at init (`_fretLabelScaleRefW` for fret-label sprite scaling, `FRET_WIDTH_MID` for camera hysteresis), and broadcasts a `fretSpacing` change over the existing `_bgEmitChange` pub-sub so every mounted panel rebuilds its board via `buildBoard()`. Per-frame note geometry already reads `fretX` live and needs no rebuild. No page reload, so the Settings screen stays put. Source-level regression tests in `tests/js/highway_3d_fret_spacing.test.js` now pin the no-reload / live-rebuild behavior. - **Changing Settings → 3D Highway → Fret spacing no longer ejects you to the home screen.** The `highway_3d` plugin's `h3dSetFretSpacing` was the lone 3D-highway setting that called `location.reload()` to apply — and since the SPA boots with `#home` as the active screen (`index.html` `.screen.active`), the reload dropped the user out of Settings onto the homescreen. It now applies live like every other 3D-highway setting: it rebinds the module-scope `_h3dFretUniform` flag (so panels mounted later this session pick up the new mode), recomputes the two `fretX`-derived scalars that were baked at init (`_fretLabelScaleRefW` for fret-label sprite scaling, `FRET_WIDTH_MID` for camera hysteresis), and broadcasts a `fretSpacing` change over the existing `_bgEmitChange` pub-sub so every mounted panel rebuilds its board via `buildBoard()`. Per-frame note geometry already reads `fretX` live and needs no rebuild. No page reload, so the Settings screen stays put. Source-level regression tests in `tests/js/highway_3d_fret_spacing.test.js` now pin the no-reload / live-rebuild behavior.
- **v3 library scroll-restore no longer breaks the classic v2 UI or drops off-screen searches** (slopsmith#857). Two regressions in the scroll-restore work above: (1) `playSong` remapped `home`-launched songs to return to the `#v3-songs` screen unconditionally, but `static/app.js` is shared with the v2 UI (served at `/v2` / `SLOPSMITH_UI=v2`) where that screen does not exist — Esc-from-player then called `showScreen('v3-songs')`, which threw on the missing element and stranded the user on a blank screen with playback still running; the remap now applies only when `#v3-songs` is present. (2) The Songs screen-entry fast-path skips reloading to preserve scroll, but the global topbar search routed through it, so once Songs had been visited, searching from another screen navigated there without applying the new query; the screen now tracks the state hash each fetch reflects and refetches when it has drifted, keeping the scroll-preserving no-op only when nothing changed. - **v3 library scroll-restore no longer breaks the classic v2 UI or drops off-screen searches** (feedBack#857). Two regressions in the scroll-restore work above: (1) `playSong` remapped `home`-launched songs to return to the `#v3-songs` screen unconditionally, but `static/app.js` is shared with the v2 UI (served at `/v2` / `FEEDBACK_UI=v2`) where that screen does not exist — Esc-from-player then called `showScreen('v3-songs')`, which threw on the missing element and stranded the user on a blank screen with playback still running; the remap now applies only when `#v3-songs` is present. (2) The Songs screen-entry fast-path skips reloading to preserve scroll, but the global topbar search routed through it, so once Songs had been visited, searching from another screen navigated there without applying the new query; the screen now tracks the state hash each fetch reflects and refetches when it has drifted, keeping the scroll-preserving no-op only when nothing changed.
- **An active custom highway renderer is no longer starved of `draw()` when it hides the canvas** (#819). The per-frame draw gate in `static/highway.js` bailed on `if (!_lastVisible) return`, which conflated two different "hidden" states: a genuine off-screen canvas (`offsetParent === null` — navigate-away / `display:none` splitscreen panel, #246) versus a renderer-set *override-hide* (`setVisible(false)`, where an opaque overlay covers the canvas but the active renderer keeps painting its own surface). The gate now only pauses everything for the off-screen case (and still pauses the default 2D renderer on an override-hide); the **active custom renderer** keeps receiving `draw()` through its own override-hide. The `highway:visibility` event still fires before the gate, so sibling overlay renderers (e.g. 3D Highway's `.h3d-wrap`) still pause. This is the core-side root cause behind the Tab View cursor freezing in single-player (slopsmith#734; worked around plugin-side in slopsmith-plugin-tabview#25). - **An active custom highway renderer is no longer starved of `draw()` when it hides the canvas** (#819). The per-frame draw gate in `static/highway.js` bailed on `if (!_lastVisible) return`, which conflated two different "hidden" states: a genuine off-screen canvas (`offsetParent === null` — navigate-away / `display:none` splitscreen panel, #246) versus a renderer-set *override-hide* (`setVisible(false)`, where an opaque overlay covers the canvas but the active renderer keeps painting its own surface). The gate now only pauses everything for the off-screen case (and still pauses the default 2D renderer on an override-hide); the **active custom renderer** keeps receiving `draw()` through its own override-hide. The `highway:visibility` event still fires before the gate, so sibling overlay renderers (e.g. 3D Highway's `.h3d-wrap`) still pause. This is the core-side root cause behind the Tab View cursor freezing in single-player (feedBack#734; worked around plugin-side in feedBack-plugin-tabview#25).
- **Screensaver no longer kicks in during windowed-mode playback** (#686). While a song is playing, `static/app.js` now holds a [Screen Wake Lock](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API) (`navigator.wakeLock.request('screen')`) so the OS display/screensaver stays awake even though only audio + the highway animation are active and the keyboard/mouse are idle. The lock is acquired on `song:play`/`song:resume` and released on `song:pause`/`song:ended`/`song:stop` (kept only while actually playing), and re-acquired on `visibilitychange` when the tab refocuses (the API auto-releases a lock whenever the page is hidden). Both the HTML5 `<audio>` and JUCE desktop playback paths emit the same `song:*` events, so the fix covers both. In slopsmith-desktop (Electron), where `navigator.wakeLock` is unreliable, it also drives a native `powerSaveBlocker` bridge via the optional `window.slopsmithDesktop.power.setScreenAwake` hook when present; both calls degrade silently where unsupported. Note: the browser Wake Lock API is secure-context only, so in a plain browser this is active on `localhost` / HTTPS only — a session opened over plain HTTP to a LAN IP (e.g. `http://192.168.1.100:8000`) won't keep the screen awake; front it with HTTPS or use the desktop app (see README → reverse-proxy notes). - **Screensaver no longer kicks in during windowed-mode playback** (#686). While a song is playing, `static/app.js` now holds a [Screen Wake Lock](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Wake_Lock_API) (`navigator.wakeLock.request('screen')`) so the OS display/screensaver stays awake even though only audio + the highway animation are active and the keyboard/mouse are idle. The lock is acquired on `song:play`/`song:resume` and released on `song:pause`/`song:ended`/`song:stop` (kept only while actually playing), and re-acquired on `visibilitychange` when the tab refocuses (the API auto-releases a lock whenever the page is hidden). Both the HTML5 `<audio>` and JUCE desktop playback paths emit the same `song:*` events, so the fix covers both. In feedBack-desktop (Electron), where `navigator.wakeLock` is unreliable, it also drives a native `powerSaveBlocker` bridge via the optional `window.feedBackDesktop.power.setScreenAwake` hook when present; both calls degrade silently where unsupported. Note: the browser Wake Lock API is secure-context only, so in a plain browser this is active on `localhost` / HTTPS only — a session opened over plain HTTP to a LAN IP (e.g. `http://192.168.1.100:8000`) won't keep the screen awake; front it with HTTPS or use the desktop app (see README → reverse-proxy notes).
### Changed ### Changed
- **Practice plugin first-class sidebar slot now points at Virtuoso.** The bundled practice plugin was rebranded/re-homed from the SlopScale fork (`id: slopscale`) to `got-feedback/feedback-plugin-virtuoso` (`id: virtuoso`); the desktop bundle swap is feedBack-desktop#31. `static/v3/shell.js` still promoted `slopscale`, whose id no longer ships — so `renderPromotedNav()` (gated on the plugin appearing in `/api/plugins`) would have found no match and the dedicated sidebar slot would have gone dark, dropping Virtuoso to the generic Plugins gallery. Update the NAV entry + `PROMOTED_PLUGINS` slot `slopscale``virtuoso` (`screen: plugin-virtuoso`, label "Virtuoso - Practice", same FeedBarcade anchor + `target` icon) so the practice plugin keeps its first-class entry. Also clear the now-dead `slopscale` id from the Plugins-gallery curated category map (`static/v3/plugins-page.js`) and add `virtuoso: 'practice'` as a defensive fallback (the manifest's `category: "practice"` is authoritative, so it lands on the practice board regardless), and refresh the stale SlopScale references in `README.md` + `docs/plugin-capability-inventory.md`. Must land with the bundle swap or the practice plugin regresses in the UI. - **Practice plugin first-class sidebar slot now points at Virtuoso.** The bundled practice plugin was rebranded/re-homed from the SlopScale fork (`id: slopscale`) to `got-feedback/feedback-plugin-virtuoso` (`id: virtuoso`); the desktop bundle swap is feedBack-desktop#31. `static/v3/shell.js` still promoted `slopscale`, whose id no longer ships — so `renderPromotedNav()` (gated on the plugin appearing in `/api/plugins`) would have found no match and the dedicated sidebar slot would have gone dark, dropping Virtuoso to the generic Plugins gallery. Update the NAV entry + `PROMOTED_PLUGINS` slot `slopscale``virtuoso` (`screen: plugin-virtuoso`, label "Virtuoso - Practice", same FeedBarcade anchor + `target` icon) so the practice plugin keeps its first-class entry. Also clear the now-dead `slopscale` id from the Plugins-gallery curated category map (`static/v3/plugins-page.js`) and add `virtuoso: 'practice'` as a defensive fallback (the manifest's `category: "practice"` is authoritative, so it lands on the practice board regardless), and refresh the stale SlopScale references in `README.md` + `docs/plugin-capability-inventory.md`. Must land with the bundle swap or the practice plugin regresses in the UI.
- **3D highway: realistic curved metal frets.** The fret wires are now bowed `TubeGeometry` (the middle strings push away from the camera so the row of frets reads as wrapping a cylindrical neck — a depth cue) rendered with a lit `MeshStandardMaterial` instead of the old flat, straight `MeshBasicMaterial` boxes, so the scene's ambient + directional light glints across the rounded surface for a polished-steel look. The existing per-frame highlight is preserved unchanged: frets inside the active anchor still turn gold (`0xD8A636`), which under the metallic shading reads as brass. Metalness is kept moderate (0.4, not full-metal) because the scene has no envMap — a PBR full-metal surface would reflect black — with a dim emissive floor so frets stay legible down the fogged neck. Backported from the `highway_babylon` plugin's "hit-zone fret bars". All knobs (`FRET_BOW_DZ`, metalness/roughness/emissive) are tunable constants. `plugins/highway_3d` v3.25.0. - **3D highway: realistic curved metal frets.** The fret wires are now bowed `TubeGeometry` (the middle strings push away from the camera so the row of frets reads as wrapping a cylindrical neck — a depth cue) rendered with a lit `MeshStandardMaterial` instead of the old flat, straight `MeshBasicMaterial` boxes, so the scene's ambient + directional light glints across the rounded surface for a polished-steel look. The existing per-frame highlight is preserved unchanged: frets inside the active anchor still turn gold (`0xD8A636`), which under the metallic shading reads as brass. Metalness is kept moderate (0.4, not full-metal) because the scene has no envMap — a PBR full-metal surface would reflect black — with a dim emissive floor so frets stay legible down the fogged neck. Backported from the `highway_babylon` plugin's "hit-zone fret bars". All knobs (`FRET_BOW_DZ`, metalness/roughness/emissive) are tunable constants. `plugins/highway_3d` v3.25.0.
- **3D highway: section + tone HUD cards now default OFF.** The v0.3.0 player chrome carries a persistent "Up Next" pill, making the in-canvas section card redundant by default (it doubled the readout, slopsmith feedback); the tone HUD follows the same less-is-more default. Both remain available in Settings → 3D Highway (visibility/position/size unchanged); users who previously toggled either explicitly keep their stored preference — only the untouched default flips. `plugins/highway_3d` v3.24.1. - **3D highway: section + tone HUD cards now default OFF.** The v0.3.0 player chrome carries a persistent "Up Next" pill, making the in-canvas section card redundant by default (it doubled the readout, feedBack feedback); the tone HUD follows the same less-is-more default. Both remain available in Settings → 3D Highway (visibility/position/size unchanged); users who previously toggled either explicitly keep their stored preference — only the untouched default flips. `plugins/highway_3d` v3.24.1.
- **Perf**: replace runtime Tailwind Play CDN with a prebuilt static stylesheet (`static/tailwind.min.css`). The Play CDN's runtime JIT scanned the DOM ~1.8x/sec on the main thread (~37 ms blocking spans), dropping ~26% of frames in long playback sessions with the 3D highway as default. Theme extensions (dark/accent/gold colors, Inter font) move to `tailwind.config.js`; regen via `bash scripts/build-tailwind.sh`. No runtime build step — the generated CSS is committed. Fixes slopsmith-desktop#110. - **Perf**: replace runtime Tailwind Play CDN with a prebuilt static stylesheet (`static/tailwind.min.css`). The Play CDN's runtime JIT scanned the DOM ~1.8x/sec on the main thread (~37 ms blocking spans), dropping ~26% of frames in long playback sessions with the 3D highway as default. Theme extensions (dark/accent/gold colors, Inter font) move to `tailwind.config.js`; regen via `bash scripts/build-tailwind.sh`. No runtime build step — the generated CSS is committed. Fixes feedBack-desktop#110.
- **Perf**: reduce per-frame allocations in the 2D highway chord + lyric render paths. `_ensureChordRenderCache` now also caches `sortedNotes` / `nonZeroNotes` / `nonZeroFrets` / `allMuted` / `hasMultipleNotes` (computed once per chord, invalidated on `src` / `_inverted` / `chordTemplates` change — the third key catches a stale `isOpen`-derived classification when the WS `chord_templates` message lands after the final `chords` chunk), so `drawChords` no longer re-sorts / re-filters / spreads min-max per visible chord per frame. The in-chord unison bend classification is folded inline (no `chordPositions.filter` × 2 per frame). `drawLyrics` memoizes `ctx.measureText` results in a two-level `Map<fontSize, Map<text, width>>` so cache hits don't allocate a composite string key. Lit-sustain shimmer in `drawSustains` swaps the 4 per-note-per-frame `Math.random()` calls for a 64-entry precomputed jitter LUT (xorshift32-seeded — the LUT contents are reload-stable and test-reproducible; rendered shimmer is deterministic per `createHighway()` instance, since the seed includes that instance's `_frameIdx`) indexed by `(frameIdx + n.s + ⌊n.t·60⌋)`, visually indistinguishable and allocation-free. - **Perf**: reduce per-frame allocations in the 2D highway chord + lyric render paths. `_ensureChordRenderCache` now also caches `sortedNotes` / `nonZeroNotes` / `nonZeroFrets` / `allMuted` / `hasMultipleNotes` (computed once per chord, invalidated on `src` / `_inverted` / `chordTemplates` change — the third key catches a stale `isOpen`-derived classification when the WS `chord_templates` message lands after the final `chords` chunk), so `drawChords` no longer re-sorts / re-filters / spreads min-max per visible chord per frame. The in-chord unison bend classification is folded inline (no `chordPositions.filter` × 2 per frame). `drawLyrics` memoizes `ctx.measureText` results in a two-level `Map<fontSize, Map<text, width>>` so cache hits don't allocate a composite string key. Lit-sustain shimmer in `drawSustains` swaps the 4 per-note-per-frame `Math.random()` calls for a 64-entry precomputed jitter LUT (xorshift32-seeded — the LUT contents are reload-stable and test-reproducible; rendered shimmer is deterministic per `createHighway()` instance, since the seed includes that instance's `_frameIdx`) indexed by `(frameIdx + n.s + ⌊n.t·60⌋)`, visually indistinguishable and allocation-free.
### Added ### Added
- **Player progression: Mastery Rank, instrument-path challenges, daily/weekly quests, Decibels currency, cosmetics shop (spec 010).** Onboarding gains two steps: pick one or more **instrument paths** (Guitar / Bass / Drums — data-driven, more can ship as content) and a **calibration challenge** offer (play the bundled Slopsmith Diagnostic with note detection at 100% accuracy — or skip; either way you reach **Mastery Rank 1**, and a skipped calibration can still be completed later from the Progress screen). Each path levels by completing a content-defined number of **challenges** (any order) from that level's set; Mastery Rank = onboarding rank + the sum of path levels, starting at 0 on a fresh install. The existing unified XP backend is untouched but the frontend renames it to **Decibels (dB)** — a spendable currency earned ONLY by playing (songs, FeedBarcade rounds, quest rewards; no real-money path exists or may be added) — with spend tracked in a separate wallet so lifetime earnings stay monotonic. Rotating **daily/weekly quests** (deterministic per period, lazy instantiation, local-midnight / Monday resets) award dB and feed `quest_completed` challenges. A new **Progress** screen (rank hero, per-path checklists, quest countdowns, add-a-path) and **Shop** screen (themes via CSS-variable swaps under `html[data-fb-theme]`, avatar frames; atomic balance-checked purchases — 402 on insufficient dB, 409 on re-buy) join the v3 nav, and the topbar badge now shows Rank + challenge-set progress + dB balance. All definitions live in `data/progression/` JSON (paths/levels/challenges, quest pools, shop catalog) — adding a rank, challenge, quest, or cosmetic is a content edit + restart, never code; invalid content degrades to logged warnings. New tables (additive + idempotent): `progression_state`, `player_paths`, `challenge_progress`, `quest_state`, `wallet`, `shop_owned`, `shop_equipped`. New endpoints: `GET /api/progression`, `POST /api/progression/paths|onboarding|events` (events whitelists `minigame_run`; `song_completed` stays server-derived inside `POST /api/stats`, which now resolves the instrument server-side and reports an additive `progression` outcome key), `GET /api/shop`, `POST /api/shop/buy|equip`; equipped cosmetics ride along on `GET /api/profile`. A new **`progression` capability domain** (core-owned, kind: command, safety: safe — `inspect`, `record-event`, `list-shop`, `buy-item`/`equip-item` gated on user action) emits `challenge-completed`/`quest-completed`/`path-level-up`/`rank-changed`/`db-changed`/`calibration-completed`/`cosmetic-equipped`, mirrored as `progression:*` window events, with a redaction-safe diagnostics contributor; backend plugins get the symmetric `record_progression_event` context hook (the bundled minigames hub reports runs through it, guarded for standalone). Spec: `specs/010-progression-domain/`. Tests: `tests/test_progression.py`, `tests/test_progression_api.py`. **Migration notes:** existing XP totals carry over as lifetime dB (balance = lifetime spent); resetting a per-source XP contribution (e.g. a minigames profile reset) after spending can clamp the spendable balance to 0 until new dB is earned; drums-path v1 content uses currently-satisfiable goals (arcade rounds, quests, any-instrument plays) until drums scoring lands. - **Player progression: Mastery Rank, instrument-path challenges, daily/weekly quests, Decibels currency, cosmetics shop (spec 010).** Onboarding gains two steps: pick one or more **instrument paths** (Guitar / Bass / Drums — data-driven, more can ship as content) and a **calibration challenge** offer (play the bundled FeedBack Diagnostic with note detection at 100% accuracy — or skip; either way you reach **Mastery Rank 1**, and a skipped calibration can still be completed later from the Progress screen). Each path levels by completing a content-defined number of **challenges** (any order) from that level's set; Mastery Rank = onboarding rank + the sum of path levels, starting at 0 on a fresh install. The existing unified XP backend is untouched but the frontend renames it to **Decibels (dB)** — a spendable currency earned ONLY by playing (songs, FeedBarcade rounds, quest rewards; no real-money path exists or may be added) — with spend tracked in a separate wallet so lifetime earnings stay monotonic. Rotating **daily/weekly quests** (deterministic per period, lazy instantiation, local-midnight / Monday resets) award dB and feed `quest_completed` challenges. A new **Progress** screen (rank hero, per-path checklists, quest countdowns, add-a-path) and **Shop** screen (themes via CSS-variable swaps under `html[data-fb-theme]`, avatar frames; atomic balance-checked purchases — 402 on insufficient dB, 409 on re-buy) join the v3 nav, and the topbar badge now shows Rank + challenge-set progress + dB balance. All definitions live in `data/progression/` JSON (paths/levels/challenges, quest pools, shop catalog) — adding a rank, challenge, quest, or cosmetic is a content edit + restart, never code; invalid content degrades to logged warnings. New tables (additive + idempotent): `progression_state`, `player_paths`, `challenge_progress`, `quest_state`, `wallet`, `shop_owned`, `shop_equipped`. New endpoints: `GET /api/progression`, `POST /api/progression/paths|onboarding|events` (events whitelists `minigame_run`; `song_completed` stays server-derived inside `POST /api/stats`, which now resolves the instrument server-side and reports an additive `progression` outcome key), `GET /api/shop`, `POST /api/shop/buy|equip`; equipped cosmetics ride along on `GET /api/profile`. A new **`progression` capability domain** (core-owned, kind: command, safety: safe — `inspect`, `record-event`, `list-shop`, `buy-item`/`equip-item` gated on user action) emits `challenge-completed`/`quest-completed`/`path-level-up`/`rank-changed`/`db-changed`/`calibration-completed`/`cosmetic-equipped`, mirrored as `progression:*` window events, with a redaction-safe diagnostics contributor; backend plugins get the symmetric `record_progression_event` context hook (the bundled minigames hub reports runs through it, guarded for standalone). Spec: `specs/010-progression-domain/`. Tests: `tests/test_progression.py`, `tests/test_progression_api.py`. **Migration notes:** existing XP totals carry over as lifetime dB (balance = lifetime spent); resetting a per-source XP contribution (e.g. a minigames profile reset) after spending can clamp the spendable balance to 0 until new dB is earned; drums-path v1 content uses currently-satisfiable goals (arcade rounds, quests, any-instrument plays) until drums scoring lands.
- **3D highway score FX (notedetect game-scoring layer).** The bundled `highway_3d` renderer now visualizes the scoring layer shipped in slopsmith-plugin-notedetect ≥1.13: floating **"+N" score pops** above each judged gem (sourced from the note-state provider's new `{ points, mult, popKey }` verdict fields — chord members share the chord-level `popKey`, so a chord pops once, not once per string), plus session-level FX from the new `notedetect:fx` event — a particle burst at the strike line on streak milestones (25/50/every 100), an expanding ring pulse on multiplier tier-ups (×2/×3/×4), and a brief red wash when a ≥10 streak breaks. Colors and the pop font follow the user's notedetect scoring-UI skin (`slopsmith_notedetect_skin`: neon/esports/metal, refreshed live on the `notedetect:skin` bus event). Everything renders on the existing 2D overlay canvas from fixed-size slot pools — no Three.js geometry, no text-sprite cache traffic, near-zero cost when idle — and degrades to a silent no-op with older notedetect builds (the new fields/events simply never arrive). Splitscreen panels scope FX to their own detector instance via the bubbling per-panel `notedetect:fx` dispatch. `plugins/highway_3d` v3.24.1. - **3D highway score FX (notedetect game-scoring layer).** The bundled `highway_3d` renderer now visualizes the scoring layer shipped in feedBack-plugin-notedetect ≥1.13: floating **"+N" score pops** above each judged gem (sourced from the note-state provider's new `{ points, mult, popKey }` verdict fields — chord members share the chord-level `popKey`, so a chord pops once, not once per string), plus session-level FX from the new `notedetect:fx` event — a particle burst at the strike line on streak milestones (25/50/every 100), an expanding ring pulse on multiplier tier-ups (×2/×3/×4), and a brief red wash when a ≥10 streak breaks. Colors and the pop font follow the user's notedetect scoring-UI skin (`feedBack_notedetect_skin`: neon/esports/metal, refreshed live on the `notedetect:skin` bus event). Everything renders on the existing 2D overlay canvas from fixed-size slot pools — no Three.js geometry, no text-sprite cache traffic, near-zero cost when idle — and degrades to a silent no-op with older notedetect builds (the new fields/events simply never arrive). Splitscreen panels scope FX to their own detector instance via the bubbling per-panel `notedetect:fx` dispatch. `plugins/highway_3d` v3.24.1.
- **3D highway: slide direction arrows + gem-follow animation.** Slide notes now show a / arrow indicating which way the slide goes — on the note/gem itself, as an early preview on the neck before the note arrives, and (optionally) chained further ahead for multi-leg slides — each independently toggleable in Settings → 3D Highway (`slideArrowApproachVisible`, `slideArrowNeckVisible`, `slideArrowChainPreviewVisible`). The note gem also now visually glides from its starting fret to the slide's destination over the note's sustain and holds there through the brief post-sustain linger, instead of snapping back to the starting fret — most noticeable on unpitched "slide to nothing" notes. `plugins/highway_3d` v3.25.2. - **3D highway: slide direction arrows + gem-follow animation.** Slide notes now show a / arrow indicating which way the slide goes — on the note/gem itself, as an early preview on the neck before the note arrives, and (optionally) chained further ahead for multi-leg slides — each independently toggleable in Settings → 3D Highway (`slideArrowApproachVisible`, `slideArrowNeckVisible`, `slideArrowChainPreviewVisible`). The note gem also now visually glides from its starting fret to the slide's destination over the note's sustain and holds there through the brief post-sustain linger, instead of snapping back to the starting fret — most noticeable on unpitched "slide to nothing" notes. `plugins/highway_3d` v3.25.2.
- **3D highway: up to 3 upcoming-note ghost previews per string, with fade-in/grow.** Each string now previews up to 3 upcoming notes (was 1) on a fixed 0.6 s fade-in/grow ramp, so tight same-string runs no longer pop in at full size right before impact and the player can read note order ahead of time. `isBlocked` (the pre-impact ghost suppression in a note's last 150 ms) is now scoped to chord notes only — for lead notes it had been blinking the ghost out right before each sustained note in dense runs. (Slide notes stay excluded too, per the slide-arrow work above, since their gem glides off the start fret.) `plugins/highway_3d` v3.26.0. - **3D highway: up to 3 upcoming-note ghost previews per string, with fade-in/grow.** Each string now previews up to 3 upcoming notes (was 1) on a fixed 0.6 s fade-in/grow ramp, so tight same-string runs no longer pop in at full size right before impact and the player can read note order ahead of time. `isBlocked` (the pre-impact ghost suppression in a note's last 150 ms) is now scoped to chord notes only — for lead notes it had been blinking the ghost out right before each sustained note in dense runs. (Slide notes stay excluded too, per the slide-arrow work above, since their gem glides off the start fret.) `plugins/highway_3d` v3.26.0.
- **Enable/disable plugins from the v3 Pedalboard (footswitch backend).** Every `/api/plugins` entry now carries an `enabled` boolean (default `true`), and a new `POST /api/plugins/{plugin_id}/enabled` endpoint (`{"enabled": <bool>}``{"id", "enabled"}`) persists the choice to `CONFIG_DIR/plugin_state.json` (only non-default `enabled:false` entries are stored; a missing/corrupt file is tolerated and never crashes startup). The loader **skips disabled plugins at startup** — no requirements install, no `routes.setup()`, no screen/nav/capabilities — while still surfacing them in `/api/plugins` as a disabled entry (`status:"disabled"`, `enabled:false`) so the UI can show an "off" pedal you can switch back on. Toggling persists immediately and flips the in-memory flag so the next `/api/plugins` reflects it at once (a runtime-disabled plugin's already-mounted routes/screen remain until the next restart; re-enabling a startup-skipped plugin mounts on restart). A disabled plugin is **excluded from the capability pipeline** — its capability metadata is emptied in `/api/plugins`. Guard rails keep `capability_inspector` and `app_tour_*` always enabled (disable → `400`); unknown id → `404`; missing/non-boolean `enabled``400`. Backend only; the v3 Pedalboard frontend consumes this contract. Docs: `docs/plugin-v3-ui.md`. - **Enable/disable plugins from the v3 Pedalboard (footswitch backend).** Every `/api/plugins` entry now carries an `enabled` boolean (default `true`), and a new `POST /api/plugins/{plugin_id}/enabled` endpoint (`{"enabled": <bool>}``{"id", "enabled"}`) persists the choice to `CONFIG_DIR/plugin_state.json` (only non-default `enabled:false` entries are stored; a missing/corrupt file is tolerated and never crashes startup). The loader **skips disabled plugins at startup** — no requirements install, no `routes.setup()`, no screen/nav/capabilities — while still surfacing them in `/api/plugins` as a disabled entry (`status:"disabled"`, `enabled:false`) so the UI can show an "off" pedal you can switch back on. Toggling persists immediately and flips the in-memory flag so the next `/api/plugins` reflects it at once (a runtime-disabled plugin's already-mounted routes/screen remain until the next restart; re-enabling a startup-skipped plugin mounts on restart). A disabled plugin is **excluded from the capability pipeline** — its capability metadata is emptied in `/api/plugins`. Guard rails keep `capability_inspector` and `app_tour_*` always enabled (disable → `400`); unknown id → `404`; missing/non-boolean `enabled``400`. Backend only; the v3 Pedalboard frontend consumes this contract. Docs: `docs/plugin-v3-ui.md`.
- **fee[dB]ack v0.3.0 rebrand + UI redesign (opt-in, isolated).** The visible product is being renamed from **Slopsmith** to **fee[dB]ack** (the `[dB]` is a decibel pun on the practice "feedback" loop) alongside a full dashboard-style UI redesign. **Rebrand scope is the app + docs wordmark only** — the repository, Python package, ghcr Docker image, `CONFIG_DIR`, and `SLOPSMITH_*` env vars all keep the `slopsmith` name, so existing deployments and data are unaffected. The redesigned UI is additive and served behind a feature flag: `SLOPSMITH_UI=v3` flips the `/` route to the new `static/v3/` shell, and `GET /v3` always serves it; the default `/` stays byte-identical to 0.2.9 until 0.3.0 flips the default. This release adds the `static/v3/` scaffold (navy app shell + styled fee[dB]ack wordmark, brand SVG + favicon + PWA manifest with 192/512 icons), an additive `fb` Tailwind color palette (legacy `dark`/`accent`/`gold` retained) with `static/v3/**` in the content globs, and the regenerated `static/tailwind.min.css`. Vanilla JS, prebuilt Tailwind, no Play CDN (Principle II). Shell wiring, screens, profile/scoring backends, and capability-runtime integration land in subsequent v0.3.0 changes. - **fee[dB]ack v0.3.0 rebrand + UI redesign (opt-in, isolated).** The visible product is being renamed from **FeedBack** to **fee[dB]ack** (the `[dB]` is a decibel pun on the practice "feedback" loop) alongside a full dashboard-style UI redesign. **Rebrand scope is the app + docs wordmark only** — the repository, Python package, ghcr Docker image, `CONFIG_DIR`, and `FEEDBACK_*` env vars all keep the `feedBack` name, so existing deployments and data are unaffected. The redesigned UI is additive and served behind a feature flag: `FEEDBACK_UI=v3` flips the `/` route to the new `static/v3/` shell, and `GET /v3` always serves it; the default `/` stays byte-identical to 0.2.9 until 0.3.0 flips the default. This release adds the `static/v3/` scaffold (navy app shell + styled fee[dB]ack wordmark, brand SVG + favicon + PWA manifest with 192/512 icons), an additive `fb` Tailwind color palette (legacy `dark`/`accent`/`gold` retained) with `static/v3/**` in the content globs, and the regenerated `static/tailwind.min.css`. Vanilla JS, prebuilt Tailwind, no Play CDN (Principle II). Shell wiring, screens, profile/scoring backends, and capability-runtime integration land in subsequent v0.3.0 changes.
- **fee[dB]ack v0.3.0 app shell (sidebar + topbar + routing).** The v3 shell (`static/v3/index.html`) is now a re-chromed copy of the legacy app: the new left **sidebar** (HOME / LIBRARY groups) and **topbar** (secondary nav, search, Support, badge-cluster mount points) replace the hidden legacy navbar, and new `#v3-*` screens (dashboard/plugins/profile/playlists/saved) are added — while all legacy screens (`#home` library, `#favorites`, `#settings`, `#player`, `#audio`, plugin nav containers) are kept verbatim so `static/app.js` boots **unmodified** and the whole engine (player/highway, plugin loader, capabilities, audio, library, settings) is reused as-is. Navigation is the shared `window.showScreen` across `#v3-*`, reused legacy, and `#plugin-*` screens, with a responsive hamburger and a `localStorage`/`v3:`-namespaced shell. Plugin nav is mirrored into the sidebar from `/api/plugins` (UI placement is a deferred capability domain, so this uses the legacy loader, not capability dispatch). `static/v3/shell.js` wraps `window.showScreen` via the idempotent rehydration pattern to keep sidebar/topbar active-state in sync. - **fee[dB]ack v0.3.0 app shell (sidebar + topbar + routing).** The v3 shell (`static/v3/index.html`) is now a re-chromed copy of the legacy app: the new left **sidebar** (HOME / LIBRARY groups) and **topbar** (secondary nav, search, Support, badge-cluster mount points) replace the hidden legacy navbar, and new `#v3-*` screens (dashboard/plugins/profile/playlists/saved) are added — while all legacy screens (`#home` library, `#favorites`, `#settings`, `#player`, `#audio`, plugin nav containers) are kept verbatim so `static/app.js` boots **unmodified** and the whole engine (player/highway, plugin loader, capabilities, audio, library, settings) is reused as-is. Navigation is the shared `window.showScreen` across `#v3-*`, reused legacy, and `#plugin-*` screens, with a responsive hamburger and a `localStorage`/`v3:`-namespaced shell. Plugin nav is mirrored into the sidebar from `/api/plugins` (UI placement is a deferred capability domain, so this uses the legacy loader, not capability dispatch). `static/v3/shell.js` wraps `window.showScreen` via the idempotent rehydration pattern to keep sidebar/topbar active-state in sync.
- **fee[dB]ack v0.3.0 player profile + first-run onboarding + unified XP + streak.** Adds a single-user core **profile** (`profile`/`profile_progress`/`xp_profile` tables in `web_library.db`, additive + idempotent): display name + avatar, a stable `player_hash` (SHA-256 of the first name + a once-generated salt — stable across later renames; a future-leaderboard label, never auth), and a **streak** (any session on a calendar day keeps it; a missed day resets to 1). New endpoints: `GET/POST /api/profile`, `POST /api/profile/avatar` (base64, re-encoded to a ≤512px PNG under `CONFIG_DIR/avatars/`), `GET /api/profile/avatar/{name}` (safe-joined), `GET /api/profile/avatars` (bundled defaults under `static/v3/avatars/`), `GET /api/profile/progress` (one call for the badge), and `POST /api/xp/award`. **Unified XP:** `lib/xp.py` is the single XP curve (same math the minigames plugin shipped); the core `xp_profile` store is the one source of truth the profile badge reads, exposed to plugins via `context["award_xp"]`/`get_xp_progress`/`seed_xp`. The bundled **minigames** plugin now delegates XP to the core store (seeding once from its existing `profile.json` so earned levels carry over) — so song-play, minigames, and tutorials all feed one level. Frontend: a blocking first-run onboarding overlay (name + avatar grid + upload), the topbar profile badge (avatar, 🔥 streak, level + XP bar), and the `#v3-profile` screen. Tests: `tests/test_xp.py`, `tests/test_profile_api.py`. - **fee[dB]ack v0.3.0 player profile + first-run onboarding + unified XP + streak.** Adds a single-user core **profile** (`profile`/`profile_progress`/`xp_profile` tables in `web_library.db`, additive + idempotent): display name + avatar, a stable `player_hash` (SHA-256 of the first name + a once-generated salt — stable across later renames; a future-leaderboard label, never auth), and a **streak** (any session on a calendar day keeps it; a missed day resets to 1). New endpoints: `GET/POST /api/profile`, `POST /api/profile/avatar` (base64, re-encoded to a ≤512px PNG under `CONFIG_DIR/avatars/`), `GET /api/profile/avatar/{name}` (safe-joined), `GET /api/profile/avatars` (bundled defaults under `static/v3/avatars/`), `GET /api/profile/progress` (one call for the badge), and `POST /api/xp/award`. **Unified XP:** `lib/xp.py` is the single XP curve (same math the minigames plugin shipped); the core `xp_profile` store is the one source of truth the profile badge reads, exposed to plugins via `context["award_xp"]`/`get_xp_progress`/`seed_xp`. The bundled **minigames** plugin now delegates XP to the core store (seeding once from its existing `profile.json` so earned levels carry over) — so song-play, minigames, and tutorials all feed one level. Frontend: a blocking first-run onboarding overlay (name + avatar grid + upload), the topbar profile badge (avatar, 🔥 streak, level + XP bar), and the `#v3-profile` screen. Tests: `tests/test_xp.py`, `tests/test_profile_api.py`.
- **fee[dB]ack v0.3.0 song-stats store (best score + accuracy, plays, resume position).** A core `song_stats` table (`web_library.db`, additive + idempotent, PK `(filename, arrangement)`) records per-song/arrangement best/last score + accuracy, play count, and last position. Endpoints: `POST /api/stats` (scored session → `plays += 1`, `best_*` = max, `last_*` = new, plus **unified-XP award** `xp_for_run(score)` and a **streak** bump, both behind try/except so a side-effect failure never drops the stat write; or position-only `lastPlayPosition` → resume touch with no `plays` change), `GET /api/stats/{filename}` (aggregated across arrangements), `GET /api/stats/recent` (joined to song title/artist/art for "Jump back in"). Scoring stays frontend-driven: `static/v3/stats-recorder.js` tallies the `note:hit`/`note:miss` events the optional `slopsmith-plugin-notedetect` already emits (and also accepts an explicit `note_detect:session-ended` summary), then POSTs on song end; it also persists resume position on pause/stop. No note-detect edit required — note-detection is a deferred capability domain, so the recorder uses those legacy events and degrades to "no accuracy" when the plugin isn't installed. Score/accuracy math is shared with the server via `lib/song_score.py`. Tests: `tests/test_song_score.py`, `tests/test_song_stats_api.py`. - **fee[dB]ack v0.3.0 song-stats store (best score + accuracy, plays, resume position).** A core `song_stats` table (`web_library.db`, additive + idempotent, PK `(filename, arrangement)`) records per-song/arrangement best/last score + accuracy, play count, and last position. Endpoints: `POST /api/stats` (scored session → `plays += 1`, `best_*` = max, `last_*` = new, plus **unified-XP award** `xp_for_run(score)` and a **streak** bump, both behind try/except so a side-effect failure never drops the stat write; or position-only `lastPlayPosition` → resume touch with no `plays` change), `GET /api/stats/{filename}` (aggregated across arrangements), `GET /api/stats/recent` (joined to song title/artist/art for "Jump back in"). Scoring stays frontend-driven: `static/v3/stats-recorder.js` tallies the `note:hit`/`note:miss` events the optional `feedBack-plugin-notedetect` already emits (and also accepts an explicit `note_detect:session-ended` summary), then POSTs on song end; it also persists resume position on pause/stop. No note-detect edit required — note-detection is a deferred capability domain, so the recorder uses those legacy events and degrades to "no accuracy" when the plugin isn't installed. Score/accuracy math is shared with the server via `lib/song_score.py`. Tests: `tests/test_song_score.py`, `tests/test_song_stats_api.py`.
- **fee[dB]ack v0.3.0 playlists, Saved for Later, and Continue-Playing.** Core playlist management (`playlists` + `playlist_songs` tables in `web_library.db`, additive + idempotent): create/rename/delete, add/remove/reorder songs, plus a reserved **Saved for Later** system playlist (created on first use; protected from rename/delete). Endpoints: `GET/POST /api/playlists`, `GET/PATCH/DELETE /api/playlists/{id}`, `POST /api/playlists/{id}/songs`, `DELETE /api/playlists/{id}/songs/{filename}`, `POST /api/playlists/{id}/reorder`, `POST /api/saved/toggle`, and `GET /api/session/continue` (derives the resume song + last position from `song_stats`, no new table). Frontend `static/v3/playlists.js` renders the `#v3-playlists` list + detail (drag-reorder, play, remove) and `#v3-saved`, and exposes `window.v3Saved.toggle()` for a "Save for later" affordance on song cards. Favorites reuse the existing favorites screen/API. Core REST, no capability domain. Tests: `tests/test_playlists_api.py`. - **fee[dB]ack v0.3.0 playlists, Saved for Later, and Continue-Playing.** Core playlist management (`playlists` + `playlist_songs` tables in `web_library.db`, additive + idempotent): create/rename/delete, add/remove/reorder songs, plus a reserved **Saved for Later** system playlist (created on first use; protected from rename/delete). Endpoints: `GET/POST /api/playlists`, `GET/PATCH/DELETE /api/playlists/{id}`, `POST /api/playlists/{id}/songs`, `DELETE /api/playlists/{id}/songs/{filename}`, `POST /api/playlists/{id}/reorder`, `POST /api/saved/toggle`, and `GET /api/session/continue` (derives the resume song + last position from `song_stats`, no new table). Frontend `static/v3/playlists.js` renders the `#v3-playlists` list + detail (drag-reorder, play, remove) and `#v3-saved`, and exposes `window.v3Saved.toggle()` for a "Save for later" affordance on song cards. Favorites reuse the existing favorites screen/API. Core REST, no capability domain. Tests: `tests/test_playlists_api.py`.
- **fee[dB]ack v0.3.0 Dashboard / Home.** The `#v3-home` dashboard (matching the v0.3.0 design target) composes the new backends: a "Welcome back, {name}!" banner with a patch-notes link (`/api/version`), a hero card (Start Playing / Create Lobby), a **Continue-Playing** card (`/api/session/continue` → art, tuning chip, 4-segment progress; click resumes via `playSong` + best-effort seek), a stats row (audio-routing widget placeholder until prompt 18, library count from `/api/library/stats`, plugins count from `/api/plugins` where `status==="ready"`), and a **Recently Played** grid (`/api/stats/recent`) with per-song accuracy badges (good/mid/low ramp). Each widget fetches + renders independently and degrades gracefully (missing/empty endpoint → placeholder, never blocks first paint). `static/v3/dashboard.js`; re-renders on return to Home and on profile update. - **fee[dB]ack v0.3.0 Dashboard / Home.** The `#v3-home` dashboard (matching the v0.3.0 design target) composes the new backends: a "Welcome back, {name}!" banner with a patch-notes link (`/api/version`), a hero card (Start Playing / Create Lobby), a **Continue-Playing** card (`/api/session/continue` → art, tuning chip, 4-segment progress; click resumes via `playSong` + best-effort seek), a stats row (audio-routing widget placeholder until prompt 18, library count from `/api/library/stats`, plugins count from `/api/plugins` where `status==="ready"`), and a **Recently Played** grid (`/api/stats/recent`) with per-song accuracy badges (good/mid/low ramp). Each widget fetches + renders independently and degrades gracefully (missing/empty endpoint → placeholder, never blocks first paint). `static/v3/dashboard.js`; re-renders on return to Home and on profile update.
- **fee[dB]ack v0.3.0 tuner + instrument topbar badges.** The topbar gains an **instrument selector** (guitar/bass + string count + tuning + reference pitch) persisted via additive `/api/settings` fields (`reference_pitch` clamped 430450, `instrument`, `string_count` 48, `tuning` name or semitone offsets); changing it emits `instrument:changed` so the note_detect scorer can re-tune (consumed once the external plugin adopts it). A **live tuner badge** stays idle until the user enables the mic (explicit gesture; `getUserMedia`), then shows a YIN note readout with a cents needle (green within ±5¢) using a new dependency-free `static/v3/tuner-core.js` (YIN + frequency→note/cents, honoring the reference pitch); clicking opens the full `slopsmith-plugin-tuner` screen when installed. CPU-friendly (~20 Hz, paused when the tab is hidden, respects `prefers-reduced-motion`). Tests: `tests/test_settings_instrument.py`, `tests/js/tuner_core.test.js`. - **fee[dB]ack v0.3.0 tuner + instrument topbar badges.** The topbar gains an **instrument selector** (guitar/bass + string count + tuning + reference pitch) persisted via additive `/api/settings` fields (`reference_pitch` clamped 430450, `instrument`, `string_count` 48, `tuning` name or semitone offsets); changing it emits `instrument:changed` so the note_detect scorer can re-tune (consumed once the external plugin adopts it). A **live tuner badge** stays idle until the user enables the mic (explicit gesture; `getUserMedia`), then shows a YIN note readout with a cents needle (green within ±5¢) using a new dependency-free `static/v3/tuner-core.js` (YIN + frequency→note/cents, honoring the reference pitch); clicking opens the full `feedBack-plugin-tuner` screen when installed. CPU-friendly (~20 Hz, paused when the tab is hidden, respects `prefers-reduced-motion`). Tests: `tests/test_settings_instrument.py`, `tests/js/tuner_core.test.js`.
- **fee[dB]ack v0.3.0 audio-routing widget (dashboard).** The dashboard's audio stat tile now reads the live audio session **through the capability runtime**`audio-mix inspect` (route + faders + required kinds), `audio-input list-sources` (selected/available input), `audio-monitoring inspect` — and renders **Audio Input → VST/NAM/IR → Audio Output** with per-node state dots and a Connected/Not Connected line. It never touches `audio-mixer.js` internals or `nam_tone` routes directly; "Not Connected" is the honest browser default (no native route), and it degrades on `no-owner`/`no-handler`/`failed` or absent capabilities. Refreshes on `instrument:changed`, play/stop, capability audio events, and each Home visit. `static/v3/audio-routing.js`. - **fee[dB]ack v0.3.0 audio-routing widget (dashboard).** The dashboard's audio stat tile now reads the live audio session **through the capability runtime**`audio-mix inspect` (route + faders + required kinds), `audio-input list-sources` (selected/available input), `audio-monitoring inspect` — and renders **Audio Input → VST/NAM/IR → Audio Output** with per-node state dots and a Connected/Not Connected line. It never touches `audio-mixer.js` internals or `nam_tone` routes directly; "Not Connected" is the honest browser default (no native route), and it degrades on `no-owner`/`no-handler`/`failed` or absent capabilities. Refreshes on `instrument:changed`, play/stop, capability audio events, and each Home visit. `static/v3/audio-routing.js`.
- **fee[dB]ack v0.3.0 Plugins page.** The `#v3-plugins` screen renders the enriched `/api/plugins`: a "{N} active" header (`status==="ready"`), a card grid per plugin (icon, name, version, status pill with the error on failed, capability summary badges — declared domains / validation warnings / unsupported versions / shim hits / bundled / type), an **Open →** action that navigates to the plugin's injected `#plugin-<id>` screen, and All/Bundled/Visualizations filters. Surfaces a deep-link to the bundled **Capability Inspector** rather than re-implementing the graph. No new backend. `static/v3/plugins-page.js`. - **fee[dB]ack v0.3.0 Plugins page.** The `#v3-plugins` screen renders the enriched `/api/plugins`: a "{N} active" header (`status==="ready"`), a card grid per plugin (icon, name, version, status pill with the error on failed, capability summary badges — declared domains / validation warnings / unsupported versions / shim hits / bundled / type), an **Open →** action that navigates to the plugin's injected `#plugin-<id>` screen, and All/Bundled/Visualizations filters. Surfaces a deep-link to the bundled **Capability Inspector** rather than re-implementing the graph. No new backend. `static/v3/plugins-page.js`.
- **fee[dB]ack v0.3.0 Songs / Library screen (`#v3-songs`).** A native vanilla-JS library browser over the existing `/api/library*` endpoints: provider selector (via the `library` capability, not DOM scraping), grid + tree views, sort, format filter, a tri-state filter drawer (arrangements / stems / lyrics / tunings), topbar-driven search (debounced), infinite scroll, fb song cards with **accuracy badges** (good/mid/low ramp, batched via a new `GET /api/stats/best`), favorite + save-for-later affordances, and upload (reuses the existing uploader). The "Songs" sidebar nav now opens this screen. No regression to `/api/library*`. `static/v3/songs.js`. - **fee[dB]ack v0.3.0 Songs / Library screen (`#v3-songs`).** A native vanilla-JS library browser over the existing `/api/library*` endpoints: provider selector (via the `library` capability, not DOM scraping), grid + tree views, sort, format filter, a tri-state filter drawer (arrangements / stems / lyrics / tunings), topbar-driven search (debounced), infinite scroll, fb song cards with **accuracy badges** (good/mid/low ramp, batched via a new `GET /api/stats/best`), favorite + save-for-later affordances, and upload (reuses the existing uploader). The "Songs" sidebar nav now opens this screen. No regression to `/api/library*`. `static/v3/songs.js`.
- **`ui.library-card-injection` capability + native song-card actions (fee[dB]ack v0.3.0).** New core capability (`static/capabilities/library-card-actions.js`, owner `core.ui.library-card-injection`, exposed as `window.slopsmith.libraryCardActions`) lets plugins **register** per-song library-card actions (id, label, placement, applicability, enabled state, run handler) with `action-registered`/`action-result` events — replacing the legacy `.song-card` DOM-injection pattern (roadmap domain #9, now delivered as a frontend host). The native Songs grid renders registered actions in each card's "⋮" menu; the built-in **Edit metadata** and **Convert to E Standard (retune)** actions ship through it (`static/v3/card-actions-core.js`, calling the existing `openEditModal`/`retuneSong` globals). Songs cards also gain **arrangement chips** (play a specific arrangement) and a **multi-select** mode with batch **Add to playlist** / **Save for Later**. Recipe in `docs/capability-recipes.md`; tests in `tests/js/library_card_actions.test.js`. Migrating the external card-action plugins (Sloppak Converter, Find More, editor) onto `register(...)` is a follow-up. - **`ui.library-card-injection` capability + native song-card actions (fee[dB]ack v0.3.0).** New core capability (`static/capabilities/library-card-actions.js`, owner `core.ui.library-card-injection`, exposed as `window.feedBack.libraryCardActions`) lets plugins **register** per-song library-card actions (id, label, placement, applicability, enabled state, run handler) with `action-registered`/`action-result` events — replacing the legacy `.song-card` DOM-injection pattern (roadmap domain #9, now delivered as a frontend host). The native Songs grid renders registered actions in each card's "⋮" menu; the built-in **Edit metadata** and **Convert to E Standard (retune)** actions ship through it (`static/v3/card-actions-core.js`, calling the existing `openEditModal`/`retuneSong` globals). Songs cards also gain **arrangement chips** (play a specific arrangement) and a **multi-select** mode with batch **Add to playlist** / **Save for Later**. Recipe in `docs/capability-recipes.md`; tests in `tests/js/library_card_actions.test.js`. Migrating the external card-action plugins (Sloppak Converter, Find More, editor) onto `register(...)` is a follow-up.
- **`centOffset` exposed via `getSongInfo()`** — the arrangement `<centOffset>` field (float, cents) is now parsed from all chart sources (loose folder XML, sloppak wire format) and sent as `centOffset` in the `song_info` WebSocket message. Plugins can read `getSongInfo().centOffset` to obtain the arrangement's pitch-shift offset — commonly `-1200.0` for extended-range bass (one octave down) or a small non-zero value for true-tuned content (e.g. A443 ≈ +11.8 cents). Defaults to `0.0` when absent. - **`centOffset` exposed via `getSongInfo()`** — the arrangement `<centOffset>` field (float, cents) is now parsed from all chart sources (loose folder XML, sloppak wire format) and sent as `centOffset` in the `song_info` WebSocket message. Plugins can read `getSongInfo().centOffset` to obtain the arrangement's pitch-shift offset — commonly `-1200.0` for extended-range bass (one octave down) or a small non-zero value for true-tuned content (e.g. A443 ≈ +11.8 cents). Defaults to `0.0` when absent.
- **`highway.getPhrases()` and `highway.getMastery()` public plugin API** — exposes phrase timing windows (`[{ index, start_time, end_time, max_difficulty }]`) and the current mastery slider value (`0..1`) as documented, stable plugin API. Both values were already in memory and reachable via internal names; this surfaces them with intent so plugins can implement section-aware logic (e.g. tracking accuracy per phrase, suppressing difficulty changes during a hard solo) without reaching into undocumented internals. Returns `null` when the song has no phrase data (GP imports, single-difficulty charts). Pair with the existing `hasPhraseData()` to gate phrase-aware code paths. - **`highway.getPhrases()` and `highway.getMastery()` public plugin API** — exposes phrase timing windows (`[{ index, start_time, end_time, max_difficulty }]`) and the current mastery slider value (`0..1`) as documented, stable plugin API. Both values were already in memory and reachable via internal names; this surfaces them with intent so plugins can implement section-aware logic (e.g. tracking accuracy per phrase, suppressing difficulty changes during a hard solo) without reaching into undocumented internals. Returns `null` when the song has no phrase data (GP imports, single-difficulty charts). Pair with the existing `hasPhraseData()` to gate phrase-aware code paths.
- **Tailwind freshness guard + wider plugin scan.** A new `tailwind-fresh` CI job (`.github/workflows/tests.yml`) rebuilds `static/tailwind.min.css` with the pinned `tailwindcss@3.4.19` and hard-fails on any diff, so the committed prebuilt stylesheet can no longer silently lag source (after PR #411 removed the runtime Play CDN, a stale file shipped unstyled elements with no guard). The `tailwind.config.js` plugin content glob is widened to `./plugins/**/*.{js,html}`, which also scans non-`screen.js` plugin JS (e.g. `plugins/app_tour_*/script.js`) that was previously invisible to the build. Regenerating under the wider glob is a no-op for runtime behaviour — it only adds classes that were already used in source. Groundwork for the plugin `styles` capability (constitution 1.1.0, Principle II): runtime-installed plugins ship their own compiled CSS rather than relying on core's build-time scan. - **Tailwind freshness guard + wider plugin scan.** A new `tailwind-fresh` CI job (`.github/workflows/tests.yml`) rebuilds `static/tailwind.min.css` with the pinned `tailwindcss@3.4.19` and hard-fails on any diff, so the committed prebuilt stylesheet can no longer silently lag source (after PR #411 removed the runtime Play CDN, a stale file shipped unstyled elements with no guard). The `tailwind.config.js` plugin content glob is widened to `./plugins/**/*.{js,html}`, which also scans non-`screen.js` plugin JS (e.g. `plugins/app_tour_*/script.js`) that was previously invisible to the build. Regenerating under the wider glob is a no-op for runtime behaviour — it only adds classes that were already used in source. Groundwork for the plugin `styles` capability (constitution 1.1.0, Principle II): runtime-installed plugins ship their own compiled CSS rather than relying on core's build-time scan.
@@ -67,21 +67,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Audio-mix control plane** — makes `audio-mix` the player mixer source of truth. Core now exposes `list-faders`, `get-fader-value`, `set-fader-value`, `inspect-route`, and `inspect-analyser` through the capability runtime, routes native and compatibility-backed fader provider operations with a 2-second timeout, reports committed values back to the mixer UI, suppresses matching legacy faders when a native participant owns the same logical control, and expands audio-session diagnostics/Capability Inspector rendering for fader availability, source modes, bridge hits, route/analyser summaries, and timeout failures. - **Audio-mix control plane** — makes `audio-mix` the player mixer source of truth. Core now exposes `list-faders`, `get-fader-value`, `set-fader-value`, `inspect-route`, and `inspect-analyser` through the capability runtime, routes native and compatibility-backed fader provider operations with a 2-second timeout, reports committed values back to the mixer UI, suppresses matching legacy faders when a native participant owns the same logical control, and expands audio-session diagnostics/Capability Inspector rendering for fader availability, source modes, bridge hits, route/analyser summaries, and timeout failures.
- **Audio-input control plane** — makes `audio-input` the redaction-safe source of truth for instrument input discovery and lifecycle. Core now exposes `list-sources`, `select-source`, `open-source`, and `close-source` through the capability runtime, persists selected logical sources, keeps inspect/list/select prompt-free, routes provider `source.open`/`source.close` operations with bounded outcomes, shares compatible open sessions across requesters, suppresses compatibility-backed duplicate sources when a native provider owns the same logical key, and expands audio-session diagnostics/Capability Inspector rendering for selected input, open sessions, bridge hits, storage status, and permission/device failures without exposing raw device labels or live audio handles. - **Audio-input control plane** — makes `audio-input` the redaction-safe source of truth for instrument input discovery and lifecycle. Core now exposes `list-sources`, `select-source`, `open-source`, and `close-source` through the capability runtime, persists selected logical sources, keeps inspect/list/select prompt-free, routes provider `source.open`/`source.close` operations with bounded outcomes, shares compatible open sessions across requesters, suppresses compatibility-backed duplicate sources when a native provider owns the same logical key, and expands audio-session diagnostics/Capability Inspector rendering for selected input, open sessions, bridge hits, storage status, and permission/device failures without exposing raw device labels or live audio handles.
- **Audio-monitoring control plane** — makes `audio-monitoring` the shared live-monitoring coordinator. Core now exposes provider registration/list/selection, explicit user-action `start`, requester-counted `stop`, prompt-free `inspect`/`monitoring.status`, and `set-direct-monitor` through the capability runtime. Monitoring starts integrate with selected `audio-input` readiness, background requesters can only attach to active compatible sessions, active sessions survive song/playback stops without auto-resuming after reload, native providers suppress compatibility-backed legacy monitor paths, and diagnostics/Capability Inspector now show providers, sessions, requesters, direct-monitor state, bridge hits, and distinct safe outcomes (`provider-selection-required`, `user-action-required`, `incompatible`, `unavailable`, `stopped`, etc.) without exposing raw audio/device data. - **Audio-monitoring control plane** — makes `audio-monitoring` the shared live-monitoring coordinator. Core now exposes provider registration/list/selection, explicit user-action `start`, requester-counted `stop`, prompt-free `inspect`/`monitoring.status`, and `set-direct-monitor` through the capability runtime. Monitoring starts integrate with selected `audio-input` readiness, background requesters can only attach to active compatible sessions, active sessions survive song/playback stops without auto-resuming after reload, native providers suppress compatibility-backed legacy monitor paths, and diagnostics/Capability Inspector now show providers, sessions, requesters, direct-monitor state, bridge hits, and distinct safe outcomes (`provider-selection-required`, `user-action-required`, `incompatible`, `unavailable`, `stopped`, etc.) without exposing raw audio/device data.
- **Playback control plane** — promotes `playback` to an active core capability domain for song transport, timing, loop, route, requester/observer, bridge, and diagnostics state. Core now exposes `inspect`, user-authorized `start`, `pause`, `resume`, `stop`, `seek`, `set-loop`, and `clear-loop` through the capability runtime while `static/app.js` keeps raw `<audio>`/JUCE handles private behind a redaction-safe adapter. Playback diagnostics use pseudonymous targets in exported bundles, local display labels only in the Capability Inspector, bounded recent outcomes/events, and bridge accounting for `window.playSong`, legacy `song:*` events, `window.slopsmith` transport helpers, loop helpers, and browser/native route handoff. - **Playback control plane** — promotes `playback` to an active core capability domain for song transport, timing, loop, route, requester/observer, bridge, and diagnostics state. Core now exposes `inspect`, user-authorized `start`, `pause`, `resume`, `stop`, `seek`, `set-loop`, and `clear-loop` through the capability runtime while `static/app.js` keeps raw `<audio>`/JUCE handles private behind a redaction-safe adapter. Playback diagnostics use pseudonymous targets in exported bundles, local display labels only in the Capability Inspector, bounded recent outcomes/events, and bridge accounting for `window.playSong`, legacy `song:*` events, `window.feedBack` transport helpers, loop helpers, and browser/native route handoff.
- **3D highway — Tone HUD, fret dividers, chord-diagram toggle, FPS counter.** The bundled `plugins/highway_3d` gains an amber **Tone-change HUD** (shows the active tone and the next scheduled tone change; position / size / visibility configurable in settings), a **fret-dividers** toggle (vertical dividers on the highway, on by default, via `h3dBgSetFretDividersVisible`), a **chord-diagram visibility** toggle (`h3dBgSetChordDiagramVisible`), and an **FPS counter** setting migrated to `BG_DEFAULTS.fpsVisible` (drops the legacy `h3d_showFps` localStorage key). Chord-diagram position is restricted to `tl`/`tr`; legacy `bl`/`br` values are coerced on load. Perf: accent-halo shell descriptors are pre-built per string in `initScene()` and the chord-verdict cache key is encoded as a number, eliminating per-frame allocations in the `drawNote()` and chord hot paths. - **3D highway — Tone HUD, fret dividers, chord-diagram toggle, FPS counter.** The bundled `plugins/highway_3d` gains an amber **Tone-change HUD** (shows the active tone and the next scheduled tone change; position / size / visibility configurable in settings), a **fret-dividers** toggle (vertical dividers on the highway, on by default, via `h3dBgSetFretDividersVisible`), a **chord-diagram visibility** toggle (`h3dBgSetChordDiagramVisible`), and an **FPS counter** setting migrated to `BG_DEFAULTS.fpsVisible` (drops the legacy `h3d_showFps` localStorage key). Chord-diagram position is restricted to `tl`/`tr`; legacy `bl`/`br` values are coerced on load. Perf: accent-halo shell descriptors are pre-built per string in `initScene()` and the chord-verdict cache key is encoded as a number, eliminating per-frame allocations in the `drawNote()` and chord hot paths.
- **Sloppak assembly preserves a short preview clip.** When a source chart carries a separate short browser-preview audio clip alongside the full song, the sloppak assembler now decodes it to `preview.ogg` at the sloppak root and records it under a new top-level `preview:` manifest key (POSIX relpath, same shape as `lyrics`/`cover`). A failed preview decode is logged at debug and skipped without aborting the overall build. Sources with no separate preview are unaffected. Older sloppak readers ignore the unknown `preview` key, so the change is purely additive (sloppak-spec.md §5.5 backward-compat). Documented in `docs/sloppak-spec.md` §2 alongside the other optional top-level keys. Enables [`slopsmith-plugin-song-preview`](https://github.com/got-feedback/feedback-plugin-song-preview) to render hover-to-listen previews for sloppaks without seeking into the full audio. - **Sloppak assembly preserves a short preview clip.** When a source chart carries a separate short browser-preview audio clip alongside the full song, the sloppak assembler now decodes it to `preview.ogg` at the sloppak root and records it under a new top-level `preview:` manifest key (POSIX relpath, same shape as `lyrics`/`cover`). A failed preview decode is logged at debug and skipped without aborting the overall build. Sources with no separate preview are unaffected. Older sloppak readers ignore the unknown `preview` key, so the change is purely additive (sloppak-spec.md §5.5 backward-compat). Documented in `docs/sloppak-spec.md` §2 alongside the other optional top-level keys. Enables [`feedBack-plugin-song-preview`](https://github.com/got-feedback/feedBack-plugin-song-preview) to render hover-to-listen previews for sloppaks without seeking into the full audio.
- **Generic plugin asset route** — `GET /api/plugins/{plugin_id}/assets/{path}` serves arbitrary static files a plugin bundles under its own `assets/` directory (AudioWorklet modules, WASM, images, etc.), so plugins can self-host browser-fetchable assets without a CDN (Principle II). Containment is enforced by `lib/safepath.safe_join` against `<plugin>/assets/`, so `..` traversal, absolute paths, and NUL bytes cannot escape `assets/` to reach a plugin's Python modules. `.js` is served as `application/javascript`. First consumer: the stems plugin's pitch-preserving time-stretch worklet. - **Generic plugin asset route** — `GET /api/plugins/{plugin_id}/assets/{path}` serves arbitrary static files a plugin bundles under its own `assets/` directory (AudioWorklet modules, WASM, images, etc.), so plugins can self-host browser-fetchable assets without a CDN (Principle II). Containment is enforced by `lib/safepath.safe_join` against `<plugin>/assets/`, so `..` traversal, absolute paths, and NUL bytes cannot escape `assets/` to reach a plugin's Python modules. `.js` is served as `application/javascript`. First consumer: the stems plugin's pitch-preserving time-stretch worklet.
- **Minigames framework — bundled as a core plugin (`plugins/minigames/`).** Promotes the upstream [`slopsmith-plugin-minigames`](https://github.com/got-feedback/feedback-plugin-minigames) repo into the core bundle so every Slopsmith install gets the framework out of the box (same promotion path used for `highway_3d`). The plugin adds a top-level **Minigames** nav link (alongside Library / Favorites / Upload — not buried in the Plugins dropdown), a library-style card grid of installed minigame plugins, and a shared profile layer (XP, level, per-game leaderboards, cross-minigame unlocks) persisted under `CONFIG_DIR/minigames/` and opted into the settings export. Other plugins that want to ship a minigame add a `minigame` block to their `plugin.json` and call `window.slopsmithMinigames.register(spec)`; the SDK exposes scoring (`createContinuous` runs a self-contained YIN tracker; `createDiscrete` / `createChord` wrap `note_detect`'s `createNoteDetector`), HUD primitives, run persistence, and a scheduler so individual minigames don't need their own DSP or backend. Backend endpoints live under `/api/plugins/minigames/{runs,profile,registry}`. The framework is plugin-shaped (not core code) per Principle III, but bundled so it ships with every install. First consumer: [`slopsmith-plugin-flappy-bend`](https://github.com/got-feedback/feedback-plugin-flappy-bend), shipped separately. - **Minigames framework — bundled as a core plugin (`plugins/minigames/`).** Promotes the upstream [`feedBack-plugin-minigames`](https://github.com/got-feedback/feedBack-plugin-minigames) repo into the core bundle so every FeedBack install gets the framework out of the box (same promotion path used for `highway_3d`). The plugin adds a top-level **Minigames** nav link (alongside Library / Favorites / Upload — not buried in the Plugins dropdown), a library-style card grid of installed minigame plugins, and a shared profile layer (XP, level, per-game leaderboards, cross-minigame unlocks) persisted under `CONFIG_DIR/minigames/` and opted into the settings export. Other plugins that want to ship a minigame add a `minigame` block to their `plugin.json` and call `window.feedBackMinigames.register(spec)`; the SDK exposes scoring (`createContinuous` runs a self-contained YIN tracker; `createDiscrete` / `createChord` wrap `note_detect`'s `createNoteDetector`), HUD primitives, run persistence, and a scheduler so individual minigames don't need their own DSP or backend. Backend endpoints live under `/api/plugins/minigames/{runs,profile,registry}`. The framework is plugin-shaped (not core code) per Principle III, but bundled so it ships with every install. First consumer: [`feedBack-plugin-flappy-bend`](https://github.com/got-feedback/feedBack-plugin-flappy-bend), shipped separately.
- **Alpha-build heads-up banner** — when `/api/version` reports a version string containing "alpha" (case-insensitive), an amber banner appears at the top of the library section warning users that the build is in active development and may have bugs or breaking changes. The banner stays hidden on stable / beta / RC builds. No persistence or dismiss state — it's a passive notice, not a modal. - **Alpha-build heads-up banner** — when `/api/version` reports a version string containing "alpha" (case-insensitive), an amber banner appears at the top of the library section warning users that the build is in active development and may have bugs or breaking changes. The banner stays hidden on stable / beta / RC builds. No persistence or dismiss state — it's a passive notice, not a modal.
- **Drum vocabulary expanded to 18 pieces** — adds `stack` (MIDI 30, from GM's extended-percussion range, unused by real drum-kit MIDIs) and `bell` (MIDI 80 "Mute Triangle", also unused in real drum-kit MIDIs) to `lib/drums.py` PIECES. Inserted in the iteration order so the editor / highway lane ordering is *hi-hat → stack → crash → … → ride bell → bell*. Both are cymbals; default shape `circle_jagged` (stack) / `circle_dot` (bell). Old drum tabs round-trip unchanged — the schema is permissive and existing piece-ids are untouched. - **Drum vocabulary expanded to 18 pieces** — adds `stack` (MIDI 30, from GM's extended-percussion range, unused by real drum-kit MIDIs) and `bell` (MIDI 80 "Mute Triangle", also unused in real drum-kit MIDIs) to `lib/drums.py` PIECES. Inserted in the iteration order so the editor / highway lane ordering is *hi-hat → stack → crash → … → ride bell → bell*. Both are cymbals; default shape `circle_jagged` (stack) / `circle_dot` (bell). Old drum tabs round-trip unchanged — the schema is permissive and existing piece-ids are untouched.
- **GP / MIDI drum import surfaces unmapped notes** — `convert_drum_track_to_drumtab` (`lib/gp2rs.py`) and `convert_drum_track_from_midi` (`lib/midi_import.py`) gain an optional keyword-only `out_unmapped` parameter. Callers that pass an empty dict receive a per-MIDI record of every silently-skipped percussion note (`{midi: {"count": int, "times": [float, ...]}}`, times capped at 100 samples per note). This lets the editor plugin show a warning + manual-mapping UI on import instead of silently dropping unmapped notes. Default behavior unchanged for callers that don't opt in. - **GP / MIDI drum import surfaces unmapped notes** — `convert_drum_track_to_drumtab` (`lib/gp2rs.py`) and `convert_drum_track_from_midi` (`lib/midi_import.py`) gain an optional keyword-only `out_unmapped` parameter. Callers that pass an empty dict receive a per-MIDI record of every silently-skipped percussion note (`{midi: {"count": int, "times": [float, ...]}}`, times capped at 100 samples per note). This lets the editor plugin show a warning + manual-mapping UI on import instead of silently dropping unmapped notes. Default behavior unchanged for callers that don't opt in.
- **Drum support from scratch** — drums are now a first-class arrangement type with their own JSON payload on disk and their own WS stream to the highway. New `lib/drums.py` defines the closed piece-id vocabulary (kick, snare, snare_xstick, hh_closed/open/pedal, tom_hi/mid/low/floor, crash_l/r, splash, china, ride, ride_bell), default GM-MIDI mappings, three preset lane configurations, and a permissive `drum_tab.json` validator. `lib/sloppak.py::load_song` reads the manifest's optional top-level `drum_tab:` key, parses + validates the JSON, and surfaces it on `LoadedSloppak.drum_tab`; the load stays permissive so a missing or malformed tab silently disables drums rather than failing the sloppak load. `/ws/highway/{filename}` gains two new message types — `drum_tab` (metadata + kit legend) and chunked `drum_hits` (500 hits per frame, same chunking as notes) — exposed to renderers via `bundle.drumTab`. `song_info` carries a `has_drum_tab` flag so viz pickers can auto-activate the drums highway regardless of which guitar arrangement is selected. `lib/gp2rs.py::convert_drum_track_to_drumtab` converts a Guitar Pro drum track to a `drum_tab.json` dict, preserving velocity verbatim, mapping hi-hat openness through the canonical piece-ids, and flagging flam / ghost / cymbal-choke articulations from GP effects. `lib/midi_import.py` gains `list_drum_tracks` + `convert_drum_track_from_midi` (channel-9 only) with heuristic flam-collapse (≤30 ms same-piece) and choke detection (cymbal note-off ≤120 ms). `docs/sloppak-spec.md` §5.3 promotes drum_tab from worked-example to canonical with the closed piece-id table and wire format. Sloppaks without a drum_tab are unaffected; legacy drums-as-guitar-notes sloppaks keep playing via the drums plugin's fallback decoder. - **Drum support from scratch** — drums are now a first-class arrangement type with their own JSON payload on disk and their own WS stream to the highway. New `lib/drums.py` defines the closed piece-id vocabulary (kick, snare, snare_xstick, hh_closed/open/pedal, tom_hi/mid/low/floor, crash_l/r, splash, china, ride, ride_bell), default GM-MIDI mappings, three preset lane configurations, and a permissive `drum_tab.json` validator. `lib/sloppak.py::load_song` reads the manifest's optional top-level `drum_tab:` key, parses + validates the JSON, and surfaces it on `LoadedSloppak.drum_tab`; the load stays permissive so a missing or malformed tab silently disables drums rather than failing the sloppak load. `/ws/highway/{filename}` gains two new message types — `drum_tab` (metadata + kit legend) and chunked `drum_hits` (500 hits per frame, same chunking as notes) — exposed to renderers via `bundle.drumTab`. `song_info` carries a `has_drum_tab` flag so viz pickers can auto-activate the drums highway regardless of which guitar arrangement is selected. `lib/gp2rs.py::convert_drum_track_to_drumtab` converts a Guitar Pro drum track to a `drum_tab.json` dict, preserving velocity verbatim, mapping hi-hat openness through the canonical piece-ids, and flagging flam / ghost / cymbal-choke articulations from GP effects. `lib/midi_import.py` gains `list_drum_tracks` + `convert_drum_track_from_midi` (channel-9 only) with heuristic flam-collapse (≤30 ms same-piece) and choke detection (cymbal note-off ≤120 ms). `docs/sloppak-spec.md` §5.3 promotes drum_tab from worked-example to canonical with the closed piece-id table and wire format. Sloppaks without a drum_tab are unaffected; legacy drums-as-guitar-notes sloppaks keep playing via the drums plugin's fallback decoder.
- **Loose folder support** — a directory containing an audio file + arrangement XMLs, with optional `manifest.json` and album art, is now discovered, indexed, and playable as a first-class library format alongside Sloppak. The scanner walks `DLC_DIR` for non-preview audio files and treats each parent directory that also contains XMLs as a loose song. Metadata follows a `manifest.json` → XML tags → folder-name priority chain (see `lib/loosefolder.py`). Songs are tagged `format: "loose"`, render an amber `FOLDER` badge in the library, and are filterable via the new "Folder" option in the format dropdown. Audio uses the shared vgmstream/`convert_wem` pipeline, cached under `AUDIO_CACHE_DIR`. The chart `<offset>` from the first non-vocals XML is now propagated to the frontend via `song_info.offset` and applied in `highway.setTime()` so loose folders authored against non-silence-padded audio stay in sync. Pairs with the companion `slopsmith-plugin-loosefolder` plugin which adds an in-player Fix Sync UI for nudging and saving offset corrections. - **Loose folder support** — a directory containing an audio file + arrangement XMLs, with optional `manifest.json` and album art, is now discovered, indexed, and playable as a first-class library format alongside Sloppak. The scanner walks `DLC_DIR` for non-preview audio files and treats each parent directory that also contains XMLs as a loose song. Metadata follows a `manifest.json` → XML tags → folder-name priority chain (see `lib/loosefolder.py`). Songs are tagged `format: "loose"`, render an amber `FOLDER` badge in the library, and are filterable via the new "Folder" option in the format dropdown. Audio uses the shared vgmstream/`convert_wem` pipeline, cached under `AUDIO_CACHE_DIR`. The chart `<offset>` from the first non-vocals XML is now propagated to the frontend via `song_info.offset` and applied in `highway.setTime()` so loose folders authored against non-silence-padded audio stay in sync. Pairs with the companion `feedBack-plugin-loosefolder` plugin which adds an in-player Fix Sync UI for nudging and saving offset corrections.
- Highway note-state hook (#254). New `highway.setNoteStateProvider(fn)` lets a scorer plugin publish a per-note judgment (`'hit'` / `'active'` for a sustain currently held correctly / `'miss'`, or `{ state, alpha, color }`) so the renderer lights up the **gem itself** on a correct hit and keeps a sustain trail glowing while it's still being played right — instead of a separate overlay ring near the note. The built-in 2D highway honors it in `drawNote` / `drawSustains` / the chord-frame path (bright string colour + additive halo on hits, bright vs dim sustain trail, faint red wash on misses); the bundled 3D highway reads the same data via `bundle.getNoteState` (bright string-tinted outline + bright body + glowing sustain on hit/active, red outline + suppressed body on miss). Custom renderers opt in by calling `bundle.getNoteState(note, chartTime)`. note_detect registers the provider (and still owns its HUD / diagnostic miss markers / "currently detected" indicator); renderers that ignore the hook simply don't light gems. On a confirmed hit/active the renderers add a contained "sparkle/sizzle" on the note — the 2D highway: additive throbbing halo + flickering hot core + crackling spark lines (+ an expanding shockwave ring on a fresh strike) on the gem and a glowing/jittery sustain trail; the 3D highway: a few twinkling bright dots and short crackling arc segments hugging the note's rectangle (no bloom past the note), drawn on its overlay and projected through the camera so they ride the note. Also adds `highway.isDefaultRenderer()` so overlays that position with the 2D-highway helpers (`project` / `fretX`) can skip rendering when a custom renderer is active — fixes note_detect's miss markers appearing in random places over the 3D highway. New 3D-highway setting **Show note preview on the fretboard** (on by default) toggles the board-projection ghost — the translucent preview of the upcoming note on the fretboard surface. (Note: the companion change in the **note_detect plugin repo** turns its full-screen green/red edge flash off by default and adds a toggle to re-enable it — ships separately with note_detect, not in this slopsmith release.) - Highway note-state hook (#254). New `highway.setNoteStateProvider(fn)` lets a scorer plugin publish a per-note judgment (`'hit'` / `'active'` for a sustain currently held correctly / `'miss'`, or `{ state, alpha, color }`) so the renderer lights up the **gem itself** on a correct hit and keeps a sustain trail glowing while it's still being played right — instead of a separate overlay ring near the note. The built-in 2D highway honors it in `drawNote` / `drawSustains` / the chord-frame path (bright string colour + additive halo on hits, bright vs dim sustain trail, faint red wash on misses); the bundled 3D highway reads the same data via `bundle.getNoteState` (bright string-tinted outline + bright body + glowing sustain on hit/active, red outline + suppressed body on miss). Custom renderers opt in by calling `bundle.getNoteState(note, chartTime)`. note_detect registers the provider (and still owns its HUD / diagnostic miss markers / "currently detected" indicator); renderers that ignore the hook simply don't light gems. On a confirmed hit/active the renderers add a contained "sparkle/sizzle" on the note — the 2D highway: additive throbbing halo + flickering hot core + crackling spark lines (+ an expanding shockwave ring on a fresh strike) on the gem and a glowing/jittery sustain trail; the 3D highway: a few twinkling bright dots and short crackling arc segments hugging the note's rectangle (no bloom past the note), drawn on its overlay and projected through the camera so they ride the note. Also adds `highway.isDefaultRenderer()` so overlays that position with the 2D-highway helpers (`project` / `fretX`) can skip rendering when a custom renderer is active — fixes note_detect's miss markers appearing in random places over the 3D highway. New 3D-highway setting **Show note preview on the fretboard** (on by default) toggles the board-projection ghost — the translucent preview of the upcoming note on the fretboard surface. (Note: the companion change in the **note_detect plugin repo** turns its full-screen green/red edge flash off by default and adds a toggle to re-enable it — ships separately with note_detect, not in this feedBack release.)
- Diagnostic bundle export (#166). New "Export Diagnostics" + "Preview Bundle" buttons in Settings produce a single redacted zip combining server logs (tail of `LOG_FILE`), system info (Python/OS/version), hardware probe (CPU model + cores + freq + RAM, GPU via `nvidia-smi`/`rocm-smi`/`system_profiler`, container/Electron/bare runtime detection), full plugin inventory with git SHA + remote URL (read directly from `.git/HEAD` so it works in minimal runtime images without `git` installed) + orphan/failed-to-load detection, the browser console transcript (all levels: log/info/warn/error/debug + window.onerror + unhandledrejection, 500-entry ring buffer), browser hardware (WebGL/WebGPU adapter info, navigator + userAgentData), filtered localStorage, and per-plugin contributed diagnostics. Top-level `manifest.json` lists every file with its versioned schema id (`system.hardware.v1`, `client.console.v1`, etc.) so AI agents can dispatch by schema. Redaction is on by default: DLC paths, song filenames (`<song:HASH8>` stable per-bundle), IPv4/IPv6 addresses, bearer tokens, and `key=`/`token=` query strings are replaced. Plugins opt their backend diagnostics in via a new `diagnostics` manifest field (`server_files` allowlist mirroring `settings.server_files` semantics, plus an optional `callable: "<module>:<function>"` resolved lazily via `load_sibling`). Frontend plugins push diagnostics via `window.slopsmith.diagnostics.contribute(plugin_id, payload)`. Three new endpoints: `POST /api/diagnostics/export`, `GET /api/diagnostics/preview`, `GET /api/diagnostics/hardware`. Full bundle format spec in `docs/diagnostics-bundle-spec.md`. - Diagnostic bundle export (#166). New "Export Diagnostics" + "Preview Bundle" buttons in Settings produce a single redacted zip combining server logs (tail of `LOG_FILE`), system info (Python/OS/version), hardware probe (CPU model + cores + freq + RAM, GPU via `nvidia-smi`/`rocm-smi`/`system_profiler`, container/Electron/bare runtime detection), full plugin inventory with git SHA + remote URL (read directly from `.git/HEAD` so it works in minimal runtime images without `git` installed) + orphan/failed-to-load detection, the browser console transcript (all levels: log/info/warn/error/debug + window.onerror + unhandledrejection, 500-entry ring buffer), browser hardware (WebGL/WebGPU adapter info, navigator + userAgentData), filtered localStorage, and per-plugin contributed diagnostics. Top-level `manifest.json` lists every file with its versioned schema id (`system.hardware.v1`, `client.console.v1`, etc.) so AI agents can dispatch by schema. Redaction is on by default: DLC paths, song filenames (`<song:HASH8>` stable per-bundle), IPv4/IPv6 addresses, bearer tokens, and `key=`/`token=` query strings are replaced. Plugins opt their backend diagnostics in via a new `diagnostics` manifest field (`server_files` allowlist mirroring `settings.server_files` semantics, plus an optional `callable: "<module>:<function>"` resolved lazily via `load_sibling`). Frontend plugins push diagnostics via `window.feedBack.diagnostics.contribute(plugin_id, payload)`. Three new endpoints: `POST /api/diagnostics/export`, `GET /api/diagnostics/preview`, `GET /api/diagnostics/hardware`. Full bundle format spec in `docs/diagnostics-bundle-spec.md`.
- Structured logging bootstrap (phase 1 of #155). Three new environment variables control server log output: `LOG_LEVEL` (default `INFO`), `LOG_FORMAT` (`text` for coloured console, `json` for one-JSON-object-per-line suitable for Loki/ELK/Promtail), and `LOG_FILE` (optional path, rotated at 10 MB with 5 backups). HTTP responses now include a `X-Request-ID` correlation header (via `CorrelationIdMiddleware`); the same request ID appears as `request_id` in structured log lines emitted via the stdlib `logging` / `structlog` APIs during that request. - Structured logging bootstrap (phase 1 of #155). Three new environment variables control server log output: `LOG_LEVEL` (default `INFO`), `LOG_FORMAT` (`text` for coloured console, `json` for one-JSON-object-per-line suitable for Loki/ELK/Promtail), and `LOG_FILE` (optional path, rotated at 10 MB with 5 backups). HTTP responses now include a `X-Request-ID` correlation header (via `CorrelationIdMiddleware`); the same request ID appears as `request_id` in structured log lines emitted via the stdlib `logging` / `structlog` APIs during that request.
- Structured logging migration completed (phase 2 of #155, #159, #242). The 42 `print()` calls and 6 `traceback.print_exc()` calls across `server.py` and `lib/` have been migrated to levelled `slopsmith.*` loggers. Silent `except: pass` blocks in `lib/sloppak.py` and `lib/sloppak_convert.py` now surface as `log.warning` / `log.debug` with the exception attached. WebSocket handlers (`highway_ws`, `ws_retune`) bind a `ws_conn_id` contextvar at accept time so every log line within a session carries a connection ID. A CI grep guard in `.github/workflows/tests.yml` fails the build if either `print(` or `traceback.print_exc(` reappears in `server.py` or `lib/`. - Structured logging migration completed (phase 2 of #155, #159, #242). The 42 `print()` calls and 6 `traceback.print_exc()` calls across `server.py` and `lib/` have been migrated to levelled `feedBack.*` loggers. Silent `except: pass` blocks in `lib/sloppak.py` and `lib/sloppak_convert.py` now surface as `log.warning` / `log.debug` with the exception attached. WebSocket handlers (`highway_ws`, `ws_retune`) bind a `ws_conn_id` contextvar at accept time so every log line within a session carries a connection ID. A CI grep guard in `.github/workflows/tests.yml` fails the build if either `print(` or `traceback.print_exc(` reappears in `server.py` or `lib/`.
- **Lyrics Karaoke plugin** — end-to-end karaoke setup for Sloppak songs in one workflow. The setup screen shows a per-song checklist (vocals stem / synced lyrics / per-syllable pitch) and a single "Build Karaoke" button that runs whatever's missing: Whisper alignment of pasted lyric text against the vocals stem, then `librosa.pyin` pitch extraction. Both artifacts persist inside the Sloppak (`lyrics.json`, `vocal_pitch.json`). In the player, a "Karaoke" toggle swaps the text-lyrics overlay for a horizontal pitch ribbon (one bar per syllable, vertically positioned by pitch, sweeping playhead). - **Lyrics Karaoke plugin** — end-to-end karaoke setup for Sloppak songs in one workflow. The setup screen shows a per-song checklist (vocals stem / synced lyrics / per-syllable pitch) and a single "Build Karaoke" button that runs whatever's missing: Whisper alignment of pasted lyric text against the vocals stem, then `librosa.pyin` pitch extraction. Both artifacts persist inside the Sloppak (`lyrics.json`, `vocal_pitch.json`). In the player, a "Karaoke" toggle swaps the text-lyrics overlay for a horizontal pitch ribbon (one bar per syllable, vertically positioned by pitch, sweeping playhead).
- Settings export/import (#113). Two buttons on the Settings page bundle server config, browser localStorage, and opted-in plugin server-side files into a single versioned JSON file for backup, migration, or sharing a calibrated setup. Server-side import is all-or-nothing for safety-critical failures: phase-1 validates the entire bundle (schema, path-traversal, encoding) before any disk writes; phase-2 commits each file via temp+rename. Plugin-state mismatches between export and import are handled leniently: files referenced for a plugin that isn't loaded are skipped with a warning, files referenced for a plugin whose manifest no longer declares them are skipped with a warning, and localStorage is merged (not cleared) so first-run defaults from plugins installed after the export are preserved. Path-traversal, absolute paths, schema mismatch, and decode failures remain hard refusals. Plugins opt their server-side files in by declaring `settings.server_files` in `plugin.json` (list of relpaths under `CONFIG_DIR`; trailing `/` denotes a directory). - Settings export/import (#113). Two buttons on the Settings page bundle server config, browser localStorage, and opted-in plugin server-side files into a single versioned JSON file for backup, migration, or sharing a calibrated setup. Server-side import is all-or-nothing for safety-critical failures: phase-1 validates the entire bundle (schema, path-traversal, encoding) before any disk writes; phase-2 commits each file via temp+rename. Plugin-state mismatches between export and import are handled leniently: files referenced for a plugin that isn't loaded are skipped with a warning, files referenced for a plugin whose manifest no longer declares them are skipped with a warning, and localStorage is merged (not cleared) so first-run defaults from plugins installed after the export are preserved. Path-traversal, absolute paths, schema mismatch, and decode failures remain hard refusals. Plugins opt their server-side files in by declaring `settings.server_files` in `plugin.json` (list of relpaths under `CONFIG_DIR`; trailing `/` denotes a directory).
- Library filtering by parts present or missing (#129, #69). New right-side Filters drawer (single button next to the format/sort row, with active-filter count badge and dismissible chips below) lets you require or exclude arrangements (Lead/Rhythm/Bass/Combo), specific stems on Sloppaks (drums/bass/vocals/piano/other), lyrics, and tuning. Multi-select within an axis is OR (Lead OR Rhythm); cross-axis is AND. State persists across reloads. New endpoint `GET /api/library/tuning-names` returns distinct tunings present in the library, ordered by musical distance. - Library filtering by parts present or missing (#129, #69). New right-side Filters drawer (single button next to the format/sort row, with active-filter count badge and dismissible chips below) lets you require or exclude arrangements (Lead/Rhythm/Bass/Combo), specific stems on Sloppaks (drums/bass/vocals/piano/other), lyrics, and tuning. Multi-select within an axis is OR (Lead OR Rhythm); cross-axis is AND. State persists across reloads. New endpoint `GET /api/library/tuning-names` returns distinct tunings present in the library, ordered by musical distance.
@@ -89,17 +89,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`highway.getLyrics()` accessor.** `createHighway()` now exposes the parsed timed lyric syllables (`[{t, d, w}]`) via `getLyrics()`, mirroring `getBeats()`/`getSections()`, so overlay plugins can render karaoke without opening a second highway WebSocket. Pure accessor; no behavior change. - **`highway.getLyrics()` accessor.** `createHighway()` now exposes the parsed timed lyric syllables (`[{t, d, w}]`) via `getLyrics()`, mirroring `getBeats()`/`getSections()`, so overlay plugins can render karaoke without opening a second highway WebSocket. Pure accessor; no behavior change.
### Changed ### Changed
- **Perf (3D highway, slopsmith#226)**: pre-warm `plugins/highway_3d/screen.js` object pools at board init. Previously the pool factory grew lazily on first `.get()` past the high-water mark, allocating a fresh `T.Mesh` mid-rAF on dense 7/8-string charts and stalling those frames; the meshes were then permanently added to `noteG` (the pool only hides on `reset()`, never removes), bloating the scene graph for the rest of the session. Pre-warming spends the cost up front. Fold the per-frame `updateStringHighlights()` per-string loop with the post-call `mGlow`/`mAccentCore` emissive writes — one walk over the per-string scratch arrays instead of two. Replace `longestConsecutiveRun`'s per-call array allocations with a `{start, len}` index pair (trades two per-call sub-array allocations for one small 2-key object — net reduction in per-visible-chord allocation churn). Opt-in perf bench harness via `?h3dbench=1` URL param: `console.log` p50/p95/max for six update() segments every 5 seconds; when the URL flag is absent the mark helpers are bound to empty functions at renderer-instance init (each `createHighway()` panel re-checks the flag), so the hot-path calls are no-ops with negligible overhead (typically JIT-inlined). - **Perf (3D highway, feedBack#226)**: pre-warm `plugins/highway_3d/screen.js` object pools at board init. Previously the pool factory grew lazily on first `.get()` past the high-water mark, allocating a fresh `T.Mesh` mid-rAF on dense 7/8-string charts and stalling those frames; the meshes were then permanently added to `noteG` (the pool only hides on `reset()`, never removes), bloating the scene graph for the rest of the session. Pre-warming spends the cost up front. Fold the per-frame `updateStringHighlights()` per-string loop with the post-call `mGlow`/`mAccentCore` emissive writes — one walk over the per-string scratch arrays instead of two. Replace `longestConsecutiveRun`'s per-call array allocations with a `{start, len}` index pair (trades two per-call sub-array allocations for one small 2-key object — net reduction in per-visible-chord allocation churn). Opt-in perf bench harness via `?h3dbench=1` URL param: `console.log` p50/p95/max for six update() segments every 5 seconds; when the URL flag is absent the mark helpers are bound to empty functions at renderer-instance init (each `createHighway()` panel re-checks the flag), so the hot-path calls are no-ops with negligible overhead (typically JIT-inlined).
- **License**: Relicensed to AGPL-3.0-only. Prior versions claimed MIT in the README, but the bundled desktop build statically links JUCE 8 (AGPL-3.0), so AGPL terms have effectively governed the desktop distribution since JUCE was added. AGPL-3.0-only is now the canonical license for the project — see [LICENSE](LICENSE) and [CONTRIBUTING.md](CONTRIBUTING.md) (DCO sign-off + plugin licensing policy). Bundled and vendored third-party code keeps its original license. - **License**: Relicensed to AGPL-3.0-only. Prior versions claimed MIT in the README, but the bundled desktop build statically links JUCE 8 (AGPL-3.0), so AGPL terms have effectively governed the desktop distribution since JUCE was added. AGPL-3.0-only is now the canonical license for the project — see [LICENSE](LICENSE) and [CONTRIBUTING.md](CONTRIBUTING.md) (DCO sign-off + plugin licensing policy). Bundled and vendored third-party code keeps its original license.
- Tuning sort is now ordered by musical distance from E Standard (#22) instead of alphabetical: E Standard first, then Drop D / F Standard at distance 2, then Eb Standard / F# Standard at distance 6, etc. Within a magnitude tier, down-tuned variants come before up-tuned, then alphabetical. - Tuning sort is now ordered by musical distance from E Standard (#22) instead of alphabetical: E Standard first, then Drop D / F Standard at distance 2, then Eb Standard / F# Standard at distance 6, etc. Within a magnitude tier, down-tuned variants come before up-tuned, then alphabetical.
- Settings page restructured into separate "Slopsmith" (core) and "Plugins" sections, with each plugin's settings rendered as a collapsible panel (collapsed by default). "Plugin Updates" moved into the Plugins section. - Settings page restructured into separate "FeedBack" (core) and "Plugins" sections, with each plugin's settings rendered as a collapsible panel (collapsed by default). "Plugin Updates" moved into the Plugins section.
- **Lyrics Sync** is now a redirect stub. Its alignment + save endpoints moved into the new Lyrics Karaoke plugin alongside the pitch extraction. Existing nav entries and bookmarks land on a "moved" page that auto-redirects to the merged plugin. - **Lyrics Sync** is now a redirect stub. Its alignment + save endpoints moved into the new Lyrics Karaoke plugin alongside the pitch extraction. Existing nav entries and bookmarks land on a "moved" page that auto-redirects to the merged plugin.
### Security ### Security
- **Path traversal in archive extractors and library path resolution.** `lib/sloppak.py::_unpack_zip` and `server.py::_resolve_dlc_path` previously concatenated attacker-controlled entry names or filenames directly onto the extraction or library directory, so a crafted sloppak zip member or library filename with `..` segments, an absolute path, or backslash separators could write or read outside the intended directory. Any code path that unpacks a user-supplied archive (library upload, click-to-play, retune) or resolves a library path was reachable. Both locations now delegate to a new `lib/safepath.py::safe_join` helper that resolves each destination once and rejects entries that don't fall under the target directory; rejected entries are logged and skipped, the rest of the archive still extracts. The stem-split paths in `lib/sloppak_convert.py::split_stems` and `scripts/split_stems.py` previously called `ZipFile.extractall()` directly on user-supplied sloppaks; both now delegate to the same hardened `lib/sloppak.py::_unpack_zip` so every sloppak-unzip site in the codebase shares one containment guarantee. Tests in `tests/test_archive_traversal.py` and `tests/test_safepath.py` pin the contract for `../`, deep traversal, absolute paths, mixed `subdir/../../` forms, Windows-style separators, NUL bytes, names that resolve to the unpack root, and symlinked roots. - **Path traversal in archive extractors and library path resolution.** `lib/sloppak.py::_unpack_zip` and `server.py::_resolve_dlc_path` previously concatenated attacker-controlled entry names or filenames directly onto the extraction or library directory, so a crafted sloppak zip member or library filename with `..` segments, an absolute path, or backslash separators could write or read outside the intended directory. Any code path that unpacks a user-supplied archive (library upload, click-to-play, retune) or resolves a library path was reachable. Both locations now delegate to a new `lib/safepath.py::safe_join` helper that resolves each destination once and rejects entries that don't fall under the target directory; rejected entries are logged and skipped, the rest of the archive still extracts. The stem-split paths in `lib/sloppak_convert.py::split_stems` and `scripts/split_stems.py` previously called `ZipFile.extractall()` directly on user-supplied sloppaks; both now delegate to the same hardened `lib/sloppak.py::_unpack_zip` so every sloppak-unzip site in the codebase shares one containment guarantee. Tests in `tests/test_archive_traversal.py` and `tests/test_safepath.py` pin the contract for `../`, deep traversal, absolute paths, mixed `subdir/../../` forms, Windows-style separators, NUL bytes, names that resolve to the unpack root, and symlinked roots.
### Fixed ### Fixed
- E Standard retune now stays metadata-consistent across a chart's arrangement files (slopsmith-plugin-notedetect#50). Previously the retune path could shift the audio and update manifests while leaving some arrangement metadata untouched, so `load_song()` later exposed the *original* tuning at runtime. `lib/retune.py` now updates every arrangement's tuning metadata consistently before applying the E Standard tuning, and raises on a partial update instead of silently packing split tuning metadata. EStd files generated before this fix should be re-converted so their metadata is consistent. - E Standard retune now stays metadata-consistent across a chart's arrangement files (feedBack-plugin-notedetect#50). Previously the retune path could shift the audio and update manifests while leaving some arrangement metadata untouched, so `load_song()` later exposed the *original* tuning at runtime. `lib/retune.py` now updates every arrangement's tuning metadata consistently before applying the E Standard tuning, and raises on a partial update instead of silently packing split tuning metadata. EStd files generated before this fix should be re-converted so their metadata is consistent.
- Keyboard shortcut help now opens from the Player/3D Highway context when Linux/Electron reports Shift+Slash as `key="/"`, including while player controls such as the visualization picker are focused (#598). - Keyboard shortcut help now opens from the Player/3D Highway context when Linux/Electron reports Shift+Slash as `key="/"`, including while player controls such as the visualization picker are focused (#598).
- 3D Highway left-handed mode now has regression coverage for fret-axis mirroring, board rebuilds on runtime lefty changes, and mirrored camera state including the lookahead target and shoulder offset; the maintainer guide no longer claims the renderer ignores `bundle.lefty` (#321). - 3D Highway left-handed mode now has regression coverage for fret-axis mirroring, board rebuilds on runtime lefty changes, and mirrored camera state including the lookahead target and shoulder offset; the maintainer guide no longer claims the renderer ignores `bundle.lefty` (#321).
- Chord-level `fretHandMute` is now parsed into each note's `fret_hand_mute` (wire `fhm`) instead of being folded into `mute` (`mt`), matching `_parse_note` and preserving wire-format fidelity for both the template-expanded (synthetic-note) and explicit-`chordNote` paths. The 3D highway renders the fret-hand-mute X for `mt` *or* `fhm` notes, so the muted-chord overlay still shows. Also fixes the per-note fret-connector label vanishing exactly at the hit line (the fade now holds full opacity through `dt = 0`). - Chord-level `fretHandMute` is now parsed into each note's `fret_hand_mute` (wire `fhm`) instead of being folded into `mute` (`mt`), matching `_parse_note` and preserving wire-format fidelity for both the template-expanded (synthetic-note) and explicit-`chordNote` paths. The 3D highway renders the fret-hand-mute X for `mt` *or* `fhm` notes, so the muted-chord overlay still shows. Also fixes the per-note fret-connector label vanishing exactly at the hit line (the fade now holds full opacity through `dt = 0`).
@@ -108,8 +108,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Demucs stem split failing on Windows desktop with `OSError: Could not load this library: libtorchcodec_core4.dll` or `ImportError: TorchCodec is required for save_with_torchcodec`. The demucs subprocess now bootstraps a `torchaudio.save``soundfile.write` shim before importing demucs, sidestepping the torchcodec dependency entirely. The override stays in place across torchaudio versions — soundfile's WAV writes are behaviorally equivalent for demucs's float32 outputs. - Demucs stem split failing on Windows desktop with `OSError: Could not load this library: libtorchcodec_core4.dll` or `ImportError: TorchCodec is required for save_with_torchcodec`. The demucs subprocess now bootstraps a `torchaudio.save``soundfile.write` shim before importing demucs, sidestepping the torchcodec dependency entirely. The override stays in place across torchaudio versions — soundfile's WAV writes are behaviorally equivalent for demucs's float32 outputs.
- Splitscreen pop-out windows briefly flashed the library/song grid before showing the popped panel. A popup loads the full app (whose default screen, `#home`, is the library) and only swaps to the player once the splitscreen plugin loads; app init now detects `?ssFollower=1` and switches to the player screen up front, so the popup shows player chrome the whole time. - Splitscreen pop-out windows briefly flashed the library/song grid before showing the popped panel. A popup loads the full app (whose default screen, `#home`, is the library) and only swaps to the player once the splitscreen plugin loads; app init now detects `?ssFollower=1` and switches to the player screen up front, so the popup shows player chrome the whole time.
- Sloppak assembly dropped all tone data — affected sloppaks showed no signal chain in the Tones plugin and no tone-change markers on the highway. The assembler (`lib/sloppak_convert.py`) now lifts each arrangement's tones from the source chart via the new `lib/tones.py` helper and embeds them inline in the arrangement JSON under a `tones` key (`base`, `changes`, `definitions` — see `docs/sloppak-spec.md` §3.9). The highway WebSocket reads `base`/`changes` for sloppaks, and the Tones plugin (≥ 1.1.0) reads `definitions` to render the gear chain. Sloppaks built before this release carry no tone data and must be rebuilt from their source chart to gain it. - Sloppak assembly dropped all tone data — affected sloppaks showed no signal chain in the Tones plugin and no tone-change markers on the highway. The assembler (`lib/sloppak_convert.py`) now lifts each arrangement's tones from the source chart via the new `lib/tones.py` helper and embeds them inline in the arrangement JSON under a `tones` key (`base`, `changes`, `definitions` — see `docs/sloppak-spec.md` §3.9). The highway WebSocket reads `base`/`changes` for sloppaks, and the Tones plugin (≥ 1.1.0) reads `definitions` to render the gear chain. Sloppaks built before this release carry no tone data and must be rebuilt from their source chart to gain it.
- Tab View (slopsmith-plugin-tabview ≥ 3.0.1): the bottom row of tablature was permanently hidden behind the player controls bar (#336). The overlay reserved 60px at the *top* (clearing the transparent HUD) and extended all the way to the bottom of `#player`, where the opaque `#player-controls` (z-index 10) drew over the last row. The overlay now measures `#player-hud` and `#player-controls` dynamically and insets both edges; a `ResizeObserver` on the controls bar re-runs the inset when it wraps to a second row on narrow viewports. - Tab View (feedBack-plugin-tabview ≥ 3.0.1): the bottom row of tablature was permanently hidden behind the player controls bar (#336). The overlay reserved 60px at the *top* (clearing the transparent HUD) and extended all the way to the bottom of `#player`, where the opaque `#player-controls` (z-index 10) drew over the last row. The overlay now measures `#player-hud` and `#player-controls` dynamically and insets both edges; a `ResizeObserver` on the controls bar re-runs the inset when it wraps to a second row on narrow viewports.
- Tab View (slopsmith-plugin-tabview ≥ 3.0.1): the cursor highlight led playback by roughly one beat (#336). alphaTab snaps `tickPosition` to the start of the *next* beat, so the cursor would race ahead by 500ms+ at typical tempos. The plugin now sends `tickPosition` one beat earlier so the snap lands on the current beat, and the highlight overlay tracks the bar cursor (`.at-cursor-bar`) instead of the next-beat cursor (`.at-cursor-beat`). - Tab View (feedBack-plugin-tabview ≥ 3.0.1): the cursor highlight led playback by roughly one beat (#336). alphaTab snaps `tickPosition` to the start of the *next* beat, so the cursor would race ahead by 500ms+ at typical tempos. The plugin now sends `tickPosition` one beat earlier so the snap lands on the current beat, and the highlight overlay tracks the bar cursor (`.at-cursor-bar`) instead of the next-beat cursor (`.at-cursor-beat`).
### Migration notes ### Migration notes
- **Constitution amended to 1.1.0 (Principle II — Vanilla Frontend).** Prebuilt Tailwind (`static/tailwind.min.css`) is now codified as non-negotiable: no Play CDN / runtime CSS JIT anywhere, core or plugin. Plugin authors: a plugin that uses Tailwind classes not guaranteed in core — especially arbitrary values like `w-[37px]` — MUST ship its own compiled stylesheet via the new `styles` manifest key, built with `corePlugins.preflight = false`. Plugins that use only core-guaranteed utilities, or that ship no Tailwind at all, need no change. Contributors: after adding any Tailwind class to core or a bundled plugin, run `bash scripts/build-tailwind.sh` and commit the regenerated CSS, or the `tailwind-fresh` CI job fails. - **Constitution amended to 1.1.0 (Principle II — Vanilla Frontend).** Prebuilt Tailwind (`static/tailwind.min.css`) is now codified as non-negotiable: no Play CDN / runtime CSS JIT anywhere, core or plugin. Plugin authors: a plugin that uses Tailwind classes not guaranteed in core — especially arbitrary values like `w-[37px]` — MUST ship its own compiled stylesheet via the new `styles` manifest key, built with `corePlugins.preflight = false`. Plugins that use only core-guaranteed utilities, or that ship no Tailwind at all, need no change. Contributors: after adding any Tailwind class to core or a bundled plugin, run `bash scripts/build-tailwind.sh` and commit the regenerated CSS, or the `tailwind-fresh` CI job fails.
+54 -54
View File
@@ -1,6 +1,6 @@
# Slopsmith — AI Agent Guide # FeedBack — AI Agent Guide
Slopsmith is a self-hosted web app for browsing, playing, and practicing interactive music notation, built around its own open `.sloppak` chart format. Charts come from importing Guitar Pro (GP5/GP8) or MusicXML, or from authoring in the built-in editor. It runs as a Docker container with a FastAPI backend (`server.py`), vanilla JavaScript frontend (`static/`), shared Python libraries (`lib/`), and an extensive plugin system (`plugins/`). There are no frontend frameworks — everything is plain JS, HTML, and Tailwind CSS. FeedBack is a self-hosted web app for browsing, playing, and practicing interactive music notation, built around its own open `.sloppak` chart format. Charts come from importing Guitar Pro (GP5/GP8) or MusicXML, or from authoring in the built-in editor. It runs as a Docker container with a FastAPI backend (`server.py`), vanilla JavaScript frontend (`static/`), shared Python libraries (`lib/`), and an extensive plugin system (`plugins/`). There are no frontend frameworks — everything is plain JS, HTML, and Tailwind CSS.
## Architecture Quick Reference ## Architecture Quick Reference
@@ -62,26 +62,26 @@ All fields except `id` and `name` are optional. Plugins can have any combination
`styles` is the **opt-in** for self-hosted CSS (Principle II — prebuilt Tailwind, no Play CDN). Core's `static/tailwind.min.css` only contains classes scanned from core source at build time, so a plugin installed at runtime (community / NAS) that uses classes core didn't scan — especially arbitrary values like `text-[11px]` — renders unstyled. Declaring `styles` makes the frontend inject one versioned `<link rel="stylesheet">` into `<head>` (covering the plugin's screen *and* its settings panel) pointing at the plugin's own compiled stylesheet. The value is a **plugin-root-relative path that must live under `assets/`** (e.g. `"assets/plugin.css"`) so it serves through the sandboxed `/api/plugins/<id>/assets/...` route. Build it with `corePlugins: { preflight: false }` (utilities only — core ships the single base reset; don't duplicate it) and **never** the Tailwind Play CDN. Plugins that use only core-guaranteed utilities, or ship no Tailwind, omit `styles` and are byte-for-byte unaffected. Full authoring guide + scaffold: [docs/plugin-styles.md](docs/plugin-styles.md). `styles` is the **opt-in** for self-hosted CSS (Principle II — prebuilt Tailwind, no Play CDN). Core's `static/tailwind.min.css` only contains classes scanned from core source at build time, so a plugin installed at runtime (community / NAS) that uses classes core didn't scan — especially arbitrary values like `text-[11px]` — renders unstyled. Declaring `styles` makes the frontend inject one versioned `<link rel="stylesheet">` into `<head>` (covering the plugin's screen *and* its settings panel) pointing at the plugin's own compiled stylesheet. The value is a **plugin-root-relative path that must live under `assets/`** (e.g. `"assets/plugin.css"`) so it serves through the sandboxed `/api/plugins/<id>/assets/...` route. Build it with `corePlugins: { preflight: false }` (utilities only — core ships the single base reset; don't duplicate it) and **never** the Tailwind Play CDN. Plugins that use only core-guaranteed utilities, or ship no Tailwind, omit `styles` and are byte-for-byte unaffected. Full authoring guide + scaffold: [docs/plugin-styles.md](docs/plugin-styles.md).
`settings.server_files` is the **opt-in** for the unified Settings export/import flow (slopsmith#113). It's a list of relpaths under `context["config_dir"]` that the plugin wants included in user-triggered backups. A trailing `/` denotes a directory (recurse). Plugins that omit this field have no server-side files exported; their state lives entirely in browser `localStorage`, which is bundled wholesale on every export. Rules: `settings.server_files` is the **opt-in** for the unified Settings export/import flow (feedBack#113). It's a list of relpaths under `context["config_dir"]` that the plugin wants included in user-triggered backups. A trailing `/` denotes a directory (recurse). Plugins that omit this field have no server-side files exported; their state lives entirely in browser `localStorage`, which is bundled wholesale on every export. Rules:
- Relpaths only. Absolute paths, drive letters, `..` segments, and backslashes are rejected at load time with a `[Plugin]` warning. - Relpaths only. Absolute paths, drive letters, `..` segments, and backslashes are rejected at load time with a `[Plugin]` warning.
- The same allowlist is consulted at both export and import: a bundle that references a file the *importing host*'s manifest no longer declares is skipped with a warning (handles plugin updates between export and import). A bundle that references a file your host's manifest never declared is also skipped — no surprise writes. - The same allowlist is consulted at both export and import: a bundle that references a file the *importing host*'s manifest no longer declares is skipped with a warning (handles plugin updates between export and import). A bundle that references a file your host's manifest never declared is also skipped — no surprise writes.
- Files are encoded as `{"encoding": "json", "data": <parsed>}` for `.json` files that parse cleanly (diff-friendly), `{"encoding": "base64", "data": "..."}` otherwise (sqlite, model blobs, IRs). - Files are encoded as `{"encoding": "json", "data": <parsed>}` for `.json` files that parse cleanly (diff-friendly), `{"encoding": "base64", "data": "..."}` otherwise (sqlite, model blobs, IRs).
- Plugins own their internal data migration. Importing a bundle whose data schema predates your current code restores bytes verbatim — your plugin must cope at next load. - Plugins own their internal data migration. Importing a bundle whose data schema predates your current code restores bytes verbatim — your plugin must cope at next load.
- Symlinks are skipped on export and never followed on import. - Symlinks are skipped on export and never followed on import.
`diagnostics` is the **opt-in** for the troubleshooting bundle (slopsmith#166 — Settings → Export Diagnostics). Two independent fields: `diagnostics` is the **opt-in** for the troubleshooting bundle (feedBack#166 — Settings → Export Diagnostics). Two independent fields:
- `diagnostics.server_files` — same allowlist semantics as `settings.server_files`: relpaths under `context["config_dir"]`, no `..`, no abs paths, no backslashes, no leading dots. Files listed here are copied verbatim into `plugins/<plugin_id>/<relpath>` inside the bundle. Use this for snapshot-style state (small DB excerpts, model lists, last-error files). - `diagnostics.server_files` — same allowlist semantics as `settings.server_files`: relpaths under `context["config_dir"]`, no `..`, no abs paths, no backslashes, no leading dots. Files listed here are copied verbatim into `plugins/<plugin_id>/<relpath>` inside the bundle. Use this for snapshot-style state (small DB excerpts, model lists, last-error files).
- `diagnostics.callable``"<module>:<function>"` (e.g. `"diagnostics:collect"`). Resolved lazily via `load_sibling` when the user clicks Export, then called as `func({"plugin_id": "...", "config_dir": Path(...)})`. Return `dict`/`list` → written to `plugins/<id>/callable.json`; `bytes``callable.bin`; `str``callable.txt`. Exceptions are caught and appended to the bundle's `manifest.notes` — a buggy plugin never crashes the export. - `diagnostics.callable``"<module>:<function>"` (e.g. `"diagnostics:collect"`). Resolved lazily via `load_sibling` when the user clicks Export, then called as `func({"plugin_id": "...", "config_dir": Path(...)})`. Return `dict`/`list` → written to `plugins/<id>/callable.json`; `bytes``callable.bin`; `str``callable.txt`. Exceptions are caught and appended to the bundle's `manifest.notes` — a buggy plugin never crashes the export.
Plugins that omit the field contribute nothing to the bundle from the backend side. Frontend plugins can independently push state via `window.slopsmith.diagnostics.contribute(plugin_id, payload)` from their `screen.js` before the user hits Export. Bundle layout + per-file schemas: [docs/diagnostics-bundle-spec.md](docs/diagnostics-bundle-spec.md). Plugins that omit the field contribute nothing to the bundle from the backend side. Frontend plugins can independently push state via `window.feedBack.diagnostics.contribute(plugin_id, payload)` from their `screen.js` before the user hits Export. Bundle layout + per-file schemas: [docs/diagnostics-bundle-spec.md](docs/diagnostics-bundle-spec.md).
Best practices: Best practices:
- Embed your own `schema` field (e.g. `"my_plugin.diag.v1"`) in JSON returned by `callable` so future tooling can dispatch by version. - Embed your own `schema` field (e.g. `"my_plugin.diag.v1"`) in JSON returned by `callable` so future tooling can dispatch by version.
- Keep payloads small (< 100 KB). Diagnostics are not a backup channel — that's `settings.server_files`. - Keep payloads small (< 100 KB). Diagnostics are not a backup channel — that's `settings.server_files`.
- Don't include user secrets, API keys, or session tokens. The bundle is shared with maintainers / posted to GitHub issues. - Don't include user secrets, API keys, or session tokens. The bundle is shared with maintainers / posted to GitHub issues.
`type` is an optional role hint (slopsmith#36). Supported values: `type` is an optional role hint (feedBack#36). Supported values:
- `"visualization"` — plugin provides a highway renderer. Declaring this makes the plugin eligible for the main-player viz picker AND splitscreen's per-panel picker. Must pair with a `window.slopsmithViz_<id>` factory exporting the setRenderer contract below. - `"visualization"` — plugin provides a highway renderer. Declaring this makes the plugin eligible for the main-player viz picker AND splitscreen's per-panel picker. Must pair with a `window.feedBackViz_<id>` factory exporting the setRenderer contract below.
- Absent → no declared role; plugin is loaded and its script runs, but it doesn't appear in role-specific UIs. - Absent → no declared role; plugin is loaded and its script runs, but it doesn't appear in role-specific UIs.
**Backend routes**`routes.py` must export a `setup(app, context)` function. The `context` dict provides: **Backend routes**`routes.py` must export a `setup(app, context)` function. The `context` dict provides:
@@ -94,9 +94,9 @@ Best practices:
- `unregister_library_provider(provider_id)` — remove a plugin-provided library source by id. The built-in `local` provider cannot be removed. - `unregister_library_provider(provider_id)` — remove a plugin-provided library source by id. The built-in `local` provider cannot be removed.
- `get_sloppak_cache_dir()` — sloppak cache path - `get_sloppak_cache_dir()` — sloppak cache path
- `load_sibling(name)` — loads a sibling module from this plugin's directory under a unique, namespaced module name. See "Sibling imports" below. - `load_sibling(name)` — loads a sibling module from this plugin's directory under a unique, namespaced module name. See "Sibling imports" below.
- `log` — stdlib `logging.Logger` namespaced to `slopsmith.plugin.<id>`. Pre-configured with the app-wide level, format (including JSON mode), and correlation IDs. Use this for all backend plugin output instead of `print()`. See "Backend plugin logging" below. - `log` — stdlib `logging.Logger` namespaced to `feedBack.plugin.<id>`. Pre-configured with the app-wide level, format (including JSON mode), and correlation IDs. Use this for all backend plugin output instead of `print()`. See "Backend plugin logging" below.
**Sibling imports — use `load_sibling`, not bare imports** (slopsmith#33). The plugin loader inserts each plugin's directory onto `sys.path` so `from extractor import X` works, but Python caches imports by **module name** in `sys.modules`. Two plugins that each ship a top-level `extractor.py` (or any other generic name — `util.py`, `client.py`, `parser.py`, `config.py`, …) collide: whichever loads first wins, and the other plugin's `from extractor import X` either gets the wrong module or fails with `cannot import name 'X' from 'extractor'`. **Sibling imports — use `load_sibling`, not bare imports** (feedBack#33). The plugin loader inserts each plugin's directory onto `sys.path` so `from extractor import X` works, but Python caches imports by **module name** in `sys.modules`. Two plugins that each ship a top-level `extractor.py` (or any other generic name — `util.py`, `client.py`, `parser.py`, `config.py`, …) collide: whichever loads first wins, and the other plugin's `from extractor import X` either gets the wrong module or fails with `cannot import name 'X' from 'extractor'`.
The fix is `context["load_sibling"](name)`, which loads the sibling under a namespaced module name (`plugin_<id>.<name>`, where plugin_id is bijectively encoded so reverse-DNS-style ids like `com.example.foo` work without colliding: `_` -> `_5f_`, `.` -> `_2e_`) so each plugin gets its own copy: The fix is `context["load_sibling"](name)`, which loads the sibling under a namespaced module name (`plugin_<id>.<name>`, where plugin_id is bijectively encoded so reverse-DNS-style ids like `com.example.foo` work without colliding: `_` -> `_5f_`, `.` -> `_2e_`) so each plugin gets its own copy:
@@ -115,7 +115,7 @@ Notes:
- Repeat calls return the cached module. Concurrent first-time calls are serialized via per-module locks so no caller observes a half-initialized module. - Repeat calls return the cached module. Concurrent first-time calls are serialized via per-module locks so no caller observes a half-initialized module.
- Bare `import sibling` from `routes.py` still works during the transition period, but the loader prints a startup warning when it detects two plugins shipping a same-named top-level module — covering both `.py` files and package directories. Migrate to `load_sibling` to silence the warning and immunize your plugin from future ecosystem collisions. (Don't mix bare imports and `load_sibling` for the same module — they'd execute the file twice and split module-level state.) - Bare `import sibling` from `routes.py` still works during the transition period, but the loader prints a startup warning when it detects two plugins shipping a same-named top-level module — covering both `.py` files and package directories. Migrate to `load_sibling` to silence the warning and immunize your plugin from future ecosystem collisions. (Don't mix bare imports and `load_sibling` for the same module — they'd execute the file twice and split module-level state.)
**Frontend scripts**`screen.js` runs in the global scope via a `<script>` tag. It can access `window.playSong`, `window.showScreen`, `window.createHighway`, the `<audio>` element, and the `window.slopsmith` event emitter. **Frontend scripts**`screen.js` runs in the global scope via a `<script>` tag. It can access `window.playSong`, `window.showScreen`, `window.createHighway`, the `<audio>` element, and the `window.feedBack` event emitter.
**The playSong wrapper chain** — Plugins commonly wrap `window.playSong` to hook into song playback. Plugins load alphabetically, so the last-loaded (alphabetically later) wrapper runs first, while the alphabetically first plugin runs closest to the original. Be aware that `await` calls in inner wrappers yield to the event loop — WebSocket messages can arrive before outer wrappers finish setup. **The playSong wrapper chain** — Plugins commonly wrap `window.playSong` to hook into song playback. Plugins load alphabetically, so the last-loaded (alphabetically later) wrapper runs first, while the alphabetically first plugin runs closest to the original. Be aware that `await` calls in inner wrappers yield to the event loop — WebSocket messages can arrive before outer wrappers finish setup.
@@ -123,10 +123,10 @@ Notes:
### v3 UI (fee[dB]ack v0.3.0) — player-chrome contract ### v3 UI (fee[dB]ack v0.3.0) — player-chrome contract
v0.3.0 ships a redesigned UI behind a flag (`SLOPSMITH_UI=v3` or the `/v3` route); v0.3.0 ships a redesigned UI behind a flag (`FEEDBACK_UI=v3` or the `/v3` route);
the classic UI (v2) stays the default until 0.3.0 ships, so **plugins must work in the classic UI (v2) stays the default until 0.3.0 ships, so **plugins must work in
both**. v3 reuses the same engine (`server.py`, `app.js`, `highway.js`, `playSong`, both**. v3 reuses the same engine (`server.py`, `app.js`, `highway.js`, `playSong`,
`showScreen`, capabilities, library providers, the `window.slopsmithViz_<id>` / `showScreen`, capabilities, library providers, the `window.feedBackViz_<id>` /
`setRenderer` contract), so a plugin's **backend, capabilities, `nav`/`screen`, `setRenderer` contract), so a plugin's **backend, capabilities, `nav`/`screen`,
visualization renderers, diagnostics, and settings export work unchanged** — v3 visualization renderers, diagnostics, and settings export work unchanged** — v3
surfaces `nav` in its sidebar and mounts screens exactly as v2 does. surfaces `nav` in its sidebar and mounts screens exactly as v2 does.
@@ -140,8 +140,8 @@ control into it, you must adapt:
legacy way means your control **auto-hides**, and the legacy insertion anchors legacy way means your control **auto-hides**, and the legacy insertion anchors
(`insertBefore` the `span.text-gray-700` separator, or `button:last-child` / ✕ (`insertBefore` the `span.text-gray-700` separator, or `button:last-child` / ✕
Close) **don't exist in v3** → it lands wrong / unreachable. Close) **don't exist in v3** → it lands wrong / unreachable.
- **Detect v3** with `window.slopsmith.uiVersion === 'v3'` and **mount into - **Detect v3** with `window.feedBack.uiVersion === 'v3'` and **mount into
`window.slopsmith.ui.playerControlSlot()`** (a stable, always-reachable container `window.feedBack.ui.playerControlSlot()`** (a stable, always-reachable container
— the "Plugins" rail popover) instead of `#player-controls`. Drop the dead — the "Plugins" rail popover) instead of `#player-controls`. Drop the dead
anchors (append), and guard re-injection against the *actual* container anchors (append), and guard re-injection against the *actual* container
(`controls.contains(myBtn)`), not a hard-coded `#player-controls`. (`controls.contains(myBtn)`), not a hard-coded `#player-controls`.
@@ -197,18 +197,18 @@ usually an unrelated plugin's per-frame DOM work.
### Visualization plugins — two complementary contracts ### Visualization plugins — two complementary contracts
Slopsmith supports two ways for a plugin to participate in the main player's visuals. They coexist; the setRenderer contract is the default for any viz that draws a highway-shaped surface, and overlays handle layered decorations on top. FeedBack supports two ways for a plugin to participate in the main player's visuals. They coexist; the setRenderer contract is the default for any viz that draws a highway-shaped surface, and overlays handle layered decorations on top.
**Pick the right shape:** **Pick the right shape:**
- Replacing the whole highway drawing on the existing highway canvas (your renderer owns its rendering context / resources; `createHighway()` still owns the canvas element and the rAF loop)? → **setRenderer** (section 1). Enters the viz picker. Works in both the main player and per-panel under splitscreen. - Replacing the whole highway drawing on the existing highway canvas (your renderer owns its rendering context / resources; `createHighway()` still owns the canvas element and the rAF loop)? → **setRenderer** (section 1). Enters the viz picker. Works in both the main player and per-panel under splitscreen.
- Adding a layer on top of whichever viz is active? → **Overlay** (section 2). Navbar toggle, not in the picker. - Adding a layer on top of whichever viz is active? → **Overlay** (section 2). Navbar toggle, not in the picker.
#### 1. setRenderer contract (slopsmith#36) — preferred #### 1. setRenderer contract (feedBack#36) — preferred
Plugins that want to replace the main highway's draw function (per panel, per session) export a renderer factory on `window.slopsmithViz_<id>` where `<id>` matches the `id` in `plugin.json` (`type: "visualization"` required). The factory returns an object matching this shape: Plugins that want to replace the main highway's draw function (per panel, per session) export a renderer factory on `window.feedBackViz_<id>` where `<id>` matches the `id` in `plugin.json` (`type: "visualization"` required). The factory returns an object matching this shape:
```js ```js
window.slopsmithViz_my_viz = function () { window.feedBackViz_my_viz = function () {
return { return {
// Required canvas context type. Default '2d' if omitted. // Required canvas context type. Default '2d' if omitted.
// highway.js reads this BEFORE calling init() so it can // highway.js reads this BEFORE calling init() so it can
@@ -239,7 +239,7 @@ window.slopsmithViz_my_viz = function () {
// a bundle-level helper isn't provided because it would // a bundle-level helper isn't provided because it would
// need your renderer's own context, not the factory's. // need your renderer's own context, not the factory's.
// //
// bundle.getNoteState(note, chartTime) (slopsmith#254) — call // bundle.getNoteState(note, chartTime) (feedBack#254) — call
// this per visible chart note / chord-note to find out whether // this per visible chart note / chord-note to find out whether
// a scorer (note_detect) has flagged it 'hit' / 'active' (a // a scorer (note_detect) has flagged it 'hit' / 'active' (a
// sustain currently being held correctly) / 'miss', so the gem // sustain currently being held correctly) / 'miss', so the gem
@@ -283,25 +283,25 @@ Selecting this plugin in the main-player viz picker — or in splitscreen's per-
- **Canvas context-type swapping.** Browsers lock a `<canvas>` to the first context type successfully acquired for its lifetime: once `getContext('2d')` succeeds, `getContext('webgl2')` on that same canvas returns `null`, and vice versa. To let arbitrary 2D ⇄ WebGL renderer swaps work mid-session, `highway.setRenderer()` reads the next renderer's `contextType` before calling its `init()` and, if it differs from the type currently bound, replaces the underlying `<canvas>` element with a fresh one via `oldCanvas.cloneNode(false)` followed by `oldCanvas.replaceWith(newCanvas)`. The factory then calls the renderer's `init(newCanvas, bundle)` with the fresh element so its `getContext()` succeeds. Practical implications: - **Canvas context-type swapping.** Browsers lock a `<canvas>` to the first context type successfully acquired for its lifetime: once `getContext('2d')` succeeds, `getContext('webgl2')` on that same canvas returns `null`, and vice versa. To let arbitrary 2D ⇄ WebGL renderer swaps work mid-session, `highway.setRenderer()` reads the next renderer's `contextType` before calling its `init()` and, if it differs from the type currently bound, replaces the underlying `<canvas>` element with a fresh one via `oldCanvas.cloneNode(false)` followed by `oldCanvas.replaceWith(newCanvas)`. The factory then calls the renderer's `init(newCanvas, bundle)` with the fresh element so its `getContext()` succeeds. Practical implications:
- **What survives the swap.** `cloneNode(false)` preserves *every HTML attribute* on the element — `id`, `class`, inline `style`, all `data-*` and `aria-*` attributes, `role`, `tabindex`, the attribute form of `width`/`height`, and anything else a plugin attached. DOM position is preserved by `replaceWith()`, so siblings, parents, and surrounding layout are unaffected. - **What survives the swap.** `cloneNode(false)` preserves *every HTML attribute* on the element — `id`, `class`, inline `style`, all `data-*` and `aria-*` attributes, `role`, `tabindex`, the attribute form of `width`/`height`, and anything else a plugin attached. DOM position is preserved by `replaceWith()`, so siblings, parents, and surrounding layout are unaffected.
- **What does NOT survive.** Event listeners attached via `addEventListener` are NOT cloned, and expando properties set imperatively on the JavaScript object (such as the bound rendering context, or any `canvas._myPlugin = …`-style data a plugin attached) are not carried over either. The bound rendering context being left behind on the detached element is exactly what allows the new canvas to start fresh and accept a different `getContext()` call. Note: `canvas.width`/`canvas.height` *are* reflected HTML attributes, so those values do survive the clone; `api.resize()` re-applies the backing-store dimensions on the new element after the swap regardless. - **What does NOT survive.** Event listeners attached via `addEventListener` are NOT cloned, and expando properties set imperatively on the JavaScript object (such as the bound rendering context, or any `canvas._myPlugin = …`-style data a plugin attached) are not carried over either. The bound rendering context being left behind on the detached element is exactly what allows the new canvas to start fresh and accept a different `getContext()` call. Note: `canvas.width`/`canvas.height` *are* reflected HTML attributes, so those values do survive the clone; `api.resize()` re-applies the backing-store dimensions on the new element after the swap regardless.
- Renderers must **declare `contextType`** on the returned instance (`'2d'` or `'webgl2'`; absent → `'2d'`). Factories may also expose it as a static (`window.slopsmithViz_<id>.contextType = 'webgl2'`) so core can read it before constructing the renderer — used today by Auto-mode evaluation. - Renderers must **declare `contextType`** on the returned instance (`'2d'` or `'webgl2'`; absent → `'2d'`). Factories may also expose it as a static (`window.feedBackViz_<id>.contextType = 'webgl2'`) so core can read it before constructing the renderer — used today by Auto-mode evaluation.
- Plugins that hold a stale reference to the highway canvas across renderer swaps — including any code that registered listeners directly on the canvas element rather than on `window`/`document` — should listen for the `highway:canvas-replaced` event on `window.slopsmith` and re-acquire / re-register. `window.slopsmith.emit` dispatches a `CustomEvent`, so the payload `{ oldCanvas, newCanvas, contextType }` lives on `event.detail`, not on the event object itself: - Plugins that hold a stale reference to the highway canvas across renderer swaps — including any code that registered listeners directly on the canvas element rather than on `window`/`document` — should listen for the `highway:canvas-replaced` event on `window.feedBack` and re-acquire / re-register. `window.feedBack.emit` dispatches a `CustomEvent`, so the payload `{ oldCanvas, newCanvas, contextType }` lives on `event.detail`, not on the event object itself:
```js ```js
window.slopsmith.on('highway:canvas-replaced', (event) => { window.feedBack.on('highway:canvas-replaced', (event) => {
const { oldCanvas, newCanvas, contextType } = event.detail; const { oldCanvas, newCanvas, contextType } = event.detail;
// re-acquire / re-register against newCanvas // re-acquire / re-register against newCanvas
}); });
``` ```
Plugins that re-query `document.getElementById('highway')` lazily inside their own event handlers don't need this listener — they pick up the new element automatically (it keeps `id="highway"`). Plugins that re-query `document.getElementById('highway')` lazily inside their own event handlers don't need this listener — they pick up the new element automatically (it keeps `id="highway"`).
- **`highway:visibility`** — fired on `window.slopsmith` whenever the highway canvas transitions between displayed and hidden. Detection is DOM-based via `canvas.offsetParent === null` (catches `display:none` on the canvas or any ancestor — e.g. splitscreen's `#highway` hide) or whatever a host explicitly sets via `highway.setVisible(bool)`. While `visible === false`, core skips the rAF `renderer.draw(bundle)` call AND the default 2D draw, so renderers don't have to no-op themselves. The event is emitted only on transitions (including the first one after `init()`), not every frame. Payload `{ visible, canvas }` lives on `event.detail`: - **`highway:visibility`** — fired on `window.feedBack` whenever the highway canvas transitions between displayed and hidden. Detection is DOM-based via `canvas.offsetParent === null` (catches `display:none` on the canvas or any ancestor — e.g. splitscreen's `#highway` hide) or whatever a host explicitly sets via `highway.setVisible(bool)`. While `visible === false`, core skips the rAF `renderer.draw(bundle)` call AND the default 2D draw, so renderers don't have to no-op themselves. The event is emitted only on transitions (including the first one after `init()`), not every frame. Payload `{ visible, canvas }` lives on `event.detail`:
```js ```js
window.slopsmith.on('highway:visibility', (event) => { window.feedBack.on('highway:visibility', (event) => {
const { visible, canvas } = event.detail; const { visible, canvas } = event.detail;
// Toggle any sibling DOM your renderer mounts. The 3D Highway // Toggle any sibling DOM your renderer mounts. The 3D Highway
// renderer hides its `.h3d-wrap` overlay here so `display:none` // renderer hides its `.h3d-wrap` overlay here so `display:none`
// on `#highway` actually hides the visible output. // on `#highway` actually hides the visible output.
}); });
``` ```
Renderers that only paint to the slopsmith canvas don't need this listener — the rAF skip is enough. Renderers that mount sibling DOM (separate WebGL contexts, overlays, etc.) do. Renderers that only paint to the feedBack canvas don't need this listener — the rAF skip is enough. Renderers that mount sibling DOM (separate WebGL contexts, overlays, etc.) do.
- **`highway.setVisible(bool | null)`** — forces the visibility state regardless of `offsetParent`. Pass `null` to clear the override and resume DOM-based detection. Useful when the host hides the highway via `visibility:hidden`, `opacity:0`, transforms, or clipping rather than `display:none`. The override re-emits any resulting transition immediately rather than waiting for the next rAF tick. - **`highway.setVisible(bool | null)`** — forces the visibility state regardless of `offsetParent`. Pass `null` to clear the override and resume DOM-based detection. Useful when the host hides the highway via `visibility:hidden`, `opacity:0`, transforms, or clipping rather than `display:none`. The override re-emits any resulting transition immediately rather than waiting for the next rAF tick.
- Default-renderer ctx is closure-cached. The replace path nulls the closure ctx so stale draw paths short-circuit; the next default-renderer `init()` re-acquires the 2D context from the new canvas cleanly. - Default-renderer ctx is closure-cached. The replace path nulls the closure ctx so stale draw paths short-circuit; the next default-renderer `init()` re-acquires the 2D context from the new canvas cleanly.
- `draw(bundle)` receives difficulty-filtered arrays — never read from `_filteredNotes` or other internals. - `draw(bundle)` receives difficulty-filtered arrays — never read from `_filteredNotes` or other internals.
@@ -314,8 +314,8 @@ The viz picker prepends an "Auto (match arrangement)" entry that is the default
Declare the predicate as a static on the factory (not the instance) so core can evaluate it without constructing a throwaway renderer: Declare the predicate as a static on the factory (not the instance) so core can evaluate it without constructing a throwaway renderer:
```js ```js
window.slopsmithViz_piano = function () { /* ... */ }; window.feedBackViz_piano = function () { /* ... */ };
window.slopsmithViz_piano.matchesArrangement = function (songInfo) { window.feedBackViz_piano.matchesArrangement = function (songInfo) {
return /keys|piano|synth/i.test((songInfo && songInfo.arrangement) || ''); return /keys|piano|synth/i.test((songInfo && songInfo.arrangement) || '');
}; };
``` ```
@@ -328,7 +328,7 @@ window.slopsmithViz_piano.matchesArrangement = function (songInfo) {
**WebGL viz in Auto mode.** Auto evaluation runs on every `song:ready` regardless of which renderer is active. Auto-installing a WebGL renderer when the canvas is currently 2D — or reverting from a WebGL Auto pick to the default 2D — works without a reload because `setRenderer` swaps the canvas element when `contextType` differs (see "Canvas context-type swapping" above). WebGL viz can therefore safely declare `matchesArrangement` and rely on Auto. For 3D Highway specifically, `_canRun3D()` in app.js still gates Auto from picking it on machines without WebGL2 — that fallback is independent of canvas swapping. **WebGL viz in Auto mode.** Auto evaluation runs on every `song:ready` regardless of which renderer is active. Auto-installing a WebGL renderer when the canvas is currently 2D — or reverting from a WebGL Auto pick to the default 2D — works without a reload because `setRenderer` swaps the canvas element when `contextType` differs (see "Canvas context-type swapping" above). WebGL viz can therefore safely declare `matchesArrangement` and rely on Auto. For 3D Highway specifically, `_canRun3D()` in app.js still gates Auto from picking it on machines without WebGL2 — that fallback is independent of canvas swapping.
**Per-instance settings for host plugins (slopsmith#849).** A viz provider may declare per-instance controls a consuming host (e.g. splitscreen's per-panel popover) renders generically, by adding a `settings` array to its `capabilities.visualization` manifest block: `[{ key, label, type: "toggle" | "range" | "select", default, min?, max?, step?, options? }]`. This is the capability-native, declarative replacement for the ad-hoc `factory.panelControls` static. The validated list is surfaced through the visualization host's `list-providers` snapshot, so a host reads it without knowing the plugin. **A provider that declares `settings` MUST implement `applySetting(key, value)` on its renderer instance** — the host calls it on the specific per-panel instance, which is inherently per-panel (no canvas→panel resolution, no shared global localStorage keys). `getSetting(key)` is optional (the host falls back to the declared `default`); the host owns persistence. `factory.panelControls` remains read as a legacy fallback for hosts that still consume it, but new viz should declare `settings` + `applySetting`. **Per-instance settings for host plugins (feedBack#849).** A viz provider may declare per-instance controls a consuming host (e.g. splitscreen's per-panel popover) renders generically, by adding a `settings` array to its `capabilities.visualization` manifest block: `[{ key, label, type: "toggle" | "range" | "select", default, min?, max?, step?, options? }]`. This is the capability-native, declarative replacement for the ad-hoc `factory.panelControls` static. The validated list is surfaced through the visualization host's `list-providers` snapshot, so a host reads it without knowing the plugin. **A provider that declares `settings` MUST implement `applySetting(key, value)` on its renderer instance** — the host calls it on the specific per-panel instance, which is inherently per-panel (no canvas→panel resolution, no shared global localStorage keys). `getSetting(key)` is optional (the host falls back to the declared `default`); the host owns persistence. `factory.panelControls` remains read as a legacy fallback for hosts that still consume it, but new viz should declare `settings` + `applySetting`.
#### 2. Overlay contract — for add-on layers #### 2. Overlay contract — for add-on layers
@@ -354,13 +354,13 @@ Overlays do NOT appear in the viz picker and do NOT declare `"type": "visualizat
- **If you position with `highway.project` / `highway.fretX` (the 2D-highway geometry), gate on `highway.isDefaultRenderer()`** — those helpers describe the *built-in 2D* highway's depth curve and fret zoom. When a custom renderer (3D highway, piano, …) is active your draw hook still fires (on that renderer's 2D overlay layer), but those coordinates won't match its scene — markers land in arbitrary places. Skip rendering when `isDefaultRenderer()` is false; the custom renderer owns that feedback. Renderer-agnostic overlays (fretboard diagram, chord-label HUD — they use `getNotes()`/`getChordTemplates()` + their own layout) don't need this guard. - **If you position with `highway.project` / `highway.fretX` (the 2D-highway geometry), gate on `highway.isDefaultRenderer()`** — those helpers describe the *built-in 2D* highway's depth curve and fret zoom. When a custom renderer (3D highway, piano, …) is active your draw hook still fires (on that renderer's 2D overlay layer), but those coordinates won't match its scene — markers land in arbitrary places. Skip rendering when `isDefaultRenderer()` is false; the custom renderer owns that feedback. Renderer-agnostic overlays (fretboard diagram, chord-label HUD — they use `getNotes()`/`getChordTemplates()` + their own layout) don't need this guard.
- **Clean up on toggle-off** — cancel rAF and remove/hide the overlay canvas so inactive overlays aren't wasting frames. - **Clean up on toggle-off** — cancel rAF and remove/hide the overlay canvas so inactive overlays aren't wasting frames.
Reference: [fretboard plugin](https://github.com/got-feedback/feedback-plugin-fretboard) — canonical overlay implementation (navbar toggle, own canvas, 80ms active-note window). Reference: [fretboard plugin](https://github.com/got-feedback/feedBack-plugin-fretboard) — canonical overlay implementation (navbar toggle, own canvas, 80ms active-note window).
**Why two?** setRenderer plugs into an existing highway — main-player or splitscreen-panel — reusing its WebSocket and data parsing, so the common "I want a different look for the same data" case is zero boilerplate AND multi-instance for free. Overlays compose with whatever renderer is active — they decorate rather than replace, so multiple can stack (fretboard + chord labels + practice feedback) without fighting over the canvas. **Why two?** setRenderer plugs into an existing highway — main-player or splitscreen-panel — reusing its WebSocket and data parsing, so the common "I want a different look for the same data" case is zero boilerplate AND multi-instance for free. Overlays compose with whatever renderer is active — they decorate rather than replace, so multiple can stack (fretboard + chord labels + practice feedback) without fighting over the canvas.
A previous standalone-pane contract (`window.createMyVisualization({ container })` with its own WebSocket per pane) was used by splitscreen pre-Wave-C. It's been retired now that splitscreen calls `setRenderer` on per-panel `createHighway()` instances; if you find references in older plugin docs or external integration guides, those describe the legacy path. A previous standalone-pane contract (`window.createMyVisualization({ container })` with its own WebSocket per pane) was used by splitscreen pre-Wave-C. It's been retired now that splitscreen calls `setRenderer` on per-panel `createHighway()` instances; if you find references in older plugin docs or external integration guides, those describe the legacy path.
#### 3. Note-state provider — for scorers that want renderers to "light up" notes (slopsmith#254) #### 3. Note-state provider — for scorers that want renderers to "light up" notes (feedBack#254)
A scoring plugin (note_detect) can publish a per-note judgment so whichever renderer is active draws the **gem itself** lit on a correct hit, and keeps a sustain trail glowing while it's still being played correctly — instead of a separate overlay ring floating near the note. A scoring plugin (note_detect) can publish a per-note judgment so whichever renderer is active draws the **gem itself** lit on a correct hit, and keeps a sustain trail glowing while it's still being played correctly — instead of a separate overlay ring floating near the note.
@@ -386,13 +386,13 @@ highway.setNoteStateProvider((note, chartTime) => {
- The built-in 2D highway consults it in `drawNote` / `drawSustains` / the chord-frame path: 'hit'/'active' → bright string colour + additive halo + a contained "sizzle" (crackling sparks, throbbing core, a shockwave ring on a fresh strike) on the gem and a bright (vs dim) sustain trail; 'miss' → faint red wash. The bundled **3D highway** reads the same data via `bundle.getNoteState` (bright string-tinted outline + bright body + glowing sustain + a contained sparkle hugging the note rect on hit/active; red outline + suppressed body on miss). Custom renderers that want it call `bundle.getNoteState(note, chartTime)` — it null-guards and returns the normalized `{ state, alpha, color }` (or null). - The built-in 2D highway consults it in `drawNote` / `drawSustains` / the chord-frame path: 'hit'/'active' → bright string colour + additive halo + a contained "sizzle" (crackling sparks, throbbing core, a shockwave ring on a fresh strike) on the gem and a bright (vs dim) sustain trail; 'miss' → faint red wash. The bundled **3D highway** reads the same data via `bundle.getNoteState` (bright string-tinted outline + bright body + glowing sustain + a contained sparkle hugging the note rect on hit/active; red outline + suppressed body on miss). Custom renderers that want it call `bundle.getNoteState(note, chartTime)` — it null-guards and returns the normalized `{ state, alpha, color }` (or null).
- This is orthogonal to the overlay contract: note_detect remains an overlay (HUD, diagnostic miss markers, the "currently detected" indicator) *and* a scorer that feeds this provider. A renderer that ignores `getNoteState` simply doesn't light gems — nothing breaks. - This is orthogonal to the overlay contract: note_detect remains an overlay (HUD, diagnostic miss markers, the "currently detected" indicator) *and* a scorer that feeds this provider. A renderer that ignores `getNoteState` simply doesn't light gems — nothing breaks.
### Audio mixer fader registration (slopsmith#87) ### Audio mixer fader registration (feedBack#87)
Plugins that produce audio outside the song's `<audio>` element (NAM amp output, synth voices, etc.) can register a labeled fader so users can balance them against the song from one mixer popover in the player controls. Plugins that produce audio outside the song's `<audio>` element (NAM amp output, synth voices, etc.) can register a labeled fader so users can balance them against the song from one mixer popover in the player controls.
```js ```js
function _registerFader() { function _registerFader() {
const api = window.slopsmith && window.slopsmith.audio; const api = window.feedBack && window.feedBack.audio;
if (!api) return; if (!api) return;
api.registerFader({ api.registerFader({
id: 'my_plugin', // unique key id: 'my_plugin', // unique key
@@ -405,10 +405,10 @@ function _registerFader() {
}); });
} }
if (window.slopsmith && window.slopsmith.audio) { if (window.feedBack && window.feedBack.audio) {
_registerFader(); _registerFader();
} else { } else {
window.addEventListener('slopsmith:audio:ready', _registerFader, { once: true }); window.addEventListener('feedBack:audio:ready', _registerFader, { once: true });
} }
``` ```
@@ -416,7 +416,7 @@ The plugin owns persistence — the registry calls `getValue()` when the popover
### Backend plugin logging ### Backend plugin logging
Use `context["log"]` for all backend plugin output. It is a stdlib `logging.Logger` namespaced to `slopsmith.plugin.<id>`, pre-configured with the app-wide level, format (including JSON mode), and correlation IDs. Never use `print()` — it bypasses correlation context and log rotation. Use `context["log"]` for all backend plugin output. It is a stdlib `logging.Logger` namespaced to `feedBack.plugin.<id>`, pre-configured with the app-wide level, format (including JSON mode), and correlation IDs. Never use `print()` — it bypasses correlation context and log rotation.
```python ```python
def setup(app, context): def setup(app, context):
@@ -437,19 +437,19 @@ if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
``` ```
### Diagnostics contribution from frontend (slopsmith#166) ### Diagnostics contribution from frontend (feedBack#166)
Plugins that hold useful debug state in the browser (active model name, last user input, internal counters) can push it into the diagnostics bundle by calling `window.slopsmith.diagnostics.contribute(plugin_id, payload)` at any time. The contribution API is idempotent — repeated calls overwrite the previous value. Whatever was last contributed before the user hits Export Diagnostics is what lands in `plugins/<plugin_id>/client.json`. Plugins that hold useful debug state in the browser (active model name, last user input, internal counters) can push it into the diagnostics bundle by calling `window.feedBack.diagnostics.contribute(plugin_id, payload)` at any time. The contribution API is idempotent — repeated calls overwrite the previous value. Whatever was last contributed before the user hits Export Diagnostics is what lands in `plugins/<plugin_id>/client.json`.
```js ```js
window.slopsmith.diagnostics.contribute('my_plugin', { window.feedBack.diagnostics.contribute('my_plugin', {
schema: 'my_plugin.client_diag.v1', schema: 'my_plugin.client_diag.v1',
active_preset: getActivePreset(), active_preset: getActivePreset(),
last_error: _lastError, last_error: _lastError,
}); });
``` ```
Loaded from `static/diagnostics.js` ASAP in `<head>` so the console-wrap is in place before any other script runs. Available on the `window.slopsmith.diagnostics` namespace alongside `snapshotConsole()`, `snapshotHardware()`, `snapshotUa()`, `snapshotLocalStorage()`, `snapshotContributions()`. Keep your payload small (< 100 KB) and don't include secrets — bundles are shared with maintainers. Loaded from `static/diagnostics.js` ASAP in `<head>` so the console-wrap is in place before any other script runs. Available on the `window.feedBack.diagnostics` namespace alongside `snapshotConsole()`, `snapshotHardware()`, `snapshotUa()`, `snapshotLocalStorage()`, `snapshotContributions()`. Keep your payload small (< 100 KB) and don't include secrets — bundles are shared with maintainers.
### Keyboard Shortcuts ### Keyboard Shortcuts
@@ -496,18 +496,18 @@ window.registerShortcut({
- Use `localStorage` for user-facing settings, prefixed with your plugin id - Use `localStorage` for user-facing settings, prefixed with your plugin id
- If hooking `window.playSong`, always call the original and `await` it - If hooking `window.playSong`, always call the original and `await` it
- If hooking `window.showScreen`, clean up your state when leaving the player screen - If hooking `window.showScreen`, clean up your state when leaving the player screen
- Use `window.slopsmith.emit()` / `window.slopsmith.on()` for inter-plugin communication - Use `window.feedBack.emit()` / `window.feedBack.on()` for inter-plugin communication
- Use `window.registerShortcut()` to add keyboard shortcuts. Clean up with `window.unregisterShortcut(key, scope)` — pass the same scope you registered with, since the default is `'global'` and won't match `player`/`library`/`settings`/`plugin-*` bindings. For panel-scoped shortcuts, prefer `panel.clearShortcuts()`. - Use `window.registerShortcut()` to add keyboard shortcuts. Clean up with `window.unregisterShortcut(key, scope)` — pass the same scope you registered with, since the default is `'global'` and won't match `player`/`library`/`settings`/`plugin-*` bindings. For panel-scoped shortcuts, prefer `panel.clearShortcuts()`.
## Song Formats ## Song Formats
Slopsmith supports two song formats: FeedBack supports two song formats:
### Loose folder (XML charts) ### Loose folder (XML charts)
A directory containing arrangement XML plus an audio file (and optional `manifest.json` + album art). Discovered, indexed, and played directly — see `lib/loosefolder.py`. Metadata follows a `manifest.json` → XML tags → folder-name priority chain. Songs are tagged `format: "loose"` in the library. A directory containing arrangement XML plus an audio file (and optional `manifest.json` + album art). Discovered, indexed, and played directly — see `lib/loosefolder.py`. Metadata follows a `manifest.json` → XML tags → folder-name priority chain. Songs are tagged `format: "loose"` in the library.
### Sloppak (open format) ### Sloppak (open format)
An open, hand-editable song package designed for Slopsmith. Exists in two interchangeable forms: An open, hand-editable song package designed for FeedBack. Exists in two interchangeable forms:
- **Zip archive** (`.sloppak` file) — distribution form - **Zip archive** (`.sloppak` file) — distribution form
- **Directory** (`.sloppak/` folder) — authoring form - **Directory** (`.sloppak/` folder) — authoring form
@@ -530,11 +530,11 @@ cover.jpg Album art (optional)
lyrics.json Syllable-level lyrics (optional) lyrics.json Syllable-level lyrics (optional)
``` ```
Sloppak is the preferred format for new features. The [Stems plugin](https://github.com/topkoa/slopsmith-plugin-stems) provides live stem mixing for sloppak songs. Sloppak is the preferred format for new features. The [Stems plugin](https://github.com/topkoa/feedBack-plugin-stems) provides live stem mixing for sloppak songs.
**Full developer reference:** the authoritative format spec now lives in its own repo — **Full developer reference:** the authoritative format spec now lives in its own repo —
[got-feedback/feedback-feedpak-spec](https://github.com/got-feedback/feedback-feedpak-spec) [got-feedback/feedpak-spec](https://github.com/got-feedback/feedpak-spec)
([`spec/feedpak-v1.md`](https://github.com/got-feedback/feedback-feedpak-spec/blob/main/spec/feedpak-v1.md)): ([`spec/feedpak-v1.md`](https://github.com/got-feedback/feedpak-spec/blob/main/spec/feedpak-v1.md)):
manifest schema, arrangement wire format, and how to extend the format with new data types (drum manifest schema, arrangement wire format, and how to extend the format with new data types (drum
tab, key/scale annotations, etc.). Published as **feedpak**; this codebase still uses the legacy tab, key/scale annotations, etc.). Published as **feedpak**; this codebase still uses the legacy
**sloppak** name internally — same on-disk format. [docs/sloppak-spec.md](docs/sloppak-spec.md) is **sloppak** name internally — same on-disk format. [docs/sloppak-spec.md](docs/sloppak-spec.md) is
@@ -548,9 +548,9 @@ a local pointer + code map.
## Frontend Conventions ## Frontend Conventions
- **No frameworks** — vanilla JS, fetch API, DOM manipulation - **No frameworks** — vanilla JS, fetch API, DOM manipulation
- **Globals** — `highway`, `audio`, `playSong()`, `showScreen()`, `createHighway()`, `window.slopsmith` - **Globals** — `highway`, `audio`, `playSong()`, `showScreen()`, `createHighway()`, `window.feedBack`
- **Storage** — `localStorage` for all user preferences - **Storage** — `localStorage` for all user preferences
- **Styling** — Tailwind CSS utility classes, dark theme (`bg-dark-600`, `text-gray-300`, accent `#4080e0`, gold `#e8c040`). Tailwind is served as a **prebuilt** stylesheet (`static/tailwind.min.css`, regenerated by `bash scripts/build-tailwind.sh`), **never** the runtime Play CDN — the CDN's on-the-fly JIT rescanned the DOM on the main thread and dropped ~26% of frames with the 3D highway (slopsmith-desktop#110). The committed CSS only contains classes the build scanner saw, so CI (`tailwind-fresh`) rebuilds and diffs it; run the build script and commit when you add new classes. A plugin that uses classes not guaranteed in core (notably arbitrary values like `w-[37px]`) MUST ship its own compiled stylesheet via the `styles` manifest key, built with `corePlugins.preflight = false` (utilities only — core ships the one base reset). Plugins MUST NOT load the Tailwind Play CDN or any runtime CSS JIT. See constitution Principle II. - **Styling** — Tailwind CSS utility classes, dark theme (`bg-dark-600`, `text-gray-300`, accent `#4080e0`, gold `#e8c040`). Tailwind is served as a **prebuilt** stylesheet (`static/tailwind.min.css`, regenerated by `bash scripts/build-tailwind.sh`), **never** the runtime Play CDN — the CDN's on-the-fly JIT rescanned the DOM on the main thread and dropped ~26% of frames with the 3D highway (feedBack-desktop#110). The committed CSS only contains classes the build scanner saw, so CI (`tailwind-fresh`) rebuilds and diffs it; run the build script and commit when you add new classes. A plugin that uses classes not guaranteed in core (notably arbitrary values like `w-[37px]`) MUST ship its own compiled stylesheet via the `styles` manifest key, built with `corePlugins.preflight = false` (utilities only — core ships the one base reset). Plugins MUST NOT load the Tailwind Play CDN or any runtime CSS JIT. See constitution Principle II.
- **Naming** — camelCase for JS functions, kebab-case for CSS classes, snake_case for plugin IDs - **Naming** — camelCase for JS functions, kebab-case for CSS classes, snake_case for plugin IDs
- **Player layout** — `#player` is `display:flex; flex-direction:column; position:fixed; inset:0`. `#highway` is `flex:1`. `#player-controls` sits at the bottom. Hiding the highway collapses the layout — use `margin-top: auto` on controls if you need to hide it. - **Player layout** — `#player` is `display:flex; flex-direction:column; position:fixed; inset:0`. `#highway` is `flex:1`. `#player-controls` sits at the bottom. Hiding the highway collapses the layout — use `margin-top: auto` on controls if you need to hide it.
@@ -583,8 +583,8 @@ Detection quality is hard to judge by eye — a player UI that "feels worse" aft
Quick orientation: Quick orientation:
- **Reference recording** lives in the gear popover on the player (gated behind Settings → Note Detection → "Detection tuning (advanced)"). Arm before pressing Play; auto-saves a WAV to `static/note_detect_recordings/` on song-end. The directory is bind-mounted, so the host-side harness can read it without a copy step. - **Reference recording** lives in the gear popover on the player (gated behind Settings → Note Detection → "Detection tuning (advanced)"). Arm before pressing Play; auto-saves a WAV to `static/note_detect_recordings/` on song-end. The directory is bind-mounted, so the host-side harness can read it without a copy step.
- **Benchmark sloppak** ships in-tree at [docs/benchmarks/note_detect_v1/note_detect_benchmark_v1.sloppak](docs/benchmarks/note_detect_v1/note_detect_benchmark_v1.sloppak) — 8 sections each isolating a different failure mode (low-freq mono, sustained holds, hammer/pull, power chords, dense open chords, bends). Drop it directly into your sloppak DLC folder to install (don't rename — slopsmith keys off the `.sloppak` suffix even though the file is a zip under the hood). The unzipped form lands at `static/sloppak_cache/note_detect_benchmark_v1.sloppak/` after first play. Builder: [docs/benchmarks/note_detect_v1/build_benchmark.py](docs/benchmarks/note_detect_v1/build_benchmark.py). - **Benchmark sloppak** ships in-tree at [docs/benchmarks/note_detect_v1/note_detect_benchmark_v1.sloppak](docs/benchmarks/note_detect_v1/note_detect_benchmark_v1.sloppak) — 8 sections each isolating a different failure mode (low-freq mono, sustained holds, hammer/pull, power chords, dense open chords, bends). Drop it directly into your sloppak DLC folder to install (don't rename — feedBack keys off the `.sloppak` suffix even though the file is a zip under the hood). The unzipped form lands at `static/sloppak_cache/note_detect_benchmark_v1.sloppak/` after first play. Builder: [docs/benchmarks/note_detect_v1/build_benchmark.py](docs/benchmarks/note_detect_v1/build_benchmark.py).
- **Headless harness** at [`tools/harness.js`](https://github.com/got-feedback/feedback-plugin-notedetect/blob/main/tools/harness.js) in the note_detect plugin's own repo (cloned into `plugins/note_detect/` locally) runs the same `processFrame` / `matchNotes` / `checkMisses` code path off Node, in seconds per run. Same `note_detect.diagnostic.v1` schema as the in-app Download Diagnostic button. - **Headless harness** at [`tools/harness.js`](https://github.com/got-feedback/feedBack-plugin-notedetect/blob/main/tools/harness.js) in the note_detect plugin's own repo (cloned into `plugins/note_detect/` locally) runs the same `processFrame` / `matchNotes` / `checkMisses` code path off Node, in seconds per run. Same `note_detect.diagnostic.v1` schema as the in-app Download Diagnostic button.
- **A/V auto-calibrate** (Settings → Note Detection) reads `timing_error_ms_hits.median` and proposes the av-offset that drives it to zero. Iterative: usually converges in 23 Apply rounds. - **A/V auto-calibrate** (Settings → Note Detection) reads `timing_error_ms_hits.median` and proposes the av-offset that drives it to zero. Iterative: usually converges in 23 Apply rounds.
**Always record at 1.0× playback speed** — half-speed takes produce all-miss garbage because chart times are absolute. **Always use `timing_error_ms_hits` (not all-matched) as a calibration signal** — the all-matched median pins near a constant when the offset is wrong, because the matcher silently snaps to neighbouring chart notes. **Always record at 1.0× playback speed** — half-speed takes produce all-miss garbage because chart times are absolute. **Always use `timing_error_ms_hits` (not all-matched) as a calibration signal** — the all-matched median pins near a constant when the offset is wrong, because the matcher silently snaps to neighbouring chart notes.
@@ -594,14 +594,14 @@ Full developer reference (workflow recipes, harness flag table, diagnostic schem
## Versioning ## Versioning
- **`VERSION`** (repo root) — single source of truth; plain semver string (e.g. `0.2.4`). Bind-mounted into the container and copied by the Dockerfile so it's always available at `/app/VERSION`. - **`VERSION`** (repo root) — single source of truth; plain semver string (e.g. `0.2.4`). Bind-mounted into the container and copied by the Dockerfile so it's always available at `/app/VERSION`.
- **`GET /api/version`** — returns `{"version": "<contents of VERSION>", "source_url": "...", "license_url": "..."}`. The version drives the navbar badge; `source_url` / `license_url` populate the Settings → About links. `source_url` is configurable via the `APP_SOURCE_URL` env var (default `https://github.com/got-feedback/feedback`); `license_url` falls back to `source_url + "/blob/main/LICENSE"` (GitHub-style, default branch `main`) and is overridable via the `APP_LICENSE_URL` env var — set it explicitly when the source is hosted on a non-GitHub forge (GitLab/Gitea/self-hosted) or under a non-`main` default branch. Both env values must be `http(s)`; non-http(s) values are rejected and fall back to the safe default to prevent `javascript:`/`data:` hrefs. - **`GET /api/version`** — returns `{"version": "<contents of VERSION>", "source_url": "...", "license_url": "..."}`. The version drives the navbar badge; `source_url` / `license_url` populate the Settings → About links. `source_url` is configurable via the `APP_SOURCE_URL` env var (default `https://github.com/got-feedback/feedBack`); `license_url` falls back to `source_url + "/blob/main/LICENSE"` (GitHub-style, default branch `main`) and is overridable via the `APP_LICENSE_URL` env var — set it explicitly when the source is hosted on a non-GitHub forge (GitLab/Gitea/self-hosted) or under a non-`main` default branch. Both env values must be `http(s)`; non-http(s) values are rejected and fall back to the safe default to prevent `javascript:`/`data:` hrefs.
- **Auto-sync** — `.github/workflows/sync-version.yml` rewrites `VERSION` via a `repository_dispatch` (`desktop-released`) fired from `slopsmith-desktop`'s release job. As an explicit automation-only exception to the "Never push directly to main" rule in Git Workflow below, the sync job commits straight to `main` as `github-actions[bot]` (version bumps are mechanical; the PR round-trip adds no signal). Human contributors must still go through feature branches + PRs. No manual VERSION edits needed. Use the workflow's `workflow_dispatch` trigger with `version: X.Y.Z` for manual runs (recovery / out-of-band bumps). - **Auto-sync** — `.github/workflows/sync-version.yml` rewrites `VERSION` via a `repository_dispatch` (`desktop-released`) fired from `feedBack-desktop`'s release job. As an explicit automation-only exception to the "Never push directly to main" rule in Git Workflow below, the sync job commits straight to `main` as `github-actions[bot]` (version bumps are mechanical; the PR round-trip adds no signal). Human contributors must still go through feature branches + PRs. No manual VERSION edits needed. Use the workflow's `workflow_dispatch` trigger with `version: X.Y.Z` for manual runs (recovery / out-of-band bumps).
- **`CHANGELOG.md`** — follows [Keep a Changelog](https://keepachangelog.com/) format. Update the `[Unreleased]` section with each PR; when `slopsmith-desktop` cuts a release, rename `[Unreleased]` to the new version + date (the VERSION bump itself is automated). - **`CHANGELOG.md`** — follows [Keep a Changelog](https://keepachangelog.com/) format. Update the `[Unreleased]` section with each PR; when `feedBack-desktop` cuts a release, rename `[Unreleased]` to the new version + date (the VERSION bump itself is automated).
## Git Workflow ## Git Workflow
- **Never push directly to main** — always create a feature branch and open a PR - **Never push directly to main** — always create a feature branch and open a PR
- **Upstream remote** — set `upstream` to the canonical Slopsmith repository; `origin` is your fork - **Upstream remote** — set `upstream` to the canonical FeedBack repository; `origin` is your fork
- **Plugins are gitlinks** — each plugin in `plugins/` is typically its own git repo (submodule or clone). Branch switches on the main repo can clobber plugin directories. Use `git update-index --assume-unchanged` for plugin dirs if needed. - **Plugins are gitlinks** — each plugin in `plugins/` is typically its own git repo (submodule or clone). Branch switches on the main repo can clobber plugin directories. Use `git update-index --assume-unchanged` for plugin dirs if needed.
- **Commit style** — short imperative subject line, blank line, then body explaining *why* - **Commit style** — short imperative subject line, blank line, then body explaining *why*
@@ -621,7 +621,7 @@ The highway WebSocket at `/ws/highway/{filename}?arrangement={index}` streams th
| `tone_changes` | `{ type: 'tone_changes', base, data: [{ time, name }] }` | Optional — tone change events relative to the arrangement base tone; only sent if tones were found | | `tone_changes` | `{ type: 'tone_changes', base, data: [{ time, name }] }` | Optional — tone change events relative to the arrangement base tone; only sent if tones were found |
| `notes` | `{ type, data: [{ t, s, f, sus, ho, po, sl, bn, ... }] }` | Single notes | | `notes` | `{ type, data: [{ t, s, f, sus, ho, po, sl, bn, ... }] }` | Single notes |
| `chords` | `{ type, data: [{ t, notes: [{ s, f, sus, ... }] }] }` | Chord events | | `chords` | `{ type, data: [{ t, notes: [{ s, f, sus, ... }] }] }` | Chord events |
| `phrases` | `{ type, data: [{ start_time, end_time, max_difficulty, levels: [{ difficulty, notes, chords, anchors, handshapes }] }], total }` | Optional — per-phrase difficulty ladder for master-difficulty slider (slopsmith#48). Only sent when the source chart carries multi-level phrase data (phrase-aware sloppak). Sent in chunks (`data` is a batch, `total` is the full count across messages) to avoid multi-MB single frames. Absent for GP imports and legacy sloppak; consumers must treat missing message as "single fixed difficulty — slider disabled". | | `phrases` | `{ type, data: [{ start_time, end_time, max_difficulty, levels: [{ difficulty, notes, chords, anchors, handshapes }] }], total }` | Optional — per-phrase difficulty ladder for master-difficulty slider (feedBack#48). Only sent when the source chart carries multi-level phrase data (phrase-aware sloppak). Sent in chunks (`data` is a batch, `total` is the full count across messages) to avoid multi-MB single frames. Absent for GP imports and legacy sloppak; consumers must treat missing message as "single fixed difficulty — slider disabled". |
| `ready` | `{ type: 'ready' }` | All data sent — safe to finalize and start rendering | | `ready` | `{ type: 'ready' }` | All data sent — safe to finalize and start rendering |
Message delivery is incremental. You may receive `loading` updates and `lyrics` before note/chord payloads; `tone_changes` comes after `lyrics` when present and may be omitted entirely. Do not finalize rendering until you receive `ready`. Message delivery is incremental. You may receive `loading` updates and `lyrics` before note/chord payloads; `tone_changes` comes after `lyrics` when present and may be omitted entirely. Do not finalize rendering until you receive `ready`.
+6 -6
View File
@@ -1,10 +1,10 @@
# Contributing to Slopsmith # Contributing to FeedBack
Thanks for wanting to contribute! This document covers the legal and workflow expectations for code, plugins, and documentation contributions. Thanks for wanting to contribute! This document covers the legal and workflow expectations for code, plugins, and documentation contributions.
## License ## License
Slopsmith is licensed under [AGPL-3.0-only](LICENSE). Contributions you submit (PRs, patches, documentation, plugin entries in the curated list) are licensed inbound under the same terms — **inbound = outbound**. By opening a pull request, you agree that your contribution may be distributed under AGPL-3.0-only as part of Slopsmith. FeedBack is licensed under [AGPL-3.0-only](LICENSE). Contributions you submit (PRs, patches, documentation, plugin entries in the curated list) are licensed inbound under the same terms — **inbound = outbound**. By opening a pull request, you agree that your contribution may be distributed under AGPL-3.0-only as part of FeedBack.
## Developer Certificate of Origin (DCO) ## Developer Certificate of Origin (DCO)
@@ -26,7 +26,7 @@ If you forget to sign off, amend the most recent commit with `git commit --amend
## Plugin licensing ## Plugin licensing
Plugins live in their own repositories and are loaded at runtime — see the [Plugin System section in CLAUDE.md](CLAUDE.md) for the technical contract, and [Plugin Best Practices](CLAUDE.md) for the conventions every plugin should follow (v2/v3 player chrome, the visualization contracts, and the **performance rules** — no per-frame DOM queries or broad `document.body` `MutationObserver`s — that keep the 60 fps highway smooth). Plugins are not subject to AGPL by being loaded into Slopsmith (the loader runs them as separate Python modules / browser scripts), but for the **curated plugin list** to accept your plugin we ask that it be released under an AGPL-3.0-compatible license: Plugins live in their own repositories and are loaded at runtime — see the [Plugin System section in CLAUDE.md](CLAUDE.md) for the technical contract, and [Plugin Best Practices](CLAUDE.md) for the conventions every plugin should follow (v2/v3 player chrome, the visualization contracts, and the **performance rules** — no per-frame DOM queries or broad `document.body` `MutationObserver`s — that keep the 60 fps highway smooth). Plugins are not subject to AGPL by being loaded into FeedBack (the loader runs them as separate Python modules / browser scripts), but for the **curated plugin list** to accept your plugin we ask that it be released under an AGPL-3.0-compatible license:
- AGPL-3.0-only or AGPL-3.0-or-later - AGPL-3.0-only or AGPL-3.0-or-later
- GPL-3.0-only or GPL-3.0-or-later - GPL-3.0-only or GPL-3.0-or-later
@@ -37,16 +37,16 @@ Plugins live in their own repositories and are loaded at runtime — see the [Pl
- ISC - ISC
- Unlicense / CC0-1.0 / 0BSD - Unlicense / CC0-1.0 / 0BSD
Plugins under GPL-2.0-only, LGPL-2.1-only, CDDL, EPL, or proprietary terms will not be added to the curated list. You're still free to publish and self-distribute them — Slopsmith will load any plugin a user installs locally — but they won't be promoted from the main project. Plugins under GPL-2.0-only, LGPL-2.1-only, CDDL, EPL, or proprietary terms will not be added to the curated list. You're still free to publish and self-distribute them — FeedBack will load any plugin a user installs locally — but they won't be promoted from the main project.
## Workflow ## Workflow
Standard PR workflow described in [CLAUDE.md → Git Workflow](CLAUDE.md): Standard PR workflow described in [CLAUDE.md → Git Workflow](CLAUDE.md):
- Never push directly to `main`. - Never push directly to `main`.
- Create a feature branch on your fork. - Create a feature branch on your fork.
- Open a PR against `got-feedback/feedback:main`. - Open a PR against `got-feedback/feedBack:main`.
- Keep commits scoped and well-described; short imperative subject + `Signed-off-by` trailer. - Keep commits scoped and well-described; short imperative subject + `Signed-off-by` trailer.
## Questions ## Questions
Open an issue or start a [Discussion](https://github.com/got-feedback/feedback/discussions) if you're unsure whether a contribution fits — much better to ask early than to find out after the work is done. Open an issue or start a [Discussion](https://github.com/got-feedback/feedBack/discussions) if you're unsure whether a contribution fits — much better to ask early than to find out after the work is done.
+15 -15
View File
@@ -47,11 +47,11 @@ RUN cmake -S /tmp/vgmstream -B /tmp/vgmstream/build \
# and update FFMPEG_RELEASE + both SHA256 ARGs below. # and update FFMPEG_RELEASE + both SHA256 ARGs below.
FROM alpine:3.20 AS ffmpeg-fetcher FROM alpine:3.20 AS ffmpeg-fetcher
ARG TARGETARCH ARG TARGETARCH
ARG FFMPEG_RELEASE=autobuild-2026-06-01-15-02 ARG FFMPEG_RELEASE=autobuild-2026-06-19-23-17
ARG FFMPEG_BUILD_AMD64=ffmpeg-n7.1.4-7-gadcf20da26-linux64-gpl-7.1.tar.xz ARG FFMPEG_BUILD_AMD64=ffmpeg-n7.1.4-145-g4cbf7a4b3d-linux64-gpl-7.1.tar.xz
ARG FFMPEG_BUILD_ARM64=ffmpeg-n7.1.4-7-gadcf20da26-linuxarm64-gpl-7.1.tar.xz ARG FFMPEG_BUILD_ARM64=ffmpeg-n7.1.4-145-g4cbf7a4b3d-linuxarm64-gpl-7.1.tar.xz
ARG FFMPEG_SHA256_AMD64=afde55344990650c117fbb7cb36b38d2ab6790b06beb06a9c43a9300c9ce277a ARG FFMPEG_SHA256_AMD64=03c0431e0d1aa75cc343d83bda9d2d4cd8eaa37f35b7b93465e9ff6864f5d7f8
ARG FFMPEG_SHA256_ARM64=03c8a7d9a7cf48d017a22a7c31acfdc8e76c5cb193923f883b0338c7baf0bd28 ARG FFMPEG_SHA256_ARM64=74629b88342fd94eea12b7481c8b8560ca6d497744123c0a27b98f39d767fd93
RUN apk add --no-cache curl xz \ RUN apk add --no-cache curl xz \
&& arch="${TARGETARCH:-$(apk --print-arch)}" \ && arch="${TARGETARCH:-$(apk --print-arch)}" \
&& case "$arch" in \ && case "$arch" in \
@@ -70,7 +70,7 @@ RUN apk add --no-cache curl xz \
# ── Stage 1d: Build the Tailwind stylesheet over the FULL plugin set ────── # ── Stage 1d: Build the Tailwind stylesheet over the FULL plugin set ──────
# The committed static/tailwind.min.css is generated against only the in-tree # The committed static/tailwind.min.css is generated against only the in-tree
# plugins. Rather than ship it as-is (leaving baked-in plugins' classes # plugins. Rather than ship it as-is (leaving baked-in plugins' classes
# unstyled now that the Play CDN's runtime JIT is gone — slopsmith#411), # unstyled now that the Play CDN's runtime JIT is gone — feedBack#411),
# rebuild it here, after static/ + plugins/ are present, so the sheet covers # rebuild it here, after static/ + plugins/ are present, so the sheet covers
# whatever plugins are baked into the image. Runs in a throwaway node stage so # whatever plugins are baked into the image. Runs in a throwaway node stage so
# this build-time toolchain never lands in the final image; the runtime node # this build-time toolchain never lands in the final image; the runtime node
@@ -94,9 +94,9 @@ FROM python:3.12-slim
# Re-declare the ffmpeg ARGs so their values are available to LABEL below. # Re-declare the ffmpeg ARGs so their values are available to LABEL below.
# ARG values don't cross stage boundaries in multi-stage builds; defaults # ARG values don't cross stage boundaries in multi-stage builds; defaults
# must be repeated here to take effect when no --build-arg is supplied. # must be repeated here to take effect when no --build-arg is supplied.
ARG FFMPEG_RELEASE=autobuild-2026-06-01-15-02 ARG FFMPEG_RELEASE=autobuild-2026-06-19-23-17
ARG FFMPEG_BUILD_AMD64=ffmpeg-n7.1.4-7-gadcf20da26-linux64-gpl-7.1.tar.xz ARG FFMPEG_BUILD_AMD64=ffmpeg-n7.1.4-145-g4cbf7a4b3d-linux64-gpl-7.1.tar.xz
ARG FFMPEG_BUILD_ARM64=ffmpeg-n7.1.4-7-gadcf20da26-linuxarm64-gpl-7.1.tar.xz ARG FFMPEG_BUILD_ARM64=ffmpeg-n7.1.4-145-g4cbf7a4b3d-linuxarm64-gpl-7.1.tar.xz
# Apply latest security updates to base packages (clears glibc deb13u3 and # Apply latest security updates to base packages (clears glibc deb13u3 and
# similar). Done first so any subsequent installs resolve against the # similar). Done first so any subsequent installs resolve against the
@@ -112,7 +112,7 @@ RUN apt-get update \
# package drags in the full codec + TLS + graphics dependency tree # package drags in the full codec + TLS + graphics dependency tree
# (mbedtls, gnutls28, mesa, x264, tiff, openjpeg2, libcaca, harfbuzz, # (mbedtls, gnutls28, mesa, x264, tiff, openjpeg2, libcaca, harfbuzz,
# cairo, openldap, libcdio…), almost all of which has unfixed CVEs and # cairo, openldap, libcdio…), almost all of which has unfixed CVEs and
# none of which Slopsmith uses. We pull a static ffmpeg binary further # none of which FeedBack uses. We pull a static ffmpeg binary further
# down instead. # down instead.
# #
# vgmstream-cli is also built with -DUSE_FFMPEG=OFF (see stage 1b), so # vgmstream-cli is also built with -DUSE_FFMPEG=OFF (see stage 1b), so
@@ -142,7 +142,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* && rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
# Node + the pinned Tailwind CLI for RUNTIME stylesheet regeneration. When a # Node + the pinned Tailwind CLI for RUNTIME stylesheet regeneration. When a
# plugin is installed into SLOPSMITH_PLUGINS_DIR at runtime (or discovered # plugin is installed into FEEDBACK_PLUGINS_DIR at runtime (or discovered
# there on startup), the server rebuilds static/tailwind.min.css so the # there on startup), the server rebuilds static/tailwind.min.css so the
# plugin's classes are styled — the image-baked sheet only covered in-tree # plugin's classes are styled — the image-baked sheet only covered in-tree
# plugins (see lib/tailwind_rebuild.py). tailwindcss is installed globally so # plugins (see lib/tailwind_rebuild.py). tailwindcss is installed globally so
@@ -176,10 +176,10 @@ COPY --from=ffmpeg-fetcher /out/LICENSE.txt /usr/share/doc/ffmpeg/LICENSE.txt
RUN chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe RUN chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe
# Record provenance so the exact BtbN source can be located for GPL compliance # Record provenance so the exact BtbN source can be located for GPL compliance
# or debugging. Inspect with: docker inspect <image> | grep -A5 ffmpeg # or debugging. Inspect with: docker inspect <image> | grep -A5 ffmpeg
LABEL org.slopsmith.ffmpeg.release="${FFMPEG_RELEASE}" \ LABEL org.feedBack.ffmpeg.release="${FFMPEG_RELEASE}" \
org.slopsmith.ffmpeg.source.amd64="${FFMPEG_BUILD_AMD64}" \ org.feedBack.ffmpeg.source.amd64="${FFMPEG_BUILD_AMD64}" \
org.slopsmith.ffmpeg.source.arm64="${FFMPEG_BUILD_ARM64}" \ org.feedBack.ffmpeg.source.arm64="${FFMPEG_BUILD_ARM64}" \
org.slopsmith.ffmpeg.upstream="https://github.com/BtbN/FFmpeg-Builds" org.feedBack.ffmpeg.upstream="https://github.com/BtbN/FFmpeg-Builds"
# Native vgmstream-cli built against the image's own libraries # Native vgmstream-cli built against the image's own libraries
COPY --from=vgmstream-builder /out/vgmstream-cli /usr/local/bin/vgmstream-cli COPY --from=vgmstream-builder /out/vgmstream-cli /usr/local/bin/vgmstream-cli
+22 -28
View File
@@ -4,50 +4,44 @@
| Plugin | Description | Install | | Plugin | Description | Install |
|------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------| |------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------|
| [Create from Tab](https://github.com/got-feedback/feedback-plugin-ug) | Search Ultimate Guitar for GP tabs and convert to playable songs | `git clone ...slopsmith-plugin-ug.git ultimate_guitar` | | [Create from Tab](https://github.com/got-feedback/feedBack-plugin-ug) | Search Ultimate Guitar for GP tabs and convert to playable songs | `git clone ...feedBack-plugin-ug.git ultimate_guitar` |
| [Import Tab](https://github.com/got-feedback/feedback-plugin-tabimport) | Drag and drop Guitar Pro files to create songs | `git clone ...slopsmith-plugin-tabimport.git tab_import` | | [Import Tab](https://github.com/got-feedback/feedBack-plugin-tabimport) | Drag and drop Guitar Pro files to create songs | `git clone ...feedBack-plugin-tabimport.git tab_import` |
| [Practice Journal](https://github.com/got-feedback/feedback-plugin-practice) | Auto-track practice time, speed, loops. Dashboard with charts | `git clone ...slopsmith-plugin-practice.git practice_journal` | | [Practice Journal](https://github.com/got-feedback/feedBack-plugin-practice) | Auto-track practice time, speed, loops. Dashboard with charts | `git clone ...feedBack-plugin-practice.git practice_journal` |
| [Setlist Builder](https://github.com/got-feedback/feedback-plugin-setlist) | Create ordered playlists with sequential playback | `git clone ...slopsmith-plugin-setlist.git setlist` | | [Setlist Builder](https://github.com/got-feedback/feedBack-plugin-setlist) | Create ordered playlists with sequential playback | `git clone ...feedBack-plugin-setlist.git setlist` |
| [Metronome](https://github.com/got-feedback/feedback-plugin-metronome) | Audible click and visual beat flash synced to song tempo | `git clone ...slopsmith-plugin-metronome.git metronome` | | [Metronome](https://github.com/got-feedback/feedBack-plugin-metronome) | Audible click and visual beat flash synced to song tempo | `git clone ...feedBack-plugin-metronome.git metronome` |
| [Tone Player](https://github.com/got-feedback/feedback-plugin-tones) | View amp/pedal/cab signal chains with gear artwork | `git clone ...slopsmith-plugin-tones.git tones` | | [Tone Player](https://github.com/got-feedback/feedBack-plugin-tones) | View amp/pedal/cab signal chains with gear artwork | `git clone ...feedBack-plugin-tones.git tones` |
| [Fretboard View](https://github.com/got-feedback/feedback-plugin-fretboard) | Live fretboard overlay showing active notes in real-time | `git clone ...slopsmith-plugin-fretboard.git fretboard` | | [Fretboard View](https://github.com/got-feedback/feedBack-plugin-fretboard) | Live fretboard overlay showing active notes in real-time | `git clone ...feedBack-plugin-fretboard.git fretboard` |
| [Tab View](https://github.com/got-feedback/feedback-plugin-tabview) | Scrolling guitar tablature notation via alphaTab | `git clone ...slopsmith-plugin-tabview.git tab_view` | | [Tab View](https://github.com/got-feedback/feedBack-plugin-tabview) | Scrolling guitar tablature notation via alphaTab | `git clone ...feedBack-plugin-tabview.git tab_view` |
| [MIDI Amp Control](https://github.com/got-feedback/feedback-plugin-midi) | Auto-switch amp/modeler presets via MIDI on tone changes | `git clone ...slopsmith-plugin-midi.git midi_amp` | | [MIDI Amp Control](https://github.com/got-feedback/feedBack-plugin-midi) | Auto-switch amp/modeler presets via MIDI on tone changes | `git clone ...feedBack-plugin-midi.git midi_amp` |
| [Section Map](https://github.com/got-feedback/feedback-plugin-sectionmap) | Color-coded song structure minimap with clickable navigation | `git clone ...slopsmith-plugin-sectionmap.git section_map` | | [Section Map](https://github.com/got-feedback/feedBack-plugin-sectionmap) | Color-coded song structure minimap with clickable navigation | `git clone ...feedBack-plugin-sectionmap.git section_map` |
| [Arrangement Editor](https://github.com/got-feedback/feedback-plugin-editor) | DAW-like visual editor for creating and editing song note charts | `git clone ...slopsmith-plugin-editor.git editor` | | [Arrangement Editor](https://github.com/got-feedback/feedBack-plugin-editor) | DAW-like visual editor for creating and editing song note charts | `git clone ...feedBack-plugin-editor.git editor` |
| [MIDI Capo](https://github.com/masc0t/slopsmith-plugin-midi-capo) | MIDI capo control for real-time transposition | `git clone ...slopsmith-plugin-midi-capo.git midi_capo` | | [MIDI Capo](https://github.com/masc0t/slopsmith-plugin-midi-capo) | MIDI capo control for real-time transposition | `git clone ...slopsmith-plugin-midi-capo.git midi_capo` |
| [Note Detection](https://github.com/got-feedback/feedback-plugin-notedetect) | Real-time pitch detection and scoring against highway notes | `git clone ...slopsmith-plugin-notedetect.git note_detect` | | [Note Detection](https://github.com/got-feedback/feedBack-plugin-notedetect) | Real-time pitch detection and scoring against highway notes | `git clone ...feedBack-plugin-notedetect.git note_detect` |
| [Find More](https://github.com/masc0t/slopsmith-plugin-find-more) | Search for more songs by the same artist | `git clone ...slopsmith-plugin-find-more.git find_more` | | [Find More](https://github.com/masc0t/slopsmith-plugin-find-more) | Search for more songs by the same artist | `git clone ...slopsmith-plugin-find-more.git find_more` |
| [Piano Highway](https://github.com/got-feedback/feedback-plugin-piano) | Scrolling piano/keyboard view for Keys arrangements with MIDI input | `git clone ...slopsmith-plugin-piano.git piano` | | [Piano Highway](https://github.com/got-feedback/feedBack-plugin-piano) | Scrolling piano/keyboard view for Keys arrangements with MIDI input | `git clone ...feedBack-plugin-piano.git piano` |
| [Studio](https://github.com/got-feedback/feedback-plugin-studio) | Collaborative band recording and multi-track mixing | `git clone ...slopsmith-plugin-studio.git studio` | | [Studio](https://github.com/got-feedback/feedBack-plugin-studio) | Collaborative band recording and multi-track mixing | `git clone ...feedBack-plugin-studio.git studio` |
| [Drum Highway](https://github.com/got-feedback/feedback-plugin-drums) | Lane-based drum highway with MIDI drum pad input and built-in sounds | `git clone ...slopsmith-plugin-drums.git drums` | | [Drum Highway](https://github.com/got-feedback/feedBack-plugin-drums) | Lane-based drum highway with MIDI drum pad input and built-in sounds | `git clone ...feedBack-plugin-drums.git drums` |
| [Split Screen](https://github.com/topkoa/slopsmith-plugin-splitscreen) | 2-4 highway panels side-by-side for multi-arrangement practice | `git clone ...slopsmith-plugin-splitscreen.git splitscreen` |
| [Stems Mixer](https://github.com/topkoa/slopsmith-plugin-stems) | Per-stem mute/volume controls for .sloppak songs | `git clone ...slopsmith-plugin-stems.git stems` |
| [Invert Highway](https://github.com/masc0t/slopsmith-plugin-invert-highway) | Flip the highway note direction | `git clone ...slopsmith-plugin-invert-highway.git invert_highway` | | [Invert Highway](https://github.com/masc0t/slopsmith-plugin-invert-highway) | Flip the highway note direction | `git clone ...slopsmith-plugin-invert-highway.git invert_highway` |
| [Jumping Tab](https://github.com/renanboni/slopsmith-plugin-jumpingtab) | Yousician-style 2D horizontal tab with trajectory arcs and hopping ball | `git clone ...slopsmith-plugin-jumpingtab.git jumpingtab` | | [Jumping Tab](https://github.com/renanboni/slopsmith-plugin-jumpingtab) | Yousician-style 2D horizontal tab with trajectory arcs and hopping ball | `git clone ...slopsmith-plugin-jumpingtab.git jumpingtab` |
| [Step Mode](https://github.com/got-feedback/feedback-plugin-stepmode) | Step-by-step practice mode — highway freezes at each note until played (via Note Detection) or Space | `git clone ...slopsmith-plugin-stepmode.git step_mode` | | [Step Mode](https://github.com/got-feedback/feedBack-plugin-stepmode) | Step-by-step practice mode — highway freezes at each note until played (via Note Detection) or Space | `git clone ...feedBack-plugin-stepmode.git step_mode` |
| [Lyrics Sync](https://github.com/got-feedback/feedback-plugin-lyrics-sync) | Generate synced LRC lyrics from text + vocals stem via Whisper alignment | `git clone ...slopsmith-plugin-lyrics-sync.git lyrics_sync` | | [Lyrics Sync](https://github.com/got-feedback/feedBack-plugin-lyrics-sync) | Generate synced LRC lyrics from text + vocals stem via Whisper alignment | `git clone ...feedBack-plugin-lyrics-sync.git lyrics_sync` |
| [Lyrics Karaoke](https://github.com/got-feedback/feedback-plugin-lyrics-karaoke) | Per-syllable karaoke pitch ribbon for sloppak songs (Whisper alignment + librosa pYIN) | `git clone ...slopsmith-plugin-lyrics-karaoke.git lyrics_karaoke` | | [Lyrics Karaoke](https://github.com/got-feedback/feedBack-plugin-lyrics-karaoke) | Per-syllable karaoke pitch ribbon for sloppak songs (Whisper alignment + librosa pYIN) | `git clone ...feedBack-plugin-lyrics-karaoke.git lyrics_karaoke` |
| [NAM Tone Engine](https://github.com/got-feedback/feedback-plugin-nam-tone) | In-browser amp modeling with NAM WASM, cabinet IRs, tone auto-switching | `git clone ...slopsmith-plugin-nam-tone.git nam_tone` | | [NAM Tone Engine](https://github.com/got-feedback/feedBack-plugin-nam-tone) | In-browser amp modeling with NAM WASM, cabinet IRs, tone auto-switching | `git clone ...feedBack-plugin-nam-tone.git nam_tone` |
| [Guitar Theory Lab](https://github.com/topkoa/slopsmith-plugin-guitar-theory) | Explore scales, chords, intervals, tunings, and voicings on a fully interactive fretboard | `git clone ...slopsmith-plugin-nam-tone.git guitar-theory-lab` | | [Guitar Theory Lab](https://github.com/topkoa/slopsmith-plugin-guitar-theory) | Explore scales, chords, intervals, tunings, and voicings on a fully interactive fretboard | `git clone ...slopsmith-plugin-guitar-theory.git guitar-theory-lab` |
| [Themes](https://github.com/masc0t/slopsmith-plugin-themes) | Offers several basic recolorings of the interface | `git clone ...slopsmith-plugin-themes.git themes` | | [Themes](https://github.com/masc0t/slopsmith-plugin-themes) | Offers several basic recolorings of the interface | `git clone ...slopsmith-plugin-themes.git themes` |
| [Update Manager](https://github.com/masc0t/slopsmith-update-manager) | Installs, updates, and uninstalls other plugins and the slopsmith core itself | `git clone ...slopsmith-update-manager.git update_manager` | | [Update Manager](https://github.com/masc0t/slopsmith-update-manager) | Installs, updates, and uninstalls other plugins and the feedBack core itself | `git clone ...slopsmith-update-manager.git update_manager` |
| [Tuner](https://github.com/OmikronApex/slopsmith-plugin-tuner) | Floating tuner with customizable tunings | `git clone ...slopsmith-plugin-tuner.git tuner` |
| [Simplify Chords](https://github.com/bkranendonk/slopsmith-plugin-simplify-chords) | Changes complex chords on the note highway to simpler ones. Inspired by Ultimate Guitar's Simplify button. | `git clone ...slopsmith-plugin-simplify-chords.git simplify-chords` | | [Simplify Chords](https://github.com/bkranendonk/slopsmith-plugin-simplify-chords) | Changes complex chords on the note highway to simpler ones. Inspired by Ultimate Guitar's Simplify button. | `git clone ...slopsmith-plugin-simplify-chords.git simplify-chords` |
| [Key Bindings](https://github.com/jackipicco/slopsmith-plugin-key-bindings) | Highway key bindings for keyboard and TV remote | `git clone ...slopsmith-plugin-key-bindings.git key_bindings` | | [Key Bindings](https://github.com/jackipicco/slopsmith-plugin-key-bindings) | Highway key bindings for keyboard and TV remote | `git clone ...slopsmith-plugin-key-bindings.git key_bindings` |
| [Folder Organizer](https://github.com/Elit3d/slopsmith-plugin-folder-organizer) | Organize your sloppak DLC songs into a folder tree view, grouped by subfolder name | `git clone ...slopsmith-plugin-folder-organizer.git folder-organizer` | | [Folder Organizer](https://github.com/Elit3d/slopsmith-plugin-folder-organizer) | Organize your sloppak DLC songs into a folder tree view, grouped by subfolder name | `git clone ...slopsmith-plugin-folder-organizer.git folder-organizer` |
| [Virtuoso](https://github.com/got-feedback/feedback-plugin-virtuoso) | Practice studio for guitar & bass — scale, technique, and rhythm drills, timed workouts, and jam backing that teach skills you take off the screen. | `git clone ...feedback-plugin-virtuoso.git virtuoso` | | [Virtuoso](https://github.com/got-feedback/feedback-plugin-virtuoso) | Practice studio for guitar & bass — scale, technique, and rhythm drills, timed workouts, and jam backing that teach skills you take off the screen. | `git clone ...feedback-plugin-virtuoso.git virtuoso` |
| [NAM Rig Builder](https://github.com/Jafz2001/slopsmith-plugin-nam-rig-builder) | Map tones to chained NAM neural-amp rigs (tone3000 captures + IRs) — full pedal→amp→cab playback, per-stage bypass, and a gear catalog | `git clone ...slopsmith-plugin-nam-rig-builder.git nam_rig_builder` |
| [Audio Preview](https://github.com/saleemk/slopsmith-plugin-audio-preview) | Quick audio previews from library cards with configurable start time, volume, and duration | `git clone ...slopsmith-plugin-audio-preview.git audio_preview` | | [Audio Preview](https://github.com/saleemk/slopsmith-plugin-audio-preview) | Quick audio previews from library cards with configurable start time, volume, and duration | `git clone ...slopsmith-plugin-audio-preview.git audio_preview` |
| [Song Mastery](https://github.com/jamesgaiser/slopsmith-plugin-song-mastery) | Auto-adjusts difficulty based on your rolling note accuracy and saves the slider position per song | `git clone ...slopsmith-plugin-song-mastery.git song_mastery` | | [Song Mastery](https://github.com/jamesgaiser/slopsmith-plugin-song-mastery) | Auto-adjusts difficulty based on your rolling note accuracy and saves the slider position per song | `git clone ...slopsmith-plugin-song-mastery.git song_mastery` |
| [Song Preview](https://github.com/DeathlySin/slopsmith-plugin-song-preview) | Quickly hear previews of songs in your library with a clean visual indicator of what's playing. Supports .sloppak and loose folders song formats, with the visual indicator matching up to whatever theme you are using! | `git clone ...slopsmith-plugin-song-preview.git song_preview` |
| [Mobile Note Highway](https://github.com/saleemk/slopsmith-plugin-mobile-note-highway) | Touch-optimized player with collapsible controls, highway gestures, and device-adaptive layouts for phones and tablets | `git clone ...slopsmith-plugin-mobile-note-highway.git mobile_note_highway` | | [Mobile Note Highway](https://github.com/saleemk/slopsmith-plugin-mobile-note-highway) | Touch-optimized player with collapsible controls, highway gestures, and device-adaptive layouts for phones and tablets | `git clone ...slopsmith-plugin-mobile-note-highway.git mobile_note_highway` |
| [Shuffle](https://github.com/Erikcb91/Slopsmith-Shuffle-Mode) | Random playback from your library — artist & tuning filters, auto-advance with countdown popup, note_detect compatible | `git clone https://github.com/Erikcb91/Slopsmith-Shuffle-Mode.git shuffle` |
Install any plugin by cloning it into your `plugins/` directory and restarting: Install any plugin by cloning it into your `plugins/` directory and restarting:
```bash ```bash
cd plugins cd plugins
git clone https://github.com/got-feedback/feedback-plugin-ug.git ultimate_guitar git clone https://github.com/got-feedback/feedBack-plugin-ug.git ultimate_guitar
docker compose restart docker compose restart
``` ```
+2 -2
View File
@@ -1,8 +1,8 @@
# Supporters # Supporters
Slopsmith's development is supported by these generous people. Thank you. ❤️ FeedBack's development is supported by these generous people. Thank you. ❤️
Want to be listed here? See [Support Slopsmith](README.md#support-slopsmith). Want to be listed here? See [Support FeedBack](README.md#support-feedBack).
## Patrons ## Patrons
+1 -1
View File
@@ -22,5 +22,5 @@ too sharp / too flat / not played).
- **[Implementation Plan](docs/NOTE_FAILURE_PLAN.md)** — 7 phases from - **[Implementation Plan](docs/NOTE_FAILURE_PLAN.md)** — 7 phases from
detection foundation through section grading and polish detection foundation through section grading and polish
- **Note Detection Plugin Plan** — see the - **Note Detection Plugin Plan** — see the
[slopsmith-plugin-notedetect](https://github.com/topkoa/slopsmith-plugin-notedetect) [feedBack-plugin-notedetect](https://github.com/topkoa/feedBack-plugin-notedetect)
repository (Phase 0 foundation) repository (Phase 0 foundation)
+12 -12
View File
@@ -9,8 +9,8 @@
# sudo bash build-proxmox-ct.sh [TARGETARCH] [OUTPUT_NAME] # sudo bash build-proxmox-ct.sh [TARGETARCH] [OUTPUT_NAME]
# #
# Examples: # Examples:
# sudo bash build-proxmox-ct.sh amd64 slopsmith-ct # sudo bash build-proxmox-ct.sh amd64 feedBack-ct
# sudo bash build-proxmox-ct.sh arm64 slopsmith-ct # sudo bash build-proxmox-ct.sh arm64 feedBack-ct
# #
# The resulting container ships empty; mount or copy your .sloppak / # The resulting container ships empty; mount or copy your .sloppak /
# loose-folder library into /dlc inside the CT after import. # loose-folder library into /dlc inside the CT after import.
@@ -26,13 +26,13 @@
# sudo apt install debootstrap systemd-container tar zstd curl unzip git # sudo apt install debootstrap systemd-container tar zstd curl unzip git
# #
# On Proxmox, after transfer: # On Proxmox, after transfer:
# pct restore <VMID> slopsmith-ct.tar.zst --storage local-lvm --rootfs 8 --unprivileged 1 # pct restore <VMID> feedBack-ct.tar.zst --storage local-lvm --rootfs 8 --unprivileged 1
# ============================================================================= # =============================================================================
set -euo pipefail set -euo pipefail
TARGETARCH="${1:-amd64}" TARGETARCH="${1:-amd64}"
OUTPUT_NAME="${2:-slopsmith-ct}" OUTPUT_NAME="${2:-feedBack-ct}"
# OUTPUT_NAME is a positional arg that flows into BUILD_BASE (interpolated into # OUTPUT_NAME is a positional arg that flows into BUILD_BASE (interpolated into
# `mkdir -p` / `rm -rf` paths) and into the final tarball name. Reject anything # `mkdir -p` / `rm -rf` paths) and into the final tarball name. Reject anything
@@ -104,7 +104,7 @@ VENV_DIR="/opt/app-venv"
PIP_VERSION="26.1.1" PIP_VERSION="26.1.1"
DLC_DIR="/dlc" DLC_DIR="/dlc"
CONFIG_DIR="/config" CONFIG_DIR="/config"
SVC_USER="slopsmith" SVC_USER="feedBack"
# Coloured logging # Coloured logging
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; } info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
@@ -420,7 +420,7 @@ ok "Build dependencies removed."
# ============================================================================= # =============================================================================
# 5d. Tailwind CLI for runtime stylesheet regeneration # 5d. Tailwind CLI for runtime stylesheet regeneration
# ============================================================================= # =============================================================================
# When a plugin is installed into SLOPSMITH_PLUGINS_DIR at runtime (or # When a plugin is installed into FEEDBACK_PLUGINS_DIR at runtime (or
# discovered there on startup), the server rebuilds static/tailwind.min.css # discovered there on startup), the server rebuilds static/tailwind.min.css
# so the plugin's classes are styled — the image-baked sheet only covers # so the plugin's classes are styled — the image-baked sheet only covers
# in-tree plugins (see lib/tailwind_rebuild.py). tailwindcss is installed # in-tree plugins (see lib/tailwind_rebuild.py). tailwindcss is installed
@@ -523,11 +523,11 @@ info "Creating service user '${SVC_USER}' …"
r "useradd --system --home-dir ${APP_DIR} --shell /usr/sbin/nologin ${SVC_USER}" r "useradd --system --home-dir ${APP_DIR} --shell /usr/sbin/nologin ${SVC_USER}"
ok "User '${SVC_USER}' created." ok "User '${SVC_USER}' created."
info "Installing slopsmith-server.service …" info "Installing feedBack-server.service …"
mkdir -p "${ROOTFS}/etc/systemd/system" mkdir -p "${ROOTFS}/etc/systemd/system"
cat > "${ROOTFS}/etc/systemd/system/slopsmith-server.service" <<EOF cat > "${ROOTFS}/etc/systemd/system/feedBack-server.service" <<EOF
[Unit] [Unit]
Description=Slopsmith uvicorn server Description=FeedBack uvicorn server
After=network.target After=network.target
[Service] [Service]
@@ -547,8 +547,8 @@ EOF
# Enable by symlinking (avoids running systemctl inside nspawn) # Enable by symlinking (avoids running systemctl inside nspawn)
mkdir -p "${ROOTFS}/etc/systemd/system/multi-user.target.wants" mkdir -p "${ROOTFS}/etc/systemd/system/multi-user.target.wants"
ln -sf /etc/systemd/system/slopsmith-server.service \ ln -sf /etc/systemd/system/feedBack-server.service \
"${ROOTFS}/etc/systemd/system/multi-user.target.wants/slopsmith-server.service" "${ROOTFS}/etc/systemd/system/multi-user.target.wants/feedBack-server.service"
ok "Service enabled." ok "Service enabled."
# ============================================================================= # =============================================================================
@@ -662,6 +662,6 @@ cat <<DONE
--start 1 --start 1
Then check the server: Then check the server:
pct exec 200 -- systemctl status slopsmith-server pct exec 200 -- systemctl status feedBack-server
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DONE DONE
+4 -4
View File
@@ -7,17 +7,17 @@ services:
- "8000:8000" - "8000:8000"
volumes: volumes:
# Song library folder on NAS # Song library folder on NAS
- /volume1/music/slopsmith:/dlc - /volume1/music/feedBack:/dlc
# Persistent config, cache, favorites, loops, practice data # Persistent config, cache, favorites, loops, practice data
- slopsmith-config:/config - feedBack-config:/config
environment: environment:
- DLC_DIR=/dlc - DLC_DIR=/dlc
- CONFIG_DIR=/config - CONFIG_DIR=/config
# Logging (optional) # Logging (optional)
# - LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR (default: INFO) # - LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR (default: INFO)
# - LOG_FORMAT=json # json | text (default: text) # - LOG_FORMAT=json # json | text (default: text)
# - LOG_FILE=/config/slopsmith.log # also write to a persistent file # - LOG_FILE=/config/feedBack.log # also write to a persistent file
restart: unless-stopped restart: unless-stopped
volumes: volumes:
slopsmith-config: feedBack-config:
+3 -3
View File
@@ -7,7 +7,7 @@ services:
# Mount your song library folder (adjust path for your system) # Mount your song library folder (adjust path for your system)
- ${LIBRARY_PATH:-./library}:/dlc - ${LIBRARY_PATH:-./library}:/dlc
# Persistent config and cache # Persistent config and cache
- slopsmith-config:/config - feedBack-config:/config
# Mount source for live reload during development # Mount source for live reload during development
- ./static:/app/static - ./static:/app/static
- ./server.py:/app/server.py - ./server.py:/app/server.py
@@ -28,10 +28,10 @@ services:
# Logging (optional) # Logging (optional)
# - LOG_LEVEL=DEBUG # DEBUG | INFO | WARNING | ERROR (default: INFO) # - LOG_LEVEL=DEBUG # DEBUG | INFO | WARNING | ERROR (default: INFO)
# - LOG_FORMAT=json # json | text (default: text — coloured console) # - LOG_FORMAT=json # json | text (default: text — coloured console)
# - LOG_FILE=/config/slopsmith.log # also write to a persistent file # - LOG_FILE=/config/feedBack.log # also write to a persistent file
dns: dns:
- 8.8.8.8 - 8.8.8.8
- 1.1.1.1 - 1.1.1.1
volumes: volumes:
slopsmith-config: feedBack-config:
+3 -3
View File
@@ -9,10 +9,10 @@ Depends on: `docs/NOTE_FAILURE_SPEC.md` (read that first)
**Goal:** Working note detection plugin streaming detected notes via WebSocket. **Goal:** Working note detection plugin streaming detected notes via WebSocket.
This phase was previously tracked in a separate NOTE_DETECTION_PLUGIN_PLAN This phase was previously tracked in a separate NOTE_DETECTION_PLUGIN_PLAN
document (in the `slopsmith-plugin-notedetect` repository). The relevant scope document (in the `feedBack-plugin-notedetect` repository). The relevant scope
is summarized here to avoid relying on an internal git-only reference: is summarized here to avoid relying on an internal git-only reference:
- [ ] Plugin skeleton: `slopsmith-plugin-notedetect/` with plugin.json, routes.py, screen.js - [ ] Plugin skeleton: `feedBack-plugin-notedetect/` with plugin.json, routes.py, screen.js
- [ ] Port TonalRecall YIN detection (aubio + sounddevice) to routes.py - [ ] Port TonalRecall YIN detection (aubio + sounddevice) to routes.py
- [ ] WebSocket at `/api/plugins/note_detect/stream` streaming `{ note, freq, confidence, time }` - [ ] WebSocket at `/api/plugins/note_detect/stream` streaming `{ note, freq, confidence, time }`
- [ ] Device selection UI in screen.html - [ ] Device selection UI in screen.html
@@ -138,7 +138,7 @@ shows the correct diagnostic labels.
``` ```
Displayed for 1.5s, then fades. Displayed for 1.5s, then fades.
- [ ] Track `bestIteration` across all iterations for "Best" display - [ ] Track `bestIteration` across all iterations for "Best" display
- [ ] Emit `loop:complete` event via `window.slopsmith.emit()` so other plugins - [ ] Emit `loop:complete` event via `window.feedBack.emit()` so other plugins
(practice journal) can record the data (practice journal) can record the data
- [ ] Reset loop history when loop boundaries change or loop is cleared - [ ] Reset loop history when loop boundaries change or loop is cleared
+6 -6
View File
@@ -13,7 +13,7 @@ late, wrong pitch, or not played at all.
## Prerequisites ## Prerequisites
This feature depends on the **note detection plugin** (`slopsmith-plugin-notedetect`), This feature depends on the **note detection plugin** (`feedBack-plugin-notedetect`),
which provides real-time pitch detection via server-side aubio/YIN over WebSocket. which provides real-time pitch detection via server-side aubio/YIN over WebSocket.
The detection plugin streams `DetectedNote` events; this spec describes the The detection plugin streams `DetectedNote` events; this spec describes the
**matching, judgment, and rendering** layer that consumes those events. **matching, judgment, and rendering** layer that consumes those events.
@@ -55,10 +55,10 @@ Guitar → USB Adapter → sounddevice (server)
Wire format: `{ note: "A2", freq: 110.0, confidence: 0.92, time: 1.234 }` Wire format: `{ note: "A2", freq: 110.0, confidence: 0.92, time: 1.234 }`
> **Plugin naming note:** The detection plugin's repository is named > **Plugin naming note:** The detection plugin's repository is named
> `slopsmith-plugin-notedetect`, but the plugin registers with the id > `feedBack-plugin-notedetect`, but the plugin registers with the id
> `note_detect` (snake_case). Its HTTP/WebSocket routes therefore appear > `note_detect` (snake_case). Its HTTP/WebSocket routes therefore appear
> under `/api/plugins/note_detect/…`. There is no `window.slopsmithPlugin_*` > under `/api/plugins/note_detect/…`. There is no `window.feedBackPlugin_*`
> global pattern in Slopsmith — to check whether the detection plugin is > global pattern in FeedBack — to check whether the detection plugin is
> available at runtime, attempt a fetch to `/api/plugins/note_detect/status` > available at runtime, attempt a fetch to `/api/plugins/note_detect/status`
> (or similar) or consult the `/api/plugins` list. Use the repo name only > (or similar) or consult the `/api/plugins` list. Use the repo name only
> in documentation links. > in documentation links.
@@ -335,7 +335,7 @@ The tracker must handle A-B looping:
| `loopA`, `loopB` | Current A-B loop boundaries | | `loopA`, `loopB` | Current A-B loop boundaries |
| `audio.currentTime` | Actual audio playback position | | `audio.currentTime` | Actual audio playback position |
### New Events Emitted (via `window.slopsmith.emit`) ### New Events Emitted (via `window.feedBack.emit`)
| Event | Payload | | Event | Payload |
|------------------------------|------------------------------------------| |------------------------------|------------------------------------------|
@@ -373,7 +373,7 @@ There are three distinct threshold tiers — keep them conceptually separate:
| `hitGlowDuration` | 0.5 | Green glow fade time (sec) | | `hitGlowDuration` | 0.5 | Green glow fade time (sec) |
Persist these settings in plugin-local storage (e.g. `localStorage` prefixed Persist these settings in plugin-local storage (e.g. `localStorage` prefixed
with the plugin id). Do **not** assume they can be saved through Slopsmith's with the plugin id). Do **not** assume they can be saved through FeedBack's
`/api/settings` endpoint under a `notedetect_feedback` key — the current server `/api/settings` endpoint under a `notedetect_feedback` key — the current server
only persists a fixed set of known settings keys. If backend support for a only persists a fixed set of known settings keys. If backend support for a
dedicated persisted key is added later, this plugin may migrate to `/api/settings`. dedicated persisted key is added later, this plugin may migrate to `/api/settings`.
@@ -1,4 +1,4 @@
# Slopsmith Note Detect Bass Benchmark — v1 # FeedBack Note Detect Bass Benchmark — v1
A bass-focused companion to the guitar benchmarks A bass-focused companion to the guitar benchmarks
(note_detect_v1 + note_detect_v2). Tests `note_detect` against bass- (note_detect_v1 + note_detect_v2). Tests `note_detect` against bass-
@@ -20,11 +20,11 @@ the guitar one:
than guitar E2 at ~82 Hz. The benchmark should exercise that than guitar E2 at ~82 Hz. The benchmark should exercise that
regime explicitly so we can spot regressions there. regime explicitly so we can spot regressions there.
How to run inside the slopsmith container: How to run inside the feedBack container:
docker cp docs/benchmarks/note_detect_bass_v1/build_benchmark.py \\ docker cp docs/benchmarks/note_detect_bass_v1/build_benchmark.py \\
slopsmith-web-1:/tmp/build_benchmark_bass.py feedBack-web-1:/tmp/build_benchmark_bass.py
docker exec slopsmith-web-1 python /tmp/build_benchmark_bass.py \\ docker exec feedBack-web-1 python /tmp/build_benchmark_bass.py \\
/app/static/sloppak_cache/note_detect_benchmark_bass_v1.sloppak /app/static/sloppak_cache/note_detect_benchmark_bass_v1.sloppak
After regenerating, copy the zip output to the tracked path with the After regenerating, copy the zip output to the tracked path with the
@@ -351,7 +351,7 @@ def build(out_dir: Path):
arrangement = { arrangement = {
'name': 'Bass', 'name': 'Bass',
# Pad to 6 slots even on bass — slopsmith's `tuning_name()` only # Pad to 6 slots even on bass — feedBack's `tuning_name()` only
# recognises named tunings (E Standard, Drop D, etc.) on 6-element # recognises named tunings (E Standard, Drop D, etc.) on 6-element
# arrays, so a 4-element array shows up in the library card as the # arrays, so a 4-element array shows up in the library card as the
# raw numeric form ("0 0 0 0") instead of "E Standard". The # raw numeric form ("0 0 0 0") instead of "E Standard". The
@@ -371,7 +371,7 @@ def build(out_dir: Path):
manifest = { manifest = {
'title': 'Note Detect Bass Benchmark v1', 'title': 'Note Detect Bass Benchmark v1',
'artist': 'Slopsmith', 'artist': 'FeedBack',
'album': 'Note Detection Benchmark', 'album': 'Note Detection Benchmark',
'year': 2026, 'year': 2026,
'duration': round(end_t, 3), 'duration': round(end_t, 3),
@@ -389,7 +389,7 @@ def build(out_dir: Path):
{'id': 'full', 'file': 'stems/full.ogg', 'default': True}, {'id': 'full', 'file': 'stems/full.ogg', 'default': True},
], ],
'benchmark': { 'benchmark': {
'id': 'slopsmith-note-detect-benchmark-bass', 'id': 'feedBack-note-detect-benchmark-bass',
'version': 1, 'version': 1,
}, },
} }
@@ -461,7 +461,7 @@ def _build_zip(src_dir: Path):
def _benchmark_readme(duration_s): def _benchmark_readme(duration_s):
return f"""# Slopsmith Note Detect Bass Benchmark — v1 return f"""# FeedBack Note Detect Bass Benchmark — v1
A bass-focused companion to the guitar benchmarks A bass-focused companion to the guitar benchmarks
(note_detect_v1 + note_detect_v2). Tests `note_detect` against bass- (note_detect_v1 + note_detect_v2). Tests `note_detect` against bass-
+3 -3
View File
@@ -1,6 +1,6 @@
# Slopsmith Note Detect Benchmark — v1 # FeedBack Note Detect Benchmark — v1
A short test piece for tuning Slopsmith's `note_detect` plugin. Eight A short test piece for tuning FeedBack's `note_detect` plugin. Eight
exercises, each isolating a specific detection failure mode. Run with exercises, each isolating a specific detection failure mode. Run with
**Detect** enabled, play through, then export the diagnostic JSON **Detect** enabled, play through, then export the diagnostic JSON
(Settings → Plugins → Note Detection → Download Diagnostic JSON, or (Settings → Plugins → Note Detection → Download Diagnostic JSON, or
@@ -43,4 +43,4 @@ Share the JSON (schema `note_detect.diagnostic.v1`). It includes:
## Source ## Source
Built by `docs/benchmarks/note_detect_v1/build_benchmark.py` in the Built by `docs/benchmarks/note_detect_v1/build_benchmark.py` in the
slopsmith repo. Tweak the exercise list there and regenerate. feedBack repo. Tweak the exercise list there and regenerate.
@@ -5,12 +5,12 @@ short exercises designed to isolate specific failure modes (open-string
mono, fretted positions, octaves, sustained held notes, hammer-on / mono, fretted positions, octaves, sustained held notes, hammer-on /
pull-off, sparse power chords, dense open chords, bends). pull-off, sparse power chords, dense open chords, bends).
How to run inside the slopsmith container (recommended — has ffmpeg + How to run inside the feedBack container (recommended — has ffmpeg +
pyyaml already): pyyaml already):
docker cp docs/benchmarks/note_detect_v1/build_benchmark.py \ docker cp docs/benchmarks/note_detect_v1/build_benchmark.py \
slopsmith-web-1:/tmp/build_benchmark.py feedBack-web-1:/tmp/build_benchmark.py
docker exec slopsmith-web-1 python /tmp/build_benchmark.py \ docker exec feedBack-web-1 python /tmp/build_benchmark.py \
/app/static/sloppak_cache/note_detect_benchmark_v1.sloppak /app/static/sloppak_cache/note_detect_benchmark_v1.sloppak
The output sloppak lands under `static/sloppak_cache/` on the host The output sloppak lands under `static/sloppak_cache/` on the host
@@ -26,7 +26,7 @@ import sys
import wave import wave
from pathlib import Path from pathlib import Path
import yaml # bundled with the slopsmith image import yaml # bundled with the feedBack image
# ── Benchmark parameters ──────────────────────────────────────────────── # ── Benchmark parameters ────────────────────────────────────────────────
BPM = 90.0 BPM = 90.0
@@ -411,7 +411,7 @@ def build(out_dir: Path):
manifest = { manifest = {
'title': 'Note Detect Benchmark v1', 'title': 'Note Detect Benchmark v1',
'artist': 'Slopsmith', 'artist': 'FeedBack',
'album': 'Note Detection Benchmark', 'album': 'Note Detection Benchmark',
'year': 2026, 'year': 2026,
'duration': round(end_t, 3), 'duration': round(end_t, 3),
@@ -430,7 +430,7 @@ def build(out_dir: Path):
# Non-standard key — picked up by future tooling that wants to # Non-standard key — picked up by future tooling that wants to
# detect "this is the benchmark, schema v1". The loader ignores it. # detect "this is the benchmark, schema v1". The loader ignores it.
'benchmark': { 'benchmark': {
'id': 'slopsmith-note-detect-benchmark', 'id': 'feedBack-note-detect-benchmark',
'version': 1, 'version': 1,
}, },
} }
@@ -545,9 +545,9 @@ def _build_zip(src_dir: Path):
def _benchmark_readme(duration_s): def _benchmark_readme(duration_s):
return f"""# Slopsmith Note Detect Benchmark — v1 return f"""# FeedBack Note Detect Benchmark — v1
A short test piece for tuning Slopsmith's `note_detect` plugin. Eight A short test piece for tuning FeedBack's `note_detect` plugin. Eight
exercises, each isolating a specific detection failure mode. Run with exercises, each isolating a specific detection failure mode. Run with
**Detect** enabled, play through, then export the diagnostic JSON **Detect** enabled, play through, then export the diagnostic JSON
(Settings → Plugins → Note Detection → Download Diagnostic JSON, or (Settings → Plugins → Note Detection → Download Diagnostic JSON, or
@@ -590,7 +590,7 @@ Share the JSON (schema `note_detect.diagnostic.v1`). It includes:
## Source ## Source
Built by `docs/benchmarks/note_detect_v1/build_benchmark.py` in the Built by `docs/benchmarks/note_detect_v1/build_benchmark.py` in the
slopsmith repo. Tweak the exercise list there and regenerate. feedBack repo. Tweak the exercise list there and regenerate.
""" """
+1 -1
View File
@@ -1,4 +1,4 @@
# Slopsmith Note Detect Benchmark — v2 # FeedBack Note Detect Benchmark — v2
A slower-paced companion to v1, focused on what players can actually A slower-paced companion to v1, focused on what players can actually
land cleanly. Half-note spacing throughout (~1.33 s between events at land cleanly. Half-note spacing throughout (~1.33 s between events at
@@ -16,11 +16,11 @@ Goals vs v1:
technique handling is the next algorithm focus, separate from technique handling is the next algorithm focus, separate from
measuring "do basic single notes + chords score correctly?" measuring "do basic single notes + chords score correctly?"
How to run inside the slopsmith container: How to run inside the feedBack container:
docker cp docs/benchmarks/note_detect_v2/build_benchmark.py \\ docker cp docs/benchmarks/note_detect_v2/build_benchmark.py \\
slopsmith-web-1:/tmp/build_benchmark_v2.py feedBack-web-1:/tmp/build_benchmark_v2.py
docker exec slopsmith-web-1 python /tmp/build_benchmark_v2.py \\ docker exec feedBack-web-1 python /tmp/build_benchmark_v2.py \\
/app/static/sloppak_cache/note_detect_benchmark_v2.sloppak /app/static/sloppak_cache/note_detect_benchmark_v2.sloppak
After regenerating, copy the zip output to the tracked path with the After regenerating, copy the zip output to the tracked path with the
@@ -375,7 +375,7 @@ def build(out_dir: Path):
manifest = { manifest = {
'title': 'Note Detect Benchmark v2', 'title': 'Note Detect Benchmark v2',
'artist': 'Slopsmith', 'artist': 'FeedBack',
'album': 'Note Detection Benchmark', 'album': 'Note Detection Benchmark',
'year': 2026, 'year': 2026,
'duration': round(end_t, 3), 'duration': round(end_t, 3),
@@ -392,7 +392,7 @@ def build(out_dir: Path):
{'id': 'full', 'file': 'stems/full.ogg', 'default': True}, {'id': 'full', 'file': 'stems/full.ogg', 'default': True},
], ],
'benchmark': { 'benchmark': {
'id': 'slopsmith-note-detect-benchmark', 'id': 'feedBack-note-detect-benchmark',
'version': 2, 'version': 2,
}, },
} }
@@ -466,7 +466,7 @@ def _build_zip(src_dir: Path):
def _benchmark_readme(duration_s): def _benchmark_readme(duration_s):
return f"""# Slopsmith Note Detect Benchmark — v2 return f"""# FeedBack Note Detect Benchmark — v2
A slower-paced companion to v1, focused on what players can actually A slower-paced companion to v1, focused on what players can actually
land cleanly. Half-note spacing throughout (~1.33 s between events at land cleanly. Half-note spacing throughout (~1.33 s between events at
+24 -24
View File
@@ -1,6 +1,6 @@
# Capability Domains # Capability Domains
Capability domains are Slopsmith-wide coordination surfaces for core, bundled first-party plugins, external plugins, and future adapters. Plugins declare the runtime surfaces they use in `plugin.json`; core declares and owns host workflows directly in the runtime. These declarations let diagnostics and support tools reason about behavior without relying on private globals. Capability domains are FeedBack-wide coordination surfaces for core, bundled first-party plugins, external plugins, and future adapters. Plugins declare the runtime surfaces they use in `plugin.json`; core declares and owns host workflows directly in the runtime. These declarations let diagnostics and support tools reason about behavior without relying on private globals.
## Standards ## Standards
@@ -63,21 +63,21 @@ Route-only external plugins that participate in library workflows without regist
} }
``` ```
The frontend exposes the current source list through `window.slopsmith.capabilities.command('library', 'list-providers')`. Public owner commands (`list-providers`, `refresh-providers`, `get-current`, `select-provider`, `sync-song`, `inspect`) are distinct from provider operations (`query-page`, `query-artists`, `query-stats`, `tuning-names`, `get-art`, `sync-song`). The app-owned handler delegates to the existing provider registry and source selector, so plugins should not scrape the `#lib-provider` dropdown. The frontend exposes the current source list through `window.feedBack.capabilities.command('library', 'list-providers')`. Public owner commands (`list-providers`, `refresh-providers`, `get-current`, `select-provider`, `sync-song`, `inspect`) are distinct from provider operations (`query-page`, `query-artists`, `query-stats`, `tuning-names`, `get-art`, `sync-song`). The app-owned handler delegates to the existing provider registry and source selector, so plugins should not scrape the `#lib-provider` dropdown.
Capability declarations may include a short `description`. The bundled Capability Inspector shows that text on expanded domain owner cards; when it is omitted, the inspector falls back to a compact generated owner summary. Capability declarations may include a short `description`. The bundled Capability Inspector shows that text on expanded domain owner cards; when it is omitted, the inspector falls back to a compact generated owner summary.
## Audio Graph/Session Domains ## Audio Graph/Session Domains
The audio graph/session slice promotes four player-audio domains into the runtime graph: `audio-mix`, `audio-input`, `audio-monitoring`, and `stems`. The browser module at [static/capabilities/audio-session.js](../static/capabilities/audio-session.js) owns the active session boundary, contributes diagnostics under `slopsmith.audio_session.diagnostics.v1`, and records compatibility bridge hits for legacy audio surfaces. The audio graph/session slice promotes four player-audio domains into the runtime graph: `audio-mix`, `audio-input`, `audio-monitoring`, and `stems`. The browser module at [static/capabilities/audio-session.js](../static/capabilities/audio-session.js) owns the active session boundary, contributes diagnostics under `feedBack.audio_session.diagnostics.v1`, and records compatibility bridge hits for legacy audio surfaces.
`audio-mix`, `audio-input`, and `audio-monitoring` are core-owned provider-coordinator domains. They expose bounded inspect/register/start/stop style commands, redaction-safe diagnostics, and bridge accounting for legacy fader, analyser, input, and monitoring handshakes. `audio-mix`, `audio-input`, and `audio-monitoring` are core-owned provider-coordinator domains. They expose bounded inspect/register/start/stop style commands, redaction-safe diagnostics, and bridge accounting for legacy fader, analyser, input, and monitoring handshakes.
For `audio-mix`, native fader providers register mix participants with stable `participantId`, `kind`, `sourceMode`, optional `logicalFaderKey`, and `fader` metadata. The public command surface is `inspect`, `list-faders`, `get-fader-value`, `set-fader-value`, `inspect-route`, `inspect-analyser`, `register-participant`, and `unregister-participant`; provider operations are `fader.get-value`, `fader.set-value`, `route.get-current`, and `analyser.get-summary`. Providers own persistence for plugin faders and must return committed values from set operations so the player mixer can display the value that actually applied. For `audio-mix`, native fader providers register mix participants with stable `participantId`, `kind`, `sourceMode`, optional `logicalFaderKey`, and `fader` metadata. The public command surface is `inspect`, `list-faders`, `get-fader-value`, `set-fader-value`, `inspect-route`, `inspect-analyser`, `register-participant`, and `unregister-participant`; provider operations are `fader.get-value`, `fader.set-value`, `route.get-current`, and `analyser.get-summary`. Providers own persistence for plugin faders and must return committed values from set operations so the player mixer can display the value that actually applied.
Legacy `window.slopsmith.audio.registerFader(...)` remains supported as an audio-mix compatibility bridge. The bridge registers a compatibility-backed participant, wraps legacy `getValue`/`setValue` callbacks as provider operations, and preserves `window.slopsmith.audio.getFaders()` for external callers. If a native participant and a legacy fader share the same logical fader key, the native participant owns the visible control; the legacy participant is retained for diagnostics with `supersededBy` and an `overshadowed` bridge hit. Removal gates for the bridge are: native providers cover bundled mixer integrations, diagnostics show no unexpected legacy hits in normal playback, and repeated plugin hydration does not create duplicate faders. Legacy `window.feedBack.audio.registerFader(...)` remains supported as an audio-mix compatibility bridge. The bridge registers a compatibility-backed participant, wraps legacy `getValue`/`setValue` callbacks as provider operations, and preserves `window.feedBack.audio.getFaders()` for external callers. If a native participant and a legacy fader share the same logical fader key, the native participant owns the visible control; the legacy participant is retained for diagnostics with `supersededBy` and an `overshadowed` bridge hit. Removal gates for the bridge are: native providers cover bundled mixer integrations, diagnostics show no unexpected legacy hits in normal playback, and repeated plugin hydration does not create duplicate faders.
Audio-mix diagnostics live under `slopsmith.audio_session.diagnostics.v1`. The `audio-mix` domain snapshot includes session state, participants, visible fader summaries, required participant-kind coverage, route summary, analyser summary, bridge hits, and bounded recent outcomes. Fader outcomes include operation name, participant id, fader id, status such as `committed`, `normalized`, `unavailable`, or `timeout`, and a bounded reason. Diagnostics must not include raw audio buffers, FFT arrays, device labels, stable hardware identifiers, secrets, or unredacted local paths; route/analyser payloads are summaries only. Audio-mix diagnostics live under `feedBack.audio_session.diagnostics.v1`. The `audio-mix` domain snapshot includes session state, participants, visible fader summaries, required participant-kind coverage, route summary, analyser summary, bridge hits, and bounded recent outcomes. Fader outcomes include operation name, participant id, fader id, status such as `committed`, `normalized`, `unavailable`, or `timeout`, and a bounded reason. Diagnostics must not include raw audio buffers, FFT arrays, device labels, stable hardware identifiers, secrets, or unredacted local paths; route/analyser payloads are summaries only.
For `audio-input`, native providers register source summaries with `sourceId`, `providerId`, `logicalSourceKey`, `kind`, redaction-safe label/pseudonym, `availability`, `channelSummary`, `sourceMode`, and provider operations. The public command surface is `inspect`, `list-sources`, `register-source`, `unregister-source`, `select-source`, `open-source`, and `close-source`; provider operations are `source.enumerate`, `source.describe`, `source.open`, and `source.close`. `inspect`, `list-sources`, and `select-source` are prompt-free and never open live input or call enumeration. `source.enumerate` runs only when explicitly requested by provider/user discovery. `open-source` is the permission boundary: it routes to `source.open`, attributes the requester, checks the selected source and requested channel shape, and records `handled`, `denied`, `degraded`, `failed`, `no-owner`, `no-handler`, `unsupported-command`, or `incompatible-version` outcomes. For `audio-input`, native providers register source summaries with `sourceId`, `providerId`, `logicalSourceKey`, `kind`, redaction-safe label/pseudonym, `availability`, `channelSummary`, `sourceMode`, and provider operations. The public command surface is `inspect`, `list-sources`, `register-source`, `unregister-source`, `select-source`, `open-source`, and `close-source`; provider operations are `source.enumerate`, `source.describe`, `source.open`, and `source.close`. `inspect`, `list-sources`, and `select-source` are prompt-free and never open live input or call enumeration. `source.enumerate` runs only when explicitly requested by provider/user discovery. `open-source` is the permission boundary: it routes to `source.open`, attributes the requester, checks the selected source and requested channel shape, and records `handled`, `denied`, `degraded`, `failed`, `no-owner`, `no-handler`, `unsupported-command`, or `incompatible-version` outcomes.
@@ -109,9 +109,9 @@ Core also owns the durable public mapping index at `/api/audio-effects/mappings`
Providers register stable `providerId`, `pluginId`, `routeKey`, priority, availability, source mode, operations, and operation handlers. Executors register stable `executorId`, `pluginId`, `routeKey`, priority, availability, source mode, supported provider ids, supported stage kinds, optional maximum stage count, operations, and handlers. The host chooses the highest-priority enabled provider for a route unless the caller requests a specific provider, then chooses the highest-priority compatible executor for that provider and resolved plan. Compatibility means both provider-compatible and plan-compatible: a browser/WASM NAM executor can advertise `providerIds: ["nam-tone"]`, `supportedKinds: ["nam", "ir"]`, and `maxStages: 2`, so it will not be asked to execute a Rig Builder VST/full-chain plan. If a selected provider has no compatible executor and the caller supplies a fallback provider, the host may fall back to that provider; if the caller explicitly requested the original provider, the host reports `unavailable` instead of silently changing providers. The initial default route is `desktop-main`, matching the desktop native executor path planned for full-chain NAM/IR/VST playback while still allowing browser executors for non-Desktop runtimes. Providers register stable `providerId`, `pluginId`, `routeKey`, priority, availability, source mode, operations, and operation handlers. Executors register stable `executorId`, `pluginId`, `routeKey`, priority, availability, source mode, supported provider ids, supported stage kinds, optional maximum stage count, operations, and handlers. The host chooses the highest-priority enabled provider for a route unless the caller requests a specific provider, then chooses the highest-priority compatible executor for that provider and resolved plan. Compatibility means both provider-compatible and plan-compatible: a browser/WASM NAM executor can advertise `providerIds: ["nam-tone"]`, `supportedKinds: ["nam", "ir"]`, and `maxStages: 2`, so it will not be asked to execute a Rig Builder VST/full-chain plan. If a selected provider has no compatible executor and the caller supplies a fallback provider, the host may fall back to that provider; if the caller explicitly requested the original provider, the host reports `unavailable` instead of silently changing providers. The initial default route is `desktop-main`, matching the desktop native executor path planned for full-chain NAM/IR/VST playback while still allowing browser executors for non-Desktop runtimes.
`chain.resolve` returns schema `slopsmith.audio_effects.chain_plan.v1`. A valid plan includes `planId`, `routeKey`, `providerId`, `stages`, optional `segments`, and optional redaction-safe summaries. Each stage exposes only stable opaque `stageId`, `kind` (`nam`, `ir`, `vst`, `utility`, or `bypass`), `role` (`pre-pedal`, `amp`, `cab`, `rack`, `master-pre`, etc.), opaque `assetRef`, optional opaque `stateRef`, bypass state, gain summary, and safe summary metadata. Raw file paths, URLs, model filenames, IR filenames, VST state blobs, native preset JSON, callbacks, handles, DOM nodes, audio buffers, samples, and waveform data are rejected or omitted. `chain.resolve` returns schema `feedBack.audio_effects.chain_plan.v1`. A valid plan includes `planId`, `routeKey`, `providerId`, `stages`, optional `segments`, and optional redaction-safe summaries. Each stage exposes only stable opaque `stageId`, `kind` (`nam`, `ir`, `vst`, `utility`, or `bypass`), `role` (`pre-pedal`, `amp`, `cab`, `rack`, `master-pre`, etc.), opaque `assetRef`, optional opaque `stateRef`, bypass state, gain summary, and safe summary metadata. Raw file paths, URLs, model filenames, IR filenames, VST state blobs, native preset JSON, callbacks, handles, DOM nodes, audio buffers, samples, and waveform data are rejected or omitted.
Diagnostics live under `slopsmith.audio_effects.diagnostics.v1`. The snapshot includes provider summaries, executor summaries, route summaries, bridge hits, bounded recent outcomes, limits, and redaction notes. It intentionally omits full chain plans, stage asset references, provider-private mapping payloads, raw filenames, and song keys; diagnostics should explain which provider/executor/route failed without leaking local library structure or licensed asset names. Legacy NAM Tone/Rig Builder fetch interception, direct Desktop `loadPreset` calls, legacy tone controls, old `nam_tone.db` `tone_mappings` access, and MIDI/external effect handoffs are attributed through `audio-effects.legacy-nam-routing`, `audio-effects.legacy-native-load`, `audio-effects.legacy-tone-controls`, `audio-effects.legacy-tone-db`, and `audio-effects.legacy-midi-amp` bridge records while providers migrate. Diagnostics live under `feedBack.audio_effects.diagnostics.v1`. The snapshot includes provider summaries, executor summaries, route summaries, bridge hits, bounded recent outcomes, limits, and redaction notes. It intentionally omits full chain plans, stage asset references, provider-private mapping payloads, raw filenames, and song keys; diagnostics should explain which provider/executor/route failed without leaking local library structure or licensed asset names. Legacy NAM Tone/Rig Builder fetch interception, direct Desktop `loadPreset` calls, legacy tone controls, old `nam_tone.db` `tone_mappings` access, and MIDI/external effect handoffs are attributed through `audio-effects.legacy-nam-routing`, `audio-effects.legacy-native-load`, `audio-effects.legacy-tone-controls`, `audio-effects.legacy-tone-db`, and `audio-effects.legacy-midi-amp` bridge records while providers migrate.
## Playback Control Plane ## Playback Control Plane
@@ -119,7 +119,7 @@ The playback slice promotes `playback` as a core-owned command domain implemente
`static/app.js` remains the transport data plane. It registers a private playback adapter that can start songs, pause/resume/stop, seek, and manage loops, but the capability snapshot never exposes the `<audio>` element, JUCE player object, raw audio buffers, native route handles, samples, waveforms, recordings, local file paths, or URL payloads. Exported diagnostics use pseudonymous `target-*` ids for arrangement-scoped identity and hashed `settings-*` keys for per-song plugin settings; the local Capability Inspector may show visible title, artist, and arrangement labels for the active song. `static/app.js` remains the transport data plane. It registers a private playback adapter that can start songs, pause/resume/stop, seek, and manage loops, but the capability snapshot never exposes the `<audio>` element, JUCE player object, raw audio buffers, native route handles, samples, waveforms, recordings, local file paths, or URL payloads. Exported diagnostics use pseudonymous `target-*` ids for arrangement-scoped identity and hashed `settings-*` keys for per-song plugin settings; the local Capability Inspector may show visible title, artist, and arrangement labels for the active song.
Legacy playback surfaces remain supported during migration and are attributed through bridges such as `playback.window-play-song`, `playback.song-events`, `playback.window-slopsmith-transport`, `playback.loop-api`, and native route handoff records. Fresh audible `start` commands require `authorization: "user-action"`; background requesters may inspect or control an existing session, but user-priority pause/stop decisions block lower-priority automation until a user action resumes or starts a new session. Legacy playback surfaces remain supported during migration and are attributed through bridges such as `playback.window-play-song`, `playback.song-events`, `playback.window-feedBack-transport`, `playback.loop-api`, and native route handoff records. Fresh audible `start` commands require `authorization: "user-action"`; background requesters may inspect or control an existing session, but user-priority pause/stop decisions block lower-priority automation until a user action resumes or starts a new session.
## Progression Domain ## Progression Domain
@@ -127,7 +127,7 @@ The progression slice (spec 010) promotes `progression` as a core-owned command
The public command surface is `inspect`, `record-event`, `list-shop`, `buy-item`, and `equip-item`. `record-event` accepts only whitelisted externally-postable event types (`minigame_run` in v1); `song_completed` is server-derived inside `/api/stats` so scored-session authority stays in one place and is denied at this surface. `buy-item` and `equip-item` require `authorization: "user-action"`. Backend plugins use the symmetric plugin-context hook `record_progression_event` (the minigames hub reports runs through it), which trusts backend code and skips the HTTP whitelist. The public command surface is `inspect`, `record-event`, `list-shop`, `buy-item`, and `equip-item`. `record-event` accepts only whitelisted externally-postable event types (`minigame_run` in v1); `song_completed` is server-derived inside `/api/stats` so scored-session authority stays in one place and is denied at this surface. `buy-item` and `equip-item` require `authorization: "user-action"`. Backend plugins use the symmetric plugin-context hook `record_progression_event` (the minigames hub reports runs through it), which trusts backend code and skips the HTTP whitelist.
The domain emits `challenge-completed`, `quest-completed`, `path-level-up`, `rank-changed`, `db-changed`, `calibration-completed`, and `cosmetic-equipped` on the capability surface, mirrored as `progression:*` events on `window.slopsmith` for non-capability consumers. Diagnostics live under `slopsmith.progression.diag.v1` and contain content-load warnings, rank/path-level/quest counts, and wallet totals only — no song filenames or display names. The domain emits `challenge-completed`, `quest-completed`, `path-level-up`, `rank-changed`, `db-changed`, `calibration-completed`, and `cosmetic-equipped` on the capability surface, mirrored as `progression:*` events on `window.feedBack` for non-capability consumers. Diagnostics live under `feedBack.progression.diag.v1` and contain content-load warnings, rank/path-level/quest counts, and wallet totals only — no song filenames or display names.
Decibels are earned exclusively by playing (songs, minigame runs, quest rewards); there is no real-money acquisition path and none may be added. The wallet tracks spend separately from the monotonic lifetime-earned total, so per-source XP resets and `db_earned` goals stay correct. A deferred release slice adds a `contributor` role so plugins can ship their own challenge/quest content (e.g. a drums plugin contributing drums challenges); content stays core-bundled until then. Decibels are earned exclusively by playing (songs, minigame runs, quest rewards); there is no real-money acquisition path and none may be added. The wallet tracks spend separately from the monotonic lifetime-earned total, so per-source XP resets and `db_earned` goals stay correct. A deferred release slice adds a `contributor` role so plugins can ship their own challenge/quest content (e.g. a drums plugin contributing drums challenges); content stays core-bundled until then.
@@ -137,11 +137,11 @@ The visualization slice (cap:6) promotes `visualization` as a core-owned provide
The public command surface is `inspect`, `list-providers`, `select-renderer`, and `clear-renderer`. Selection delegates to the existing picker (`setViz`) so localStorage persistence, WebGL2 gating, and fallback semantics have exactly one implementation. The domain emits `providers-refreshed`, `renderer-changed` (with a `source` of `auto-match`, `user-select`, `fallback`, or `command:<requester>`), `renderer-ready`, and `renderer-failed`. The public command surface is `inspect`, `list-providers`, `select-renderer`, and `clear-renderer`. Selection delegates to the existing picker (`setViz`) so localStorage persistence, WebGL2 gating, and fallback semantics have exactly one implementation. The domain emits `providers-refreshed`, `renderer-changed` (with a `source` of `auto-match`, `user-select`, `fallback`, or `command:<requester>`), `renderer-ready`, and `renderer-failed`.
Provider discovery is still the legacy surface — `type: "visualization"` manifests populate the picker and `window.slopsmithViz_*` factory globals carry the renderer contract — and both are registered as compatibility shims (`visualization:type-visualization-manifest`, `visualization:window.slopsmithViz_*`) with hit accounting, so the Inspector shows exactly how much of the domain still rides the bridge. Plugins migrate by declaring a `visualization` provider capability in their manifests; the renderer factory contract (`init`/`draw`/`resize`/`destroy`, `contextType`, `matchesArrangement`) is unchanged. Provider discovery is still the legacy surface — `type: "visualization"` manifests populate the picker and `window.feedBackViz_*` factory globals carry the renderer contract — and both are registered as compatibility shims (`visualization:type-visualization-manifest`, `visualization:window.feedBackViz_*`) with hit accounting, so the Inspector shows exactly how much of the domain still rides the bridge. Plugins migrate by declaring a `visualization` provider capability in their manifests; the renderer factory contract (`init`/`draw`/`resize`/`destroy`, `contextType`, `matchesArrangement`) is unchanged.
**Per-instance provider settings (#849).** A provider may declare a `settings` array on its `visualization` capability — generic control descriptors (`{ key, label, type: "toggle" | "range" | "select", default, min/max/step, options }`) the capability-pipelines schema validates on *any* domain (the field lives on the shared `capabilityDeclaration`, not a visualization-only spot — see `docs/plugin-manifest.schema.json`). Descriptors flow through the **generic participant model**: the backend validates them for `/api/plugins` (`plugins/__init__.py`), `static/capabilities.js` normalizes + preserves them on the registered participant (so generic `inspect('visualization')` carries them), and the visualization owner reads them back from the participant by id — no app.js/picker side channel. They surface in the `list-providers` snapshot (each provider's `settings`, deep-frozen) plus a `provider_policy.hasSettings` flag in diagnostics, so a consuming host — splitscreen's per-panel control popover — can render the controls generically without per-plugin hardcoding. The visualization domain's **apply contract**: a provider that declares `settings` MUST implement `applySetting(key, value)` on its renderer instance (the host calls it on the specific per-panel instance, which is inherently per-panel — no canvas→panel resolution, no shared global keys); `getSetting(key)` is optional (the host falls back to the declared `default`). The host owns persistence. **This core slice lands the declarative surface + participant plumbing only** — no bundled provider declares `settings` yet (`highway_3d` still ships the legacy `factory.panelControls` static). The `highway_3d` migration and the splitscreen generic consumer are the remaining #849 follow-ups. **Per-instance provider settings (#849).** A provider may declare a `settings` array on its `visualization` capability — generic control descriptors (`{ key, label, type: "toggle" | "range" | "select", default, min/max/step, options }`) the capability-pipelines schema validates on *any* domain (the field lives on the shared `capabilityDeclaration`, not a visualization-only spot — see `docs/plugin-manifest.schema.json`). Descriptors flow through the **generic participant model**: the backend validates them for `/api/plugins` (`plugins/__init__.py`), `static/capabilities.js` normalizes + preserves them on the registered participant (so generic `inspect('visualization')` carries them), and the visualization owner reads them back from the participant by id — no app.js/picker side channel. They surface in the `list-providers` snapshot (each provider's `settings`, deep-frozen) plus a `provider_policy.hasSettings` flag in diagnostics, so a consuming host — splitscreen's per-panel control popover — can render the controls generically without per-plugin hardcoding. The visualization domain's **apply contract**: a provider that declares `settings` MUST implement `applySetting(key, value)` on its renderer instance (the host calls it on the specific per-panel instance, which is inherently per-panel — no canvas→panel resolution, no shared global keys); `getSetting(key)` is optional (the host falls back to the declared `default`). The host owns persistence. **This core slice lands the declarative surface + participant plumbing only** — no bundled provider declares `settings` yet (`highway_3d` still ships the legacy `factory.panelControls` static). The `highway_3d` migration and the splitscreen generic consumer are the remaining #849 follow-ups.
Diagnostics live under `slopsmith.visualization_capability.v1` and contain provider ids/labels/context types, the active renderer id and its selection source, the last auto-match outcome (resolved id + whether any predicate claimed the song), and the last failure (provider id + reason) — never song filenames, titles, or arrangement names. Per-panel selection (splitscreen #90) and per-panel provider settings (#849) are tracked follow-ups; the domain currently models the primary highway surface. Diagnostics live under `feedBack.visualization_capability.v1` and contain provider ids/labels/context types, the active renderer id and its selection source, the last auto-match outcome (resolved id + whether any predicate claimed the song), and the last failure (provider id + reason) — never song filenames, titles, or arrangement names. Per-panel selection (splitscreen #90) and per-panel provider settings (#849) are tracked follow-ups; the domain currently models the primary highway surface.
## Note-Detection Domain ## Note-Detection Domain
@@ -151,7 +151,7 @@ The public command surface is `inspect`, `register-provider`, `unregister-provid
The legacy chart-coupled surface — `highway.setNoteStateProvider(fn)`, the single-global-detector path spec 009 retires — keeps working unchanged and is wrapped for compatibility-shim hit accounting (`note-detection:highway.setNoteStateProvider`). Migrating the chart `note_detect` consumer, Step Mode verify, minigames YIN scoring, and the `setVerifyTarget` bridge onto real bindings — and wiring per-binding tuning contexts into the engine verifier — is the remainder of the spec-009 slice and lands behind the Spec 003 migration gate. The legacy chart-coupled surface — `highway.setNoteStateProvider(fn)`, the single-global-detector path spec 009 retires — keeps working unchanged and is wrapped for compatibility-shim hit accounting (`note-detection:highway.setNoteStateProvider`). Migrating the chart `note_detect` consumer, Step Mode verify, minigames YIN scoring, and the `setVerifyTarget` bridge onto real bindings — and wiring per-binding tuning contexts into the engine verifier — is the remainder of the spec-009 slice and lands behind the Spec 003 migration gate.
Diagnostics live under `slopsmith.note_detection_capability.v1` and contain provider ids/labels/kinds, binding summaries (requester, provider, redacted context, target size), availability, and the last bounded outcome (event, binding, provider, MIDI number, hit flag) — never raw audio buffers, sample data, device labels, or song identity. Diagnostics live under `feedBack.note_detection_capability.v1` and contain provider ids/labels/kinds, binding summaries (requester, provider, redacted context, target size), availability, and the last bounded outcome (event, binding, provider, MIDI number, hit flag) — never raw audio buffers, sample data, device labels, or song identity.
## MIDI-Input Domain ## MIDI-Input Domain
@@ -159,11 +159,11 @@ The MIDI-input slice (spec 012, issues #873/#880) promotes `midi-input` as a **c
Native providers register source summaries with `providerId`, a stable `sourceId`, a derived redaction-safe `logicalSourceKey` (`providerId::sourceId`), `kind: "midi"`, a label, and `availability`. The public command surface is `inspect`, `list-sources`, `discover`, `select-source`, `open-source`, and `close-source`; provider operations are `source.enumerate`, `source.describe`, `source.open`, and `source.close`. `inspect`, `list-sources`, and `select-source` are prompt-free and never request MIDI access. Unlike audio (where `getUserMedia` gates labels and `open-source` is the prompt), Web-MIDI's `requestMIDIAccess()` gates the whole input list, so **`discover` is the permission boundary** and records `denied`/`unavailable` outcomes; `open-source` then attaches a shared listener session and never re-prompts. Native providers register source summaries with `providerId`, a stable `sourceId`, a derived redaction-safe `logicalSourceKey` (`providerId::sourceId`), `kind: "midi"`, a label, and `availability`. The public command surface is `inspect`, `list-sources`, `discover`, `select-source`, `open-source`, and `close-source`; provider operations are `source.enumerate`, `source.describe`, `source.open`, and `source.close`. `inspect`, `list-sources`, and `select-source` are prompt-free and never request MIDI access. Unlike audio (where `getUserMedia` gates labels and `open-source` is the prompt), Web-MIDI's `requestMIDIAccess()` gates the whole input list, so **`discover` is the permission boundary** and records `denied`/`unavailable` outcomes; `open-source` then attaches a shared listener session and never re-prompts.
Selected input is persisted by `logicalSourceKey` (`slopsmith.midiInput.selectedLogicalSourceKey`) when browser storage is available. Compatible requesters share one open session per source; each later calls `close-source`, and the provider receives `source.close` only after the last requester releases. Live MIDI message delivery (for the "play a note / hit a pad" calibration check) is exposed to in-page consumers through the public `window.slopsmith.midiInput` session handle only — never as raw capability events or diagnostics. Selected input is persisted by `logicalSourceKey` (`feedBack.midiInput.selectedLogicalSourceKey`) when browser storage is available. Compatible requesters share one open session per source; each later calls `close-source`, and the provider receives `source.close` only after the last requester releases. Live MIDI message delivery (for the "play a note / hit a pad" calibration check) is exposed to in-page consumers through the public `window.feedBack.midiInput` session handle only — never as raw capability events or diagnostics.
The reserved `midi-control` domain is the planned **sibling** for control mappings (CC/pitchbend/note → action routing) and will consume `midi-input` for device access (spec 013 / #882); this slice carves the device control plane out so `midi-control` can stay mappings-only. `midi-control` stays RESERVED (documentation-only) until a concrete mapping consumer + tests exist, per the future-domain governance. The reserved `midi-control` domain is the planned **sibling** for control mappings (CC/pitchbend/note → action routing) and will consume `midi-input` for device access (spec 013 / #882); this slice carves the device control plane out so `midi-control` can stay mappings-only. `midi-control` stays RESERVED (documentation-only) until a concrete mapping consumer + tests exist, per the future-domain governance.
Diagnostics live under `slopsmith.midi_input.diagnostics.v1` and contain provider ids, source ids/keys/kinds/availability, the selected key, and open-session keys — **device labels are redacted** and no raw MIDI messages are ever included. Diagnostics live under `feedBack.midi_input.diagnostics.v1` and contain provider ids, source ids/keys/kinds/availability, the selected key, and open-session keys — **device labels are redacted** and no raw MIDI messages are ever included.
## Capability Roles ## Capability Roles
@@ -185,11 +185,11 @@ Use capability declarations for provider/requester/observer relationships:
Future app-level workflows can then express intent through capability domains instead of hard-coding plugin-private implementation details. Future app-level workflows can then express intent through capability domains instead of hard-coding plugin-private implementation details.
Core registers manifest capability declarations from `/api/plugins` before plugin scripts hydrate. Runtime owners can then re-register the same participant with command handlers, event handlers, and current availability state. The merged participant view is visible through `window.slopsmith.capabilities.snapshotDiagnostics()` and `getDiagnostics()`. Core registers manifest capability declarations from `/api/plugins` before plugin scripts hydrate. Runtime owners can then re-register the same participant with command handlers, event handlers, and current availability state. The merged participant view is visible through `window.feedBack.capabilities.snapshotDiagnostics()` and `getDiagnostics()`.
Core domains include review metadata in diagnostics: Core domains include review metadata in diagnostics:
- `active`: wired to current Slopsmith behavior and expected to work as an integration point. - `active`: wired to current FeedBack behavior and expected to work as an integration point.
- `diagnostic`: support/inspection-only runtime surfaces. - `diagnostic`: support/inspection-only runtime surfaces.
PR1 includes only the delivered domains listed in [capability-roadmap.md](capability-roadmap.md): `pipeline`, `diagnostics`, and `library`. The follow-up audio graph/session slice promotes `audio-mix`, `audio-input`, `audio-monitoring`, and a coordinated `stems` surface. The playback slice promotes `playback` as an active transport control plane. The audio-effects slice promotes provider-selected effect-chain planning while leaving physical processor loading to compatible executors such as trusted Desktop native audio or a browser/WASM executor. The visualization slice promotes `visualization` as the highway renderer provider-coordinator, and the note-detection slice (spec 009) promotes `note-detection` as the detection-binding control plane. Backend routes, app UI, settings, and other hardware-facing domains remain documented in the roadmap and safety matrix until their own host workflow/provider slice exists. PR1 includes only the delivered domains listed in [capability-roadmap.md](capability-roadmap.md): `pipeline`, `diagnostics`, and `library`. The follow-up audio graph/session slice promotes `audio-mix`, `audio-input`, `audio-monitoring`, and a coordinated `stems` surface. The playback slice promotes `playback` as an active transport control plane. The audio-effects slice promotes provider-selected effect-chain planning while leaving physical processor loading to compatible executors such as trusted Desktop native audio or a browser/WASM executor. The visualization slice promotes `visualization` as the highway renderer provider-coordinator, and the note-detection slice (spec 009) promotes `note-detection` as the detection-binding control plane. Backend routes, app UI, settings, and other hardware-facing domains remain documented in the roadmap and safety matrix until their own host workflow/provider slice exists.
@@ -201,7 +201,7 @@ Capability metadata is versioned by the `capability-pipelines.v1` standard. Inva
Requesters should use the public claim/dispatch/release flow instead of mutating another plugin's globals: Requesters should use the public claim/dispatch/release flow instead of mutating another plugin's globals:
```js ```js
const api = window.slopsmith.capabilities; const api = window.feedBack.capabilities;
const releaseClaim = api.claim({ capability: 'example.plugin-domain', claimId: 'example.automation-active', requester: 'example_requester' }); const releaseClaim = api.claim({ capability: 'example.plugin-domain', claimId: 'example.automation-active', requester: 'example_requester' });
await api.dispatch({ await api.dispatch({
capability: 'example.plugin-domain', capability: 'example.plugin-domain',
@@ -244,9 +244,9 @@ Dispatch results use explicit outcomes: `handled`, `transformed`, `denied`, `fai
## Deferred Core Adapters ## Deferred Core Adapters
UI placement and settings contributions are real Slopsmith surfaces, but they are not PR1 capability contracts (visualization is active as of the cap:6 slice; note-detection as of the spec-009 slice). Audio mixer/session domains are active as of the audio graph/session slice, playback is active as of the playback control-plane slice, and audio-effects is active as a provider/route/chain-plan coordinator; plugins should keep using current documented APIs for remaining areas until the corresponding domain PR ships the host workflow, command/event contract, compatibility shims, diagnostics fields, and tests. UI placement and settings contributions are real FeedBack surfaces, but they are not PR1 capability contracts (visualization is active as of the cap:6 slice; note-detection as of the spec-009 slice). Audio mixer/session domains are active as of the audio graph/session slice, playback is active as of the playback control-plane slice, and audio-effects is active as a provider/route/chain-plan coordinator; plugins should keep using current documented APIs for remaining areas until the corresponding domain PR ships the host workflow, command/event contract, compatibility shims, diagnostics fields, and tests.
The library provider workflow is the PR1 core adapter and is implemented natively as the `library` capability module. Provider refresh, selection, and sync run through `library` owner commands; backend provider registration remains the way providers enter the library registry, and the browser module turns that registry into provider participants. The app event bus continues to dispatch local `window.slopsmith` events for legacy listeners; playback now mirrors song transport, route, seek, and loop lifecycle into `playback`, and visualization attributes renderer selection/failure, while navigation, note, and route-only surfaces remain outside capability domains until their own slices land. The library provider workflow is the PR1 core adapter and is implemented natively as the `library` capability module. Provider refresh, selection, and sync run through `library` owner commands; backend provider registration remains the way providers enter the library registry, and the browser module turns that registry into provider participants. The app event bus continues to dispatch local `window.feedBack` events for legacy listeners; playback now mirrors song transport, route, seek, and loop lifecycle into `playback`, and visualization attributes renderer selection/failure, while navigation, note, and route-only surfaces remain outside capability domains until their own slices land.
The direct `window.highway` object remains the renderer data plane. Per-frame reads such as notes, chords, beats, and renderer hooks should not be moved behind asynchronous capability commands until there is a dedicated chart/render facade. The direct `window.highway` object remains the renderer data plane. Per-frame reads such as notes, chords, beats, and renderer hooks should not be moved behind asynchronous capability commands until there is a dedicated chart/render facade.
@@ -254,11 +254,11 @@ The direct `window.highway` object remains the renderer data plane. Per-frame re
Large management surfaces should prefer plugin-owned UI over crowding normal Settings. First-party management plugins can contribute screens and settings panels while core keeps shared services and diagnostics contracts centralized. Large management surfaces should prefer plugin-owned UI over crowding normal Settings. First-party management plugins can contribute screens and settings panels while core keeps shared services and diagnostics contracts centralized.
The bundled Capability Inspector plugin is the support surface for the current graph. It reads `window.slopsmith.capabilities.snapshotDiagnostics()`, filters by domain, and summarizes manifest participants, runtime participants, conflicts, unsupported versions, safety classes, expected legacy event surfaces, and compatibility shim hits without rendering raw runtime objects. Domains are grouped in review order: application/library, player/audio runtime, plugin-defined surfaces, then capability runtime. In the all-domains view, each domain starts collapsed with a domain-specific icon plus compact summary badges for participant-lane count, endpoint count, observed links, shimmed links, and status; badge labels live in tooltips/ARIA labels so the header stays scannable. Clicking the domain label expands or collapses the domain, opening the same graph view used by the single-domain filter. The graph places owner details and right-aligned command/event groups on the left, with short owner descriptions bottom-aligned as the final part of that pane. Participant usage is grouped the same way on the right, with observed or shimmed links between border-aligned endpoint ports. In multi-provider domains, links to provider participants use provider-family colors: purple for owner-to-provider command delegation and a lighter violet for provider events. Provider participants, including `library` sources, stay on the right lane and show a provider icon in their header. Headers show role-aware core/non-core origin badges such as Core owner, Core provider, or Non-core participant; owner headers place the origin badge directly after the owner icon, and the built-in local library provider is marked as core-origin. Observer and requester roles are implied by the command/event links rather than separate header badges. Participant cards are shown only when the plugin or runtime source has visible command or event usage for the current graph filter; domains with no such usage show zero participants, and attribution-only shims with no matching endpoint stay out of the lane. Command and event groups can collapse; when collapsed, all links for that side and group converge on the single group port. Hovering a participant, endpoint, or command/event group emphasizes the matching links and dims unrelated links; owner-side labels outside the current focus de-emphasize so the active source endpoints are easy to track. Expanded domain graphs progressively enhance to Cytoscape.js overlays that route bezier links between measured DOM endpoint ports, while keeping the HTML lanes as the fallback and readable data surface. Its Plugins-menu entry is hidden by default; enable **Capability Inspector → Show in Plugins menu** from Settings when reviewing or debugging capability behavior. The bundled Capability Inspector plugin is the support surface for the current graph. It reads `window.feedBack.capabilities.snapshotDiagnostics()`, filters by domain, and summarizes manifest participants, runtime participants, conflicts, unsupported versions, safety classes, expected legacy event surfaces, and compatibility shim hits without rendering raw runtime objects. Domains are grouped in review order: application/library, player/audio runtime, plugin-defined surfaces, then capability runtime. In the all-domains view, each domain starts collapsed with a domain-specific icon plus compact summary badges for participant-lane count, endpoint count, observed links, shimmed links, and status; badge labels live in tooltips/ARIA labels so the header stays scannable. Clicking the domain label expands or collapses the domain, opening the same graph view used by the single-domain filter. The graph places owner details and right-aligned command/event groups on the left, with short owner descriptions bottom-aligned as the final part of that pane. Participant usage is grouped the same way on the right, with observed or shimmed links between border-aligned endpoint ports. In multi-provider domains, links to provider participants use provider-family colors: purple for owner-to-provider command delegation and a lighter violet for provider events. Provider participants, including `library` sources, stay on the right lane and show a provider icon in their header. Headers show role-aware core/non-core origin badges such as Core owner, Core provider, or Non-core participant; owner headers place the origin badge directly after the owner icon, and the built-in local library provider is marked as core-origin. Observer and requester roles are implied by the command/event links rather than separate header badges. Participant cards are shown only when the plugin or runtime source has visible command or event usage for the current graph filter; domains with no such usage show zero participants, and attribution-only shims with no matching endpoint stay out of the lane. Command and event groups can collapse; when collapsed, all links for that side and group converge on the single group port. Hovering a participant, endpoint, or command/event group emphasizes the matching links and dims unrelated links; owner-side labels outside the current focus de-emphasize so the active source endpoints are easy to track. Expanded domain graphs progressively enhance to Cytoscape.js overlays that route bezier links between measured DOM endpoint ports, while keeping the HTML lanes as the fallback and readable data surface. Its Plugins-menu entry is hidden by default; enable **Capability Inspector → Show in Plugins menu** from Settings when reviewing or debugging capability behavior.
## Diagnostics Contract ## Diagnostics Contract
Capability diagnostics use schema `slopsmith.capabilities.diagnostics.v1`. Snapshots are redaction-safe and capped at 64 KB by trimming older `recentDecisions` first while preserving current participants, active or orphaned claims, conflicts, domain review metadata, shim summaries, safety notes, and unsupported-version reports. Server diagnostics bundles include plugin manifest capability metadata, validation warnings, unsupported-version metadata, and compatibility shim summaries. Capability diagnostics use schema `feedBack.capabilities.diagnostics.v1`. Snapshots are redaction-safe and capped at 64 KB by trimming older `recentDecisions` first while preserving current participants, active or orphaned claims, conflicts, domain review metadata, shim summaries, safety notes, and unsupported-version reports. Server diagnostics bundles include plugin manifest capability metadata, validation warnings, unsupported-version metadata, and compatibility shim summaries.
Compatibility shim entries include `shimId`, `source`, `capability`, `legacySurface`, `status`, `reason`, and optional hit fields. A shim with `hitCount > 0` means legacy behavior was observed, not merely declared. The `library` domain no longer uses compatibility shims for provider registration or source selection; provider attribution comes from `owner_plugin_id` and runtime provider participants. Future domains should add expected shim entries only in the PR that implements their actual legacy bridge. Compatibility shim entries include `shimId`, `source`, `capability`, `legacySurface`, `status`, `reason`, and optional hit fields. A shim with `hitCount > 0` means legacy behavior was observed, not merely declared. The `library` domain no longer uses compatibility shims for provider registration or source selection; provider attribution comes from `owner_plugin_id` and runtime provider participants. Future domains should add expected shim entries only in the PR that implements their actual legacy bridge.
@@ -274,10 +274,10 @@ Future privileged domains must state user value, included and excluded commands,
## Rehydration Pattern ## Rehydration Pattern
Plugins that wrap shared functions such as `window.playSong` or `window.showScreen` should store wrapper state on a stable `window.__slopsmith...Hooks` object. Re-running the script should replace the implementation object and return before installing another wrapper. Plugins that wrap shared functions such as `window.playSong` or `window.showScreen` should store wrapper state on a stable `window.__feedBack...Hooks` object. Re-running the script should replace the implementation object and return before installing another wrapper.
```js ```js
const hookState = window.__slopsmithMyPluginHooks || (window.__slopsmithMyPluginHooks = {}); const hookState = window.__feedBackMyPluginHooks || (window.__feedBackMyPluginHooks = {});
hookState.impl = { afterPlaySong(filename) { /* current implementation */ } }; hookState.impl = { afterPlaySong(filename) { /* current implementation */ } };
if (hookState.installed) return; if (hookState.installed) return;
hookState.installed = true; hookState.installed = true;
@@ -290,7 +290,7 @@ window.playSong = async function(filename, arrangement) {
## Validation Commands ## Validation Commands
From the `slopsmith/` directory: From the `feedBack/` directory:
```bash ```bash
node --check static/app.js node --check static/app.js
+18 -18
View File
@@ -1,6 +1,6 @@
# Capability Authoring Recipes # Capability Authoring Recipes
Use these examples as small manifest fragments when migrating plugin-facing integrations to capability pipelines. The capability model is system-wide; these recipes focus on plugin manifests because core-owned domains are registered by Slopsmith itself. Each example is intentionally complete enough to pass the loader contract in [plugin-manifest.schema.json](plugin-manifest.schema.json). Use these examples as small manifest fragments when migrating plugin-facing integrations to capability pipelines. The capability model is system-wide; these recipes focus on plugin manifests because core-owned domains are registered by FeedBack itself. Each example is intentionally complete enough to pass the loader contract in [plugin-manifest.schema.json](plugin-manifest.schema.json).
> **Self-hosted CSS?** If your plugin uses Tailwind classes core doesn't ship (notably arbitrary values like `text-[11px]`), declare a `styles` key and bundle your own preflight-off stylesheet — see [plugin-styles.md](plugin-styles.md). That is separate from the capability-pipeline recipes below. > **Self-hosted CSS?** If your plugin uses Tailwind classes core doesn't ship (notably arbitrary values like `text-[11px]`), declare a `styles` key and bundle your own preflight-off stylesheet — see [plugin-styles.md](plugin-styles.md). That is separate from the capability-pipeline recipes below.
@@ -124,7 +124,7 @@ A route-only wrapper that uses the library capability without registering a brow
## Audio Mix Fader Provider ## Audio Mix Fader Provider
Existing plugins can keep using `window.slopsmith.audio.registerFader(spec)` while migrating. The compatibility bridge records the fader as an `audio-mix` participant. New bundled code should prefer a native participant declaration plus the audio-session helper once available in its integration point. Existing plugins can keep using `window.feedBack.audio.registerFader(spec)` while migrating. The compatibility bridge records the fader as an `audio-mix` participant. New bundled code should prefer a native participant declaration plus the audio-session helper once available in its integration point.
```json ```json
{ {
@@ -147,11 +147,11 @@ Existing plugins can keep using `window.slopsmith.audio.registerFader(spec)` whi
Native audio-mix fader providers should register a stable participant id and fader id, return the committed value from every set operation, and settle get/set operations within two seconds. The player mixer displays the committed value rather than the raw requested value. If the fader is temporarily unavailable, keep the participant registered with unavailable/disabled state so the mixer can render a disabled control and diagnostics can explain why it cannot be changed. Native audio-mix fader providers should register a stable participant id and fader id, return the committed value from every set operation, and settle get/set operations within two seconds. The player mixer displays the committed value rather than the raw requested value. If the fader is temporarily unavailable, keep the participant registered with unavailable/disabled state so the mixer can render a disabled control and diagnostics can explain why it cannot be changed.
During migration, a plugin may still call `window.slopsmith.audio.registerFader(spec)`. Core maps that legacy fader into a compatibility-backed audio-mix participant and records bridge hits. If a native participant and a legacy fader represent the same logical source, the native participant owns the visible control and the legacy path is reported as compatibility-backed/overshadowed. During migration, a plugin may still call `window.feedBack.audio.registerFader(spec)`. Core maps that legacy fader into a compatibility-backed audio-mix participant and records bridge hits. If a native participant and a legacy fader represent the same logical source, the native participant owns the visible control and the legacy path is reported as compatibility-backed/overshadowed.
## Audio Effects Provider ## Audio Effects Provider
Plugins that can provide guitar/bass processing chains should declare `audio-effects` as a provider and register at runtime with `window.slopsmith.audioEffects.registerProvider(...)`. The provider returns opaque chain plans; it must not expose local filenames, URLs, native preset JSON, VST state blobs, or raw handles through diagnostics or public route state. Plugins that can provide guitar/bass processing chains should declare `audio-effects` as a provider and register at runtime with `window.feedBack.audioEffects.registerProvider(...)`. The provider returns opaque chain plans; it must not expose local filenames, URLs, native preset JSON, VST state blobs, or raw handles through diagnostics or public route state.
```json ```json
{ {
@@ -173,7 +173,7 @@ Plugins that can provide guitar/bass processing chains should declare `audio-eff
``` ```
```js ```js
const effects = window.slopsmith && window.slopsmith.audioEffects; const effects = window.feedBack && window.feedBack.audioEffects;
effects.registerProvider({ effects.registerProvider({
providerId: 'rig-builder', providerId: 'rig-builder',
pluginId: 'rig_builder', pluginId: 'rig_builder',
@@ -184,7 +184,7 @@ effects.registerProvider({
'chain.resolve': request => ({ 'chain.resolve': request => ({
outcome: 'handled', outcome: 'handled',
plan: { plan: {
schema: 'slopsmith.audio_effects.chain_plan.v1', schema: 'feedBack.audio_effects.chain_plan.v1',
planId: 'song-tone-plan', planId: 'song-tone-plan',
routeKey: request.routeKey, routeKey: request.routeKey,
providerId: 'rig-builder', providerId: 'rig-builder',
@@ -203,14 +203,14 @@ effects.registerProvider({
User-facing controls should dispatch through the domain instead of mutating another plugin's private state: User-facing controls should dispatch through the domain instead of mutating another plugin's private state:
```js ```js
await window.slopsmith.capabilities.dispatch({ await window.feedBack.capabilities.dispatch({
capability: 'audio-effects', capability: 'audio-effects',
command: 'select-chain', command: 'select-chain',
source: 'rig_builder', source: 'rig_builder',
payload: { routeKey: 'desktop-main', providerId: 'rig-builder', authorization: 'user-action' } payload: { routeKey: 'desktop-main', providerId: 'rig-builder', authorization: 'user-action' }
}); });
const resolved = await window.slopsmith.capabilities.dispatch({ const resolved = await window.feedBack.capabilities.dispatch({
capability: 'audio-effects', capability: 'audio-effects',
command: 'resolve-plan', command: 'resolve-plan',
source: 'nam_tone', source: 'nam_tone',
@@ -221,7 +221,7 @@ const resolved = await window.slopsmith.capabilities.dispatch({
Providers should store public song/tone routing through the host-owned mapping index and keep their own preset or chain rows private. The mapping's `provider_ref` is opaque to core: NAM Tone can use a preset id, Rig Builder can use a chain/preset id, and each provider resolves that reference in `chain.resolve`. Providers should store public song/tone routing through the host-owned mapping index and keep their own preset or chain rows private. The mapping's `provider_ref` is opaque to core: NAM Tone can use a preset id, Rig Builder can use a chain/preset id, and each provider resolves that reference in `chain.resolve`.
```js ```js
await window.slopsmith.audioEffects.upsertMapping({ await window.feedBack.audioEffects.upsertMapping({
song_key: playbackTarget.settingsKey, song_key: playbackTarget.settingsKey,
filename: playbackTarget.filename, // optional migration/debug context filename: playbackTarget.filename, // optional migration/debug context
tone_key: 'Dist', tone_key: 'Dist',
@@ -232,7 +232,7 @@ await window.slopsmith.audioEffects.upsertMapping({
active: true active: true
}); });
const mappings = await window.slopsmith.audioEffects.listMappings({ const mappings = await window.feedBack.audioEffects.listMappings({
song_key: playbackTarget.settingsKey, song_key: playbackTarget.settingsKey,
tone_key: 'Dist' tone_key: 'Dist'
}); });
@@ -243,7 +243,7 @@ Only one mapping is active for a `song_key + tone_key` at a time, but multiple p
Browser or native executors should declare both provider scope and plan scope. A NAM-only browser executor should not claim Rig Builder plans just because it can load NAM files: Browser or native executors should declare both provider scope and plan scope. A NAM-only browser executor should not claim Rig Builder plans just because it can load NAM files:
```js ```js
window.slopsmith.audioEffects.registerExecutor({ window.feedBack.audioEffects.registerExecutor({
executorId: 'nam-tone-browser-wasm', executorId: 'nam-tone-browser-wasm',
pluginId: 'nam_tone', pluginId: 'nam_tone',
routeKey: 'desktop-main', routeKey: 'desktop-main',
@@ -296,7 +296,7 @@ Plugins that need live instrument input should declare requester/observer intent
Requesters should list or inspect sources before opening them. `inspect`, `list-sources`, and `select-source` are prompt-free and must not call provider enumeration or open live input. When a requester needs audio, it dispatches `open-source` with a purpose and required channel shape. The requester identity is taken from the dispatch `source` (the authenticated caller) — a payload-supplied `requesterId` is ignored, so a requester cannot spoof another's identity or release a shared session it does not own. Compatible requesters share one open session; each requester later dispatches `close-source`, and the provider is closed only after the last requester releases it. Requesters should list or inspect sources before opening them. `inspect`, `list-sources`, and `select-source` are prompt-free and must not call provider enumeration or open live input. When a requester needs audio, it dispatches `open-source` with a purpose and required channel shape. The requester identity is taken from the dispatch `source` (the authenticated caller) — a payload-supplied `requesterId` is ignored, so a requester cannot spoof another's identity or release a shared session it does not own. Compatible requesters share one open session; each requester later dispatches `close-source`, and the provider is closed only after the last requester releases it.
```js ```js
const api = window.slopsmith.capabilities; const api = window.feedBack.capabilities;
await api.dispatch({ capability: 'audio-input', command: 'select-source', source: 'user', payload: { logicalSourceKey: 'browser:instrument:primary' } }); await api.dispatch({ capability: 'audio-input', command: 'select-source', source: 'user', payload: { logicalSourceKey: 'browser:instrument:primary' } });
const opened = await api.dispatch({ const opened = await api.dispatch({
capability: 'audio-input', capability: 'audio-input',
@@ -436,7 +436,7 @@ Plugins that need to inspect or coordinate song transport should declare `playba
Fresh audible starts require a user action. Background plugins should call `inspect` first and attach to an existing compatible session; if a plugin needs to offer a play/start action, wire it to a visible user gesture and pass `authorization: "user-action"`. Fresh audible starts require a user action. Background plugins should call `inspect` first and attach to an existing compatible session; if a plugin needs to offer a play/start action, wire it to a visible user gesture and pass `authorization: "user-action"`.
```js ```js
const api = window.slopsmith.capabilities; const api = window.feedBack.capabilities;
const state = await api.dispatch({ const state = await api.dispatch({
capability: 'playback', capability: 'playback',
@@ -455,7 +455,7 @@ if (state.status !== 'idle') {
} }
``` ```
During migration, legacy uses of `window.playSong`, `song:*` events, `window.slopsmith.seek`, and loop helpers remain available and are recorded as playback bridge hits. Treat bridge hits as migration telemetry: native capability requests should eventually cover normal plugin workflows so unexpected legacy hits disappear from diagnostics. During migration, legacy uses of `window.playSong`, `song:*` events, `window.feedBack.seek`, and loop helpers remain available and are recorded as playback bridge hits. Treat bridge hits as migration telemetry: native capability requests should eventually cover normal plugin workflows so unexpected legacy hits disappear from diagnostics.
## Progression Requester And Observer ## Progression Requester And Observer
@@ -484,7 +484,7 @@ Plugins that report gameplay outcomes or react to player progression (spec 010)
`buy-item` and `equip-item` require a visible user gesture (`authorization: "user-action"`). Decibels are play-earned only; plugins must not present any purchase path. `buy-item` and `equip-item` require a visible user gesture (`authorization: "user-action"`). Decibels are play-earned only; plugins must not present any purchase path.
```js ```js
const api = window.slopsmith.capabilities; const api = window.feedBack.capabilities;
const result = await api.dispatch({ const result = await api.dispatch({
capability: 'progression', capability: 'progression',
@@ -494,14 +494,14 @@ const result = await api.dispatch({
}); });
// result.payload lists challenges/quests completed by this event (toast UX). // result.payload lists challenges/quests completed by this event (toast UX).
window.slopsmith.on('progression:quest-completed', (e) => { window.feedBack.on('progression:quest-completed', (e) => {
console.log('quest done:', e.detail.title, '+' + e.detail.reward_db + ' dB'); console.log('quest done:', e.detail.title, '+' + e.detail.reward_db + ' dB');
}); });
``` ```
## Future Expansion Domains ## Future Expansion Domains
Some domain names are reserved for expected future contracts, but they are not registered in the runtime graph yet. For example, `ui.player-panels` is documented as a likely panel-host surface, but Slopsmith does not currently expose a capability command for panel contributions. See [capability-roadmap.md](capability-roadmap.md) for the PR1 domain set and deferred-domain checklist. Some domain names are reserved for expected future contracts, but they are not registered in the runtime graph yet. For example, `ui.player-panels` is documented as a likely panel-host surface, but FeedBack does not currently expose a capability command for panel contributions. See [capability-roadmap.md](capability-roadmap.md) for the PR1 domain set and deferred-domain checklist.
Plugins should not declare future expansion domains until the corresponding host workflow ships. For current integrations, prefer active domains such as `library`, `playback`, `audio-mix`, `audio-input`, `audio-monitoring`, or `stems` intent matching the recipes above. Plugins should not declare future expansion domains until the corresponding host workflow ships. For current integrations, prefer active domains such as `library`, `playback`, `audio-mix`, `audio-input`, `audio-monitoring`, or `stems` intent matching the recipes above.
@@ -537,7 +537,7 @@ the owner is visible in the Capability Inspector.
Register the action from the plugin's `screen.js`: Register the action from the plugin's `screen.js`:
```js ```js
window.slopsmith.libraryCardActions.register({ window.feedBack.libraryCardActions.register({
id: 'my_card_action.run', id: 'my_card_action.run',
pluginId: 'my_card_action', pluginId: 'my_card_action',
label: 'Do the thing', label: 'Do the thing',
+5 -5
View File
@@ -38,7 +38,7 @@ The audio graph/session and effects slices promote these domains after PR1:
`core.audio.session` is the runtime coordinator for all four domains. It owns `audio-mix`, `audio-input`, and `audio-monitoring`; for `stems`, it coordinates the active Stems provider without replacing the Stems plugin as the owner of actual stem playback/state. `core.audio.session` is the runtime coordinator for all four domains. It owns `audio-mix`, `audio-input`, and `audio-monitoring`; for `stems`, it coordinates the active Stems provider without replacing the Stems plugin as the owner of actual stem playback/state.
The focused audio-mix control-plane slice promotes fader discovery, read/write operations, committed-value events, native-over-legacy duplicate handling, route/analyser inspection, and compatibility removal gates into `audio-mix`. During migration, `window.slopsmith.audio.registerFader(...)` remains available as a compatibility adapter, but the player mixer consumes the audio-mix control plane as its source of truth. The focused audio-mix control-plane slice promotes fader discovery, read/write operations, committed-value events, native-over-legacy duplicate handling, route/analyser inspection, and compatibility removal gates into `audio-mix`. During migration, `window.feedBack.audio.registerFader(...)` remains available as a compatibility adapter, but the player mixer consumes the audio-mix control plane as its source of truth.
The focused audio-input control-plane slice promotes source listing, prompt-free selection/inspection, explicit provider enumeration, open/close dispatch, channel-shape compatibility, selected-source persistence, shared requester sessions, and redaction-safe failure diagnostics into `audio-input`. During migration, legacy browser, desktop, or plugin-specific input handoffs should be recorded as `audio-input.legacy-source` bridge hits. Native providers own the visible source when they share a logical source key with a compatibility-backed source; the compatibility source remains diagnostics-only until normal playback shows no unexpected legacy hits. The focused audio-input control-plane slice promotes source listing, prompt-free selection/inspection, explicit provider enumeration, open/close dispatch, channel-shape compatibility, selected-source persistence, shared requester sessions, and redaction-safe failure diagnostics into `audio-input`. During migration, legacy browser, desktop, or plugin-specific input handoffs should be recorded as `audio-input.legacy-source` bridge hits. Native providers own the visible source when they share a logical source key with a compatibility-backed source; the compatibility source remains diagnostics-only until normal playback shows no unexpected legacy hits.
@@ -50,7 +50,7 @@ The focused audio-effects control-plane slice promotes provider registration, us
The playback slice promotes `playback` from a deferred domain to an active exclusive-owner core domain. It owns transport commands (`start`, `pause`, `resume`, `stop`, `seek`, `set-loop`, `clear-loop`, `inspect`), lifecycle events (`playback:requested`, `playback:loading`, `playback:ready`, `playback:started`, `playback:paused`, `playback:resumed`, `playback:seeking`, `playback:seeked`, `playback:ended`, `playback:stopped`, route events, bridge hits, and loop events), and redaction-safe diagnostics for session, target, timing, route, loop, requester, observer, bridge, and recent outcome state. The playback slice promotes `playback` from a deferred domain to an active exclusive-owner core domain. It owns transport commands (`start`, `pause`, `resume`, `stop`, `seek`, `set-loop`, `clear-loop`, `inspect`), lifecycle events (`playback:requested`, `playback:loading`, `playback:ready`, `playback:started`, `playback:paused`, `playback:resumed`, `playback:seeking`, `playback:seeked`, `playback:ended`, `playback:stopped`, route events, bridge hits, and loop events), and redaction-safe diagnostics for session, target, timing, route, loop, requester, observer, bridge, and recent outcome state.
The implementation deliberately keeps raw transport handles in `static/app.js`: the domain host registers a private adapter and receives sanitized snapshots instead of exposing the `<audio>` element, JUCE player, decoded audio buffers, waveform data, or native route handles. Playback targets expose a pseudonymous arrangement-scoped `targetId` plus a hashed per-song `settingsKey` so observers can store local per-song settings without reading raw filenames or paths. Compatibility bridges currently account for `window.playSong`, `window.slopsmith` transport helpers, legacy song events, loop helpers, media snapshots, route switching, and native-route handoff. Fresh audible starts require `authorization: "user-action"`; background requesters can inspect or control only an existing compatible session according to the command conflict policy. The implementation deliberately keeps raw transport handles in `static/app.js`: the domain host registers a private adapter and receives sanitized snapshots instead of exposing the `<audio>` element, JUCE player, decoded audio buffers, waveform data, or native route handles. Playback targets expose a pseudonymous arrangement-scoped `targetId` plus a hashed per-song `settingsKey` so observers can store local per-song settings without reading raw filenames or paths. Compatibility bridges currently account for `window.playSong`, `window.feedBack` transport helpers, legacy song events, loop helpers, media snapshots, route switching, and native-route handoff. Fresh audible starts require `authorization: "user-action"`; background requesters can inspect or control only an existing compatible session according to the command conflict policy.
Playback bridge removal gates are: bundled and first-party plugins use native playback dispatch for normal requester/observer workflows; normal play/pause/seek/loop/route smoke runs show no unexpected bridge hits beyond compatibility-only listeners; playback diagnostics distinguish denied, no-target, stale, cancelled, degraded, unavailable, failed, and stopped outcomes; repeated plugin hydration does not duplicate requesters, observers, wrappers, or bridge entries; and exported support snapshots contain no raw song filenames, paths, URLs, media handles, buffers, waveforms, samples, or recordings. Playback bridge removal gates are: bundled and first-party plugins use native playback dispatch for normal requester/observer workflows; normal play/pause/seek/loop/route smoke runs show no unexpected bridge hits beyond compatibility-only listeners; playback diagnostics distinguish denied, no-target, stale, cancelled, degraded, unavailable, failed, and stopped outcomes; repeated plugin hydration does not duplicate requesters, observers, wrappers, or bridge entries; and exported support snapshots contain no raw song filenames, paths, URLs, media handles, buffers, waveforms, samples, or recordings.
@@ -82,7 +82,7 @@ This is the recommended order for UI/UX capability work only. It excludes audio
| 5 | Player controls | `ui.player-controls` | Direct player control DOM edits, control popovers, button/slider globals | Ordered player-control regions with stable command buttons, popovers, sliders, disabled states, and contribution teardown | Player controls can be added/removed/reordered without plugins mutating the control bar directly. | | 5 | Player controls | `ui.player-controls` | Direct player control DOM edits, control popovers, button/slider globals | Ordered player-control regions with stable command buttons, popovers, sliders, disabled states, and contribution teardown | Player controls can be added/removed/reordered without plugins mutating the control bar directly. |
| 6 | Player overlays | `ui.player-overlays`, `tours` | Overlay canvases, tour overlays, highway visibility listeners, direct z-index management | Overlay host with anchors, z-order, hit-testing, renderer compatibility flags, visibility events, and cleanup | Fretboard, section map, tours, transpose, step mode, and similar overlays can coexist without private layering rules. | | 6 | Player overlays | `ui.player-overlays`, `tours` | Overlay canvases, tour overlays, highway visibility listeners, direct z-index management | Overlay host with anchors, z-order, hit-testing, renderer compatibility flags, visibility events, and cleanup | Fretboard, section map, tours, transpose, step mode, and similar overlays can coexist without private layering rules. |
| 7 | Player panels | `ui.player-panels` | Splitscreen panel DOM, panel-local highway instances, panel-local shortcuts | Panel host with layout slots, active-panel focus, per-panel renderer selection, per-panel shortcuts, visibility, and teardown | Splitscreen-style panels can be composed through host APIs instead of wrapping playback/screen globals. | | 7 | Player panels | `ui.player-panels` | Splitscreen panel DOM, panel-local highway instances, panel-local shortcuts | Panel host with layout slots, active-panel focus, per-panel renderer selection, per-panel shortcuts, visibility, and teardown | Splitscreen-style panels can be composed through host APIs instead of wrapping playback/screen globals. |
| 8 | Visualization UX | `visualization` | `type: "visualization"`, `window.slopsmithViz_*`, viz picker state, auto-match hooks | Renderer provider registry with picker integration, auto-match ordering, context-type metadata, fallback/revert events, and per-panel selection | Renderer selection and failure recovery are fully attributed in diagnostics; picker options no longer depend on global scans. | | 8 | Visualization UX | `visualization` | `type: "visualization"`, `window.feedBackViz_*`, viz picker state, auto-match hooks | Renderer provider registry with picker integration, auto-match ordering, context-type metadata, fallback/revert events, and per-panel selection | Renderer selection and failure recovery are fully attributed in diagnostics; picker options no longer depend on global scans. |
| 9 | Library and guided UX extensions | `ui.library-card-injection`, `tours` | Library card buttons, tour registration globals, target selectors | Contribution APIs for library card actions and guided-tour steps with applicability, target resolution, and action-result events | Library actions and tours can be inspected, disabled, and tested independently of plugin-private DOM injection. | | 9 | Library and guided UX extensions | `ui.library-card-injection`, `tours` | Library card buttons, tour registration globals, target selectors | Contribution APIs for library card actions and guided-tour steps with applicability, target resolution, and action-result events | Library actions and tours can be inspected, disabled, and tested independently of plugin-private DOM injection. |
| 10 | Theme and polish surfaces | `settings` or candidate `ui.theme` | Global theme settings, direct stylesheet/class mutation | Theme contribution metadata for tokens, selected theme, preview/apply/restore lifecycle, and diagnostics without user secrets | Themes are reversible and attributable, and visual changes do not depend on hidden global state. | | 10 | Theme and polish surfaces | `settings` or candidate `ui.theme` | Global theme settings, direct stylesheet/class mutation | Theme contribution metadata for tokens, selected theme, preview/apply/restore lifecycle, and diagnostics without user secrets | Themes are reversible and attributable, and visual changes do not depend on hidden global state. |
@@ -127,7 +127,7 @@ These candidate domains were surfaced by the included plugin inventory but are n
| `recording` | multi-provider | sensitive | Arm/start/stop capture, take upload/import, capture-source binding, latency metadata, and storage cleanup. | Studio and karaoke workflows need capture/session semantics distinct from raw audio input. | | `recording` | multi-provider | sensitive | Arm/start/stop capture, take upload/import, capture-source binding, latency metadata, and storage cleanup. | Studio and karaoke workflows need capture/session semantics distinct from raw audio input. |
| `practice-session` | multi-provider | safe | Practice session lifecycle, goals, score/progress events, chart segment focus, and journal persistence boundaries. | Practice Journal, Minigames, Guitar Theory, Flappy Bend, and Note Detect imply practice/progression state. | | `practice-session` | multi-provider | safe | Practice session lifecycle, goals, score/progress events, chart segment focus, and journal persistence boundaries. | Practice Journal, Minigames, Guitar Theory, Flappy Bend, and Note Detect imply practice/progression state. |
| `collaboration` | multi-provider | sensitive | Room/session lifecycle, participant identity redaction, shared playback sync, conflict policy, and disconnect recovery. | Multiplayer is a distinct real-time coordination surface. | | `collaboration` | multi-provider | sensitive | Room/session lifecycle, participant identity redaction, shared playback sync, conflict policy, and disconnect recovery. | Multiplayer is a distinct real-time coordination surface. |
| `external-services` | diagnostic or privileged metadata | privileged | Network/download/subprocess integration inventory, endpoint attribution, confirmation policy, and failure diagnostics. | Update Manager, Find More, Sloppak Converter, and media jobs reach outside local Slopsmith state. | | `external-services` | diagnostic or privileged metadata | privileged | Network/download/subprocess integration inventory, endpoint attribution, confirmation policy, and failure diagnostics. | Update Manager, Find More, Sloppak Converter, and media jobs reach outside local FeedBack state. |
Candidate domains can also remain as safety metadata on existing domains. For example, `external-services` may be more useful as a cross-cutting review tag than as a dispatchable runtime capability. Candidate domains can also remain as safety metadata on existing domains. For example, `external-services` may be more useful as a cross-cutting review tag than as a dispatchable runtime capability.
@@ -141,7 +141,7 @@ PR1 does not add per-domain versioning. The `capability-pipelines.v1` standard v
- Changing command payloads, return payloads, or dispatch outcomes incompatibly is breaking. - Changing command payloads, return payloads, or dispatch outcomes incompatibly is breaking.
- A breaking change requires either a future `capability-pipelines` version or a clearly new domain name if parallel support is needed. - A breaking change requires either a future `capability-pipelines` version or a clearly new domain name if parallel support is needed.
Per-domain versions should wait until Slopsmith has a concrete need for multiple incompatible versions of the same domain to coexist. Per-domain versions should wait until FeedBack has a concrete need for multiple incompatible versions of the same domain to coexist.
## Future Domain PR Checklist ## Future Domain PR Checklist
+4 -4
View File
@@ -2,7 +2,7 @@
Capability declarations include a safety class so reviewers can decide whether a domain can ship as a normal plugin contract or needs extra enforcement first. Capability declarations include a safety class so reviewers can decide whether a domain can ship as a normal plugin contract or needs extra enforcement first.
Core domains also have a review scope. **Active contract** domains are wired to current Slopsmith behavior and should be tested as working integration points. Expected future domains are documented below, but are intentionally not registered in the runtime graph until Slopsmith ships the corresponding host UI or provider workflow. Core domains also have a review scope. **Active contract** domains are wired to current FeedBack behavior and should be tested as working integration points. Expected future domains are documented below, but are intentionally not registered in the runtime graph until FeedBack ships the corresponding host UI or provider workflow.
| Domain | Owner Kind | Safety Class | Stable Commands | Provider Operations | Notes | | Domain | Owner Kind | Safety Class | Stable Commands | Provider Operations | Notes |
|--------|------------|--------------|-----------------|---------------------|-------| |--------|------------|--------------|-----------------|---------------------|-------|
@@ -14,10 +14,10 @@ Core domains also have a review scope. **Active contract** domains are wired to
| audio-monitoring | provider-coordinator | sensitive | inspect, list-providers, register-provider, unregister-provider, select-provider, start, stop, set-direct-monitor | monitoring.start, monitoring.stop, monitoring.status, monitoring.set-direct-monitor | Inspect/list/select/status are prompt-free. Fresh monitoring start requires explicit user action; background requesters may only attach to an active compatible session. Outcomes distinguish handled, stopped, denied, unavailable, degraded, failed, no-owner, no-handler, unsupported-command, incompatible, incompatible-version, provider-selection-required, and user-action-required. Diagnostics redact raw device labels, hardware ids, paths, secrets, live handles, buffers, samples, waveforms, and recordings. | | audio-monitoring | provider-coordinator | sensitive | inspect, list-providers, register-provider, unregister-provider, select-provider, start, stop, set-direct-monitor | monitoring.start, monitoring.stop, monitoring.status, monitoring.set-direct-monitor | Inspect/list/select/status are prompt-free. Fresh monitoring start requires explicit user action; background requesters may only attach to an active compatible session. Outcomes distinguish handled, stopped, denied, unavailable, degraded, failed, no-owner, no-handler, unsupported-command, incompatible, incompatible-version, provider-selection-required, and user-action-required. Diagnostics redact raw device labels, hardware ids, paths, secrets, live handles, buffers, samples, waveforms, and recordings. |
| stems | coordinator plus plugin provider | safe | inspect, mute, restore | stem.get-state, stem.apply-automation, stem.restore-automation | Core coordinates claims/overrides; the active Stems provider owns actual stem state/playback. | | stems | coordinator plus plugin provider | safe | inspect, mute, restore | stem.get-state, stem.apply-automation, stem.restore-automation | Core coordinates claims/overrides; the active Stems provider owns actual stem state/playback. |
| playback | exclusive-owner | safe | inspect, start, pause, resume, stop, seek, set-loop, clear-loop, register-requester, register-observer | none | Core owns the transport control plane while `app.js` keeps raw media handles private. Fresh audible starts require explicit user action. Diagnostics expose pseudonymous targets, sanitized route/timing/loop state, requester/observer summaries, bridge hits, bounded recent outcomes, and no audio elements, native handles, decoded buffers, samples, waveforms, or recordings. | | playback | exclusive-owner | safe | inspect, start, pause, resume, stop, seek, set-loop, clear-loop, register-requester, register-observer | none | Core owns the transport control plane while `app.js` keeps raw media handles private. Fresh audible starts require explicit user action. Diagnostics expose pseudonymous targets, sanitized route/timing/loop state, requester/observer summaries, bridge hits, bounded recent outcomes, and no audio elements, native handles, decoded buffers, samples, waveforms, or recordings. |
| progression | exclusive-owner | safe | inspect, record-event, list-shop, buy-item, equip-item | none | Core owns mastery rank, the challenge/quest engine, the Decibels wallet, and the cosmetics shop (spec 010). `record-event` accepts whitelisted types only (`minigame_run`); `song_completed` is server-derived in `/api/stats` and denied here. `buy-item`/`equip-item` require explicit user action. Decibels are play-earned only — no real-money path exists or may be added. Diagnostics (`slopsmith.progression.diag.v1`) carry content warnings, rank/level/quest counts, and wallet totals; no song filenames or display names. | | progression | exclusive-owner | safe | inspect, record-event, list-shop, buy-item, equip-item | none | Core owns mastery rank, the challenge/quest engine, the Decibels wallet, and the cosmetics shop (spec 010). `record-event` accepts whitelisted types only (`minigame_run`); `song_completed` is server-derived in `/api/stats` and denied here. `buy-item`/`equip-item` require explicit user action. Decibels are play-earned only — no real-money path exists or may be added. Diagnostics (`feedBack.progression.diag.v1`) carry content warnings, rank/level/quest counts, and wallet totals; no song filenames or display names. |
| audio-effects | provider-coordinator | sensitive | inspect, list-providers, register-provider, unregister-provider, select-chain, resolve-plan, inspect-route, bypass, restore, fallback, activate-segment, set-stage-bypass, set-stage-parameter, record-bridge-hit | chain.resolve, chain.inspect, segment.activate, stage.set-bypass, stage.set-parameter, route.bypass, route.restore | Core owns provider selection, route state, chain-plan schema validation, fallback accounting, and diagnostics. Providers propose opaque NAM/IR/VST/utility chain plans; trusted desktop/native code validates and loads processors. Chain selection and route bypass/restore require explicit user action or restored selection. Diagnostics omit raw paths, filenames, URLs, model/IR names, native preset JSON, VST state blobs, handles, callbacks, DOM nodes, audio buffers, samples, and waveforms. | | audio-effects | provider-coordinator | sensitive | inspect, list-providers, register-provider, unregister-provider, select-chain, resolve-plan, inspect-route, bypass, restore, fallback, activate-segment, set-stage-bypass, set-stage-parameter, record-bridge-hit | chain.resolve, chain.inspect, segment.activate, stage.set-bypass, stage.set-parameter, route.bypass, route.restore | Core owns provider selection, route state, chain-plan schema validation, fallback accounting, and diagnostics. Providers propose opaque NAM/IR/VST/utility chain plans; trusted desktop/native code validates and loads processors. Chain selection and route bypass/restore require explicit user action or restored selection. Diagnostics omit raw paths, filenames, URLs, model/IR names, native preset JSON, VST state blobs, handles, callbacks, DOM nodes, audio buffers, samples, and waveforms. |
| visualization | provider-coordinator | safe | inspect, list-providers, select-renderer, clear-renderer | renderer.create, renderer.destroy | Highway renderer provider registry, picker-delegated selection, auto-match attribution, and failure fallback. `renderer.create` maps to the legacy `window.slopsmithViz_*` factory `init(canvas, ctx)` call; `renderer.destroy` maps to the factory `destroy()` teardown. Legacy `type: "visualization"` manifests and `window.slopsmithViz_*` globals are accounted compatibility shims. Diagnostics carry provider ids/labels, selection source, last auto-match outcome, and last failure — no song filenames, titles, or arrangement names. | | visualization | provider-coordinator | safe | inspect, list-providers, select-renderer, clear-renderer | renderer.create, renderer.destroy | Highway renderer provider registry, picker-delegated selection, auto-match attribution, and failure fallback. `renderer.create` maps to the legacy `window.feedBackViz_*` factory `init(canvas, ctx)` call; `renderer.destroy` maps to the factory `destroy()` teardown. Legacy `type: "visualization"` manifests and `window.feedBackViz_*` globals are accounted compatibility shims. Diagnostics carry provider ids/labels, selection source, last auto-match outcome, and last failure — no song filenames, titles, or arrangement names. |
| note-detection | provider-coordinator | sensitive | inspect, register-provider, unregister-provider, open-binding, close-binding, set-target, clear-target | pitch.estimate, verify.target | Detection-binding control plane (spec 009): providers (midi/engine/js) serve primitives; each requester binds its own redacted tuning context; consumers own judgment, hit/miss flow as observability events. Legacy `highway.setNoteStateProvider` is an accounted shim. Diagnostics carry provider/binding summaries and bounded outcomes — no raw audio, sample data, device labels, or song identity. | | note-detection | provider-coordinator | sensitive | inspect, register-provider, unregister-provider, open-binding, close-binding, set-target, clear-target | pitch.estimate, verify.target | Detection-binding control plane (spec 009): providers (midi/engine/js) serve primitives; each requester binds its own redacted tuning context; consumers own judgment, hit/miss flow as observability events. Legacy `highway.setNoteStateProvider` is an accounted shim. Diagnostics carry provider/binding summaries and bounded outcomes — no raw audio, sample data, device labels, or song identity. |
@@ -43,6 +43,6 @@ These domains are expected future capability contracts, not current runtime grap
| midi-control | multi-provider | sensitive | list-mappings, get-mapping, set-mapping, delete-mapping, activate-mapping, inspect | Mappings ONLY — CC/pitchbend/note → semantic action routing (spec 013). Device discovery/selection/open is NOT this domain's job: it consumes the delivered `midi-input` domain for device access. Needs a concrete mapping consumer (the MIDI control plugin / drums learn-mode) + redacted diagnostics (no raw MIDI streams) before promotion. | | midi-control | multi-provider | sensitive | list-mappings, get-mapping, set-mapping, delete-mapping, activate-mapping, inspect | Mappings ONLY — CC/pitchbend/note → semantic action routing (spec 013). Device discovery/selection/open is NOT this domain's job: it consumes the delivered `midi-input` domain for device access. Needs a concrete mapping consumer (the MIDI control plugin / drums learn-mode) + redacted diagnostics (no raw MIDI streams) before promotion. |
| tempo-clock | multi-provider | safe | register, inspect | Needs a concrete provider and consumer workflow. | | tempo-clock | multi-provider | safe | register, inspect | Needs a concrete provider and consumer workflow. |
Planned domains should also stay out of the runtime graph until Slopsmith ships the corresponding user-facing workflows. Planned domains should also stay out of the runtime graph until FeedBack ships the corresponding user-facing workflows.
When promoting a planned domain, use [capability-review-preflight.md](capability-review-preflight.md) before opening the PR. The preflight captures recurring review requirements for identity, redaction, outcome propagation, diagnostics freshness, schema consistency, and teardown. When promoting a planned domain, use [capability-review-preflight.md](capability-review-preflight.md) before opening the PR. The preflight captures recurring review requirements for identity, redaction, outcome propagation, diagnostics freshness, schema consistency, and teardown.
+20 -20
View File
@@ -1,7 +1,7 @@
# Slopsmith Diagnostics Bundle — Format Specification # FeedBack Diagnostics Bundle — Format Specification
This document is the authoritative reference for the `slopsmith-diag-*.zip` This document is the authoritative reference for the `feedBack-diag-*.zip`
file produced by Settings → Export Diagnostics (slopsmith#166). file produced by Settings → Export Diagnostics (feedBack#166).
The bundle is consumed by humans (maintainers reading bug reports) **and** The bundle is consumed by humans (maintainers reading bug reports) **and**
AI agents (auto-triage, code-aware assistants). Every JSON file inside AI agents (auto-triage, code-aware assistants). Every JSON file inside
@@ -15,17 +15,17 @@ version without guessing.
A diagnostic bundle is a plain ZIP archive. The default filename is: A diagnostic bundle is a plain ZIP archive. The default filename is:
``` ```
slopsmith-diag-<slopsmith-version>-<YYYYMMDD-HHMMSS>.zip feedBack-diag-<feedBack-version>-<YYYYMMDD-HHMMSS>.zip
``` ```
Top-level layout: Top-level layout:
``` ```
slopsmith-diag-0.2.4-20260503-143022.zip feedBack-diag-0.2.4-20260503-143022.zip
├── manifest.json AI-friendly index, schema 1 ├── manifest.json AI-friendly index, schema 1
├── README.txt Human-friendly: what's in here, how to read ├── README.txt Human-friendly: what's in here, how to read
├── system/ ├── system/
│ ├── version.json slopsmith + python + OS │ ├── version.json feedBack + python + OS
│ ├── env.json allowlisted env vars only (no secrets) │ ├── env.json allowlisted env vars only (no secrets)
│ ├── hardware.json backend hardware (container-limited if Docker) │ ├── hardware.json backend hardware (container-limited if Docker)
│ └── plugins.json loaded + orphan plugins, with git info │ └── plugins.json loaded + orphan plugins, with git info
@@ -53,7 +53,7 @@ logs, console, plugins). Missing sections are not represented in
{ {
"schema": 1, // bundle schema; bump = breaking change "schema": 1, // bundle schema; bump = breaking change
"exported_at": "2026-05-03T14:30:22Z", "exported_at": "2026-05-03T14:30:22Z",
"slopsmith_version": "0.2.4", "feedBack_version": "0.2.4",
"runtime": "docker", // "docker" | "electron" | "bare" "runtime": "docker", // "docker" | "electron" | "bare"
"redacted": true, // were redactions applied? "redacted": true, // were redactions applied?
"files": [ "files": [
@@ -94,7 +94,7 @@ Field semantics:
```jsonc ```jsonc
{ {
"schema": "system.version.v1", "schema": "system.version.v1",
"slopsmith_version": "0.2.4", "feedBack_version": "0.2.4",
"python": { "version": "3.12.4", "implementation": "CPython", "executable": "/usr/bin/python" }, "python": { "version": "3.12.4", "implementation": "CPython", "executable": "/usr/bin/python" },
"os": { "system": "Linux", "release": "6.5.0", "machine": "x86_64" }, "os": { "system": "Linux", "release": "6.5.0", "machine": "x86_64" },
"exported_at": "2026-05-03T14:30:22Z" "exported_at": "2026-05-03T14:30:22Z"
@@ -109,13 +109,13 @@ Field semantics:
"vars": { "vars": {
"LOG_LEVEL": "INFO", "LOG_LEVEL": "INFO",
"LOG_FORMAT": "json", "LOG_FORMAT": "json",
"SLOPSMITH_RUNTIME": "electron" "FEEDBACK_RUNTIME": "electron"
} }
} }
``` ```
Allowlisted env var keys only (see `ENV_ALLOWLIST` in `lib/diagnostics_bundle.py`): Allowlisted env var keys only (see `ENV_ALLOWLIST` in `lib/diagnostics_bundle.py`):
`LOG_LEVEL`, `LOG_FORMAT`, `LOG_FILE`, `SLOPSMITH_RUNTIME`, `PORT`, `HOST`, `LOG_LEVEL`, `LOG_FORMAT`, `LOG_FILE`, `FEEDBACK_RUNTIME`, `PORT`, `HOST`,
`TZ`, `PYTHONUNBUFFERED`, `DEMUCS_SERVER_URL`. New entries require an `TZ`, `PYTHONUNBUFFERED`, `DEMUCS_SERVER_URL`. New entries require an
allowlist edit; secrets must never be added. allowlist edit; secrets must never be added.
@@ -177,7 +177,7 @@ entry explaining why.
"capability_validation_warnings": [], "capability_validation_warnings": [],
"capability_unsupported_versions": [], "capability_unsupported_versions": [],
"compatibility_shims": [], "compatibility_shims": [],
"git": { "sha": "abc123d", "remote": "https://github.com/topkoa/slopsmith-plugin-stems.git" } "git": { "sha": "abc123d", "remote": "https://github.com/topkoa/feedBack-plugin-stems.git" }
} }
], ],
"orphans": [ "orphans": [
@@ -187,7 +187,7 @@ entry explaining why.
"version": "0.1.0", "version": "0.1.0",
"loaded": false, "loaded": false,
"dir": "broken", "dir": "broken",
"path": "/home/user/.config/slopsmith/plugins/broken" "path": "/home/user/.config/feedBack/plugins/broken"
} }
] ]
} }
@@ -223,7 +223,7 @@ appear in `capability_unsupported_versions` and should be treated as
non-executable runtime intent. non-executable runtime intent.
Client-side capability snapshots contributed under `plugins/capabilities/client.json` Client-side capability snapshots contributed under `plugins/capabilities/client.json`
use schema `slopsmith.capabilities.diagnostics.v1`. They include current use schema `feedBack.capabilities.diagnostics.v1`. They include current
pipelines, participants, conflicts, missing providers, user overrides, active pipelines, participants, conflicts, missing providers, user overrides, active
or orphaned claims, claim lifecycle records, compatibility shim hit counts, or orphaned claims, claim lifecycle records, compatibility shim hit counts,
unsupported-version reports, and recent decisions. The runtime caps this unsupported-version reports, and recent decisions. The runtime caps this
@@ -235,7 +235,7 @@ current graph state.
```jsonc ```jsonc
{ {
"schema": "logs.server.v1", "schema": "logs.server.v1",
"log_file": "/data/log/slopsmith.log", "log_file": "/data/log/feedBack.log",
"exists": true, "exists": true,
"size_bytes": 8388608, "size_bytes": 8388608,
"tail_bytes": 5242880, "tail_bytes": 5242880,
@@ -341,7 +341,7 @@ serialized as `"[circular]"`.
`runtime.kind` rules: `runtime.kind` rules:
- `"electron"` if `navigator.userAgent` contains `Electron/`. Versions - `"electron"` if `navigator.userAgent` contains `Electron/`. Versions
populated when the desktop launcher exposes `window.slopsmithElectron` populated when the desktop launcher exposes `window.feedBackElectron`
via a preload `contextBridge`. via a preload `contextBridge`.
- `"browser"` otherwise. - `"browser"` otherwise.
@@ -367,7 +367,7 @@ typically prefix their keys with their `plugin_id`.
{ {
"schema": "client.ua.v1", "schema": "client.ua.v1",
"userAgent": "...", "userAgent": "...",
"url": "https://slopsmith.local/", "url": "https://feedBack.local/",
"screen": { ... } "screen": { ... }
} }
``` ```
@@ -406,10 +406,10 @@ dispatch by plugin schema.
Detection precedence (backend): Detection precedence (backend):
1. `SLOPSMITH_RUNTIME` env var (`"electron"`/`"docker"`/`"bare"`) 1. `FEEDBACK_RUNTIME` env var (`"electron"`/`"docker"`/`"bare"`)
2. `/.dockerenv` exists OR `/proc/1/cgroup` mentions `docker`/ 2. `/.dockerenv` exists OR `/proc/1/cgroup` mentions `docker`/
`containerd`/`kubepods``docker` `containerd`/`kubepods``docker`
3. Parent process name matches `electron` or `Slopsmith``electron` 3. Parent process name matches `electron` or `FeedBack``electron`
4. Default: `bare` 4. Default: `bare`
Detection (frontend): `Electron/` in user agent → `electron`, else Detection (frontend): `Electron/` in user agent → `electron`, else
@@ -430,7 +430,7 @@ between bundles):
|--------------------|-----------------------------------------------------| |--------------------|-----------------------------------------------------|
| `<DLC_DIR>` | configured DLC root path | | `<DLC_DIR>` | configured DLC root path |
| `<HOME>` | user's home directory | | `<HOME>` | user's home directory |
| `<CONFIG_DIR>` | slopsmith config directory | | `<CONFIG_DIR>` | feedBack config directory |
| `<song:HASH8>` | song filename / basename (8-char salted SHA-256) | | `<song:HASH8>` | song filename / basename (8-char salted SHA-256) |
| `<ip:HASH6>` | IPv4 / IPv6 address | | `<ip:HASH6>` | IPv4 / IPv6 address |
| `<redacted>` | bearer token, `key=`/`token=`/`api_key=` query strings | | `<redacted>` | bearer token, `key=`/`token=`/`api_key=` query strings |
@@ -508,7 +508,7 @@ machine.
``` ```
Frontend plugins push diagnostics by calling Frontend plugins push diagnostics by calling
`window.slopsmith.diagnostics.contribute(plugin_id, payload)` before the `window.feedBack.diagnostics.contribute(plugin_id, payload)` before the
user clicks Export. The payload is written to `plugins/<id>/client.json` user clicks Export. The payload is written to `plugins/<id>/client.json`
(gated on the same "Plugin diagnostics" toggle as backend plugin files). (gated on the same "Plugin diagnostics" toggle as backend plugin files).
+10 -10
View File
@@ -1,4 +1,4 @@
# Slopsmith diagnostic sloppaks # FeedBack diagnostic sloppaks
Generated, non-copyrighted mini-songs for technique-assessment style Generated, non-copyrighted mini-songs for technique-assessment style
checks. Report-only — they do not change gameplay settings or detection checks. Report-only — they do not change gameplay settings or detection
@@ -6,7 +6,7 @@ thresholds.
## Basic Guitar (POC) ## Basic Guitar (POC)
**Artifact:** `slopsmith-diagnostic-basic-guitar.sloppak` **Artifact:** `feedBack-diagnostic-basic-guitar.sloppak`
**Contents (~55 s):** **Contents (~55 s):**
@@ -23,7 +23,7 @@ for future Technique Assessment integration).
## Rebuild ## Rebuild
From the slopsmith repo root (requires `ffmpeg`; the slopsmith Docker image From the feedBack repo root (requires `ffmpeg`; the feedBack Docker image
has `libvorbis`, Homebrew ffmpeg may use the built-in `vorbis` encoder): has `libvorbis`, Homebrew ffmpeg may use the built-in `vorbis` encoder):
```bash ```bash
@@ -36,14 +36,14 @@ On library scan startup (and periodic rescans), the server copies bundled
diagnostic sloppaks into the user DLC folder when missing or when the diagnostic sloppaks into the user DLC folder when missing or when the
bundled source is newer: bundled source is newer:
`DLC_DIR/diagnostics-builtin/slopsmith-diagnostic-basic-guitar.sloppak` `DLC_DIR/diagnostics-builtin/feedBack-diagnostic-basic-guitar.sloppak`
Source: `docs/diagnostics/slopsmith-diagnostic-basic-guitar.sloppak` (next to Source: `docs/diagnostics/feedBack-diagnostic-basic-guitar.sloppak` (next to
`server.py` in dev; must be included in the desktop bundle — see `server.py` in dev; must be included in the desktop bundle — see
`slopsmith-desktop/scripts/bundle-slopsmith.sh`). `feedBack-desktop/scripts/bundle-feedBack.sh`).
Unlike `tutorials-builtin/`, `diagnostics-builtin/` **is** included in the Unlike `tutorials-builtin/`, `diagnostics-builtin/` **is** included in the
library scan. Tracks appear under **Slopsmith** / library scan. Tracks appear under **FeedBack** /
**Technique Assessment Diagnostics**. **Technique Assessment Diagnostics**.
Existing destination files are not overwritten unless the bundled source Existing destination files are not overwritten unless the bundled source
@@ -55,10 +55,10 @@ are never touched.
Normally seeding is automatic once a DLC folder is configured. To test a Normally seeding is automatic once a DLC folder is configured. To test a
custom copy or an unreleased build: custom copy or an unreleased build:
1. Copy `slopsmith-diagnostic-basic-guitar.sloppak` into your Slopsmith 1. Copy `feedBack-diagnostic-basic-guitar.sloppak` into your FeedBack
DLC folder (e.g. `diagnostics-test/` or any scanned path). DLC folder (e.g. `diagnostics-test/` or any scanned path).
2. Restart Slopsmith or trigger a library rescan if the song does not appear. 2. Restart FeedBack or trigger a library rescan if the song does not appear.
3. Load **Slopsmith Diagnostic — Basic Guitar**. 3. Load **FeedBack Diagnostic — Basic Guitar**.
4. Play the **Diagnostic Guitar** arrangement. 4. Play the **Diagnostic Guitar** arrangement.
5. Confirm the 3D highway shows open notes and power-chord gems. 5. Confirm the 3D highway shows open notes and power-chord gems.
6. Turn **Detect** on — note_detect should push the chart to the desktop 6. Turn **Detect** on — note_detect should push the chart to the desktop
@@ -1,16 +1,16 @@
"""Build the Slopsmith Diagnostic — Basic Guitar sloppak (POC). """Build the FeedBack Diagnostic — Basic Guitar sloppak (POC).
A short, generated, non-copyrighted mini-song for technique-assessment A short, generated, non-copyrighted mini-song for technique-assessment
style checks: open strings, one fretted note, and repeated E5 power chords. style checks: open strings, one fretted note, and repeated E5 power chords.
Click-track backing only no external audio. Click-track backing only no external audio.
Run from the slopsmith repo root: Run from the feedBack repo root:
python3 docs/diagnostics/build_diagnostic_basic_guitar.py python3 docs/diagnostics/build_diagnostic_basic_guitar.py
Output (zip archive): Output (zip archive):
docs/diagnostics/slopsmith-diagnostic-basic-guitar.sloppak docs/diagnostics/feedBack-diagnostic-basic-guitar.sloppak
Pattern matches docs/benchmarks/note_detect_v1/build_benchmark.py. Pattern matches docs/benchmarks/note_detect_v1/build_benchmark.py.
""" """
@@ -289,8 +289,8 @@ def build_chart():
} }
manifest = { manifest = {
'title': 'Slopsmith Diagnostic — Basic Guitar', 'title': 'FeedBack Diagnostic — Basic Guitar',
'artist': 'Slopsmith', 'artist': 'FeedBack',
'album': 'Technique Assessment Diagnostics', 'album': 'Technique Assessment Diagnostics',
'year': 2026, 'year': 2026,
'duration': round(end_t, 3), 'duration': round(end_t, 3),
@@ -405,7 +405,7 @@ def build(output_zip: Path) -> dict:
def _diagnostic_readme(duration_s: float) -> str: def _diagnostic_readme(duration_s: float) -> str:
return f"""# Slopsmith Diagnostic — Basic Guitar return f"""# FeedBack Diagnostic — Basic Guitar
Short generated diagnostic track for technique-assessment style checks. Short generated diagnostic track for technique-assessment style checks.
Non-copyrighted click-track backing only. Non-copyrighted click-track backing only.
@@ -422,7 +422,7 @@ Built by docs/diagnostics/build_diagnostic_basic_guitar.py
def main(): def main():
repo_root = Path(__file__).resolve().parents[2] repo_root = Path(__file__).resolve().parents[2]
default_out = Path(__file__).resolve().parent / 'slopsmith-diagnostic-basic-guitar.sloppak' default_out = Path(__file__).resolve().parent / 'feedBack-diagnostic-basic-guitar.sloppak'
out = Path(sys.argv[1]) if len(sys.argv) > 1 else default_out out = Path(sys.argv[1]) if len(sys.argv) > 1 else default_out
if not out.is_absolute(): if not out.is_absolute():
out = repo_root / out out = repo_root / out
+11 -11
View File
@@ -13,7 +13,7 @@ Detection quality varies by guitar pickup, audio interface, monitor latency, the
## The benchmark sloppak ## The benchmark sloppak
The distributable sloppak ships in-tree at [docs/benchmarks/note_detect_v1/note_detect_benchmark_v1.sloppak](benchmarks/note_detect_v1/note_detect_benchmark_v1.sloppak) — drop it directly in your library folder (e.g. `<your-library>/sloppak/`) and it shows up in the library. The file is a zip under the hood but slopsmith's loader (`is_sloppak`) keys off the `.sloppak` suffix, so don't rename. After playing it once it ends up extracted under `static/sloppak_cache/note_detect_benchmark_v1.sloppak/`, which is where the harness reads its `arrangements/lead.json` from. 90 BPM, 8 numbered sections, ~2:20 total: The distributable sloppak ships in-tree at [docs/benchmarks/note_detect_v1/note_detect_benchmark_v1.sloppak](benchmarks/note_detect_v1/note_detect_benchmark_v1.sloppak) — drop it directly in your library folder (e.g. `<your-library>/sloppak/`) and it shows up in the library. The file is a zip under the hood but feedBack's loader (`is_sloppak`) keys off the `.sloppak` suffix, so don't rename. After playing it once it ends up extracted under `static/sloppak_cache/note_detect_benchmark_v1.sloppak/`, which is where the harness reads its `arrangements/lead.json` from. 90 BPM, 8 numbered sections, ~2:20 total:
| Section | Notes | Isolates | | Section | Notes | Isolates |
|---|---|---| |---|---|---|
@@ -28,10 +28,10 @@ The distributable sloppak ships in-tree at [docs/benchmarks/note_detect_v1/note_
Every chart note has `sus > 0` — so anything you tune against this benchmark exercises the sustain path, not staccato detection. (If we add a staccato section later, the cleanest split is by section name; don't categorize by `sus` value on the event log — see the "Common pitfalls" section.) Every chart note has `sus > 0` — so anything you tune against this benchmark exercises the sustain path, not staccato detection. (If we add a staccato section later, the cleanest split is by section name; don't categorize by `sus` value on the event log — see the "Common pitfalls" section.)
To rebuild after edits to the exercise list, follow the docstring at the top of `build_benchmark.py`. The script writes both an unzipped directory (`.sloppak/`) and a zipped archive (`.sloppak.zip`). The slopsmith library scanner (`lib/sloppak.py::is_sloppak()`) matches on the `.sloppak` suffix, **not** on `.sloppak.zip` — the directory form is usable as-is, but the zip output needs its suffix swapped before it'll be discovered. After regenerating, copy the zip output to the tracked path with the `.sloppak` suffix so it stays a drop-in install. Run from the slopsmith repo root so the relative paths resolve: To rebuild after edits to the exercise list, follow the docstring at the top of `build_benchmark.py`. The script writes both an unzipped directory (`.sloppak/`) and a zipped archive (`.sloppak.zip`). The feedBack library scanner (`lib/sloppak.py::is_sloppak()`) matches on the `.sloppak` suffix, **not** on `.sloppak.zip` — the directory form is usable as-is, but the zip output needs its suffix swapped before it'll be discovered. After regenerating, copy the zip output to the tracked path with the `.sloppak` suffix so it stays a drop-in install. Run from the feedBack repo root so the relative paths resolve:
```bash ```bash
# From the slopsmith repo root. # From the feedBack repo root.
cp static/sloppak_cache/note_detect_benchmark_v1.sloppak.zip \ cp static/sloppak_cache/note_detect_benchmark_v1.sloppak.zip \
docs/benchmarks/note_detect_v1/note_detect_benchmark_v1.sloppak docs/benchmarks/note_detect_v1/note_detect_benchmark_v1.sloppak
``` ```
@@ -46,7 +46,7 @@ The typical cycle for one tuning hypothesis:
2. **Arm a recording** from the gear popover next to the Detect button on the player. Arm before pressing Play. 2. **Arm a recording** from the gear popover next to the Detect button on the player. Arm before pressing Play.
3. **Play through the benchmark** (or any song) at **1.0× playback speed**. Half-speed playback breaks audio↔chart alignment and produces all-miss garbage — see Pitfalls. 3. **Play through the benchmark** (or any song) at **1.0× playback speed**. Half-speed playback breaks audio↔chart alignment and produces all-miss garbage — see Pitfalls.
4. **Auto-save fires on song end.** The WAV lands in `static/note_detect_recordings/note_detect_<slug>_<timestamp>.wav` (bind-mounted, so it's reachable from the host without a copy step). 4. **Auto-save fires on song end.** The WAV lands in `static/note_detect_recordings/note_detect_<slug>_<timestamp>.wav` (bind-mounted, so it's reachable from the host without a copy step).
5. **Run the headless harness** with a known config. Paths below assume the note_detect plugin is cloned into `plugins/note_detect/` (see the slopsmith README for the plugin-install flow — note_detect ships as a separate repo): 5. **Run the headless harness** with a known config. Paths below assume the note_detect plugin is cloned into `plugins/note_detect/` (see the feedBack README for the plugin-install flow — note_detect ships as a separate repo):
```bash ```bash
node plugins/note_detect/tools/harness.js \ node plugins/note_detect/tools/harness.js \
--audio static/note_detect_recordings/note_detect_<…>.wav \ --audio static/note_detect_recordings/note_detect_<…>.wav \
@@ -162,7 +162,7 @@ The same workflow works on any tuning change — A/V offset sweep, frame-size sw
### "Did my detector change improve things?" — ad hoc ### "Did my detector change improve things?" — ad hoc
Same recording, same chart, two harness runs. Recipe assumes you're at the slopsmith repo root *and* that the Note Detection plugin is cloned at `plugins/note_detect/` per the README. The detector source lives in that nested plugin repo, which slopsmith's `.gitignore` excludes via `plugins/*/`, so the stash dance has to run **inside** the plugin repo — `git stash` from the slopsmith root would either bail out or, worse, stash unrelated slopsmith edits. Same recording, same chart, two harness runs. Recipe assumes you're at the feedBack repo root *and* that the Note Detection plugin is cloned at `plugins/note_detect/` per the README. The detector source lives in that nested plugin repo, which feedBack's `.gitignore` excludes via `plugins/*/`, so the stash dance has to run **inside** the plugin repo — `git stash` from the feedBack root would either bail out or, worse, stash unrelated feedBack edits.
The stash dance below uses **`git stash push -u -m "..."`** to give the stash a known name *and* include untracked files. `-u` matters: if your detector change added a new module or fixture, an untracked-file-blind stash would leave it on disk during the "before" run and contaminate the baseline. The script then asserts a stash was actually created before popping (so a clean worktree doesn't silently pop someone else's WIP), wraps each step in **`set -euo pipefail`** so a failed `git stash pop` (e.g., conflict) aborts before the "after" harness records an invalid result, and uses `trap` to surface any failure with a clear message. The stash dance below uses **`git stash push -u -m "..."`** to give the stash a known name *and* include untracked files. `-u` matters: if your detector change added a new module or fixture, an untracked-file-blind stash would leave it on disk during the "before" run and contaminate the baseline. The script then asserts a stash was actually created before popping (so a clean worktree doesn't silently pop someone else's WIP), wraps each step in **`set -euo pipefail`** so a failed `git stash pop` (e.g., conflict) aborts before the "after" harness records an invalid result, and uses `trap` to surface any failure with a clear message.
@@ -172,7 +172,7 @@ PLUGIN_DIR=plugins/note_detect
HARNESS=$PLUGIN_DIR/tools/harness.js HARNESS=$PLUGIN_DIR/tools/harness.js
STASH_MSG="harness-before-$$" STASH_MSG="harness-before-$$"
trap 'echo "harness recipe aborted — stash may still be in $PLUGIN_DIR (\"git -C $PLUGIN_DIR stash list\")" >&2' ERR trap 'echo "harness recipe aborted — stash may still be in $PLUGIN_DIR (\"git -C $PLUGIN_DIR stash list\")" >&2' ERR
# Stash the detector edits inside the plugin repo, not the slopsmith root. # Stash the detector edits inside the plugin repo, not the feedBack root.
# -u also stashes untracked files (new modules, fixtures) so they don't # -u also stashes untracked files (new modules, fixtures) so they don't
# leak into the "before" baseline. `|| true` only swallows the # leak into the "before" baseline. `|| true` only swallows the
# clean-worktree case, which the next line catches explicitly. # clean-worktree case, which the next line catches explicitly.
@@ -223,9 +223,9 @@ Find the note's `t` in the chart, then grep the event log for entries near that
The Note Detection plugin lives in its own repository — these links go to the canonical source at github.com. If you've cloned the plugin into a local `plugins/note_detect/` next to this repo, the same files are at the equivalent path on disk. The Note Detection plugin lives in its own repository — these links go to the canonical source at github.com. If you've cloned the plugin into a local `plugins/note_detect/` next to this repo, the same files are at the equivalent path on disk.
- Plugin source: [`screen.js`](https://github.com/got-feedback/feedback-plugin-notedetect/blob/main/screen.js) — `matchNotes`, `checkMisses`, `_diagTimingErrors` / `_diagTimingErrorsHits`, `getDiagnostic`. - Plugin source: [`screen.js`](https://github.com/got-feedback/feedBack-plugin-notedetect/blob/main/screen.js) — `matchNotes`, `checkMisses`, `_diagTimingErrors` / `_diagTimingErrorsHits`, `getDiagnostic`.
- Routes: [`routes.py`](https://github.com/got-feedback/feedback-plugin-notedetect/blob/main/routes.py) — the `/api/plugins/note_detect/recording` and `/api/plugins/note_detect/live-judgment` endpoints. - Routes: [`routes.py`](https://github.com/got-feedback/feedBack-plugin-notedetect/blob/main/routes.py) — the `/api/plugins/note_detect/recording` and `/api/plugins/note_detect/live-judgment` endpoints.
- Harness: [`tools/harness.js`](https://github.com/got-feedback/feedback-plugin-notedetect/blob/main/tools/harness.js). - Harness: [`tools/harness.js`](https://github.com/got-feedback/feedBack-plugin-notedetect/blob/main/tools/harness.js).
- Regression driver: [`tools/regression.js`](https://github.com/got-feedback/feedback-plugin-notedetect/blob/main/tools/regression.js). - Regression driver: [`tools/regression.js`](https://github.com/got-feedback/feedBack-plugin-notedetect/blob/main/tools/regression.js).
- Benchmark builder: [docs/benchmarks/note_detect_v1/build_benchmark.py](benchmarks/note_detect_v1/build_benchmark.py). - Benchmark builder: [docs/benchmarks/note_detect_v1/build_benchmark.py](benchmarks/note_detect_v1/build_benchmark.py).
- Settings UI: [`settings.html`](https://github.com/got-feedback/feedback-plugin-notedetect/blob/main/settings.html) — A/V auto-calibrate panel, tuning-mode toggle, diagnostic block. - Settings UI: [`settings.html`](https://github.com/got-feedback/feedBack-plugin-notedetect/blob/main/settings.html) — A/V auto-calibrate panel, tuning-mode toggle, diagnostic block.
+5 -5
View File
@@ -1,6 +1,6 @@
# Plugin Capability Inventory # Plugin Capability Inventory
This report inventories the currently included plugins staged in `plugins/` and maps their observed behavior to Slopsmith capability domains. It is intended to inform the capability roadmap and the next migration specs now that PR1, the audio graph/session slice, playback, and audio-effects are active capability surfaces. This report inventories the currently included plugins staged in `plugins/` and maps their observed behavior to FeedBack capability domains. It is intended to inform the capability roadmap and the next migration specs now that PR1, the audio graph/session slice, playback, and audio-effects are active capability surfaces.
## Scope And Method ## Scope And Method
@@ -8,7 +8,7 @@ This report inventories the currently included plugins staged in `plugins/` and
- Verification pass: the original bundled-plugin scan found 25 plugins with backend `routes.py` and 14 plugins with `settings.html`. First-party plugin repos outside `plugins/` were checked separately from their current manifests and handoff docs. - Verification pass: the original bundled-plugin scan found 25 plugins with backend `routes.py` and 14 plugins with `settings.html`. First-party plugin repos outside `plugins/` were checked separately from their current manifests and handoff docs.
- Most bundled plugin entries below are still inferred/recommended declarations. Current first-party manifests now declare active capability intent for `diagnostics`, `pipeline`, `library`, `audio-mix`, `audio-input`, `audio-monitoring`, `stems`, `playback`, `audio-effects`, `jobs`, and privileged capability inventory surfaces where their repos have already migrated. - Most bundled plugin entries below are still inferred/recommended declarations. Current first-party manifests now declare active capability intent for `diagnostics`, `pipeline`, `library`, `audio-mix`, `audio-input`, `audio-monitoring`, `stems`, `playback`, `audio-effects`, `jobs`, and privileged capability inventory surfaces where their repos have already migrated.
- Manifest fields such as `nav`, `screen`, `settings`, `routes`, and `type: "visualization"` were treated as high-confidence evidence. - Manifest fields such as `nav`, `screen`, `settings`, `routes`, and `type: "visualization"` were treated as high-confidence evidence.
- Code patterns such as `window.slopsmithViz_*`, `window.playSong` wrappers, `window.showScreen` wrappers, `window.registerShortcut`, `window.slopsmithTour.register`, `window.slopsmith.audio.registerFader`, `highway.setNoteStateProvider`, and route/WebSocket handlers were treated as behavior evidence. - Code patterns such as `window.feedBackViz_*`, `window.playSong` wrappers, `window.showScreen` wrappers, `window.registerShortcut`, `window.feedBackTour.register`, `window.feedBack.audio.registerFader`, `highway.setNoteStateProvider`, and route/WebSocket handlers were treated as behavior evidence.
## Roadmap Baseline ## Roadmap Baseline
@@ -231,11 +231,11 @@ For active domains, command and operation names should follow [capability-domain
## Highway String Colors (data-plane API) ## Highway String Colors (data-plane API)
User-customizable per-string highway colors (the "Highway String Colors" setting in the 3D Highway plugin's panel) are **not** a capability domain. Consistent with `capability-domains.md` keeping highway-rendering and `visualization` surfaces off the capability graph until a dedicated render-facade slice lands, they are exposed as a synchronous **data-plane** API on `window.slopsmith.highwayColors` plus a change event. Visualization/overlay plugins (custom highways, minigames, fretboard widgets) should read colors from here so their gems/strings match the user's theme. User-customizable per-string highway colors (the "Highway String Colors" setting in the 3D Highway plugin's panel) are **not** a capability domain. Consistent with `capability-domains.md` keeping highway-rendering and `visualization` surfaces off the capability graph until a dedicated render-facade slice lands, they are exposed as a synchronous **data-plane** API on `window.feedBack.highwayColors` plus a change event. Visualization/overlay plugins (custom highways, minigames, fretboard widgets) should read colors from here so their gems/strings match the user's theme.
Colors are keyed by **named string slot**, not raw index, so a string keeps its color across arrangements (Low E stays Low E's color on a 6-string guitar, 4-string bass, or 7/8-string, where the extra low strings use the `low7`/`low8` slots). Slots: `highE`, `B`, `G`, `D`, `A`, `lowE`, `low7` (7-string Low B), `low8` (8-string Low F#). Colors are keyed by **named string slot**, not raw index, so a string keeps its color across arrangements (Low E stays Low E's color on a 6-string guitar, 4-string bass, or 7/8-string, where the extra low strings use the `low7`/`low8` slots). Slots: `highE`, `B`, `G`, `D`, `A`, `lowE`, `low7` (7-string Low B), `low8` (8-string Low F#).
`window.slopsmith.highwayColors` (`version: 1`): `window.feedBack.highwayColors` (`version: 1`):
| Member | Returns | Purpose | | Member | Returns | Purpose |
|--------|---------|---------| |--------|---------|---------|
@@ -250,7 +250,7 @@ Colors are keyed by **named string slot**, not raw index, so a string keeps its
| `encodeShare(name, map)` / `decodeShare(code)` | `string` / `{name,colors}` | The `SLOPHWY2.` copy/paste share format. | | `encodeShare(name, map)` / `decodeShare(code)` | `string` / `{name,colors}` | The `SLOPHWY2.` copy/paste share format. |
| `onChange(fn)` / `offChange(fn)` | unsubscribe fn | `fn(resolvedMap)` fires on any color change (also on song load when the slot→index mapping shifts). | | `onChange(fn)` / `offChange(fn)` | unsubscribe fn | `fn(resolvedMap)` fires on any color change (also on song load when the slot→index mapping shifts). |
The underlying change event is `window.slopsmith.emit('highway:stringColors', …)`; `onChange` wraps it and hands back the resolved map. The raw `window.highway.getStringColors()` data-plane accessor (per-index) remains available for renderers that only need the current applied array. When a `visualization` capability slice eventually lands, this facade is the natural thing to fold into it. The underlying change event is `window.feedBack.emit('highway:stringColors', …)`; `onChange` wraps it and hands back the resolved map. The raw `window.highway.getStringColors()` data-plane accessor (per-index) remains available for renderers that only need the current applied array. When a `visualization` capability slice eventually lands, this facade is the natural thing to fold into it.
## Validation Notes ## Validation Notes
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://slopsmith.local/contracts/plugin-manifest-capabilities.schema.json", "$id": "https://feedBack.local/contracts/plugin-manifest-capabilities.schema.json",
"title": "Slopsmith Plugin Manifest Capability Contract", "title": "FeedBack Plugin Manifest Capability Contract",
"type": "object", "type": "object",
"required": ["id", "name"], "required": ["id", "name"],
"properties": { "properties": {
+3 -3
View File
@@ -1,14 +1,14 @@
# Plugin styling — the `styles` capability # Plugin styling — the `styles` capability
> Building for the redesigned **v3 UI** (`SLOPSMITH_UI=v3` / `/v3`)? v3 uses `fb-*` > Building for the redesigned **v3 UI** (`FEEDBACK_UI=v3` / `/v3`)? v3 uses `fb-*`
> design tokens and a restructured player chrome with a dedicated plugin-control > design tokens and a restructured player chrome with a dedicated plugin-control
> slot. See **[plugin-v3-ui.md](plugin-v3-ui.md)** for the player-chrome contract > slot. See **[plugin-v3-ui.md](plugin-v3-ui.md)** for the player-chrome contract
> plugins must follow in v3. > plugins must follow in v3.
Slopsmith serves Tailwind as a **prebuilt** stylesheet FeedBack serves Tailwind as a **prebuilt** stylesheet
(`static/tailwind.min.css`), never the runtime Play CDN. The CDN's on-the-fly (`static/tailwind.min.css`), never the runtime Play CDN. The CDN's on-the-fly
JIT rescanned the DOM on the main thread and dropped ~26% of frames with the 3D JIT rescanned the DOM on the main thread and dropped ~26% of frames with the 3D
highway running (slopsmith-desktop#110). See **constitution Principle II**. highway running (feedBack-desktop#110). See **constitution Principle II**.
A prebuilt stylesheet only contains the classes the build scanner saw in **core A prebuilt stylesheet only contains the classes the build scanner saw in **core
source at core build time**. That has a consequence for plugins: source at core build time**. That has a consequence for plugins:
+8 -8
View File
@@ -1,12 +1,12 @@
# Building plugins for the v3 UI (fee[dB]ack v0.3.0) # Building plugins for the v3 UI (fee[dB]ack v0.3.0)
v0.3.0 ("fee[dB]ack") ships a redesigned UI **behind a flag**`SLOPSMITH_UI=v3` v0.3.0 ("fee[dB]ack") ships a redesigned UI **behind a flag**`FEEDBACK_UI=v3`
or the `/v3` route. The classic UI (v2) remains the default until 0.3.0 ships, so or the `/v3` route. The classic UI (v2) remains the default until 0.3.0 ships, so
plugins must work in **both**. plugins must work in **both**.
The good news: v3 **reuses the same engine** as v2 — same `server.py`, `app.js`, The good news: v3 **reuses the same engine** as v2 — same `server.py`, `app.js`,
`highway.js`, `playSong`, `showScreen`, capability registry, library providers, `highway.js`, `playSong`, `showScreen`, capability registry, library providers,
and the `window.slopsmithViz_<id>` / `setRenderer` visualization contract. So your and the `window.feedBackViz_<id>` / `setRenderer` visualization contract. So your
plugin's **backend, capabilities, library providers, `nav`/`screen`, visualization plugin's **backend, capabilities, library providers, `nav`/`screen`, visualization
renderers, diagnostics, and settings export all work unchanged in v3.** v3 surfaces renderers, diagnostics, and settings export all work unchanged in v3.** v3 surfaces
your `nav` entry in the new sidebar (via `shell.js` `renderPluginNav`) and your your `nav` entry in the new sidebar (via `shell.js` `renderPluginNav`) and your
@@ -34,8 +34,8 @@ So the legacy way of injecting a control breaks in v3 two ways:
The host exposes: The host exposes:
- `window.slopsmith.uiVersion === 'v3'` — detect v3 (absent / not `'v3'` in v2). - `window.feedBack.uiVersion === 'v3'` — detect v3 (absent / not `'v3'` in v2).
- `window.slopsmith.ui.playerControlSlot()` — returns a **stable, always-reachable - `window.feedBack.ui.playerControlSlot()` — returns a **stable, always-reachable
container** (the "Plugins" rail popover). In v3, append your control(s) here container** (the "Plugins" rail popover). In v3, append your control(s) here
instead of `#player-controls`. instead of `#player-controls`.
@@ -43,9 +43,9 @@ Canonical pattern for any control you inject into the player:
```js ```js
function playerSlot() { function playerSlot() {
return (window.slopsmith && window.slopsmith.uiVersion === 'v3' return (window.feedBack && window.feedBack.uiVersion === 'v3'
&& window.slopsmith.ui && typeof window.slopsmith.ui.playerControlSlot === 'function') && window.feedBack.ui && typeof window.feedBack.ui.playerControlSlot === 'function')
? window.slopsmith.ui.playerControlSlot() : null; ? window.feedBack.ui.playerControlSlot() : null;
} }
function injectMyButton() { function injectMyButton() {
@@ -183,7 +183,7 @@ out of the capability graph.
- [ ] Backend / capabilities / library provider / `nav` + `screen` / - [ ] Backend / capabilities / library provider / `nav` + `screen` /
visualization renderer — **no change needed** (they work in v3 as-is). visualization renderer — **no change needed** (they work in v3 as-is).
- [ ] If you inject a control into the player: detect v3 and mount into - [ ] If you inject a control into the player: detect v3 and mount into
`window.slopsmith.ui.playerControlSlot()`; drop the dead separator / `window.feedBack.ui.playerControlSlot()`; drop the dead separator /
`button:last-child` anchor; guard `contains()` against the actual container. `button:last-child` anchor; guard `contains()` against the actual container.
- [ ] Dropdowns positioned via `getBoundingClientRect()`, not `#player-controls`. - [ ] Dropdowns positioned via `getBoundingClientRect()`, not `#player-controls`.
- [ ] `#player` overlays keep `z-index` ≤ the chrome layers (transport/HUD 20, - [ ] `#player` overlays keep `z-index` ≤ the chrome layers (transport/HUD 20,
+3 -3
View File
@@ -1,12 +1,12 @@
# Debugging Keyboard Shortcuts # Debugging Keyboard Shortcuts
This skill helps you debug keyboard shortcut issues in Slopsmith. This skill helps you debug keyboard shortcut issues in FeedBack.
## Quick Start ## Quick Start
1. **Start Slopsmith:** 1. **Start FeedBack:**
```bash ```bash
cd ~/path/to/slopsmith cd ~/path/to/feedBack
LIBRARY_PATH=/path/to/your/library docker compose up -d LIBRARY_PATH=/path/to/your/library docker compose up -d
``` ```
+17 -17
View File
@@ -4,7 +4,7 @@ A `.sloppak` is just a zip of plain files: some YAML, some JSON, some OGG audio,
This guide walks through the most common edits, aimed at musicians who are comfortable with a text editor and Audacity but don't live on the command line. This guide walks through the most common edits, aimed at musicians who are comfortable with a text editor and Audacity but don't live on the command line.
> For the format **schema** (what every field means, how the wire format works, how to extend the format with new data types), see the authoritative [feedpak spec](https://github.com/got-feedback/feedback-feedpak-spec/blob/main/spec/feedpak-v1.md) (the local [sloppak-spec.md](sloppak-spec.md) is now a pointer to it). This document is the **how-do-I-actually-edit-mine** companion. > For the format **schema** (what every field means, how the wire format works, how to extend the format with new data types), see the authoritative [feedpak spec](https://github.com/got-feedback/feedpak-spec/blob/main/spec/feedpak-v1.md) (the local [sloppak-spec.md](sloppak-spec.md) is now a pointer to it). This document is the **how-do-I-actually-edit-mine** companion.
--- ---
@@ -17,27 +17,27 @@ A sloppak exists in two interchangeable forms:
| **Directory** | A folder named `something.sloppak/` with the files loose inside | **Authoring** — easy to edit, no zip/unzip cycle | | **Directory** | A folder named `something.sloppak/` with the files loose inside | **Authoring** — easy to edit, no zip/unzip cycle |
| **Zip** | A `something.sloppak` file (zip with the same files inside) | **Distributing** — single file to share | | **Zip** | A `something.sloppak` file (zip with the same files inside) | **Distributing** — single file to share |
Slopsmith reads both. You can drop either one straight into your DLC folder and it'll show up in the library. FeedBack reads both. You can drop either one straight into your DLC folder and it'll show up in the library.
### Unzipping for editing ### Unzipping for editing
Slopsmith's converter ships sloppaks in zip form. To edit one, unzip it: FeedBack's converter ships sloppaks in zip form. To edit one, unzip it:
- **Windows:** rename `mysong.sloppak``mysong.zip`, right-click → Extract All. Then rename the resulting folder back to `mysong.sloppak/` (with the trailing slash / folder form). Or use [7-Zip](https://www.7-zip.org/) and unzip without renaming. - **Windows:** rename `mysong.sloppak``mysong.zip`, right-click → Extract All. Then rename the resulting folder back to `mysong.sloppak/` (with the trailing slash / folder form). Or use [7-Zip](https://www.7-zip.org/) and unzip without renaming.
- **macOS:** rename `.sloppak``.zip`, double-click. Or use The Unarchiver. - **macOS:** rename `.sloppak``.zip`, double-click. Or use The Unarchiver.
- **Linux:** `unzip mysong.sloppak -d mysong.sloppak/`. - **Linux:** `unzip mysong.sloppak -d mysong.sloppak/`.
Once you have the directory form, you can edit any file inside and Slopsmith will pick it up — no re-zipping required for your own use. Once you have the directory form, you can edit any file inside and FeedBack will pick it up — no re-zipping required for your own use.
### Cache: when changes don't appear ### Cache: when changes don't appear
The first time Slopsmith opens a zip-form sloppak, it extracts a working copy into its config directory's cache: `${CONFIG_DIR}/sloppak_cache/<safe-id>` (in the standard Docker setup that's inside the `slopsmith-config` volume, mounted at `/config` in the container). The `<safe-id>` is the sloppak filename with each path separator (`/` or `\`) replaced by `__` and each space replaced by `_`. So `My-Song.sloppak` stays `My-Song.sloppak`, and `Artist/My Song.sloppak` becomes `Artist__My_Song.sloppak`. The first time FeedBack opens a zip-form sloppak, it extracts a working copy into its config directory's cache: `${CONFIG_DIR}/sloppak_cache/<safe-id>` (in the standard Docker setup that's inside the `feedBack-config` volume, mounted at `/config` in the container). The `<safe-id>` is the sloppak filename with each path separator (`/` or `\`) replaced by `__` and each space replaced by `_`. So `My-Song.sloppak` stays `My-Song.sloppak`, and `Artist/My Song.sloppak` becomes `Artist__My_Song.sloppak`.
You almost never need to touch this cache directly. If you edit the **original zip** in your DLC folder, Slopsmith re-extracts automatically when the zip's modification time or size changes — just save your edits and reload. You almost never need to touch this cache directly. If you edit the **original zip** in your DLC folder, FeedBack re-extracts automatically when the zip's modification time or size changes — just save your edits and reload.
If a change still isn't appearing, the simplest reset is to remove the matching cache folder so Slopsmith rebuilds it on the next song load. In a default Docker install that's `docker exec <container> rm -rf /config/sloppak_cache/<safe-id>` (or the equivalent for your setup). If a change still isn't appearing, the simplest reset is to remove the matching cache folder so FeedBack rebuilds it on the next song load. In a default Docker install that's `docker exec <container> rm -rf /config/sloppak_cache/<safe-id>` (or the equivalent for your setup).
If you'd rather skip the cache layer entirely, **drop the directory form straight into your DLC folder**Slopsmith uses it in place and there's nothing to invalidate. If you'd rather skip the cache layer entirely, **drop the directory form straight into your DLC folder**FeedBack uses it in place and there's nothing to invalidate.
--- ---
@@ -72,8 +72,8 @@ The use case: the converted rhythm guitar sounds muddy (Demucs has a tough time
1. Copy `rhythm_custom.ogg` into the sloppak's `stems/` folder. 1. Copy `rhythm_custom.ogg` into the sloppak's `stems/` folder.
2. Open `manifest.yaml` in any text editor (Notepad++, VS Code, BBEdit, gedit — all fine; just **don't use Word**). 2. Open `manifest.yaml` in any text editor (Notepad++, VS Code, BBEdit, gedit — all fine; just **don't use Word**).
3. Find the `stems:` block. Two things matter here: 3. Find the `stems:` block. Two things matter here:
- **Order:** Slopsmith's base `<audio>` element always plays the **first** stem listed in `stems[]`, regardless of `default:` flags. So if you want your custom stem to be what the player plays out-of-the-box (and what users without the Stems plugin will hear), put it **first**. - **Order:** FeedBack's base `<audio>` element always plays the **first** stem listed in `stems[]`, regardless of `default:` flags. So if you want your custom stem to be what the player plays out-of-the-box (and what users without the Stems plugin will hear), put it **first**.
- **`default:` flags:** consulted by the [Stems plugin](https://github.com/topkoa/slopsmith-plugin-stems) to decide which faders start un-muted. They do **not** affect what the base `<audio>` element plays — that's purely the first-stem rule above. - **`default:` flags:** consulted by the [Stems plugin](https://github.com/topkoa/feedBack-plugin-stems) to decide which faders start un-muted. They do **not** affect what the base `<audio>` element plays — that's purely the first-stem rule above.
Example for a Demucs-split sloppak where you re-recorded the rhythm guitar: Example for a Demucs-split sloppak where you re-recorded the rhythm guitar:
@@ -110,14 +110,14 @@ The use case: the converted rhythm guitar sounds muddy (Demucs has a tough time
### Step 5 — Reload and verify ### Step 5 — Reload and verify
Reload the song in Slopsmith. The [Stems plugin](https://github.com/topkoa/slopsmith-plugin-stems) will show a fader for `rhythm_custom` next to the others. If you don't see it, check the cache notes in §1. Reload the song in FeedBack. The [Stems plugin](https://github.com/topkoa/feedBack-plugin-stems) will show a fader for `rhythm_custom` next to the others. If you don't see it, check the cache notes in §1.
### Common gotchas ### Common gotchas
- **Sample-rate mismatch** → choppy/pitched-wrong playback. Re-export from Audacity at exactly the rate the other stems use. - **Sample-rate mismatch** → choppy/pitched-wrong playback. Re-export from Audacity at exactly the rate the other stems use.
- **Mono vs stereo mismatch** is fine for playback but levels can feel different — match what the other stems use if you want consistent behavior in the mixer. - **Mono vs stereo mismatch** is fine for playback but levels can feel different — match what the other stems use if you want consistent behavior in the mixer.
- **Silence padding at the start** of your recording → your stem will play late. Trim it tight in Audacity before exporting. - **Silence padding at the start** of your recording → your stem will play late. Trim it tight in Audacity before exporting.
- **Tabs in `manifest.yaml`**Slopsmith will refuse to load the song. Use two spaces. - **Tabs in `manifest.yaml`**FeedBack will refuse to load the song. Use two spaces.
--- ---
@@ -242,7 +242,7 @@ For 4-string bass, only indices 03 are meaningful; leave 4 and 5 at `0`.
### What *not* to put in `manifest.yaml` ### What *not* to put in `manifest.yaml`
Don't add per-machine settings (audio device picks, MIDI port IDs), UI state, or your own play counts. The sloppak holds the song's authored data — anything that varies by user or machine lives in Slopsmith's config dir or the metadata DB. See [feedpak spec §9.5](https://github.com/got-feedback/feedback-feedpak-spec/blob/main/spec/feedpak-v1.md#95-what-does-not-belong-in-a-feedpak) for the full list. Don't add per-machine settings (audio device picks, MIDI port IDs), UI state, or your own play counts. The sloppak holds the song's authored data — anything that varies by user or machine lives in FeedBack's config dir or the metadata DB. See [feedpak spec §9.5](https://github.com/got-feedback/feedpak-spec/blob/main/spec/feedpak-v1.md#95-what-does-not-belong-in-a-feedpak) for the full list.
--- ---
@@ -252,15 +252,15 @@ If you want to share your modified sloppak with someone else, re-zip it:
1. Open the `mysong.sloppak/` directory. 1. Open the `mysong.sloppak/` directory.
2. Select **everything inside**`manifest.yaml`, `arrangements/`, `stems/`, `lyrics.json`, `cover.jpg`. 2. Select **everything inside**`manifest.yaml`, `arrangements/`, `stems/`, `lyrics.json`, `cover.jpg`.
3. Zip the **contents**, not the parent folder. (If you zip the folder, the zip will have a top-level `mysong.sloppak/` directory inside, which Slopsmith won't parse — the manifest must be at the zip root.) 3. Zip the **contents**, not the parent folder. (If you zip the folder, the zip will have a top-level `mysong.sloppak/` directory inside, which FeedBack won't parse — the manifest must be at the zip root.)
4. Rename `mysong.zip``mysong.sloppak`. 4. Rename `mysong.zip``mysong.sloppak`.
For your own use, you can skip this entirely — Slopsmith reads the directory form straight from your DLC folder. For your own use, you can skip this entirely — FeedBack reads the directory form straight from your DLC folder.
--- ---
## Out of scope (for now) ## Out of scope (for now)
- **Authoring a sloppak from scratch** (no Guitar Pro / MusicXML source file) — that's a developer task. Start at [feedpak spec §8 (Reading and writing)](https://github.com/got-feedback/feedback-feedpak-spec/blob/main/spec/feedpak-v1.md#8-reading-and-writing). - **Authoring a sloppak from scratch** (no Guitar Pro / MusicXML source file) — that's a developer task. Start at [feedpak spec §8 (Reading and writing)](https://github.com/got-feedback/feedpak-spec/blob/main/spec/feedpak-v1.md#8-reading-and-writing).
- **Editing notes / chords in `arrangements/*.json`** — technically possible but extremely tedious by hand: hundreds of objects with short field names per song. The fields are documented in [feedpak spec §6 (Arrangement JSON)](https://github.com/got-feedback/feedback-feedpak-spec/blob/main/spec/feedpak-v1.md#6-arrangement-json), but for any real chart edit you want the [Arrangement Editor plugin](https://github.com/got-feedback/feedback-plugin-editor). - **Editing notes / chords in `arrangements/*.json`** — technically possible but extremely tedious by hand: hundreds of objects with short field names per song. The fields are documented in [feedpak spec §6 (Arrangement JSON)](https://github.com/got-feedback/feedpak-spec/blob/main/spec/feedpak-v1.md#6-arrangement-json), but for any real chart edit you want the [Arrangement Editor plugin](https://github.com/got-feedback/feedBack-plugin-editor).
- **Loudness normalization / advanced stem processing** — out of scope here; standard Audacity or ffmpeg workflows apply to any OGG file before you drop it into `stems/`. - **Loudness normalization / advanced stem processing** — out of scope here; standard Audacity or ffmpeg workflows apply to any OGG file before you drop it into `stems/`.
+4 -4
View File
@@ -3,8 +3,8 @@
The full format specification that used to live here has moved to its own repository and is now The full format specification that used to live here has moved to its own repository and is now
the **authoritative, versioned reference**: the **authoritative, versioned reference**:
> **📖 https://github.com/got-feedback/feedback-feedpak-spec** > **📖 https://github.com/got-feedback/feedpak-spec**
> — normative spec ([`spec/feedpak-v1.md`](https://github.com/got-feedback/feedback-feedpak-spec/blob/main/spec/feedpak-v1.md)), > — normative spec ([`spec/feedpak-v1.md`](https://github.com/got-feedback/feedpak-spec/blob/main/spec/feedpak-v1.md)),
> JSON Schemas, examples, and a reference validator. > JSON Schemas, examples, and a reference validator.
Update bookmarks to point there. This page is a thin pointer kept at the original path so existing Update bookmarks to point there. This page is a thin pointer kept at the original path so existing
@@ -14,7 +14,7 @@ links keep resolving.
The published format is named **feedpak** (extension `.feedpak`, manifest key `feedpak_version`). The published format is named **feedpak** (extension `.feedpak`, manifest key `feedpak_version`).
This codebase still uses the legacy **sloppak** name internally — `lib/sloppak.py`, the This codebase still uses the legacy **sloppak** name internally — `lib/sloppak.py`, the
`.sloppak` extension, `SLOPSMITH_*` env vars, etc. **They describe the same on-disk format.** The `.sloppak` extension, `FEEDBACK_*` env vars, etc. **They describe the same on-disk format.** The
rename is repo/public-facing only for now (see the top-level workspace `CLAUDE.md`), so when the rename is repo/public-facing only for now (see the top-level workspace `CLAUDE.md`), so when the
spec says `feedpak` / `feedpak_version`, the packs this server reads and writes today are the same spec says `feedpak` / `feedpak_version`, the packs this server reads and writes today are the same
structure under the `.sloppak` name. The internal rename is a separate, later effort. structure under the `.sloppak` name. The internal rename is a separate, later effort.
@@ -45,5 +45,5 @@ concepts to the code that reads and writes them. It is **not** part of the forma
> **Note on older section references.** Some inline code comments in this repo cite section > **Note on older section references.** Some inline code comments in this repo cite section
> numbers from the previous version of this document (e.g. "sloppak-spec §5.3"). The external spec > numbers from the previous version of this document (e.g. "sloppak-spec §5.3"). The external spec
> renumbered its sections, so those citations are approximate — find the topic by name in the > renumbered its sections, so those citations are approximate — find the topic by name in the
> [feedpak spec](https://github.com/got-feedback/feedback-feedpak-spec/blob/main/spec/feedpak-v1.md) > [feedpak spec](https://github.com/got-feedback/feedpak-spec/blob/main/spec/feedpak-v1.md)
> rather than by the old number. > rather than by the old number.
+3 -3
View File
@@ -7,7 +7,7 @@ import shutil
import subprocess import subprocess
from pathlib import Path from pathlib import Path
log = logging.getLogger("slopsmith.lib.audio") log = logging.getLogger("feedBack.lib.audio")
# Maximum length of any single decoder-error fragment that we surface to # Maximum length of any single decoder-error fragment that we surface to
# the client. ffmpeg can emit multi-kB build-configuration / version # the client. ffmpeg can emit multi-kB build-configuration / version
@@ -123,7 +123,7 @@ def _scrub_quoted_match(match: re.Match) -> str:
def _bundled_bin_dir() -> Path | None: def _bundled_bin_dir() -> Path | None:
"""Resolve the desktop bundle's resources/bin/ directory if we're """Resolve the desktop bundle's resources/bin/ directory if we're
running inside one. Layout: resources/slopsmith/lib/audio.py running inside one. Layout: resources/feedBack/lib/audio.py
resources/bin/. Gate on vgmstream-cli's presence so we don't resources/bin/. Gate on vgmstream-cli's presence so we don't
misidentify random parent dirs (e.g. Docker's `/bin`, dev misidentify random parent dirs (e.g. Docker's `/bin`, dev
layouts where parents[2] resolves to the repo root) vgmstream-cli layouts where parents[2] resolves to the repo root) vgmstream-cli
@@ -284,7 +284,7 @@ def _scrub_paths(text: str, *paths: str) -> str:
"""Replace absolute filesystem paths in `text` with their basenames. """Replace absolute filesystem paths in `text` with their basenames.
Decoder error strings get joined into the RuntimeError that Decoder error strings get joined into the RuntimeError that
`convert_wem` raises, and slopsmith surfaces that text in the `convert_wem` raises, and feedBack surfaces that text in the
browser as `audio_error`. Leaking install / user / DLC paths to the browser as `audio_error`. Leaking install / user / DLC paths to the
client is a needless info disclosure, so before any decoder error client is a needless info disclosure, so before any decoder error
leaves this module we strip absolute paths down to their final leaves this module we strip absolute paths down to their final
+24 -24
View File
@@ -130,7 +130,7 @@ ENV_ALLOWLIST = (
"LOG_LEVEL", "LOG_LEVEL",
"LOG_FORMAT", "LOG_FORMAT",
"LOG_FILE", "LOG_FILE",
"SLOPSMITH_RUNTIME", "FEEDBACK_RUNTIME",
"PORT", "PORT",
"HOST", "HOST",
"TZ", "TZ",
@@ -154,13 +154,13 @@ def _safe_json_dumps(obj) -> str:
return json.dumps({"error": "unserializable payload"}, indent=2) return json.dumps({"error": "unserializable payload"}, indent=2)
def _system_version(slopsmith_version: str, redactor=None) -> dict: def _system_version(feedBack_version: str, redactor=None) -> dict:
executable = sys.executable executable = sys.executable
if redactor is not None: if redactor is not None:
executable = redactor.redact_text(executable) executable = redactor.redact_text(executable)
return { return {
"schema": "system.version.v1", "schema": "system.version.v1",
"slopsmith_version": slopsmith_version, "feedBack_version": feedBack_version,
"python": { "python": {
"version": platform.python_version(), "version": platform.python_version(),
"implementation": platform.python_implementation(), "implementation": platform.python_implementation(),
@@ -233,7 +233,7 @@ def _summarize_payload(path: str, parsed) -> dict | None:
py = parsed.get("python") or {} py = parsed.get("python") or {}
os_ = parsed.get("os") or {} os_ = parsed.get("os") or {}
return { return {
"slopsmith": parsed.get("slopsmith_version"), "feedBack": parsed.get("feedBack_version"),
"python": py.get("version"), "python": py.get("version"),
"os": os_.get("system"), "os": os_.get("system"),
} }
@@ -338,7 +338,7 @@ def _git_info(plugin_dir: Path) -> dict | None:
"""Return git short SHA + remote URL for a plugin checkout. """Return git short SHA + remote URL for a plugin checkout.
Pure-Python reads `.git/HEAD` and `.git/config` directly so this Pure-Python reads `.git/HEAD` and `.git/config` directly so this
works in containers without the `git` binary installed (slopsmith's works in containers without the `git` binary installed (feedBack's
runtime image is minimal). Plugins are gitlinks (see CLAUDE.md); runtime image is minimal). Plugins are gitlinks (see CLAUDE.md);
the SHA is the most reliable "what build is this" identifier. the SHA is the most reliable "what build is this" identifier.
@@ -393,7 +393,7 @@ def _system_plugins(loaded_plugins: list[dict], plugins_root: "Path | list[Path]
show up in the bundle. show up in the bundle.
*plugins_root* accepts a single Path, a list of Paths (to cover both *plugins_root* accepts a single Path, a list of Paths (to cover both
the built-in ``plugins/`` directory and ``SLOPSMITH_PLUGINS_DIR``), or the built-in ``plugins/`` directory and ``FEEDBACK_PLUGINS_DIR``), or
None to skip orphan detection entirely. None to skip orphan detection entirely.
Plugin directories not in ``LOADED_PLUGINS`` appear in ``orphans``. Plugin directories not in ``LOADED_PLUGINS`` appear in ``orphans``.
@@ -484,7 +484,7 @@ def _system_plugins(loaded_plugins: list[dict], plugins_root: "Path | list[Path]
# plugin failed to load — common when requirements.txt installs # plugin failed to load — common when requirements.txt installs
# fail in a read-only container). Accepts a single Path, a list of # fail in a read-only container). Accepts a single Path, a list of
# Paths (to cover both the built-in plugins/ dir and # Paths (to cover both the built-in plugins/ dir and
# SLOPSMITH_PLUGINS_DIR), or None. # FEEDBACK_PLUGINS_DIR), or None.
orphans: list[dict] = [] orphans: list[dict] = []
if plugins_root is not None: if plugins_root is not None:
roots: list[Path] = plugins_root if isinstance(plugins_root, list) else [plugins_root] roots: list[Path] = plugins_root if isinstance(plugins_root, list) else [plugins_root]
@@ -840,11 +840,11 @@ def _redact_value(value: object, redactor: "Redactor") -> object:
README_TEMPLATE = """\ README_TEMPLATE = """\
Slopsmith Diagnostics Bundle FeedBack Diagnostics Bundle
============================ ============================
Generated: {exported_at} Generated: {exported_at}
Slopsmith: {slopsmith_version} FeedBack: {feedBack_version}
Runtime: {runtime_kind} Runtime: {runtime_kind}
Redacted: {redacted} Redacted: {redacted}
@@ -1005,7 +1005,7 @@ def _build_files_meta(files: dict[str, bytes]) -> list[dict]:
def _assemble_files_and_notes( def _assemble_files_and_notes(
*, *,
slopsmith_version: str, feedBack_version: str,
config_dir: Path, config_dir: Path,
dlc_dir: Path | None, dlc_dir: Path | None,
log_file: Path | None, log_file: Path | None,
@@ -1038,7 +1038,7 @@ def _assemble_files_and_notes(
if include.get("system", True): if include.get("system", True):
# Pass the redactor so python.executable is redacted when paths # Pass the redactor so python.executable is redacted when paths
# should be hidden (it often lives under $HOME or a per-user venv). # should be hidden (it often lives under $HOME or a per-user venv).
ver_payload = _safe_json_dumps(_system_version(slopsmith_version, redactor=redactor)).encode("utf-8") ver_payload = _safe_json_dumps(_system_version(feedBack_version, redactor=redactor)).encode("utf-8")
files["system/version.json"] = ver_payload files["system/version.json"] = ver_payload
env_payload = _safe_json_dumps(_system_env(redactor=redactor)).encode("utf-8") env_payload = _safe_json_dumps(_system_env(redactor=redactor)).encode("utf-8")
files["system/env.json"] = env_payload files["system/env.json"] = env_payload
@@ -1125,7 +1125,7 @@ def _assemble_files_and_notes(
files.update(plugin_files) files.update(plugin_files)
# Per-plugin client-side contributions from # Per-plugin client-side contributions from
# window.slopsmith.diagnostics.contribute(plugin_id, payload). # window.feedBack.diagnostics.contribute(plugin_id, payload).
# Gated on the same "plugins" toggle as backend plugin diagnostics. # Gated on the same "plugins" toggle as backend plugin diagnostics.
if include.get("plugins", True) and client_contributions and isinstance(client_contributions, dict): if include.get("plugins", True) and client_contributions and isinstance(client_contributions, dict):
# Build the set of actually-loaded plugin IDs so we only accept # Build the set of actually-loaded plugin IDs so we only accept
@@ -1160,7 +1160,7 @@ def _assemble_files_and_notes(
def _make_manifest( def _make_manifest(
*, *,
slopsmith_version: str, feedBack_version: str,
runtime_kind: str, runtime_kind: str,
redact: bool, redact: bool,
files: dict[str, bytes], files: dict[str, bytes],
@@ -1170,7 +1170,7 @@ def _make_manifest(
return { return {
"schema": BUNDLE_SCHEMA, "schema": BUNDLE_SCHEMA,
"exported_at": _now_iso(), "exported_at": _now_iso(),
"slopsmith_version": slopsmith_version, "feedBack_version": feedBack_version,
"runtime": runtime_kind, "runtime": runtime_kind,
"redacted": redact, "redacted": redact,
"files": _build_files_meta(files), "files": _build_files_meta(files),
@@ -1181,7 +1181,7 @@ def _make_manifest(
def build_bundle( def build_bundle(
*, *,
slopsmith_version: str, feedBack_version: str,
config_dir: Path, config_dir: Path,
dlc_dir: Path | None, dlc_dir: Path | None,
log_file: Path | None, log_file: Path | None,
@@ -1198,7 +1198,7 @@ def build_bundle(
) -> tuple[bytes, str, dict]: ) -> tuple[bytes, str, dict]:
"""Returns (zip_bytes, filename, manifest_dict).""" """Returns (zip_bytes, filename, manifest_dict)."""
files, notes, runtime_kind, redactor = _assemble_files_and_notes( files, notes, runtime_kind, redactor = _assemble_files_and_notes(
slopsmith_version=slopsmith_version, feedBack_version=feedBack_version,
config_dir=config_dir, config_dir=config_dir,
dlc_dir=dlc_dir, dlc_dir=dlc_dir,
log_file=log_file, log_file=log_file,
@@ -1215,7 +1215,7 @@ def build_bundle(
) )
manifest = _make_manifest( manifest = _make_manifest(
slopsmith_version=slopsmith_version, feedBack_version=feedBack_version,
runtime_kind=runtime_kind, runtime_kind=runtime_kind,
redact=redact, redact=redact,
files=files, files=files,
@@ -1225,7 +1225,7 @@ def build_bundle(
readme = README_TEMPLATE.format( readme = README_TEMPLATE.format(
exported_at=manifest["exported_at"], exported_at=manifest["exported_at"],
slopsmith_version=slopsmith_version, feedBack_version=feedBack_version,
runtime_kind=runtime_kind, runtime_kind=runtime_kind,
redacted=redact, redacted=redact,
) )
@@ -1259,13 +1259,13 @@ def build_bundle(
for path, payload in sorted(files.items()): for path, payload in sorted(files.items()):
zf.writestr(path, payload) zf.writestr(path, payload)
filename = f"slopsmith-diag-{slopsmith_version}-{_now_filename_slug()}.zip" filename = f"feedBack-diag-{feedBack_version}-{_now_filename_slug()}.zip"
return buf.getvalue(), filename, manifest return buf.getvalue(), filename, manifest
def preview_bundle( def preview_bundle(
*, *,
slopsmith_version: str, feedBack_version: str,
config_dir: Path, config_dir: Path,
dlc_dir: Path | None, dlc_dir: Path | None,
log_file: Path | None, log_file: Path | None,
@@ -1303,7 +1303,7 @@ def preview_bundle(
for p in loaded_plugins for p in loaded_plugins
] ]
files, notes, runtime_kind, redactor = _assemble_files_and_notes( files, notes, runtime_kind, redactor = _assemble_files_and_notes(
slopsmith_version=slopsmith_version, feedBack_version=feedBack_version,
config_dir=config_dir, config_dir=config_dir,
dlc_dir=dlc_dir, dlc_dir=dlc_dir,
log_file=log_file, log_file=log_file,
@@ -1336,7 +1336,7 @@ def preview_bundle(
if key not in files: if key not in files:
files[key] = _CALLABLE_PREVIEW_PLACEHOLDER files[key] = _CALLABLE_PREVIEW_PLACEHOLDER
# Frontend plugins (those with a screen or script) may call # Frontend plugins (those with a screen or script) may call
# window.slopsmith.diagnostics.contribute() and produce a # window.feedBack.diagnostics.contribute() and produce a
# plugins/<id>/client.json in the real export. Advertise a # plugins/<id>/client.json in the real export. Advertise a
# placeholder so the preview file tree is accurate. # placeholder so the preview file tree is accurate.
if p.get("has_screen") or p.get("has_script"): if p.get("has_screen") or p.get("has_script"):
@@ -1377,14 +1377,14 @@ def preview_bundle(
}).encode("utf-8") }).encode("utf-8")
manifest = _make_manifest( manifest = _make_manifest(
slopsmith_version=slopsmith_version, feedBack_version=feedBack_version,
runtime_kind=runtime_kind, runtime_kind=runtime_kind,
redact=redact, redact=redact,
files=files, files=files,
notes=notes, notes=notes,
redactor=redactor, redactor=redactor,
) )
filename = f"slopsmith-diag-{slopsmith_version}-{_now_filename_slug()}.zip" filename = f"feedBack-diag-{feedBack_version}-{_now_filename_slug()}.zip"
return { return {
"filename": filename, "filename": filename,
"manifest": manifest, "manifest": manifest,
+4 -2
View File
@@ -16,6 +16,8 @@ import platform
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from env_compat import getenv_compat
SCHEMA = "system.hardware.v1" SCHEMA = "system.hardware.v1"
@@ -41,7 +43,7 @@ def detect_runtime() -> dict:
nvidia-smi / psutil CPU probes. nvidia-smi / psutil CPU probes.
""" """
out: dict = {"kind": "bare", "in_docker": False, "in_kubernetes": False} out: dict = {"kind": "bare", "in_docker": False, "in_kubernetes": False}
env_runtime = os.environ.get("SLOPSMITH_RUNTIME", "").strip().lower() env_runtime = (getenv_compat("FEEDBACK_RUNTIME", "") or "").strip().lower()
if env_runtime in ("electron", "docker", "bare"): if env_runtime in ("electron", "docker", "bare"):
out["kind"] = env_runtime out["kind"] = env_runtime
if Path("/.dockerenv").exists(): if Path("/.dockerenv").exists():
@@ -65,7 +67,7 @@ def detect_runtime() -> dict:
import psutil # type: ignore import psutil # type: ignore
parent = psutil.Process(os.getppid()).name().lower() parent = psutil.Process(os.getppid()).name().lower()
if "electron" in parent or "slopsmith" in parent: if "electron" in parent or "feedBack" in parent:
out["kind"] = "electron" out["kind"] = "electron"
except Exception: except Exception:
pass pass
+1 -1
View File
@@ -8,7 +8,7 @@ different salts so tokens cannot be cross-correlated between exports.
Stable token grammar (see docs/diagnostics-bundle-spec.md): Stable token grammar (see docs/diagnostics-bundle-spec.md):
<DLC_DIR> DLC root path <DLC_DIR> DLC root path
<HOME> user's home directory <HOME> user's home directory
<CONFIG_DIR> slopsmith config dir <CONFIG_DIR> feedBack config dir
<song:hash8> song filename / basename (8 hex chars) <song:hash8> song filename / basename (8 hex chars)
<ip:hash6> IPv4 / IPv6 address (6 hex chars) <ip:hash6> IPv4 / IPv6 address (6 hex chars)
<redacted> bearer tokens, key=/token= query strings <redacted> bearer tokens, key=/token= query strings
+1 -1
View File
@@ -21,7 +21,7 @@ from __future__ import annotations
import logging import logging
import math import math
log = logging.getLogger("slopsmith.lib.drums") log = logging.getLogger("feedBack.lib.drums")
# ── Piece vocabulary ────────────────────────────────────────────────────────── # ── Piece vocabulary ──────────────────────────────────────────────────────────
+38
View File
@@ -0,0 +1,38 @@
"""Backward-compatible environment lookup for the slopsmith -> feedBack rename.
Canonical configuration variables are now ``FEEDBACK_*``. Deployments that
predate the rename may still set the old ``SLOPSMITH_*`` names (docker-compose
overrides, shell profiles, CI), so we honour those as a fallback. New code
should always read the canonical ``FEEDBACK_*`` name and let this shim resolve
the legacy alias.
Flat-importable, no import-time IO or global state (constitution P-V).
"""
import os
_CANON_PREFIX = "FEEDBACK_"
_LEGACY_PREFIX = "SLOPSMITH_"
_TRUE_VALUES = {"1", "true", "yes", "on"}
def getenv_compat(name, default=None):
"""``os.environ.get`` with a legacy ``SLOPSMITH_*`` fallback.
For a canonical ``FEEDBACK_<X>`` name, returns the value of ``FEEDBACK_<X>``
if set, else ``SLOPSMITH_<X>`` if set, else ``default``. Names that do not
start with ``FEEDBACK_`` behave exactly like ``os.environ.get``.
"""
value = os.environ.get(name)
if value is not None:
return value
if name.startswith(_CANON_PREFIX):
legacy = os.environ.get(_LEGACY_PREFIX + name[len(_CANON_PREFIX):])
if legacy is not None:
return legacy
return default
def env_flag_compat(name):
"""Parse a conventional boolean env flag, honouring the legacy alias."""
return (getenv_compat(name, "") or "").strip().lower() in _TRUE_VALUES
+12 -10
View File
@@ -8,7 +8,9 @@ import sys
import tempfile import tempfile
from pathlib import Path from pathlib import Path
log = logging.getLogger("slopsmith.lib.gp2midi") from env_compat import getenv_compat
log = logging.getLogger("feedBack.lib.gp2midi")
import guitarpro import guitarpro
from midiutil import MIDIFile from midiutil import MIDIFile
@@ -152,15 +154,15 @@ def _find_soundfont() -> str | None:
"""Locate a .sf2 soundfont for MIDI rendering. """Locate a .sf2 soundfont for MIDI rendering.
Precedence: Precedence:
1. ``SLOPSMITH_SOUNDFONT`` env var (user override / desktop-app-supplied) 1. ``FEEDBACK_SOUNDFONT`` env var (user override / desktop-app-supplied)
2. Bundled ``<RESOURCESPATH>/soundfonts/*.sf2`` (Electron desktop builds) 2. Bundled ``<RESOURCESPATH>/soundfonts/*.sf2`` (Electron desktop builds)
3. Common system locations per OS. 3. Common system locations per OS.
""" """
override = os.environ.get("SLOPSMITH_SOUNDFONT") override = getenv_compat("FEEDBACK_SOUNDFONT")
if override: if override:
if os.path.isfile(override): if os.path.isfile(override):
return override return override
log.warning("SLOPSMITH_SOUNDFONT is set to %r but that file does not exist; falling back to other sources", override) log.warning("FEEDBACK_SOUNDFONT is set to %r but that file does not exist; falling back to other sources", override)
resources = os.environ.get("RESOURCESPATH") resources = os.environ.get("RESOURCESPATH")
if resources: if resources:
@@ -187,10 +189,10 @@ def _find_soundfont() -> str | None:
elif sys.platform == "win32": elif sys.platform == "win32":
appdata = os.environ.get("APPDATA") appdata = os.environ.get("APPDATA")
if appdata: if appdata:
# "Slopsmith" matches slopsmith-desktop's Electron productName # "FeedBack" matches feedBack-desktop's Electron productName
# (app.getPath('userData') resolves to %APPDATA%\Slopsmith on Windows). # (app.getPath('userData') resolves to %APPDATA%\FeedBack on Windows).
for pattern in ( for pattern in (
os.path.join(appdata, "Slopsmith", "soundfonts", "*.sf2"), os.path.join(appdata, "FeedBack", "soundfonts", "*.sf2"),
os.path.join(appdata, "SoundFonts", "*.sf2"), os.path.join(appdata, "SoundFonts", "*.sf2"),
): ):
candidates += sorted(glob.glob(pattern)) candidates += sorted(glob.glob(pattern))
@@ -218,16 +220,16 @@ def _soundfont_install_hint() -> str:
"or FluidR3_GM from musical-artifacts.com) and either place the .sf2 " "or FluidR3_GM from musical-artifacts.com) and either place the .sf2 "
"file in /usr/local/share/sounds/sf2/ (Intel) or " "file in /usr/local/share/sounds/sf2/ (Intel) or "
"/opt/homebrew/share/sounds/sf2/ (Apple Silicon), or set the " "/opt/homebrew/share/sounds/sf2/ (Apple Silicon), or set the "
"SLOPSMITH_SOUNDFONT environment variable to its full path." "FEEDBACK_SOUNDFONT environment variable to its full path."
) )
if sys.platform == "win32": if sys.platform == "win32":
return ( return (
"Download a soundfont (e.g. GeneralUser GS from schristiancollins.com or " "Download a soundfont (e.g. GeneralUser GS from schristiancollins.com or "
"FluidR3_GM from musical-artifacts.com) and either place the .sf2 file in " "FluidR3_GM from musical-artifacts.com) and either place the .sf2 file in "
"%APPDATA%\\Slopsmith\\soundfonts\\ or set the SLOPSMITH_SOUNDFONT " "%APPDATA%\\FeedBack\\soundfonts\\ or set the FEEDBACK_SOUNDFONT "
"environment variable to its full path." "environment variable to its full path."
) )
return "Set SLOPSMITH_SOUNDFONT to the full path of a .sf2 file." return "Set FEEDBACK_SOUNDFONT to the full path of a .sf2 file."
def _fluidsynth_install_hint() -> str: def _fluidsynth_install_hint() -> str:
+3 -3
View File
@@ -19,8 +19,8 @@ bar-indexed tempo map, per-beat rhythm durations (dots + tuplets; see
``_beat_secs`` for the one deliberate double-dot divergence), and ``_beat_secs`` for the one deliberate double-dot divergence), and
``_note_midi`` so the ``_note_midi`` so the
notation beats line up with the RS-XML notes the highway plays (see notation beats line up with the RS-XML notes the highway plays (see
slopsmith#618 for the longer-term goal of sharing the note-building walk feedBack#618 for the longer-term goal of sharing the note-building walk
itself, and slopsmith#261 for the time-signature-denominator pitfalls the itself, and feedBack#261 for the time-signature-denominator pitfalls the
``beat_groups`` emission here exists to avoid re-introducing). ``beat_groups`` emission here exists to avoid re-introducing).
Where this plugs in: ``gp2rs_gpx.convert_file`` calls Where this plugs in: ``gp2rs_gpx.convert_file`` calls
@@ -43,7 +43,7 @@ from pathlib import Path
import notation as notation_mod import notation as notation_mod
log = logging.getLogger("slopsmith.lib.gp2notation") log = logging.getLogger("feedBack.lib.gp2notation")
# GPX NoteValue string → notation duration denominator (sloppak-spec §5.3: # GPX NoteValue string → notation duration denominator (sloppak-spec §5.3:
+2 -2
View File
@@ -10,7 +10,7 @@ from pathlib import Path
import guitarpro import guitarpro
log = logging.getLogger("slopsmith.lib.gp2rs") log = logging.getLogger("feedBack.lib.gp2rs")
_YEAR_RE = re.compile(r"\b(1[89]\d{2}|20\d{2})\b") _YEAR_RE = re.compile(r"\b(1[89]\d{2}|20\d{2})\b")
@@ -1122,7 +1122,7 @@ def _build_xml(
# Tuning. RS2014 schema names 6 string slots; we always emit those # Tuning. RS2014 schema names 6 string slots; we always emit those
# for compatibility, and emit additional string6+ attributes (up to # for compatibility, and emit additional string6+ attributes (up to
# `len(tuning)-1`) for 7+ string arrangements. Slopsmith parses # `len(tuning)-1`) for 7+ string arrangements. FeedBack parses
# them; the format ignores them. # them; the format ignores them.
tuning_el = ET.SubElement(root, "tuning") tuning_el = ET.SubElement(root, "tuning")
for i in range(max(6, len(tuning))): for i in range(max(6, len(tuning))):
+2 -2
View File
@@ -1,7 +1,7 @@
""" """
lib/gp2rs_gpx.py Guitar Pro 6 (.gpx) support shim for gp2rs. lib/gp2rs_gpx.py Guitar Pro 6 (.gpx) support shim for gp2rs.
Drop this file into slopsmith/lib/ alongside gp2rs.py. Drop this file into feedBack/lib/ alongside gp2rs.py.
No third-party dependencies pure Python stdlib only. No third-party dependencies pure Python stdlib only.
Public API mirrors the two functions that the editor plugin calls: Public API mirrors the two functions that the editor plugin calls:
@@ -20,7 +20,7 @@ from pathlib import Path
from safepath import safe_join from safepath import safe_join
_log = logging.getLogger("slopsmith.lib.gp2rs_gpx") _log = logging.getLogger("feedBack.lib.gp2rs_gpx")
def _safe_filename_stem(name: str) -> str: def _safe_filename_stem(name: str) -> str:
+2 -2
View File
@@ -3,7 +3,7 @@ lib/gp8_audio_sync.py — Extract embedded audio and sync data from GP8 (.gp) fi
Guitar Pro 8 can embed a backing track (OGG audio) into a .gp file alongside Guitar Pro 8 can embed a backing track (OGG audio) into a .gp file alongside
sync points that map bar positions to exact audio timestamps. This module sync points that map bar positions to exact audio timestamps. This module
extracts both, giving Slopsmith: extracts both, giving FeedBack:
1. A real backing track audio file (OGG) no MIDI synthesis needed 1. A real backing track audio file (OGG) no MIDI synthesis needed
2. A precise audio_offset (seconds) from the FramePadding value 2. A precise audio_offset (seconds) from the FramePadding value
@@ -45,7 +45,7 @@ import io
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
_log = logging.getLogger("slopsmith.lib.gp8_audio_sync") _log = logging.getLogger("feedBack.lib.gp8_audio_sync")
# GP8 embeds the backing track under Content/Assets/ as OGG *or* one of # GP8 embeds the backing track under Content/Assets/ as OGG *or* one of
# several other formats (MP3 is common — e.g. tracks rendered straight # several other formats (MP3 is common — e.g. tracks rendered straight
+1 -1
View File
@@ -30,7 +30,7 @@ import zipfile
import io import io
from pathlib import Path from pathlib import Path
_log = logging.getLogger("slopsmith.lib.gp_autosync") _log = logging.getLogger("feedBack.lib.gp_autosync")
# ── Dependency check ────────────────────────────────────────────────────────── # ── Dependency check ──────────────────────────────────────────────────────────
+11 -11
View File
@@ -1,10 +1,10 @@
"""Logging configuration for Slopsmith. """Logging configuration for FeedBack.
Call ``configure_logging()`` once at server startup, before any slopsmith Call ``configure_logging()`` once at server startup, before any feedBack
module imports that might emit log records. module imports that might emit log records.
Environment variables: Environment variables:
LOG_LEVEL severity threshold for the ``slopsmith.*`` logger tree LOG_LEVEL severity threshold for the ``feedBack.*`` logger tree
(default: INFO). Also accepted: DEBUG, WARNING, ERROR. (default: INFO). Also accepted: DEBUG, WARNING, ERROR.
LOG_FORMAT "json" for structured output (Loki, ELK, Promtail); LOG_FORMAT "json" for structured output (Loki, ELK, Promtail);
"text" (default) for human-readable coloured console output. "text" (default) for human-readable coloured console output.
@@ -43,7 +43,7 @@ def _add_correlation_id(
def configure_logging() -> None: def configure_logging() -> None:
"""Wire up the slopsmith logger hierarchy. """Wire up the feedBack logger hierarchy.
Safe to call multiple times; always reflects the current LOG_LEVEL, Safe to call multiple times; always reflects the current LOG_LEVEL,
LOG_FORMAT, and LOG_FILE environment variables. LOG_FORMAT, and LOG_FILE environment variables.
@@ -52,7 +52,7 @@ def configure_logging() -> None:
level = getattr(logging, raw_level, None) level = getattr(logging, raw_level, None)
if not isinstance(level, int): if not isinstance(level, int):
sys.stderr.write( sys.stderr.write(
f"[slopsmith] WARNING: unrecognised LOG_LEVEL={raw_level!r};" f"[feedBack] WARNING: unrecognised LOG_LEVEL={raw_level!r};"
" falling back to INFO.\n" " falling back to INFO.\n"
) )
level = logging.INFO level = logging.INFO
@@ -60,7 +60,7 @@ def configure_logging() -> None:
raw_fmt = os.environ.get("LOG_FORMAT", "text").lower() raw_fmt = os.environ.get("LOG_FORMAT", "text").lower()
if raw_fmt not in ("json", "text"): if raw_fmt not in ("json", "text"):
sys.stderr.write( sys.stderr.write(
f"[slopsmith] WARNING: unrecognised LOG_FORMAT={raw_fmt!r};" f"[feedBack] WARNING: unrecognised LOG_FORMAT={raw_fmt!r};"
" falling back to 'text'.\n" " falling back to 'text'.\n"
) )
raw_fmt = "text" raw_fmt = "text"
@@ -137,17 +137,17 @@ def configure_logging() -> None:
handlers.append(fh) handlers.append(fh)
except OSError as exc: except OSError as exc:
sys.stderr.write( sys.stderr.write(
f"[slopsmith] WARNING: could not open LOG_FILE={log_file!r}: {exc}" f"[feedBack] WARNING: could not open LOG_FILE={log_file!r}: {exc}"
" — continuing with console-only logging.\n" " — continuing with console-only logging.\n"
) )
_uvicorn_names = ("uvicorn", "uvicorn.error", "uvicorn.access") _uvicorn_names = ("uvicorn", "uvicorn.error", "uvicorn.access")
all_loggers = [logging.getLogger("slopsmith")] + [ all_loggers = [logging.getLogger("feedBack")] + [
logging.getLogger(n) for n in _uvicorn_names logging.getLogger(n) for n in _uvicorn_names
] ]
# Collect all unique old handlers across every logger *before* any close so # Collect all unique old handlers across every logger *before* any close so
# that a shared handler (slopsmith and uvicorn* were intentionally given the # that a shared handler (feedBack and uvicorn* were intentionally given the
# same objects) isn't closed while still attached to another logger tree. # same objects) isn't closed while still attached to another logger tree.
old_handlers: set[logging.Handler] = set() old_handlers: set[logging.Handler] = set()
for lg in all_loggers: for lg in all_loggers:
@@ -160,8 +160,8 @@ def configure_logging() -> None:
for h in old_handlers: for h in old_handlers:
h.close() h.close()
# Install fresh handlers on the slopsmith root. # Install fresh handlers on the feedBack root.
root = logging.getLogger("slopsmith") root = logging.getLogger("feedBack")
for h in handlers: for h in handlers:
root.addHandler(h) root.addHandler(h)
root.setLevel(level) root.setLevel(level)
+4 -4
View File
@@ -23,14 +23,14 @@ Engine selection
Two transcription paths share a common output: Two transcription paths share a common output:
* `transcribe_vocals_remote(path, server_url, ...)` POST the vocal * `transcribe_vocals_remote(path, server_url, ...)` POST the vocal
stem to the `/align` endpoint on a slopsmith-demucs-server (Byron's stem to the `/align` endpoint on a feedBack-demucs-server (got-feedBack's
reference server already hosts WhisperX alongside Demucs at the same reference server already hosts WhisperX alongside Demucs at the same
URL). URL).
* `transcribe_vocals_local(path, ...)` load WhisperX in-process. Heavy * `transcribe_vocals_local(path, ...)` load WhisperX in-process. Heavy
(~3 GB of model weights for `large-v2` + the wav2vec2 aligner) and (~3 GB of model weights for `large-v2` + the wav2vec2 aligner) and
slow on CPU. Deferred imports of `whisperx`, `torch`, and `soundfile` slow on CPU. Deferred imports of `whisperx`, `torch`, and `soundfile`
keep the rest of slopsmith free of those dependencies. keep the rest of feedBack free of those dependencies.
Callers pick between them based on a `whisperx.server_url` config and Callers pick between them based on a `whisperx.server_url` config and
fall back as appropriate. This module does not read config both fall back as appropriate. This module does not read config both
@@ -58,7 +58,7 @@ import logging
from pathlib import Path from pathlib import Path
from typing import Callable, Optional from typing import Callable, Optional
log = logging.getLogger("slopsmith.lib.lyrics_transcribe") log = logging.getLogger("feedBack.lib.lyrics_transcribe")
ProgressCB = Optional[Callable[[float, str, str], None]] ProgressCB = Optional[Callable[[float, str, str], None]]
@@ -179,7 +179,7 @@ _MIN_WORD_DURATION = 0.05
# Semver for the lyric-transcription artifact contract that gets stamped # Semver for the lyric-transcription artifact contract that gets stamped
# into the sloppak manifest's `lyric_transcription` block alongside the # into the sloppak manifest's `lyric_transcription` block alongside the
# engine + model. Bump per the semantics defined in slopsmith#357 (the # engine + model. Bump per the semantics defined in feedBack#357 (the
# parent `stem_separation` RFC): # parent `stem_separation` RFC):
# * patch — metadata-only or implementation fixes; no regeneration # * patch — metadata-only or implementation fixes; no regeneration
# * minor — backward-compatible additions # * minor — backward-compatible additions
+1 -1
View File
@@ -24,7 +24,7 @@ from __future__ import annotations
import logging import logging
import math import math
log = logging.getLogger("slopsmith.lib.notation") log = logging.getLogger("feedBack.lib.notation")
# ── Vocabulary ──────────────────────────────────────────────────────────────── # ── Vocabulary ────────────────────────────────────────────────────────────────
+4 -4
View File
@@ -31,7 +31,7 @@ from tunings import tuning_name
import sloppak as sloppak_mod import sloppak as sloppak_mod
import loosefolder as loosefolder_mod import loosefolder as loosefolder_mod
log = logging.getLogger("slopsmith.scan_worker") log = logging.getLogger("feedBack.scan_worker")
def _relpath(f: Path, dlc: Path) -> str: def _relpath(f: Path, dlc: Path) -> str:
@@ -53,7 +53,7 @@ def _extract_meta_sloppak(path: Path) -> dict:
meta["tuning_sort_key"] = sum(offsets) meta["tuning_sort_key"] = sum(offsets)
meta["tuning_offsets"] = " ".join(str(o) for o in offsets) meta["tuning_offsets"] = " ".join(str(o) for o in offsets)
meta["format"] = "sloppak" meta["format"] = "sloppak"
# `extract_meta` already populates `stem_ids` (slopsmith#129); # `extract_meta` already populates `stem_ids` (feedBack#129);
# default to empty for older callers / mocks. # default to empty for older callers / mocks.
meta.setdefault("stem_ids", []) meta.setdefault("stem_ids", [])
# Compute smart names for sloppak arrangements using name-based fallback # Compute smart names for sloppak arrangements using name-based fallback
@@ -109,7 +109,7 @@ def _extract_meta_for_file(path: Path, dlc_root=None) -> dict:
the root it already resolved; in-process callers can pass the resolver the root it already resolved; in-process callers can pass the resolver
itself (e.g. `_get_dlc_dir`) to keep the lookup lazy. itself (e.g. `_get_dlc_dir`) to keep the lookup lazy.
Slopsmith reads only its own `.sloppak` format and loose-folder XML FeedBack reads only its own `.sloppak` format and loose-folder XML
songs. Encrypted/proprietary archive formats are not supported and are songs. Encrypted/proprietary archive formats are not supported and are
silently ignored (empty metadata) rather than decrypted. silently ignored (empty metadata) rather than decrypted.
""" """
@@ -121,7 +121,7 @@ def _extract_meta_for_file(path: Path, dlc_root=None) -> dict:
if loosefolder_mod.is_loose_song(path): if loosefolder_mod.is_loose_song(path):
root = dlc_root() if callable(dlc_root) else dlc_root root = dlc_root() if callable(dlc_root) else dlc_root
return _extract_meta_loosefolder(path, root) return _extract_meta_loosefolder(path, root)
# Unknown/unsupported shape — return empty metadata. Slopsmith never # Unknown/unsupported shape — return empty metadata. FeedBack never
# reads encrypted archive formats. # reads encrypted archive formats.
return { return {
"title": "", "artist": "", "album": "", "year": "", "title": "", "artist": "", "album": "", "year": "",
+2 -2
View File
@@ -22,7 +22,7 @@ import zipfile
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
log = logging.getLogger("slopsmith.lib.sloppak") log = logging.getLogger("feedBack.lib.sloppak")
# The feedpak format version this build targets / writes (manifest # The feedpak format version this build targets / writes (manifest
# `feedpak_version`, a semver string per spec §4). Readers tolerate any version # `feedpak_version`, a semver string per spec §4). Readers tolerate any version
@@ -892,6 +892,6 @@ def extract_meta(path: Path) -> dict:
"arrangements": arrangements, "arrangements": arrangements,
"has_lyrics": has_lyrics, "has_lyrics": has_lyrics,
"stem_count": stem_count, "stem_count": stem_count,
# slopsmith#129: per-stem filter needs the id list, not just count. # feedBack#129: per-stem filter needs the id list, not just count.
"stem_ids": stem_ids, "stem_ids": stem_ids,
} }
+8 -8
View File
@@ -8,7 +8,7 @@ import logging
import math import math
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
log = logging.getLogger("slopsmith.lib.song") log = logging.getLogger("feedBack.lib.song")
@dataclass @dataclass
@@ -124,10 +124,10 @@ class PhraseLevel:
"""One difficulty tier's worth of note/chord/anchor/hand-shape data for a """One difficulty tier's worth of note/chord/anchor/hand-shape data for a
single phrase iteration. the arrangement XML stores these as `<level single phrase iteration. the arrangement XML stores these as `<level
difficulty="N">` blocks that repeat for every difficulty tier the chart difficulty="N">` blocks that repeat for every difficulty tier the chart
author wrote; slopsmith used to collapse them to the phrase's author wrote; feedBack used to collapse them to the phrase's
maxDifficulty and throw the rest away. Keeping them around lets the maxDifficulty and throw the rest away. Keeping them around lets the
highway render a "master difficulty" slider that picks a per-phrase highway render a "master difficulty" slider that picks a per-phrase
difficulty tier at render time (slopsmith#48).""" difficulty tier at render time (feedBack#48)."""
difficulty: int difficulty: int
notes: list[Note] = field(default_factory=list) notes: list[Note] = field(default_factory=list)
@@ -175,7 +175,7 @@ class Arrangement:
# `base`/`changes` drive the highway tone-change markers; `definitions` # `base`/`changes` drive the highway tone-change markers; `definitions`
# feed the Tones plugin gear panel. # feed the Tones plugin gear panel.
tones: dict | None = None tones: dict | None = None
# arrangement XML <arrangementProperties> flags for smart naming (slopsmith feat/arrangement). # arrangement XML <arrangementProperties> flags for smart naming (feedBack feat/arrangement).
# Populated from the XML; default False/0 for sloppak / GP-imported sources. # Populated from the XML; default False/0 for sloppak / GP-imported sources.
path_lead: bool = False path_lead: bool = False
path_rhythm: bool = False path_rhythm: bool = False
@@ -622,7 +622,7 @@ def arrangement_string_count(arr: Arrangement) -> int:
"""Derive the active arrangement's string count. """Derive the active arrangement's string count.
Used by the server to emit ``stringCount`` in the song_info Used by the server to emit ``stringCount`` in the song_info
WebSocket payload (slopsmith-plugin-3dhighway#7). WebSocket payload (feedBack-plugin-3dhighway#7).
The arrangement XML schema always emits 6 ``<tuning>`` slots regardless The arrangement XML schema always emits 6 ``<tuning>`` slots regardless
of instrument (bass charts populate `string0``string3` and pad of instrument (bass charts populate `string0``string3` and pad
@@ -1276,7 +1276,7 @@ def parse_arrangement(xml_path: str) -> Arrangement:
def _collect_from_parsed(parsed, t_start, t_end): def _collect_from_parsed(parsed, t_start, t_end):
"""Append a pre-parsed level's time-clipped slice to the flat """Append a pre-parsed level's time-clipped slice to the flat
arrangement lists. Used for the max-mastery merge that preserves arrangement lists. Used for the max-mastery merge that preserves
the pre-slopsmith#48 behaviour for existing consumers.""" the pre-feedBack#48 behaviour for existing consumers."""
lv_notes, lv_chords, lv_anchors, lv_hand_shapes = _extract_level_slice( lv_notes, lv_chords, lv_anchors, lv_hand_shapes = _extract_level_slice(
parsed, t_start, t_end parsed, t_start, t_end
) )
@@ -1295,7 +1295,7 @@ def parse_arrangement(xml_path: str) -> Arrangement:
_collect_from_parsed(best, 0.0, float("inf")) _collect_from_parsed(best, 0.0, float("inf"))
# Per-phrase difficulty data for the master-difficulty slider # Per-phrase difficulty data for the master-difficulty slider
# (slopsmith#48). Only populated when the XML has multiple levels AND # (feedBack#48). Only populated when the XML has multiple levels AND
# phrase data — left as None for single-level sources so the frontend # phrase data — left as None for single-level sources so the frontend
# knows to disable the slider. # knows to disable the slider.
phrases: list[Phrase] | None = None phrases: list[Phrase] | None = None
@@ -1445,7 +1445,7 @@ def _convert_sng_to_xml(extracted_dir: str):
"""No-op stub. """No-op stub.
Historically this converted proprietary encrypted ``.notechart`` arrangement Historically this converted proprietary encrypted ``.notechart`` arrangement
files to XML via an external tool. That path has been removed: slopsmith files to XML via an external tool. That path has been removed: feedBack
reads only its own ``.sloppak`` format and loose-folder/GP/MusicXML-derived reads only its own ``.sloppak`` format and loose-folder/GP/MusicXML-derived
arrangement XML, and never decodes or decrypts proprietary archives. Kept arrangement XML, and never decodes or decrypts proprietary archives. Kept
as a no-op so ``load_song`` (which loads plain arrangement XML/JSON from a as a no-op so ``load_song`` (which loads plain arrangement XML/JSON from a
+1 -1
View File
@@ -10,7 +10,7 @@ source of truth, so the change survives both incremental and full rescans.
only the keys present are overwritten, so an edit of just the title can't blank only the keys present are overwritten, so an edit of just the title can't blank
out the artist. out the artist.
Only slopsmith's own ``.sloppak`` format (zip- or directory-form) is writable. Only feedBack's own ``.sloppak`` format (zip- or directory-form) is writable.
Unknown / unsupported shapes return False and the caller keeps the DB-only Unknown / unsupported shapes return False and the caller keeps the DB-only
update. update.
""" """
+6 -4
View File
@@ -1,9 +1,9 @@
"""Regenerate ``static/tailwind.min.css`` over the full installed-plugin set. """Regenerate ``static/tailwind.min.css`` over the full installed-plugin set.
Core's committed (and image-baked) stylesheet is built scanning only the Core's committed (and image-baked) stylesheet is built scanning only the
in-tree plugins. A plugin installed at runtime into ``SLOPSMITH_PLUGINS_DIR`` in-tree plugins. A plugin installed at runtime into ``FEEDBACK_PLUGINS_DIR``
ships Tailwind classes the sheet never saw, so it renders unstyled. The ships Tailwind classes the sheet never saw, so it renders unstyled. The
Play CDN's runtime JIT that used to cover this was removed (slopsmith#411), Play CDN's runtime JIT that used to cover this was removed (feedBack#411),
so we rebuild the sheet ourselves with node + the pinned ``tailwindcss``, so we rebuild the sheet ourselves with node + the pinned ``tailwindcss``,
scanning the baked-in plugins *and* the user plugins dir. scanning the baked-in plugins *and* the user plugins dir.
@@ -24,7 +24,9 @@ import tempfile
import threading import threading
from pathlib import Path from pathlib import Path
log = logging.getLogger("slopsmith.tailwind") from env_compat import getenv_compat
log = logging.getLogger("feedBack.tailwind")
# Pin matches scripts/build-tailwind.sh and the Dockerfile build stage so every # Pin matches scripts/build-tailwind.sh and the Dockerfile build stage so every
# sheet — committed, image-baked, and runtime-regenerated — comes from the same # sheet — committed, image-baked, and runtime-regenerated — comes from the same
@@ -45,7 +47,7 @@ APP_DIR = Path(__file__).resolve().parent.parent
def _user_plugins_dir() -> Path | None: def _user_plugins_dir() -> Path | None:
raw = os.environ.get("SLOPSMITH_PLUGINS_DIR", "").strip() raw = (getenv_compat("FEEDBACK_PLUGINS_DIR", "") or "").strip()
if not raw: if not raw:
return None return None
p = Path(raw) p = Path(raw)
+3 -3
View File
@@ -1,13 +1,13 @@
"""Tone helpers for sloppak playback. """Tone helpers for sloppak playback.
A slopsmith arrangement may carry a tone block the initial tone name plus A feedBack arrangement may carry a tone block the initial tone name plus
in-song tone switches embedded inline in the arrangement JSON (see in-song tone switches embedded inline in the arrangement JSON (see
``lib/song.py`` ``arrangement_to_wire`` / the ``tones`` wire key). This module ``lib/song.py`` ``arrangement_to_wire`` / the ``tones`` wire key). This module
turns that already-embedded block into the (base, changes) payload the highway turns that already-embedded block into the (base, changes) payload the highway
WebSocket sends to the client. WebSocket sends to the client.
The proprietary-archive tone-extraction path (lifting tone definitions out of The proprietary-archive tone-extraction path (lifting tone definitions out of
an unpacked encrypted archive) has been removed. Slopsmith reads tones only an unpacked encrypted archive) has been removed. FeedBack reads tones only
from its own ``.sloppak`` / arrangement JSON; it never reads or decrypts from its own ``.sloppak`` / arrangement JSON; it never reads or decrypts
proprietary archive formats. proprietary archive formats.
""" """
@@ -18,7 +18,7 @@ import logging
import math import math
import re import re
log = logging.getLogger("slopsmith.lib.tones") log = logging.getLogger("feedBack.lib.tones")
def tokens(s: str) -> set[str]: def tokens(s: str) -> set[str]:
+6 -6
View File
@@ -5,7 +5,7 @@ isolated vocals + per-syllable lyric timing (both produced by the
WhisperX fallback or shipped in the source archive), the /pitch endpoint WhisperX fallback or shipped in the source archive), the /pitch endpoint
runs CREPE over the vocals stem and returns one MIDI note per supplied runs CREPE over the vocals stem and returns one MIDI note per supplied
timing token. The result lands in `<sloppak>/vocal_pitch.json` in the timing token. The result lands in `<sloppak>/vocal_pitch.json` in the
shape the got-feedback/feedback-plugin-lyrics-karaoke renderer shape the got-feedback/feedBack-plugin-lyrics-karaoke renderer
already consumes: already consumes:
{"version": 1, "notes": [{"t": float, "d": float, "midi": int}, ...]} {"version": 1, "notes": [{"t": float, "d": float, "midi": int}, ...]}
@@ -23,18 +23,18 @@ runs locally. Adding a local CREPE path here would mean pulling
`crepe` + `tensorflow` as plugin deps (~500 MB+ on top of the `crepe` + `tensorflow` as plugin deps (~500 MB+ on top of the
existing torch/demucs/whisperx). Deferred until users hit the gap. existing torch/demucs/whisperx). Deferred until users hit the gap.
If you need a local fallback today, install If you need a local fallback today, install
`got-feedback/feedback-plugin-lyrics-karaoke` and let its local `got-feedback/feedBack-plugin-lyrics-karaoke` and let its local
pYIN run when the server isn't reachable. pYIN run when the server isn't reachable.
Cache key parity with stem_separation / lyric_transcription Cache key parity with stem_separation / lyric_transcription
A `pitch_extraction` manifest block mirrors the shape introduced by A `pitch_extraction` manifest block mirrors the shape introduced by
slopsmith#357: `{engine, model, version}`. Today engine is fixed at feedBack#357: `{engine, model, version}`. Today engine is fixed at
`"crepe"` (the server's choice) and model at `"v1"` (server doesn't `"crepe"` (the server's choice) and model at `"v1"` (server doesn't
yet expose the CREPE capacity dial it uses internally; this is the yet expose the CREPE capacity dial it uses internally; this is the
requested value, same caveat as `lyric_transcription.model`). The requested value, same caveat as `lyric_transcription.model`). The
schema version is independent of the upstream CREPE version and bumps schema version is independent of the upstream CREPE version and bumps
per slopsmith's contract: per feedBack's contract:
* patch metadata-only or implementation fixes * patch metadata-only or implementation fixes
* minor backward-compatible additions * minor backward-compatible additions
* major output shape / semantics changed; existing * major output shape / semantics changed; existing
@@ -50,7 +50,7 @@ import math
from pathlib import Path from pathlib import Path
from typing import Callable, Optional from typing import Callable, Optional
log = logging.getLogger("slopsmith.lib.vocal_pitch") log = logging.getLogger("feedBack.lib.vocal_pitch")
ProgressCB = Optional[Callable[[float, str, str], None]] ProgressCB = Optional[Callable[[float, str, str], None]]
@@ -72,7 +72,7 @@ def extract_pitch_remote(
) -> list[dict]: ) -> list[dict]:
"""POST the vocal stem + lyric timings to `{server_url}/pitch`. """POST the vocal stem + lyric timings to `{server_url}/pitch`.
`lyrics` is the same `[{t, d, w}, ...]` list slopsmith writes to `lyrics` is the same `[{t, d, w}, ...]` list feedBack writes to
`lyrics.json`. The endpoint only consumes `t` + `d` (it doesn't `lyrics.json`. The endpoint only consumes `t` + `d` (it doesn't
need the word text), but we pass the full payload through need the word text), but we pass the full payload through
slimmer to forward what we already have than to project. slimmer to forward what we already have than to project.
+1 -1
View File
@@ -6,7 +6,7 @@ import logging
import struct import struct
import os import os
log = logging.getLogger("slopsmith.lib.wem_decode") log = logging.getLogger("feedBack.lib.wem_decode")
def convert_wem_to_ogg(wem_path: str, output_path: str) -> bool: def convert_wem_to_ogg(wem_path: str, output_path: str) -> bool:
+1 -1
View File
@@ -1,4 +1,4 @@
"""Programmatic entry point for the Slopsmith server. """Programmatic entry point for the FeedBack server.
Using ``uvicorn.run()`` with ``log_config=None`` prevents uvicorn from calling Using ``uvicorn.run()`` with ``log_config=None`` prevents uvicorn from calling
``logging.config.dictConfig(LOGGING_CONFIG)`` during its startup sequence. ``logging.config.dictConfig(LOGGING_CONFIG)`` during its startup sequence.
+2 -2
View File
@@ -1,11 +1,11 @@
{ {
"name": "slopsmith-browser-tests", "name": "feedBack-browser-tests",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "slopsmith-browser-tests", "name": "feedBack-browser-tests",
"version": "1.0.0", "version": "1.0.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"devDependencies": { "devDependencies": {
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "slopsmith-browser-tests", "name": "feedBack-browser-tests",
"version": "1.0.0", "version": "1.0.0",
"description": "Browser tests for Slopsmith keyboard shortcuts and JS plugin-API contract tests under tests/js/.", "description": "Browser tests for FeedBack keyboard shortcuts and JS plugin-API contract tests under tests/js/.",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"scripts": { "scripts": {
"test": "playwright test", "test": "playwright test",
+20 -20
View File
@@ -15,7 +15,7 @@ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
from safepath import safe_join from safepath import safe_join
log = logging.getLogger("slopsmith.plugins") log = logging.getLogger("feedBack.plugins")
PLUGINS_DIR = Path(__file__).parent PLUGINS_DIR = Path(__file__).parent
@@ -43,7 +43,7 @@ PLUGINS_LOCK = threading.RLock()
# registry mutation (the pending seed, _graduate, _mark_failed) re-checks it # registry mutation (the pending seed, _graduate, _mark_failed) re-checks it
# under the lock before touching LOADED_PLUGINS / PENDING_PLUGINS. This keeps a # under the lock before touching LOADED_PLUGINS / PENDING_PLUGINS. This keeps a
# still-running loader from an EARLIER pass — e.g. a "reload plugins" action, # still-running loader from an EARLIER pass — e.g. a "reload plugins" action,
# SLOPSMITH_SYNC_STARTUP hot-reload, or test teardown re-invoking load_plugins() # FEEDBACK_SYNC_STARTUP hot-reload, or test teardown re-invoking load_plugins()
# while the first pass's background install thread is mid-flight — from # while the first pass's background install thread is mid-flight — from
# repopulating or duplicating entries after a NEWER pass has already cleared the # repopulating or duplicating entries after a NEWER pass has already cleared the
# registries. Only the latest pass is allowed to publish. # registries. Only the latest pass is allowed to publish.
@@ -512,7 +512,7 @@ def _capability_warnings(manifest: dict, plugin_id: str) -> tuple[dict, list[dic
if isinstance(declaration.get("provider_policy"), dict): if isinstance(declaration.get("provider_policy"), dict):
clean["provider_policy"] = declaration["provider_policy"] clean["provider_policy"] = declaration["provider_policy"]
# Declarative per-instance control descriptors a consuming host renders # Declarative per-instance control descriptors a consuming host renders
# generically (slopsmith#849). Domain-agnostic: validated for any # generically (feedBack#849). Domain-agnostic: validated for any
# capability here and surfaced via /api/plugins; each domain defines how # capability here and surfaced via /api/plugins; each domain defines how
# a value is applied (visualization is the first consumer). # a value is applied (visualization is the first consumer).
if clean_settings: if clean_settings:
@@ -567,7 +567,7 @@ def _load_plugin_sibling(plugin_id: str, plugin_dir: Path, name: str):
import precedence). Mirrors the routes-loading pattern in import precedence). Mirrors the routes-loading pattern in
`load_plugins()` and shares its `sys.modules` cache, so two plugins `load_plugins()` and shares its `sys.modules` cache, so two plugins
that each ship `extractor.py` get distinct cached modules instead that each ship `extractor.py` get distinct cached modules instead
of stomping each other through `sys.path`. See slopsmith#33.""" of stomping each other through `sys.path`. See feedBack#33."""
if not isinstance(plugin_id, str) or not plugin_id: if not isinstance(plugin_id, str) or not plugin_id:
raise ValueError( raise ValueError(
f"load_sibling: plugin_id must be a non-empty string, got {plugin_id!r}" f"load_sibling: plugin_id must be a non-empty string, got {plugin_id!r}"
@@ -613,7 +613,7 @@ def _load_plugin_sibling(plugin_id: str, plugin_dir: Path, name: str):
# sys.modules entry — same key load_sibling produces # sys.modules entry — same key load_sibling produces
# `setdefault` is atomic under the GIL so two threads racing to # `setdefault` is atomic under the GIL so two threads racing to
# create the parent can't overwrite each other's registration. # create the parent can't overwrite each other's registration.
# Spotted by codex/Copilot reviews on PRs for slopsmith#33. # Spotted by codex/Copilot reviews on PRs for feedBack#33.
import types import types
new_parent = types.ModuleType(parent_name) new_parent = types.ModuleType(parent_name)
new_parent.__path__ = [str(plugin_dir)] new_parent.__path__ = [str(plugin_dir)]
@@ -641,14 +641,14 @@ def _warn_on_module_collisions(plugin_specs):
"""Scan top-level importable modules across all plugins about to """Scan top-level importable modules across all plugins about to
be loaded. Print a warning for any module name shipped by 2+ be loaded. Print a warning for any module name shipped by 2+
plugins, since bare `import <name>` from those plugins will hit plugins, since bare `import <name>` from those plugins will hit
the sys.path-based cache and cross-load (slopsmith#33). the sys.path-based cache and cross-load (feedBack#33).
Both top-level `.py` files AND top-level packages (directories Both top-level `.py` files AND top-level packages (directories
containing `__init__.py`) are scanned the same collision containing `__init__.py`) are scanned the same collision
pattern applies to either, e.g. one plugin's `extractor.py` vs pattern applies to either, e.g. one plugin's `extractor.py` vs
another plugin's `extractor/__init__.py` both produce a shared another plugin's `extractor/__init__.py` both produce a shared
`sys.modules['extractor']` entry. Spotted by codex review on `sys.modules['extractor']` entry. Spotted by codex review on
PR for slopsmith#33. PR for feedBack#33.
`routes.py` itself is excluded because the loader already `routes.py` itself is excluded because the loader already
namespaces it as `plugin_{id}_routes`. Top-level dunder files namespaces it as `plugin_{id}_routes`. Top-level dunder files
@@ -663,7 +663,7 @@ def _warn_on_module_collisions(plugin_specs):
# — that intra-plugin layout is supported by load_sibling # — that intra-plugin layout is supported by load_sibling
# (package form wins, matching CPython precedence) and shouldn't # (package form wins, matching CPython precedence) and shouldn't
# trip a cross-plugin collision warning. Spotted by codex review # trip a cross-plugin collision warning. Spotted by codex review
# on PR for slopsmith#33. # on PR for feedBack#33.
by_name: dict[str, dict[str, set[str]]] = {} by_name: dict[str, dict[str, set[str]]] = {}
for plugin_id, plugin_dir in plugin_specs: for plugin_id, plugin_dir in plugin_specs:
try: try:
@@ -700,7 +700,7 @@ def _warn_on_module_collisions(plugin_specs):
log.warning( log.warning(
"Module-name collision: %r (%s) is shipped by %d plugins (%s). " "Module-name collision: %r (%s) is shipped by %d plugins (%s). "
"Bare `import %s` may load the wrong file. " "Bare `import %s` may load the wrong file. "
"Migrate to context['load_sibling']('%s') — see CLAUDE.md (slopsmith#33).", "Migrate to context['load_sibling']('%s') — see CLAUDE.md (feedBack#33).",
name, kind_label, len(by_plugin), ids_quoted, name, name, name, kind_label, len(by_plugin), ids_quoted, name, name,
) )
@@ -745,7 +745,7 @@ def _is_valid_tour_manifest(val) -> bool:
def _normalize_export_paths(settings_field, plugin_id: str) -> list[str]: def _normalize_export_paths(settings_field, plugin_id: str) -> list[str]:
"""Validate and normalize a plugin's `settings.server_files` manifest """Validate and normalize a plugin's `settings.server_files` manifest
list into clean POSIX-style relpaths suitable for the settings list into clean POSIX-style relpaths suitable for the settings
export/import bundle (slopsmith#113). export/import bundle (feedBack#113).
Each entry must be a non-empty string with no absolute prefix and Each entry must be a non-empty string with no absolute prefix and
no `..` segment. A trailing `/` denotes a directory (recurse on no `..` segment. A trailing `/` denotes a directory (recurse on
@@ -1118,7 +1118,7 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
# Collect plugin directories — user plugins first so they override built-in # Collect plugin directories — user plugins first so they override built-in
plugin_dirs = [] plugin_dirs = []
user_plugins_dir = os.environ.get("SLOPSMITH_PLUGINS_DIR") user_plugins_dir = os.environ.get("FEEDBACK_PLUGINS_DIR") or os.environ.get("SLOPSMITH_PLUGINS_DIR")
if user_plugins_dir: if user_plugins_dir:
user_path = Path(user_plugins_dir) user_path = Path(user_plugins_dir)
if user_path.is_dir() and user_path != PLUGINS_DIR: if user_path.is_dir() and user_path != PLUGINS_DIR:
@@ -1179,7 +1179,7 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
) )
# Two-pass discovery so we can warn about cross-plugin module-name # Two-pass discovery so we can warn about cross-plugin module-name
# collisions BEFORE any plugin's setup runs (slopsmith#33). The # collisions BEFORE any plugin's setup runs (feedBack#33). The
# first pass collects (plugin_id, plugin_dir, manifest) tuples in # first pass collects (plugin_id, plugin_dir, manifest) tuples in
# load order; the second pass actually executes each plugin's # load order; the second pass actually executes each plugin's
# setup with a per-plugin context. # setup with a per-plugin context.
@@ -1231,7 +1231,7 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
kept_is_bundled = _is_bundled(kept[1], kept[2]) if kept else False kept_is_bundled = _is_bundled(kept[1], kept[2]) if kept else False
if this_is_bundled and not kept_is_bundled: if this_is_bundled and not kept_is_bundled:
# The incoming copy is the canonical bundled plugin; the # The incoming copy is the canonical bundled plugin; the
# already-kept copy is user-installed (SLOPSMITH_PLUGINS_DIR # already-kept copy is user-installed (FEEDBACK_PLUGINS_DIR
# or cloned directly into plugins/). Bundled always wins — # or cloned directly into plugins/). Bundled always wins —
# evict the user copy and fall through to register the # evict the user copy and fall through to register the
# bundled version instead. # bundled version instead.
@@ -1537,7 +1537,7 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
continue continue
# Add plugin directory to sys.path so the plugin's bare # Add plugin directory to sys.path so the plugin's bare
# `import sibling` keeps working during the slopsmith#33 # `import sibling` keeps working during the feedBack#33
# transition. New plugins should prefer # transition. New plugins should prefer
# `context['load_sibling']('sibling')` instead — see # `context['load_sibling']('sibling')` instead — see
# CLAUDE.md / Plugin System / Backend routes. # CLAUDE.md / Plugin System / Backend routes.
@@ -1556,13 +1556,13 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
# bijectively encoded by _safe_plugin_id_for_module_name: # bijectively encoded by _safe_plugin_id_for_module_name:
# `_` -> `_5f_`, `.` -> `_2e_`) so two plugins shipping the # `_` -> `_5f_`, `.` -> `_2e_`) so two plugins shipping the
# same filename get distinct cached modules. See # same filename get distinct cached modules. See
# slopsmith#33. # feedBack#33.
plugin_context = dict(context) plugin_context = dict(context)
plugin_context["load_sibling"] = ( plugin_context["load_sibling"] = (
lambda name, _pid=plugin_id, _pdir=plugin_dir: lambda name, _pid=plugin_id, _pdir=plugin_dir:
_load_plugin_sibling(_pid, _pdir, name) _load_plugin_sibling(_pid, _pdir, name)
) )
plugin_context["log"] = logging.getLogger(f"slopsmith.plugin.{plugin_id}") plugin_context["log"] = logging.getLogger(f"feedBack.plugin.{plugin_id}")
if callable(plugin_context.get("register_library_provider")): if callable(plugin_context.get("register_library_provider")):
_register_library_provider = plugin_context["register_library_provider"] _register_library_provider = plugin_context["register_library_provider"]
@@ -1709,9 +1709,9 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
# Normalized list of relpaths under CONFIG_DIR that this # Normalized list of relpaths under CONFIG_DIR that this
# plugin opts in to settings export/import. Empty for # plugin opts in to settings export/import. Empty for
# plugins that don't declare `settings.server_files`. See # plugins that don't declare `settings.server_files`. See
# slopsmith#113. # feedBack#113.
"_export_paths": _normalize_export_paths(manifest.get("settings"), plugin_id), "_export_paths": _normalize_export_paths(manifest.get("settings"), plugin_id),
# Diagnostics opt-in (slopsmith#166): same allowlist semantics # Diagnostics opt-in (feedBack#166): same allowlist semantics
# as `_export_paths` but for the troubleshooting bundle. # as `_export_paths` but for the troubleshooting bundle.
"_diagnostics_paths": _normalize_diagnostics_paths(manifest.get("diagnostics"), plugin_id), "_diagnostics_paths": _normalize_diagnostics_paths(manifest.get("diagnostics"), plugin_id),
"_diagnostics_callable_spec": _parse_diagnostics_callable(manifest.get("diagnostics"), plugin_id), "_diagnostics_callable_spec": _parse_diagnostics_callable(manifest.get("diagnostics"), plugin_id),
@@ -1800,7 +1800,7 @@ def load_plugins(app: FastAPI, context: dict, progress_cb=None, route_setup_fn=N
lambda name, _pid=evicted_id, _pdir=ev_dir: lambda name, _pid=evicted_id, _pdir=ev_dir:
_load_plugin_sibling(_pid, _pdir, name) _load_plugin_sibling(_pid, _pdir, name)
) )
ev_context["log"] = logging.getLogger(f"slopsmith.plugin.{evicted_id}") ev_context["log"] = logging.getLogger(f"feedBack.plugin.{evicted_id}")
if callable(ev_context.get("register_library_provider")): if callable(ev_context.get("register_library_provider")):
_ev_register_library_provider = ev_context["register_library_provider"] _ev_register_library_provider = ev_context["register_library_provider"]
@@ -2053,7 +2053,7 @@ def register_plugin_api(app: FastAPI):
"category": p.get("category") if "category" in p else ((p.get("_manifest") or {}).get("category") or None), "category": p.get("category") if "category" in p else ((p.get("_manifest") or {}).get("category") or None),
"icon": p.get("icon") if "icon" in p else ((p.get("_manifest") or {}).get("icon") or None), "icon": p.get("icon") if "icon" in p else ((p.get("_manifest") or {}).get("icon") or None),
# `bundled` is reserved metadata flagging plugins that # `bundled` is reserved metadata flagging plugins that
# ship with the default container image (slopsmith#160). # ship with the default container image (feedBack#160).
# Surfaced in /api/plugins so the plugin-list UI can # Surfaced in /api/plugins so the plugin-list UI can
# render a "Bundled" badge (lock icon) next to the # render a "Bundled" badge (lock icon) next to the
# plugin name in the settings collapsible. # plugin name in the settings collapsible.
+9 -9
View File
@@ -10,7 +10,7 @@
id: 'library-provider', id: 'library-provider',
selector: '#lib-provider', selector: '#lib-provider',
title: 'Choose a library', title: 'Choose a library',
content: 'Use this menu to switch between your local library and any connected remote libraries. Slopsmith remembers the last library you picked.', content: 'Use this menu to switch between your local library and any connected remote libraries. FeedBack remembers the last library you picked.',
shape: 'spotlight', shape: 'spotlight',
position: 'bottom', position: 'bottom',
waitFor: '#lib-provider' waitFor: '#lib-provider'
@@ -57,20 +57,20 @@
function _register() { function _register() {
try { try {
window.slopsmithTour.register(PLUGIN_ID, { screens: SCREENS, buildSteps: _buildSteps }); window.feedBackTour.register(PLUGIN_ID, { screens: SCREENS, buildSteps: _buildSteps });
} catch (e) { } catch (e) {
console.warn('[app_tour_library] register failed', e); console.warn('[app_tour_library] register failed', e);
} }
} }
if (window.slopsmithTour && typeof window.slopsmithTour.register === 'function') { if (window.feedBackTour && typeof window.feedBackTour.register === 'function') {
_register(); _register();
} else { } else {
// Engine inits on DOMContentLoaded after fetching /api/plugins. Plugin // Engine inits on DOMContentLoaded after fetching /api/plugins. Plugin
// scripts can load before or after that handler runs, so poll briefly. // scripts can load before or after that handler runs, so poll briefly.
var deadline = performance.now() + 5000; var deadline = performance.now() + 5000;
var pollId = setInterval(function () { var pollId = setInterval(function () {
if (window.slopsmithTour && typeof window.slopsmithTour.register === 'function') { if (window.feedBackTour && typeof window.feedBackTour.register === 'function') {
clearInterval(pollId); clearInterval(pollId);
_register(); _register();
} else if (performance.now() > deadline) { } else if (performance.now() > deadline) {
@@ -93,9 +93,9 @@
var s = document.createElement('style'); var s = document.createElement('style');
s.id = STYLE_ID; s.id = STYLE_ID;
s.textContent = s.textContent =
'body.' + NUDGE_CLASS + ' .slopsmith-tour-menu-btn { bottom: 68px; }' + 'body.' + NUDGE_CLASS + ' .feedBack-tour-menu-btn { bottom: 68px; }' +
'body.' + NUDGE_CLASS + ' .slopsmith-tour-menu-popover { bottom: 112px; }' + 'body.' + NUDGE_CLASS + ' .feedBack-tour-menu-popover { bottom: 112px; }' +
'body.' + NUDGE_CLASS + ' .slopsmith-tour-prompt { bottom: 112px; }'; 'body.' + NUDGE_CLASS + ' .feedBack-tour-prompt { bottom: 112px; }';
document.head.appendChild(s); document.head.appendChild(s);
} }
@@ -109,8 +109,8 @@
// Prime from whichever screen is already active. // Prime from whichever screen is already active.
var active = document.querySelector('.screen.active'); var active = document.querySelector('.screen.active');
_applyNudge(active ? active.id : null); _applyNudge(active ? active.id : null);
if (window.slopsmith && typeof window.slopsmith.on === 'function') { if (window.feedBack && typeof window.feedBack.on === 'function') {
window.slopsmith.on('screen:changed', function (ev) { window.feedBack.on('screen:changed', function (ev) {
_applyNudge(ev && ev.detail && ev.detail.id); _applyNudge(ev && ev.detail && ev.detail.id);
}); });
} }
+1 -1
View File
@@ -3,7 +3,7 @@
"tour": [ "tour": [
{ {
"id": "welcome", "id": "welcome",
"title": "Welcome to Slopsmith", "title": "Welcome to FeedBack",
"content": "This is your library — every song we found in your library folder. Let's take a quick spin through the controls.", "content": "This is your library — every song we found in your library folder. Let's take a quick spin through the controls.",
"shape": "bubble", "shape": "bubble",
"position": "auto" "position": "auto"
+8 -8
View File
@@ -6,18 +6,18 @@
function _register() { function _register() {
try { try {
window.slopsmithTour.register(PLUGIN_ID, { screens: SCREENS }); window.feedBackTour.register(PLUGIN_ID, { screens: SCREENS });
} catch (e) { } catch (e) {
console.warn('[app_tour_settings] register failed', e); console.warn('[app_tour_settings] register failed', e);
} }
} }
if (window.slopsmithTour && typeof window.slopsmithTour.register === 'function') { if (window.feedBackTour && typeof window.feedBackTour.register === 'function') {
_register(); _register();
} else { } else {
var deadline = performance.now() + 5000; var deadline = performance.now() + 5000;
var pollId = setInterval(function () { var pollId = setInterval(function () {
if (window.slopsmithTour && typeof window.slopsmithTour.register === 'function') { if (window.feedBackTour && typeof window.feedBackTour.register === 'function') {
clearInterval(pollId); clearInterval(pollId);
_register(); _register();
} else if (performance.now() > deadline) { } else if (performance.now() > deadline) {
@@ -38,9 +38,9 @@
var s = document.createElement('style'); var s = document.createElement('style');
s.id = STYLE_ID; s.id = STYLE_ID;
s.textContent = s.textContent =
'body.' + NUDGE_CLASS + ' .slopsmith-tour-menu-btn { bottom: 68px; }' + 'body.' + NUDGE_CLASS + ' .feedBack-tour-menu-btn { bottom: 68px; }' +
'body.' + NUDGE_CLASS + ' .slopsmith-tour-menu-popover { bottom: 112px; }' + 'body.' + NUDGE_CLASS + ' .feedBack-tour-menu-popover { bottom: 112px; }' +
'body.' + NUDGE_CLASS + ' .slopsmith-tour-prompt { bottom: 112px; }'; 'body.' + NUDGE_CLASS + ' .feedBack-tour-prompt { bottom: 112px; }';
document.head.appendChild(s); document.head.appendChild(s);
} }
@@ -53,8 +53,8 @@
_ensureStyle(); _ensureStyle();
var active = document.querySelector('.screen.active'); var active = document.querySelector('.screen.active');
_applyNudge(active ? active.id : null); _applyNudge(active ? active.id : null);
if (window.slopsmith && typeof window.slopsmith.on === 'function') { if (window.feedBack && typeof window.feedBack.on === 'function') {
window.slopsmith.on('screen:changed', function (ev) { window.feedBack.on('screen:changed', function (ev) {
_applyNudge(ev && ev.detail && ev.detail.id); _applyNudge(ev && ev.detail && ev.detail.id);
}); });
} }
+2 -2
View File
@@ -12,7 +12,7 @@
"id": "dlc-path", "id": "dlc-path",
"selector": "#dlc-path", "selector": "#dlc-path",
"title": "Library folder", "title": "Library folder",
"content": "Point Slopsmith at your library folder. Songs here become your library. Hit Save after changing.", "content": "Point FeedBack at your library folder. Songs here become your library. Hit Save after changing.",
"shape": "spotlight", "shape": "spotlight",
"position": "bottom" "position": "bottom"
}, },
@@ -69,7 +69,7 @@
"id": "about", "id": "about",
"selector": "#app-version-about", "selector": "#app-version-about",
"title": "About", "title": "About",
"content": "Version, source code, and license. Slopsmith is AGPL-3.0 — if you fork it, the source has to stay open.", "content": "Version, source code, and license. FeedBack is AGPL-3.0 — if you fork it, the source has to stay open.",
"shape": "spotlight", "shape": "spotlight",
"position": "top" "position": "top"
}, },
+8 -8
View File
@@ -1,7 +1,7 @@
(function () { (function () {
'use strict'; 'use strict';
const state = window.__slopsmithCapabilityInspector || (window.__slopsmithCapabilityInspector = {}); const state = window.__feedBackCapabilityInspector || (window.__feedBackCapabilityInspector = {});
state.render = render; state.render = render;
if (state.installed) return; if (state.installed) return;
state.installed = true; state.installed = true;
@@ -126,7 +126,7 @@
} }
function registry() { function registry() {
return window.slopsmith && window.slopsmith.capabilities; return window.feedBack && window.feedBack.capabilities;
} }
function snapshot() { function snapshot() {
@@ -319,7 +319,7 @@
lifecycle: review.lifecycle || 'plugin-defined', lifecycle: review.lifecycle || 'plugin-defined',
label: review.label || 'Plugin-defined', label: review.label || 'Plugin-defined',
tone: review.tone || 'info', tone: review.tone || 'info',
summary: review.summary || 'Declared by a plugin or test fixture rather than registered as a core Slopsmith domain.', summary: review.summary || 'Declared by a plugin or test fixture rather than registered as a core FeedBack domain.',
}; };
} }
@@ -1087,7 +1087,7 @@
const groups = new Map(); const groups = new Map();
for (const expected of Array.isArray(expectedShims) ? expectedShims : []) { for (const expected of Array.isArray(expectedShims) ? expectedShims : []) {
const surface = String(expected && expected.legacySurface || ''); const surface = String(expected && expected.legacySurface || '');
const eventMatch = surface.match(/^window\.slopsmith\.(emit|on):(.+)$/); const eventMatch = surface.match(/^window\.feedBack\.(emit|on):(.+)$/);
const key = eventMatch ? eventMatch[2] : surface; const key = eventMatch ? eventMatch[2] : surface;
const type = eventMatch ? (eventMatch[1] === 'emit' ? 'emit' : 'listener') : 'surface'; const type = eventMatch ? (eventMatch[1] === 'emit' ? 'emit' : 'listener') : 'surface';
const entry = groups.get(key) || { group: key, emit: null, listener: null, surfaces: [] }; const entry = groups.get(key) || { group: key, emit: null, listener: null, surfaces: [] };
@@ -1461,14 +1461,14 @@
} }
function audioSessionSnapshot() { function audioSessionSnapshot() {
const api = window.slopsmith && window.slopsmith.audioSession; const api = window.feedBack && window.feedBack.audioSession;
if (!api || typeof api.snapshot !== 'function') return null; if (!api || typeof api.snapshot !== 'function') return null;
try { return api.snapshot(); } try { return api.snapshot(); }
catch (_) { return null; } catch (_) { return null; }
} }
function playbackSnapshot() { function playbackSnapshot() {
const api = window.slopsmith && window.slopsmith.playback; const api = window.feedBack && window.feedBack.playback;
if (!api || typeof api.snapshot !== 'function') return null; if (!api || typeof api.snapshot !== 'function') return null;
try { return api.snapshot({ exportMode: 'local-inspector' }); } try { return api.snapshot({ exportMode: 'local-inspector' }); }
catch (_) { return null; } catch (_) { return null; }
@@ -1747,6 +1747,6 @@
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', install); if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', install);
else install(); else install();
window.addEventListener('slopsmith:capabilities:ready', render); window.addEventListener('feedBack:capabilities:ready', render);
window.addEventListener('slopsmith:capabilities:changed', scheduleRender); window.addEventListener('feedBack:capabilities:changed', scheduleRender);
})(); })();
+15 -15
View File
@@ -2,9 +2,9 @@
This guide tells future AI assistants where each visual element lives in `screen.js`, what controls it, and the gotchas to watch for. The goal is for small polishes (color tweaks, sizing, animation timing, add/remove a label) to land in the right place on the first try without grep spelunking. This guide tells future AI assistants where each visual element lives in `screen.js`, what controls it, and the gotchas to watch for. The goal is for small polishes (color tweaks, sizing, animation timing, add/remove a label) to land in the right place on the first try without grep spelunking.
The whole renderer is **one file**`screen.js`, wrapped in an IIFE, registered as `window.slopsmithViz_highway_3d` (a slopsmith#36 setRenderer factory). No imports beyond Three.js loaded from the vendored `/static/vendor/three/three.module.min.js` (pinned r170; swapped from CDN when bundled into core). The whole renderer is **one file**`screen.js`, wrapped in an IIFE, registered as `window.feedBackViz_highway_3d` (a feedBack#36 setRenderer factory). No imports beyond Three.js loaded from the vendored `/static/vendor/three/three.module.min.js` (pinned r170; swapped from CDN when bundled into core).
**Styling (slopsmith `styles` capability).** This plugin owns its Tailwind CSS: it ships `assets/plugin.css` and declares `"styles": "assets/plugin.css"` in `plugin.json`, so core's prebuilt `static/tailwind.min.css` no longer scans it (it's excluded from core's content globs). The frontend injects `assets/plugin.css` as a `<link>` when the renderer activates. This is the one maintainer-time build step: after you add/change a Tailwind class in `screen.js` or `settings.html`, run `bash build-tailwind.sh` (pinned `tailwindcss@3.4.19`, `corePlugins.preflight=false` — utilities only) and **bump the `version` in `plugin.json`** so the injected `<link>`'s `?v=` cache-buster fetches the fresh file. The generated `assets/plugin.css` is committed; end users never build. See [docs/plugin-styles.md](../../docs/plugin-styles.md). **Styling (feedBack `styles` capability).** This plugin owns its Tailwind CSS: it ships `assets/plugin.css` and declares `"styles": "assets/plugin.css"` in `plugin.json`, so core's prebuilt `static/tailwind.min.css` no longer scans it (it's excluded from core's content globs). The frontend injects `assets/plugin.css` as a `<link>` when the renderer activates. This is the one maintainer-time build step: after you add/change a Tailwind class in `screen.js` or `settings.html`, run `bash build-tailwind.sh` (pinned `tailwindcss@3.4.19`, `corePlugins.preflight=false` — utilities only) and **bump the `version` in `plugin.json`** so the injected `<link>`'s `?v=` cache-buster fetches the fresh file. The generated `assets/plugin.css` is committed; end users never build. See [docs/plugin-styles.md](../../docs/plugin-styles.md).
> **Navigation note:** This guide references functions by name and uses the existing banner comments (`/* ── Scene initialisation ─ */`, etc.) as section anchors. Line numbers are deliberately avoided so this stays correct as the file evolves. Use `Grep` for the function name or banner text to jump to a section. > **Navigation note:** This guide references functions by name and uses the existing banner comments (`/* ── Scene initialisation ─ */`, etc.) as section anchors. Line numbers are deliberately avoided so this stays correct as the file evolves. Use `Grep` for the function name or banner text to jump to a section.
@@ -15,7 +15,7 @@ The file is laid out top-to-bottom as:
1. **Constants block** — palette (`S_COL`), scale (`SCALE`, `K`), fret/string counts, geometry sizes, camera, fog 1. **Constants block** — palette (`S_COL`), scale (`SCALE`, `K`), fret/string counts, geometry sizes, camera, fog
2. **Pure helpers**`fretX`, `fretMid`, `dZ`, `computeBPM` 2. **Pure helpers**`fretX`, `fretMid`, `dZ`, `computeBPM`
3. **Three.js loader**`loadThree()` (loads vendored `/static/vendor/three/three.module.min.js`, memoized) 3. **Three.js loader**`loadThree()` (loads vendored `/static/vendor/three/three.module.min.js`, memoized)
4. **Splitscreen helpers**`_ssActive`, `_ssIsCanvasFocused` (read `window.slopsmithSplitscreen`) 4. **Splitscreen helpers**`_ssActive`, `_ssIsCanvasFocused` (read `window.feedBackSplitscreen`)
5. **`createFactory()`** — the rest of the file is one big closure 5. **`createFactory()`** — the rest of the file is one big closure
- Per-instance state (Three.js refs, pools, camera state, lifecycle flags) - Per-instance state (Three.js refs, pools, camera state, lifecycle flags)
- `txtMat()` text-sprite cache, `pool()` factory - `txtMat()` text-sprite cache, `pool()` factory
@@ -63,7 +63,7 @@ Each entry names the function or banner you should grep for, plus key sub-blocks
### Strings ### Strings
- **String colors**`S_COL` array in the top-level constants block. Eight-element vibrant palette; index `s` is the string (0 = high E for guitar). `MAX_RENDER_STRINGS` keys off `S_COL.length`. - **String colors**`S_COL` array in the top-level constants block. Eight-element vibrant palette; index `s` is the string (0 = high E for guitar). `MAX_RENDER_STRINGS` keys off `S_COL.length`.
- **String count for the active arrangement**`resolveStringCount(bundle)` (top-level helper). Reads `bundle.stringCount` (slopsmith#93) with a `bass`-name fallback. Don't reintroduce `tuning.length` — see Pitfall #4. - **String count for the active arrangement**`resolveStringCount(bundle)` (top-level helper). Reads `bundle.stringCount` (feedBack#93) with a `bass`-name fallback. Don't reintroduce `tuning.length` — see Pitfall #4.
- **String thickness / gap / base Y**`STR_THICK`, `S_BASE`, `S_GAP` constants. - **String thickness / gap / base Y**`STR_THICK`, `S_BASE`, `S_GAP` constants.
- **String-to-Y mapping (respects invert)** → the `sY(s)` arrow function inside `createFactory()`. Single source of truth for "where on Y is string s." - **String-to-Y mapping (respects invert)** → the `sY(s)` arrow function inside `createFactory()`. Single source of truth for "where on Y is string s."
- **Static string mesh creation**`buildBoard()`, the `// Thin Line strings (glow layer)` and `// BoxGeometry strings — emissive glow ...` comment blocks. Two layers: low-opacity `Line` for soft glow, `BoxGeometry` mesh per string with its own material clone (kept in `stringLines[]` for live emissive updates). - **Static string mesh creation**`buildBoard()`, the `// Thin Line strings (glow layer)` and `// BoxGeometry strings — emissive glow ...` comment blocks. Two layers: low-opacity `Line` for soft glow, `BoxGeometry` mesh per string with its own material clone (kept in `stringLines[]` for live emissive updates).
@@ -87,7 +87,7 @@ Each entry names the function or banner you should grep for, plus key sub-blocks
- **Technique markers** (bend, slide, hammer/pull/tap, accent, tremolo, palm-mute, pinch harmonic) → `// ── Technique labels ──` block in `drawNote()`. Most are small if-blocks using `txtMat(text, color, wide, style)` (cached sprite material; `'technique'` preset in `TXT_STYLES`). Exceptions: a **bend** draws a string-coloured chevron strength stack (`bendChevronMat`, one chevron per half-step), and **hammer-on / pull-off** draw a white ▲/▼ triangle with a string-coloured border (`triMat`) — both pinned to the gem; the bend ribbon's up→hold→down contour is driven by `bendSemisAtTime`. - **Technique markers** (bend, slide, hammer/pull/tap, accent, tremolo, palm-mute, pinch harmonic) → `// ── Technique labels ──` block in `drawNote()`. Most are small if-blocks using `txtMat(text, color, wide, style)` (cached sprite material; `'technique'` preset in `TXT_STYLES`). Exceptions: a **bend** draws a string-coloured chevron strength stack (`bendChevronMat`, one chevron per half-step), and **hammer-on / pull-off** draw a white ▲/▼ triangle with a string-coloured border (`triMat`) — both pinned to the gem; the bend ribbon's up→hold→down contour is driven by `bendSemisAtTime`.
- **Open-string note** → special-cased throughout `drawNote()`: `n.f === 0`. Wider/flatter geometry, "0" label sprite, uses `openX` (the chord's open-string centroid) when supplied. - **Open-string note** → special-cased throughout `drawNote()`: `n.f === 0`. Wider/flatter geometry, "0" label sprite, uses `openX` (the chord's open-string centroid) when supplied.
- **Board projection ("ghost" preview)**`// ── Board projection ──` block in `drawNote()`. Two meshes per string (`projMeshArr`, `projGlowArr`), one visible per frame for the next note. Linger window `PROJ_WIN`. Gated on the `projectionVisible` setting (BG_DEFAULTS / `h3dBgSetProjectionVisible` / the "Show note preview on the fretboard" checkbox in `settings.html`) — when off, the block is skipped and `update()`'s per-frame `m.visible = false` reset leaves the ghost hidden. **The glow has `renderOrder = -1`** which fights the strings — see Pitfall #6. - **Board projection ("ghost" preview)**`// ── Board projection ──` block in `drawNote()`. Two meshes per string (`projMeshArr`, `projGlowArr`), one visible per frame for the next note. Linger window `PROJ_WIN`. Gated on the `projectionVisible` setting (BG_DEFAULTS / `h3dBgSetProjectionVisible` / the "Show note preview on the fretboard" checkbox in `settings.html`) — when off, the block is skipped and `update()`'s per-frame `m.visible = false` reset leaves the ghost hidden. **The glow has `renderOrder = -1`** which fights the strings — see Pitfall #6.
- **Note-hit "sizzle" (slopsmith#254)**`drawNotedetectSizzle()` (called from the `lyricsCtx` block in `draw()`, just before `drawNotedetectLabels()`). For each confirmed hit/active note (`_ndGood` in `drawNote()` pushes `{x, y, z, s, alpha, color}` onto the per-frame `_ndSizzle` array — `alpha` is the provider's clamped fade, `color` an optional palette override), it projects the note's world point through the up-to-date `cam`, sizes the burst from a fretboard-X-axis offset projection (reliable even when the note's rotated flat at the line), and twinkles a few short crackling ellipse-arc segments + tiny dots hugging the note's rectangle — re-randomised every frame, contained to ≲1.4× the note, half white / half the string colour (or the provider's `color` when given). Every dot/arc's `globalAlpha` and `shadowBlur` are scaled by the entry's `alpha`, and the per-element "off-this-frame" probability rises as `alpha` decays, so a struck-note glow visibly thins and fades. Also: `_ndGood` swaps the note's outline to `mGlow[s]` (bright string-tinted, not green). Knobs are inline: arc/dot count, base on-probability, line widths, `shadowBlur`, spread radii. Lives entirely on the 2D overlay layer — no Three.js geometry/disposal. - **Note-hit "sizzle" (feedBack#254)**`drawNotedetectSizzle()` (called from the `lyricsCtx` block in `draw()`, just before `drawNotedetectLabels()`). For each confirmed hit/active note (`_ndGood` in `drawNote()` pushes `{x, y, z, s, alpha, color}` onto the per-frame `_ndSizzle` array — `alpha` is the provider's clamped fade, `color` an optional palette override), it projects the note's world point through the up-to-date `cam`, sizes the burst from a fretboard-X-axis offset projection (reliable even when the note's rotated flat at the line), and twinkles a few short crackling ellipse-arc segments + tiny dots hugging the note's rectangle — re-randomised every frame, contained to ≲1.4× the note, half white / half the string colour (or the provider's `color` when given). Every dot/arc's `globalAlpha` and `shadowBlur` are scaled by the entry's `alpha`, and the per-element "off-this-frame" probability rises as `alpha` decays, so a struck-note glow visibly thins and fades. Also: `_ndGood` swaps the note's outline to `mGlow[s]` (bright string-tinted, not green). Knobs are inline: arc/dot count, base on-probability, line widths, `shadowBlur`, spread radii. Lives entirely on the 2D overlay layer — no Three.js geometry/disposal.
### Chords ### Chords
- **Chord rendering loop**`update()`, `// ── Chords ──` block. Iterates `bundle.chords`, calls `drawNote()` per chord-note, then draws the frame box, name label, and barre indicator. - **Chord rendering loop**`update()`, `// ── Chords ──` block. Iterates `bundle.chords`, calls `drawNote()` per chord-note, then draws the frame box, name label, and barre indicator.
@@ -131,7 +131,7 @@ Each entry names the function or banner you should grep for, plus key sub-blocks
## The `bundle` object ## The `bundle` object
Every per-frame renderer call receives a `bundle` from slopsmith core. Fields used by this plugin: Every per-frame renderer call receives a `bundle` from feedBack core. Fields used by this plugin:
- `currentTime` — playback time in seconds (drives `dt` for everything) - `currentTime` — playback time in seconds (drives `dt` for everything)
- `notes`, `chords`, `beats`, `sections` — chart arrays (already difficulty-filtered by core) - `notes`, `chords`, `beats`, `sections` — chart arrays (already difficulty-filtered by core)
@@ -141,9 +141,9 @@ Every per-frame renderer call receives a `bundle` from slopsmith core. Fields us
- `lyricsVisible` — gate for lyrics overlay - `lyricsVisible` — gate for lyrics overlay
- `renderScale` — pixel-ratio multiplier from the user's quality setting - `renderScale` — pixel-ratio multiplier from the user's quality setting
- `songInfo.arrangement` — only field of `songInfo` this plugin reads, used as the bass-name fallback in `resolveStringCount()` - `songInfo.arrangement` — only field of `songInfo` this plugin reads, used as the bass-name fallback in `resolveStringCount()`
- `stringCount`slopsmith#93; always prefer this over deriving from tuning/arrangement - `stringCount`feedBack#93; always prefer this over deriving from tuning/arrangement
- `lefty` — display flag consumed by this renderer from `bundle.lefty`. Captured into `_leftyCached` before each frame so `xFret()`, `xFretMid()`, `boardSpanX()`, board geometry, note placement, and the camera shoulder offset mirror the fret axis for left-handed mode. A runtime lefty flip rebuilds board state and mirrors `curX`/`tgtX` plus the lookahead camera X cache so the camera does not drift across the neck. - `lefty` — display flag consumed by this renderer from `bundle.lefty`. Captured into `_leftyCached` before each frame so `xFret()`, `xFretMid()`, `boardSpanX()`, board geometry, note placement, and the camera shoulder offset mirror the fret axis for left-handed mode. A runtime lefty flip rebuilds board state and mirrors `curX`/`tgtX` plus the lookahead camera X cache so the camera does not drift across the neck.
- `getNoteState(note, chartTime)`slopsmith#254; per-note judgment from a scorer (note_detect). Captured each frame into `_ndGetNoteState` at the top of `update()` and consulted in `drawNote()` AFTER the event-driven `_ndHitMarks`/`_ndMissMarks` lookup AND over the proximity-based `hit` heuristic, both of which it overrides when it has a verdict: `'hit'`/`'active'``mGlow[s]` outline (bright string-tinted, *not* green) + `mGlow[s]` body + `mGlow[s]` sustain trail + a queue entry for `drawNotedetectSizzle` (so a held sustain keeps glowing/sparkling as long as the provider keeps returning `'active'`); `'miss'``mMissOutline` and `_showHit = false` (suppresses the bright body even if the note is near the line). Called with the note's chart time (`n.t`), which is how note_detect keys its `noteResults` map — *not* `now`. Returns null on cores without the API or songs with no scorer — then the event path / `hit` heuristic drive feedback for older note_detect builds. **notedetect ≥1.13 object verdicts additionally carry `{ points, mult, popKey }`** (game-scoring layer): `points` is the note's awarded score, `mult` the multiplier tier it landed at, and `popKey` a dedup key — chord members all return the chord-level judgment's key so a chord pops once, not once per gem. Consumed by the score-pop spawn in `drawNote()` (see Score FX below); all three are absent on older notedetect builds, so guard with `!== undefined`. - `getNoteState(note, chartTime)`feedBack#254; per-note judgment from a scorer (note_detect). Captured each frame into `_ndGetNoteState` at the top of `update()` and consulted in `drawNote()` AFTER the event-driven `_ndHitMarks`/`_ndMissMarks` lookup AND over the proximity-based `hit` heuristic, both of which it overrides when it has a verdict: `'hit'`/`'active'``mGlow[s]` outline (bright string-tinted, *not* green) + `mGlow[s]` body + `mGlow[s]` sustain trail + a queue entry for `drawNotedetectSizzle` (so a held sustain keeps glowing/sparkling as long as the provider keeps returning `'active'`); `'miss'``mMissOutline` and `_showHit = false` (suppresses the bright body even if the note is near the line). Called with the note's chart time (`n.t`), which is how note_detect keys its `noteResults` map — *not* `now`. Returns null on cores without the API or songs with no scorer — then the event path / `hit` heuristic drive feedback for older note_detect builds. **notedetect ≥1.13 object verdicts additionally carry `{ points, mult, popKey }`** (game-scoring layer): `points` is the note's awarded score, `mult` the multiplier tier it landed at, and `popKey` a dedup key — chord members all return the chord-level judgment's key so a chord pops once, not once per gem. Consumed by the score-pop spawn in `drawNote()` (see Score FX below); all three are absent on older notedetect builds, so guard with `!== undefined`.
`tuning` and `capo` aren't consumed by this plugin. `tuning` and `capo` aren't consumed by this plugin.
@@ -151,11 +151,11 @@ Every per-frame renderer call receives a `bundle` from slopsmith core. Fields us
- **"+N" score pops** → `_fxSpawnPop()` from `drawNote()` (just after the provider verdict-override block), drawn by `drawScoreFx()` (called from the `lyricsCtx` block in `draw()`, right after `drawNotedetectLabels()`). Fixed 24-slot pool (`_fxPops`), deduped per `popKey` via the TTL'd `_fxSeen` map (pruned in `drawScoreFx`). Pops rise/fade over 700 ms; font size scales with the multiplier tier. - **"+N" score pops** → `_fxSpawnPop()` from `drawNote()` (just after the provider verdict-override block), drawn by `drawScoreFx()` (called from the `lyricsCtx` block in `draw()`, right after `drawNotedetectLabels()`). Fixed 24-slot pool (`_fxPops`), deduped per `popKey` via the TTL'd `_fxSeen` map (pruned in `drawScoreFx`). Pops rise/fade over 700 ms; font size scales with the multiplier tier.
- **Session FX**`notedetect:fx` events (`{ fxType: 'multiplier'|'milestone'|'streakBreak', ... }`). notedetect dispatches each detail object twice in the same task: on `window` (unscoped, first) and as a bubbling CustomEvent from its per-panel instanceRoot (scoped, second). The listener (`_fxOnFx`, bound with the other notedetect listeners) treats element-targeted copies as authoritative — accepted only when their root lives in this panel's container — and **defers the window copy by a task** (`setTimeout 0`): if the element copy (same detail reference) arrived meanwhile it's dropped as a duplicate, otherwise it's the compat fallback for a detector whose root isn't in the DOM. This keeps splitscreen panels from rendering each other's FX even for the first event of a session. Effects: milestone → particle burst from a 4-slot Float32Array pool (`_fxBursts`), multiplier tier-up → expanding ring pulse at the strike-line centre, streak break → brief red wash. - **Session FX**`notedetect:fx` events (`{ fxType: 'multiplier'|'milestone'|'streakBreak', ... }`). notedetect dispatches each detail object twice in the same task: on `window` (unscoped, first) and as a bubbling CustomEvent from its per-panel instanceRoot (scoped, second). The listener (`_fxOnFx`, bound with the other notedetect listeners) treats element-targeted copies as authoritative — accepted only when their root lives in this panel's container — and **defers the window copy by a task** (`setTimeout 0`): if the element copy (same detail reference) arrived meanwhile it's dropped as a duplicate, otherwise it's the compat fallback for a detector whose root isn't in the DOM. This keeps splitscreen panels from rendering each other's FX even for the first event of a session. Effects: milestone → particle burst from a 4-slot Float32Array pool (`_fxBursts`), multiplier tier-up → expanding ring pulse at the strike-line centre, streak break → brief red wash.
- **Skin palette**`_fxResolvePalette()` reads `localStorage['slopsmith_notedetect_skin']` (`neon`/`esports`/`metal``_FX_PALETTES`) at listener-bind time and on the `notedetect:skin` bus event. The display fonts are document-loaded by notedetect's stylesheet, so the overlay canvas can reference the family names directly. - **Skin palette**`_fxResolvePalette()` reads `localStorage['feedBack_notedetect_skin']` (`neon`/`esports`/`metal``_FX_PALETTES`) at listener-bind time and on the `notedetect:skin` bus event. The display fonts are document-loaded by notedetect's stylesheet, so the overlay canvas can reference the family names directly.
- Everything lives on the 2D overlay layer — no Three.js geometry, no `txtMat()` cache traffic, nothing to dispose; `teardown()` deactivates the pools and removes both listeners. - Everything lives on the 2D overlay layer — no Three.js geometry, no `txtMat()` cache traffic, nothing to dispose; `teardown()` deactivates the pools and removes both listeners.
- **This block is the reference implementation for other renderer plugins** (drum highway, piano, custom highways) that want score pops / session FX: copy the `_fxOnFx` dedup+scoping listener, the `popKey`-keyed seen-map (cleared on backward seek), and the `_FX_PALETTES` skin mapping. The full consumer contract (events, payloads, provider verdict fields, theming variables) is documented in slopsmith-plugin-notedetect's `CLAUDE.md`. - **This block is the reference implementation for other renderer plugins** (drum highway, piano, custom highways) that want score pops / session FX: copy the `_fxOnFx` dedup+scoping listener, the `popKey`-keyed seen-map (cleared on backward seek), and the `_FX_PALETTES` skin mapping. The full consumer contract (events, payloads, provider verdict fields, theming variables) is documented in feedBack-plugin-notedetect's `CLAUDE.md`.
If you need a bundle field that isn't here yet, check `_makeBundle()` in `static/highway.js` in the **slopsmith core repo** — this is the plugin repo, `static/highway.js` is not here. The full path in the parent slopsmith checkout is `slopsmith/static/highway.js`. If you need a bundle field that isn't here yet, check `_makeBundle()` in `static/highway.js` in the **feedBack core repo** — this is the plugin repo, `static/highway.js` is not here. The full path in the parent feedBack checkout is `feedBack/static/highway.js`.
## Per-string state arrays ## Per-string state arrays
@@ -185,7 +185,7 @@ If a pool's mesh has per-instance state (its own material clone, its own texture
1. **Adding a new pool? Reset it.** The reset block at the top of `update()` is easy to miss when adding a new pool elsewhere. 1. **Adding a new pool? Reset it.** The reset block at the top of `update()` is easy to miss when adding a new pool elsewhere.
2. **`txtMat()` is cache-keyed by `(style, text, color, wide)`.** Calling it with a numeric `text` works (it's coerced via `String(...)`), but new label content creates a new texture forever. Don't generate dynamic per-frame text (e.g. interpolated values) through `txtMat()` or you'll leak GPU memory. For static labels that change occasionally (chord names, fret numbers), the cache is fine. The `style` arg picks a preset from the `TXT_STYLES` table — see "Tweaking text-sprite styling" below. 2. **`txtMat()` is cache-keyed by `(style, text, color, wide)`.** Calling it with a numeric `text` works (it's coerced via `String(...)`), but new label content creates a new texture forever. Don't generate dynamic per-frame text (e.g. interpolated values) through `txtMat()` or you'll leak GPU memory. For static labels that change occasionally (chord names, fret numbers), the cache is fine. The `style` arg picks a preset from the `TXT_STYLES` table — see "Tweaking text-sprite styling" below.
3. **Disposal in `teardown()` matters.** Three.js doesn't garbage-collect GPU resources. Every `material.dispose()`, `geometry.dispose()`, `map.dispose()`, and `ren.dispose()` call there is load-bearing. `teardown()` is called from `init()` (when re-initing), `destroy()` (setRenderer swap or `highway.stop()`), and on init failure. 3. **Disposal in `teardown()` matters.** Three.js doesn't garbage-collect GPU resources. Every `material.dispose()`, `geometry.dispose()`, `map.dispose()`, and `ren.dispose()` call there is load-bearing. `teardown()` is called from `init()` (when re-initing), `destroy()` (setRenderer swap or `highway.stop()`), and on init failure.
4. **Don't use `tuning.length` for string count.** `bundle.tuning` (and `arr.tuning` server-side) is always 6 elements even for bass — slopsmith pre-fills the array with zeros for unused strings. Use `bundle.stringCount` (slopsmith#93), with `/bass/i.test(arrangement)` as the only acceptable fallback. There's a comment in `resolveStringCount()` documenting this. 4. **Don't use `tuning.length` for string count.** `bundle.tuning` (and `arr.tuning` server-side) is always 6 elements even for bass — feedBack pre-fills the array with zeros for unused strings. Use `bundle.stringCount` (feedBack#93), with `/bass/i.test(arrangement)` as the only acceptable fallback. There's a comment in `resolveStringCount()` documenting this.
5. **lyricsCanvas DOM order.** The 2D overlay canvas is appended to `wrap` AFTER `ren.domElement` and given `z-index:1`. This is the empirically-correct order — earlier versions had it before the WebGL canvas, which broke in splitscreen panels with `position:relative; overflow:hidden`. Don't reorder without testing both modes. 5. **lyricsCanvas DOM order.** The 2D overlay canvas is appended to `wrap` AFTER `ren.domElement` and given `z-index:1`. This is the empirically-correct order — earlier versions had it before the WebGL canvas, which broke in splitscreen panels with `position:relative; overflow:hidden`. Don't reorder without testing both modes.
6. **Projection glow `renderOrder = -1`** in `initScene()`. This is a known-suboptimal setting — it forces the glow to draw before the strings in the transparent queue, so the string visibly cuts through the preview. Removing the line lets natural Z-sort layer it correctly. Plus the projection's world-Y matches the string Y, which after perspective projection puts the preview slightly screen-lower than the string; bumping `projY = y + NH * 0.4` recenters it. (Both fixes live on the `fix/preview-stacking` branch.) 6. **Projection glow `renderOrder = -1`** in `initScene()`. This is a known-suboptimal setting — it forces the glow to draw before the strings in the transparent queue, so the string visibly cuts through the preview. Removing the line lets natural Z-sort layer it correctly. Plus the projection's world-Y matches the string Y, which after perspective projection puts the preview slightly screen-lower than the string; bumping `projY = y + NH * 0.4` recenters it. (Both fixes live on the `fix/preview-stacking` branch.)
7. **`renderOrder` on transparent objects is sticky.** Three.js sorts the transparent queue by `renderOrder` first, then back-to-front. A stray `m.renderOrder = -1` on something will pull it under everything regardless of Z. When in doubt, leave `renderOrder` at the default 0 and rely on Z position. 7. **`renderOrder` on transparent objects is sticky.** Three.js sorts the transparent queue by `renderOrder` first, then back-to-front. A stray `m.renderOrder = -1` on something will pull it under everything regardless of Z. When in doubt, leave `renderOrder` at the default 0 and rely on Z position.
@@ -231,21 +231,21 @@ Style fields:
## Lifecycle (setRenderer contract) ## Lifecycle (setRenderer contract)
Per slopsmith#36, the factory returns `{ init, draw, resize, destroy }`: Per feedBack#36, the factory returns `{ init, draw, resize, destroy }`:
- **`init(canvas, bundle)`** tears down any prior state, sets `highwayCanvas`, lazily loads Three.js, runs `initScene()`, calls `applySize()` (with a `retrySize` rAF loop fallback if the canvas isn't laid out yet). - **`init(canvas, bundle)`** tears down any prior state, sets `highwayCanvas`, lazily loads Three.js, runs `initScene()`, calls `applySize()` (with a `retrySize` rAF loop fallback if the canvas isn't laid out yet).
- **`draw(bundle)`** is gated on `_isReady`. Re-resolves `nStr` / inverted / renderScale, then `update(bundle) → camUpdate(bundle) → ren.render → 2D overlays`. The `_lastHwW/_lastHwH` check at the top auto-resizes when the splitscreen plugin bypasses `resize()`. - **`draw(bundle)`** is gated on `_isReady`. Re-resolves `nStr` / inverted / renderScale, then `update(bundle) → camUpdate(bundle) → ren.render → 2D overlays`. The `_lastHwW/_lastHwH` check at the top auto-resizes when the splitscreen plugin bypasses `resize()`.
- **`resize(w, h)`** is gated on `_isReady`. Just calls `applySize()`. - **`resize(w, h)`** is gated on `_isReady`. Just calls `applySize()`.
- **`destroy()`** is idempotent. Sets flags, runs `teardown()`, drops `highwayCanvas`. Tolerates being called on an instance that's been destroyed and re-init'd already (resets `_lastHwW/H`, `_diagChord`, etc.). - **`destroy()`** is idempotent. Sets flags, runs `teardown()`, drops `highwayCanvas`. Tolerates being called on an instance that's been destroyed and re-init'd already (resets `_lastHwW/H`, `_diagChord`, etc.).
The factory **returns a fresh instance per call**, so splitscreen's per-panel `setRenderer(slopsmithViz_highway_3d())` gets independent state per panel — important because the chord diagram, projection meshes, etc. are all per-instance. The factory **returns a fresh instance per call**, so splitscreen's per-panel `setRenderer(feedBackViz_highway_3d())` gets independent state per panel — important because the chord diagram, projection meshes, etc. are all per-instance.
## Branching / PR conventions ## Branching / PR conventions
- Feature branches off `main`, descriptive name (e.g. `fix/preview-stacking`, `feat/palette-picker`). - Feature branches off `main`, descriptive name (e.g. `fix/preview-stacking`, `feat/palette-picker`).
- PR target: target the contributor's own fork by default unless they ask otherwise; confirm before opening a PR upstream. Run `git remote -v` in this directory to see the remotes that are configured locally. - PR target: target the contributor's own fork by default unless they ask otherwise; confirm before opening a PR upstream. Run `git remote -v` in this directory to see the remotes that are configured locally.
- Commit messages: short imperative subject, optional body explaining *why*. Don't summarize the diff — the diff already does that. - Commit messages: short imperative subject, optional body explaining *why*. Don't summarize the diff — the diff already does that.
- This plugin is bundled **in-tree** at `plugins/highway_3d/` inside the `got-feedback/feedback` repository (not a gitlink/submodule). It ships with the default container image. Changes go through the normal slopsmith PR process — no separate upstream repo to sync. - This plugin is bundled **in-tree** at `plugins/highway_3d/` inside the `got-feedback/feedBack` repository (not a gitlink/submodule). It ships with the default container image. Changes go through the normal feedBack PR process — no separate upstream repo to sync.
## When in doubt ## When in doubt
+6 -6
View File
@@ -1,6 +1,6 @@
# 3D Highway # 3D Highway
A 3D note highway visualization for [Slopsmith](https://github.com/got-feedback/feedback) — an alternative to the default 2D highway, with a sense of depth and perspective inspired by stage views in modern rhythm games. A 3D note highway visualization for [FeedBack](https://github.com/got-feedback/feedBack) — an alternative to the default 2D highway, with a sense of depth and perspective inspired by stage views in modern rhythm games.
## What you get ## What you get
@@ -19,15 +19,15 @@ A 3D note highway visualization for [Slopsmith](https://github.com/got-feedback/
## Install ## Install
3D Highway ships **bundled** with Slopsmith — no separate installation needed. Pick **3D Highway** from the visualization picker in the player. 3D Highway ships **bundled** with FeedBack — no separate installation needed. Pick **3D Highway** from the visualization picker in the player.
> **Note:** The bundled version is preferred over any user-installed copy with the same plugin ID. If you have an old `slopsmith-plugin-3dhighway` clone on disk (from before 3D Highway was promoted to core), it will be ignored at startup — a warning in the server log names the path of the discarded copy. You can safely delete the stale clone. > **Note:** The bundled version is preferred over any user-installed copy with the same plugin ID. If you have an old `feedBack-plugin-3dhighway` clone on disk (from before 3D Highway was promoted to core), it will be ignored at startup — a warning in the server log names the path of the discarded copy. You can safely delete the stale clone.
> >
> **Fallback:** In the unlikely event that the bundled copy fails to load its routes (e.g., a broken bundled release), Slopsmith will automatically fall back to your user-installed copy and show a yellow "Fallback" badge in the Settings panel. Check the server startup log for the root cause in that case. > **Fallback:** In the unlikely event that the bundled copy fails to load its routes (e.g., a broken bundled release), FeedBack will automatically fall back to your user-installed copy and show a yellow "Fallback" badge in the Settings panel. Check the server startup log for the root cause in that case.
## Settings ## Settings
Most of the visual controls (background style, intensity, audio reactivity, color palette) live on Slopsmith's **Settings** screen under the *3D Highway* section. Most of the visual controls (background style, intensity, audio reactivity, color palette) live on FeedBack's **Settings** screen under the *3D Highway* section.
## Contributing / development ## Contributing / development
@@ -35,4 +35,4 @@ For maintainers and AI assistants working on the codebase, see [`CLAUDE.md`](CLA
### Perf bench (`?h3dbench=1`) ### Perf bench (`?h3dbench=1`)
Append `?h3dbench=1` to the player URL to enable opt-in `console.log` reporting of `update()` self-time, broken into six segments — `frame` (everything between `pbBeg(0)` at the top of `update()` and `pbEnd(0)` at the bottom; excludes the trailing `pbReportTick()` logging that fires after `pbEnd(0)`), `state` (per-frame state-derivation loop), `next` (next-note-by-string lookahead), `mat` (per-string material writes), `noteDraw` (single-note draw loop), `chordDraw` (chord draw loop). Reported every 5 seconds with p50 / p95 / max per segment and frame count, so before/after numbers on a target chart are reproducible (slopsmith#226). Off-by-default; the bench helpers (`pbBeg` / `pbEnd` / `pbReportTick`) are bound to a shared empty-function literal when the renderer instance is created (each `createHighway()` panel re-checks the flag), so the hot-path call sites are no-ops with negligible overhead (typically JIT-inlined). Append `?h3dbench=1` to the player URL to enable opt-in `console.log` reporting of `update()` self-time, broken into six segments — `frame` (everything between `pbBeg(0)` at the top of `update()` and `pbEnd(0)` at the bottom; excludes the trailing `pbReportTick()` logging that fires after `pbEnd(0)`), `state` (per-frame state-derivation loop), `next` (next-note-by-string lookahead), `mat` (per-string material writes), `noteDraw` (single-note draw loop), `chordDraw` (chord draw loop). Reported every 5 seconds with p50 / p95 / max per segment and frame count, so before/after numbers on a target chart are reproducible (feedBack#226). Off-by-default; the bench helpers (`pbBeg` / `pbEnd` / `pbReportTick`) are bound to a shared empty-function literal when the renderer instance is created (each `createHighway()` panel re-checks the flag), so the hot-path call sites are no-ops with negligible overhead (typically JIT-inlined).
+2 -2
View File
@@ -1,8 +1,8 @@
"""Plugin-registered FastAPI routes for the 3dhighway visualization plugin. """Plugin-registered FastAPI routes for the 3dhighway visualization plugin.
Registered by slopsmith core via plugin.json's "routes" field — the Registered by feedBack core via plugin.json's "routes" field — the
loader at plugins/__init__.py:589604 imports this module and calls loader at plugins/__init__.py:589604 imports this module and calls
setup(app, context). context["config_dir"] points at the slopsmith setup(app, context). context["config_dir"] points at the feedBack
data directory; we namespace user uploads under data directory; we namespace user uploads under
{config_dir}/plugin_uploads/highway_3d/. {config_dir}/plugin_uploads/highway_3d/.
+67 -67
View File
@@ -2,7 +2,7 @@
// Visual layer from joel's prototype (vibrant palette, glowing strings, // Visual layer from joel's prototype (vibrant palette, glowing strings,
// fret heat, dynamic lane, chord frame-boxes, per-note connector labels, // fret heat, dynamic lane, chord frame-boxes, per-note connector labels,
// board projection, outline+core note meshes) adapted into the // board projection, outline+core note meshes) adapted into the
// slopsmithViz setRenderer contract (slopsmith#36) so it works in the // feedBackViz setRenderer contract (feedBack#36) so it works in the
// main player and per-panel in splitscreen without any architectural // main player and per-panel in splitscreen without any architectural
// changes. // changes.
@@ -27,12 +27,12 @@
// high E=purple); Neon pushes saturation harder; Pastel desaturates // high E=purple); Neon pushes saturation harder; Pastel desaturates
// for long-session comfort; Colorblind (high contrast) is derived from // for long-session comfort; Colorblind (high contrast) is derived from
// the chart format's built-in colorblind-mode palette, but this preset // the chart format's built-in colorblind-mode palette, but this preset
// intentionally keeps some entries tuned for slopsmith rather than // intentionally keeps some entries tuned for feedBack rather than
// reproducing every original hex value verbatim. The chart-format base // reproducing every original hex value verbatim. The chart-format base
// values came from community reverse-engineering of the original chart // values came from community reverse-engineering of the original chart
// files; do not treat the tuned values below as the exact original // files; do not treat the tuned values below as the exact original
// palette. // palette.
// In slopsmith's index convention s=0 is the low E (thickest) and // In feedBack's index convention s=0 is the low E (thickest) and
// s=5 is the high E (thinnest), matching the chart format's native string // s=5 is the high E (thinnest), matching the chart format's native string
// indexing. Per-index ordering is preserved across all palettes so // indexing. Per-index ordering is preserved across all palettes so
// switching between them never reassigns a string to a different // switching between them never reassigns a string to a different
@@ -128,10 +128,10 @@
const MAX_RENDER_STRINGS = S_COL.length; const MAX_RENDER_STRINGS = S_COL.length;
// Resolve the string count for the active arrangement. Prefer // Resolve the string count for the active arrangement. Prefer
// bundle.stringCount (exposed by slopsmith core since #93 — derived // bundle.stringCount (exposed by feedBack core since #93 — derived
// from notes/chords/tuning, so it works for 5-string bass, 7- and // from notes/chords/tuning, so it works for 5-string bass, 7- and
// 8-string guitar, etc.). Fall back to arrangement-name detection // 8-string guitar, etc.). Fall back to arrangement-name detection
// for older slopsmith cores that don't emit the field. Clamp to the // for older feedBack cores that don't emit the field. Clamp to the
// palette size so a malformed bundle or a 12-string chart doesn't // palette size so a malformed bundle or a 12-string chart doesn't
// index past the per-string material arrays. // index past the per-string material arrays.
function resolveStringCount(bundle) { function resolveStringCount(bundle) {
@@ -231,7 +231,7 @@
const AHEAD = 3.0; const AHEAD = 3.0;
const BEHIND = 0.5; const BEHIND = 0.5;
// How long a note/chord-frame stays renderable past the hit line while a // How long a note/chord-frame stays renderable past the hit line while a
// note-state provider (slopsmith#254) is attached. The provider's // note-state provider (feedBack#254) is attached. The provider's
// hit/miss verdict is asynchronous — the engine-side verifier reports it // hit/miss verdict is asynchronous — the engine-side verifier reports it
// ~0.35-0.5 s after the line — so the default ~50 ms note linger / // ~0.35-0.5 s after the line — so the default ~50 ms note linger /
// ~0.48 s chord linger lapses before the tint can apply. Drives both // ~0.48 s chord linger lapses before the tint can apply. Drives both
@@ -664,7 +664,7 @@
/** Arpeggio rim accent and lane tint. */ /** Arpeggio rim accent and lane tint. */
const ARPEGGIO_RIM_BLUE_HEX = 0x454BB6; const ARPEGGIO_RIM_BLUE_HEX = 0x454BB6;
/** Post-hit chord-frame rim tints driven by the note-state provider /** Post-hit chord-frame rim tints driven by the note-state provider
* (slopsmith#254). Applied only to the teal frame during the linger * (feedBack#254). Applied only to the teal frame during the linger
* fade (chDt <= 0) when a scorer is attached. * fade (chDt <= 0) when a scorer is attached.
* Matches the gem hit/miss colours so chord frame and note body * Matches the gem hit/miss colours so chord frame and note body
* give a consistent signal: * give a consistent signal:
@@ -922,7 +922,7 @@
* ====================================================================== */ * ====================================================================== */
function _ssActive() { function _ssActive() {
const ss = window.slopsmithSplitscreen; const ss = window.feedBackSplitscreen;
if (!ss || typeof ss.isActive !== 'function' || !ss.isActive()) return false; if (!ss || typeof ss.isActive !== 'function' || !ss.isActive()) return false;
return typeof ss.isCanvasFocused === 'function' return typeof ss.isCanvasFocused === 'function'
&& typeof ss.onFocusChange === 'function' && typeof ss.onFocusChange === 'function'
@@ -930,7 +930,7 @@
} }
function _ssIsCanvasFocused(highwayCanvas) { function _ssIsCanvasFocused(highwayCanvas) {
const ss = window.slopsmithSplitscreen; const ss = window.feedBackSplitscreen;
if (!_ssActive()) return true; if (!_ssActive()) return true;
return !!(ss && typeof ss.isCanvasFocused === 'function' && return !!(ss && typeof ss.isCanvasFocused === 'function' &&
ss.isCanvasFocused(highwayCanvas)); ss.isCanvasFocused(highwayCanvas));
@@ -941,7 +941,7 @@
* *
* Audio-reactive ambient scenery in the fog band beyond the highway. * Audio-reactive ambient scenery in the fog band beyond the highway.
* Module-level singletons share an AudioContext + AnalyserNode tap on * Module-level singletons share an AudioContext + AnalyserNode tap on
* the slopsmith core <audio id="audio"> element across all panel * the feedBack core <audio id="audio"> element across all panel
* instances; per-panel settings live in localStorage with a global * instances; per-panel settings live in localStorage with a global
* fallback so settings.html drives a single default while per-panel * fallback so settings.html drives a single default while per-panel
* overrides (h3d_bg_panel<idx>_*) can be set for splitscreen layouts. * overrides (h3d_bg_panel<idx>_*) can be set for splitscreen layouts.
@@ -981,7 +981,7 @@
const key = `${outcome}:${status}:${reason}`; const key = `${outcome}:${status}:${reason}`;
if (_bgBridgeKeys.get(bridgeId) === key) return; if (_bgBridgeKeys.get(bridgeId) === key) return;
_bgBridgeKeys.set(bridgeId, key); _bgBridgeKeys.set(bridgeId, key);
const session = window.slopsmith && window.slopsmith.audioSession; const session = window.feedBack && window.feedBack.audioSession;
if (!session || typeof session.recordBridgeHit !== 'function') return; if (!session || typeof session.recordBridgeHit !== 'function') return;
try { try {
session.recordBridgeHit({ session.recordBridgeHit({
@@ -998,14 +998,14 @@
function _bgGetAnalyser() { function _bgGetAnalyser() {
// Prefer the stems plugin's side-chain analyser when a sloppak is // Prefer the stems plugin's side-chain analyser when a sloppak is
// loaded. As of slopsmith-plugin-stems 0.5.0 (sample-locked playback) // loaded. As of feedBack-plugin-stems 0.5.0 (sample-locked playback)
// the #audio element is a silent virtual transport on sloppaks, so // the #audio element is a silent virtual transport on sloppaks, so
// tapping it sees only silence; the stems mix is exposed at // tapping it sees only silence; the stems mix is exposed at
// window.slopsmith.stems.getAnalyser() instead. The stems plugin // window.feedBack.stems.getAnalyser() instead. The stems plugin
// creates and destroys that AnalyserNode per song, so we re-check // creates and destroys that AnalyserNode per song, so we re-check
// each call and key the cache on its identity — when the node // each call and key the cache on its identity — when the node
// changes (song switch), the cache is replaced automatically. // changes (song switch), the cache is replaced automatically.
const stemsApi = window.slopsmith && window.slopsmith.stems; const stemsApi = window.feedBack && window.feedBack.stems;
const stemsAnalyser = (stemsApi && typeof stemsApi.getAnalyser === 'function') const stemsAnalyser = (stemsApi && typeof stemsApi.getAnalyser === 'function')
? stemsApi.getAnalyser() : null; ? stemsApi.getAnalyser() : null;
if (stemsAnalyser) { if (stemsAnalyser) {
@@ -1023,7 +1023,7 @@
freq: new Uint8Array(Math.max(BG_FREQ_BINS, stemsAnalyser.frequencyBinCount)), freq: new Uint8Array(Math.max(BG_FREQ_BINS, stemsAnalyser.frequencyBinCount)),
source: 'stems', source: 'stems',
}; };
_bgRecordAudioBridge('audio-mix.analyser', 'window.slopsmith.stems.getAnalyser', 'handled', '', 'stems'); _bgRecordAudioBridge('audio-mix.analyser', 'window.feedBack.stems.getAnalyser', 'handled', '', 'stems');
} }
return _bgAudio; return _bgAudio;
} }
@@ -1352,7 +1352,7 @@
const FRET_NUMBER_GHOST_SCOPE_IDS = ['chords', 'all']; const FRET_NUMBER_GHOST_SCOPE_IDS = ['chords', 'all'];
function _bgPanelKey(canvas) { function _bgPanelKey(canvas) {
const ss = window.slopsmithSplitscreen; const ss = window.feedBackSplitscreen;
const idx = (ss && typeof ss.panelIndexFor === 'function') ? ss.panelIndexFor(canvas) : null; const idx = (ss && typeof ss.panelIndexFor === 'function') ? ss.panelIndexFor(canvas) : null;
return (idx == null) ? 'main' : 'panel' + idx; return (idx == null) ? 'main' : 'panel' + idx;
} }
@@ -2324,7 +2324,7 @@
// never sees a tainted canvas. Setting // never sees a tainted canvas. Setting
// `crossOrigin = "anonymous"` would also strip // `crossOrigin = "anonymous"` would also strip
// cookies from the fetch, which would 401 against // cookies from the fetch, which would 401 against
// any cookie-protected slopsmith deployment. If // any cookie-protected feedBack deployment. If
// this ever needs to fetch cross-origin, switch // this ever needs to fetch cross-origin, switch
// to `use-credentials` AND have the server send // to `use-credentials` AND have the server send
// the matching CORS headers. // the matching CORS headers.
@@ -2470,7 +2470,7 @@
let _nextInstanceId = 0; let _nextInstanceId = 0;
/* ====================================================================== /* ======================================================================
* Factory slopsmith#36 setRenderer contract * Factory feedBack#36 setRenderer contract
* ====================================================================== */ * ====================================================================== */
function createFactory() { function createFactory() {
@@ -2479,8 +2479,8 @@
// ── Per-instance Three.js state ─────────────────────────────────── // ── Per-instance Three.js state ───────────────────────────────────
let scene = null, cam = null, ren = null; let scene = null, cam = null, ren = null;
let wrap = null; let wrap = null;
// highway:visibility listener (slopsmith#246). Hides the .h3d-wrap // highway:visibility listener (feedBack#246). Hides the .h3d-wrap
// overlay when slopsmith's canvas is display:none'd (splitscreen // overlay when feedBack's canvas is display:none'd (splitscreen
// case). Without this, the wrap is a *sibling* of #highway so // case). Without this, the wrap is a *sibling* of #highway so
// hiding #highway leaves the WebGL scene painting full-screen. // hiding #highway leaves the WebGL scene painting full-screen.
// Bound in initScene after wrap creation, unbound in destroy(). // Bound in initScene after wrap creation, unbound in destroy().
@@ -2842,9 +2842,9 @@
// Notedetect feedback (issue #9). Per-panel mark queues populated // Notedetect feedback (issue #9). Per-panel mark queues populated
// by two event sources: (a) legacy `notedetect:hit` / // by two event sources: (a) legacy `notedetect:hit` /
// `notedetect:miss` window CustomEvents, and (b) Slopsmith // `notedetect:miss` window CustomEvents, and (b) FeedBack
// event-bus `note:hit` / `note:miss` events (subscribed in // event-bus `note:hit` / `note:miss` events (subscribed in
// initScene() when window.slopsmith exposes both `on` and `off`). // initScene() when window.feedBack exposes both `on` and `off`).
// Both sources feed the same _ndPushMark() helper which dedupes // Both sources feed the same _ndPushMark() helper which dedupes
// dual emissions. drawNote looks up its (s, f, t) against these // dual emissions. drawNote looks up its (s, f, t) against these
// arrays each frame and swaps the outline material when a match // arrays each frame and swaps the outline material when a match
@@ -2901,7 +2901,7 @@
// no longer reads it — pruning lives once per frame so // no longer reads it — pruning lives once per frame so
// drawNote's hot path is just the bounded (s, f, t) match. // drawNote's hot path is just the bounded (s, f, t) match.
let _ndFrameNowMs = 0; let _ndFrameNowMs = 0;
// slopsmith#254 — core's per-note judgment provider, captured // feedBack#254 — core's per-note judgment provider, captured
// from `bundle.getNoteState` at the top of each update(). When // from `bundle.getNoteState` at the top of each update(). When
// present it's authoritative over the event-driven marks above: // present it's authoritative over the event-driven marks above:
// 'hit'/'active' → bright string-tinted outline (mGlow[s]) + // 'hit'/'active' → bright string-tinted outline (mGlow[s]) +
@@ -2912,7 +2912,7 @@
// with no scorer registered. Older note_detect builds that only // with no scorer registered. Older note_detect builds that only
// emit notedetect:hit/miss events still work via _ndHitMarks. // emit notedetect:hit/miss events still work via _ndHitMarks.
let _ndGetNoteState = null; let _ndGetNoteState = null;
let _ndHasProvider = false; // true iff a note-state provider is registered (slopsmith#254) let _ndHasProvider = false; // true iff a note-state provider is registered (feedBack#254)
// Sustain verdict latch — persists a provider's hit/miss verdict for the // Sustain verdict latch — persists a provider's hit/miss verdict for the
// full duration of a sustained note. Once hitGlowDuration expires the // full duration of a sustained note. Once hitGlowDuration expires the
// provider stops returning state; the latch re-injects the last verdict // provider stops returning state; the latch re-injects the last verdict
@@ -2976,7 +2976,7 @@
let _fxPalette = _FX_PALETTES.neon; let _fxPalette = _FX_PALETTES.neon;
function _fxResolvePalette() { function _fxResolvePalette() {
let skin = null; let skin = null;
try { skin = localStorage.getItem('slopsmith_notedetect_skin'); } catch (e) {} try { skin = localStorage.getItem('feedBack_notedetect_skin'); } catch (e) {}
_fxPalette = _FX_PALETTES[skin] || _FX_PALETTES.neon; _fxPalette = _FX_PALETTES[skin] || _FX_PALETTES.neon;
} }
function _fxSpawnPop(popKey, points, mult, x, y, z) { function _fxSpawnPop(popKey, points, mult, x, y, z) {
@@ -3380,7 +3380,7 @@
function _unsubscribeFocus() { function _unsubscribeFocus() {
if (!_focusSubscribed) return; if (!_focusSubscribed) return;
const ss = window.slopsmithSplitscreen; const ss = window.feedBackSplitscreen;
if (ss && typeof ss.offFocusChange === 'function') ss.offFocusChange(_onFocusChange); if (ss && typeof ss.offFocusChange === 'function') ss.offFocusChange(_onFocusChange);
_focusSubscribed = false; _focusSubscribed = false;
} }
@@ -4087,7 +4087,7 @@
} }
// ── Object pool ──────────────────────────────────────────────────── // ── Object pool ────────────────────────────────────────────────────
// ── Opt-in perf bench harness (slopsmith#226) ────────────────────── // ── Opt-in perf bench harness (feedBack#226) ──────────────────────
// Enable with `?h3dbench=1` on the player URL. Aggregates per-segment // Enable with `?h3dbench=1` on the player URL. Aggregates per-segment
// timings of update() into a console.log every _PB_REPORT_MS. // timings of update() into a console.log every _PB_REPORT_MS.
// //
@@ -5073,21 +5073,21 @@
wrap.setAttribute('data-h3d-primary', ''); wrap.setAttribute('data-h3d-primary', '');
highwayCanvas.parentNode.insertBefore(wrap, highwayCanvas.nextSibling); highwayCanvas.parentNode.insertBefore(wrap, highwayCanvas.nextSibling);
// Subscribe to highway:visibility (slopsmith#246) so the // Subscribe to highway:visibility (feedBack#246) so the
// .h3d-wrap overlay hides in sync with the slopsmith canvas. // .h3d-wrap overlay hides in sync with the feedBack canvas.
// The wrap is a sibling of #highway, so display:none on // The wrap is a sibling of #highway, so display:none on
// #highway leaves us painting full-screen otherwise. // #highway leaves us painting full-screen otherwise.
// Guarded lazy bind: tolerate hosts that don't yet expose // Guarded lazy bind: tolerate hosts that don't yet expose
// slopsmith.on/off (older slopsmith versions, headless // feedBack.on/off (older feedBack versions, headless
// tests). // tests).
if (window.slopsmith if (window.feedBack
&& typeof window.slopsmith.on === 'function' && typeof window.feedBack.on === 'function'
&& typeof window.slopsmith.off === 'function') { && typeof window.feedBack.off === 'function') {
_visibilityHandler = (e) => { _visibilityHandler = (e) => {
if (!wrap) return; if (!wrap) return;
// Filter by canvas identity (splitscreen-safe). // Filter by canvas identity (splitscreen-safe).
// Each createHighway() instance emits its own // Each createHighway() instance emits its own
// visibility events on the shared slopsmith bus — // visibility events on the shared feedBack bus —
// without this gate, one hidden panel would also // without this gate, one hidden panel would also
// hide every other panel's 3D overlay. // hide every other panel's 3D overlay.
if (!e || !e.detail || e.detail.canvas !== highwayCanvas) return; if (!e || !e.detail || e.detail.canvas !== highwayCanvas) return;
@@ -5095,7 +5095,7 @@
wrap.style.display = v === false ? 'none' : ''; wrap.style.display = v === false ? 'none' : '';
}; };
try { try {
window.slopsmith.on('highway:visibility', _visibilityHandler); window.feedBack.on('highway:visibility', _visibilityHandler);
} catch (e) { } catch (e) {
_visibilityHandler = null; _visibilityHandler = null;
} }
@@ -5116,7 +5116,7 @@
} }
}; };
try { try {
window.slopsmith.on('highway:canvas-replaced', _canvasReplacedHandler); window.feedBack.on('highway:canvas-replaced', _canvasReplacedHandler);
} catch (e) { } catch (e) {
_canvasReplacedHandler = null; _canvasReplacedHandler = null;
} }
@@ -6170,7 +6170,7 @@
return _sp; return _sp;
}); });
// ── Pre-warm pools (slopsmith#226) ───────────────────────────── // ── Pre-warm pools (feedBack#226) ─────────────────────────────
// Dense 7/8-string charts can outrun the lazy-grow path in the // Dense 7/8-string charts can outrun the lazy-grow path in the
// first 1-2s of playback, stalling those frames with `new T.Mesh` // first 1-2s of playback, stalling those frames with `new T.Mesh`
// allocations *and* growing noteG forever (the pool only hides on // allocations *and* growing noteG forever (the pool only hides on
@@ -6459,13 +6459,13 @@
_ndOnMiss = (e) => { _ndMissMarks = _ndPushMark(_ndMissMarks, e.detail); }; _ndOnMiss = (e) => { _ndMissMarks = _ndPushMark(_ndMissMarks, e.detail); };
window.addEventListener('notedetect:hit', _ndOnHit); window.addEventListener('notedetect:hit', _ndOnHit);
window.addEventListener('notedetect:miss', _ndOnMiss); window.addEventListener('notedetect:miss', _ndOnMiss);
if (window.slopsmith && if (window.feedBack &&
typeof window.slopsmith.on === 'function' && typeof window.feedBack.on === 'function' &&
typeof window.slopsmith.off === 'function') { typeof window.feedBack.off === 'function') {
_ndOnBusHit = (e) => { _ndHitMarks = _ndPushMark(_ndHitMarks, e.detail); }; _ndOnBusHit = (e) => { _ndHitMarks = _ndPushMark(_ndHitMarks, e.detail); };
_ndOnBusMiss = (e) => { _ndMissMarks = _ndPushMark(_ndMissMarks, e.detail); }; _ndOnBusMiss = (e) => { _ndMissMarks = _ndPushMark(_ndMissMarks, e.detail); };
window.slopsmith.on('note:hit', _ndOnBusHit); window.feedBack.on('note:hit', _ndOnBusHit);
window.slopsmith.on('note:miss', _ndOnBusMiss); window.feedBack.on('note:miss', _ndOnBusMiss);
} }
// Score FX (notedetect ≥1.13). notedetect dispatches each fx // Score FX (notedetect ≥1.13). notedetect dispatches each fx
@@ -6499,10 +6499,10 @@
}, 0); }, 0);
}; };
window.addEventListener('notedetect:fx', _fxOnFx); window.addEventListener('notedetect:fx', _fxOnFx);
if (window.slopsmith && typeof window.slopsmith.on === 'function' if (window.feedBack && typeof window.feedBack.on === 'function'
&& typeof window.slopsmith.off === 'function') { && typeof window.feedBack.off === 'function') {
_fxOnSkin = () => _fxResolvePalette(); _fxOnSkin = () => _fxResolvePalette();
window.slopsmith.on('notedetect:skin', _fxOnSkin); window.feedBack.on('notedetect:skin', _fxOnSkin);
} }
return true; return true;
@@ -8315,7 +8315,7 @@
function smoothNow(bundle) { function smoothNow(bundle) {
const raw = bundle.currentTime; const raw = bundle.currentTime;
const p = performance.now(); const p = performance.now();
// Host pause signal (slopsmith core's bundle.isPlaying): when the // Host pause signal (feedBack core's bundle.isPlaying): when the
// chart clock isn't advancing (paused / stalled / mid-seek), don't // chart clock isn't advancing (paused / stalled / mid-seek), don't
// extrapolate forward against a frozen audio sample — that creeps // extrapolate forward against a frozen audio sample — that creeps
// the highway ahead by up to the interp cap and then snaps back // the highway ahead by up to the interp cap and then snaps back
@@ -8437,7 +8437,7 @@
if (_ndMissMarks[_pi].expiresAt <= _ndFrameNowMs) _ndMissMarks.splice(_pi, 1); if (_ndMissMarks[_pi].expiresAt <= _ndFrameNowMs) _ndMissMarks.splice(_pi, 1);
} }
} }
// slopsmith#254 — capture core's per-note judgment provider for // feedBack#254 — capture core's per-note judgment provider for
// this frame's drawNote() calls (held-sustain glow + lit gems). // this frame's drawNote() calls (held-sustain glow + lit gems).
// bundle.getNoteState is ALWAYS present (the core stub returns // bundle.getNoteState is ALWAYS present (the core stub returns
// null when no provider is registered), so its existence isn't // null when no provider is registered), so its existence isn't
@@ -9140,10 +9140,10 @@
{ {
const si = bundle.songInfo; const si = bundle.songInfo;
// bundle.songInfo has no filename field (the WS song_info message // bundle.songInfo has no filename field (the WS song_info message
// never includes it). Use window.slopsmith.currentSong.filename // never includes it). Use window.feedBack.currentSong.filename
// — set by highway.js from the WS URL — combined with the // — set by highway.js from the WS URL — combined with the
// arrangement index as a reliable per-song-arrangement key. // arrangement index as a reliable per-song-arrangement key.
const currentSong = window.slopsmith && window.slopsmith.currentSong; const currentSong = window.feedBack && window.feedBack.currentSong;
const key = currentSong ? currentSong.filename + '\0' + (si ? (si.arrangement_index ?? '') : '') : null; const key = currentSong ? currentSong.filename + '\0' + (si ? (si.arrangement_index ?? '') : '') : null;
if (key !== null && key !== _songKey) { if (key !== null && key !== _songKey) {
_songKey = key; _songKey = key;
@@ -9729,7 +9729,7 @@
// lingering past that point. // lingering past that point.
chordTailHoldS = Math.min(CHORD_HWY_LINGER_S, Math.max(cjNext.t - ch.t, 1e-3)); chordTailHoldS = Math.min(CHORD_HWY_LINGER_S, Math.max(cjNext.t - ch.t, 1e-3));
} }
// slopsmith#254 — engine verdicts land ~0.4 s after the // feedBack#254 — engine verdicts land ~0.4 s after the
// chord crosses; on a fast different-voicing sequence // chord crosses; on a fast different-voicing sequence
// the clip above can shrink the rim's draw life below // the clip above can shrink the rim's draw life below
// that, so the green/red latch is set but the rim isn't // that, so the green/red latch is set but the rim isn't
@@ -10042,7 +10042,7 @@
// Used for the mute X lines so hit/miss feedback only shows on // Used for the mute X lines so hit/miss feedback only shows on
// the outer borders of the framebox, not inside the X pattern. // the outer borders of the framebox, not inside the X pattern.
const baseRimHex = rimHex; const baseRimHex = rimHex;
// slopsmith#254 — once the chord crosses the hit // feedBack#254 — once the chord crosses the hit
// line, tint the teal frame by the note-state // line, tint the teal frame by the note-state
// provider verdict: green on a clean grab, red on a // provider verdict: green on a clean grab, red on a
// miss. The verdict is async (the engine verifier // miss. The verdict is async (the engine verifier
@@ -11723,7 +11723,7 @@
const effectiveProjWin = _rawGap > 0 ? Math.min(0.6, Math.max(0.05, _rawGap)) : 0.6; const effectiveProjWin = _rawGap > 0 ? Math.min(0.6, Math.max(0.05, _rawGap)) : 0.6;
const projFactorG = Math.max(0, Math.min(1, 1 - Math.max(dt, 0) / effectiveProjWin)); const projFactorG = Math.max(0, Math.min(1, 1 - Math.max(dt, 0) / effectiveProjWin));
const inGhostWin = n.f > 0 && isNextOnString && dt > -ghostHold && dt < effectiveProjWin && projFactorG > 0.001; const inGhostWin = n.f > 0 && isNextOnString && dt > -ghostHold && dt < effectiveProjWin && projFactorG > 0.001;
// slopsmith#254 — query the provider once per note, before both !skipBody // feedBack#254 — query the provider once per note, before both !skipBody
// blocks, so _showHit can be a const and _ndGood is available for the // blocks, so _showHit can be a const and _ndGood is available for the
// sustain trail (which renders even when skipBody=true for slide targets). // sustain trail (which renders even when skipBody=true for slide targets).
let _ndGood = false; // true when provider confirms hit/active let _ndGood = false; // true when provider confirms hit/active
@@ -11898,7 +11898,7 @@
const rimXY = n.ac ? ACCENT_RIM_XY_SCALE_MUL : 1; const rimXY = n.ac ? ACCENT_RIM_XY_SCALE_MUL : 1;
const rimZ = n.ac ? ACCENT_RIM_Z_SCALE_MUL : 1; const rimZ = n.ac ? ACCENT_RIM_Z_SCALE_MUL : 1;
// slopsmith#254 — apply outline + lateral face-fill overrides from provider verdict. // feedBack#254 — apply outline + lateral face-fill overrides from provider verdict.
// hit/active → green outline (mHitBright[s]) + green lateral faces; // hit/active → green outline (mHitBright[s]) + green lateral faces;
// miss → magenta-red outline (mMissOutline) + dark lateral faces; front/back stay transparent. // miss → magenta-red outline (mMissOutline) + dark lateral faces; front/back stay transparent.
if (_ndCs) { if (_ndCs) {
@@ -13019,15 +13019,15 @@
if (_ndOnHit) { window.removeEventListener('notedetect:hit', _ndOnHit); _ndOnHit = null; } if (_ndOnHit) { window.removeEventListener('notedetect:hit', _ndOnHit); _ndOnHit = null; }
if (_ndOnMiss) { window.removeEventListener('notedetect:miss', _ndOnMiss); _ndOnMiss = null; } if (_ndOnMiss) { window.removeEventListener('notedetect:miss', _ndOnMiss); _ndOnMiss = null; }
if (_fxOnFx) { window.removeEventListener('notedetect:fx', _fxOnFx); _fxOnFx = null; } if (_fxOnFx) { window.removeEventListener('notedetect:fx', _fxOnFx); _fxOnFx = null; }
if (window.slopsmith && typeof window.slopsmith.off === 'function') { if (window.feedBack && typeof window.feedBack.off === 'function') {
if (_fxOnSkin) { try { window.slopsmith.off('notedetect:skin', _fxOnSkin); } catch (e) {} _fxOnSkin = null; } if (_fxOnSkin) { try { window.feedBack.off('notedetect:skin', _fxOnSkin); } catch (e) {} _fxOnSkin = null; }
if (_ndOnBusHit) window.slopsmith.off('note:hit', _ndOnBusHit); if (_ndOnBusHit) window.feedBack.off('note:hit', _ndOnBusHit);
if (_ndOnBusMiss) window.slopsmith.off('note:miss', _ndOnBusMiss); if (_ndOnBusMiss) window.feedBack.off('note:miss', _ndOnBusMiss);
if (_visibilityHandler) { if (_visibilityHandler) {
try { window.slopsmith.off('highway:visibility', _visibilityHandler); } catch (e) {} try { window.feedBack.off('highway:visibility', _visibilityHandler); } catch (e) {}
} }
if (_canvasReplacedHandler) { if (_canvasReplacedHandler) {
try { window.slopsmith.off('highway:canvas-replaced', _canvasReplacedHandler); } catch (e) {} try { window.feedBack.off('highway:canvas-replaced', _canvasReplacedHandler); } catch (e) {}
} }
} }
_ndOnBusHit = _ndOnBusMiss = null; _ndOnBusHit = _ndOnBusMiss = null;
@@ -13261,11 +13261,11 @@
_bgReactiveOptOut = !!(bundle && bundle.bgReactive === false); _bgReactiveOptOut = !!(bundle && bundle.bgReactive === false);
if (_ssActive()) { if (_ssActive()) {
window.slopsmithSplitscreen.onFocusChange(_onFocusChange); window.feedBackSplitscreen.onFocusChange(_onFocusChange);
_focusSubscribed = true; _focusSubscribed = true;
} }
// Async-ready contract (slopsmith#36 readyPromise). Resolves // Async-ready contract (feedBack#36 readyPromise). Resolves
// when Three.js loaded + scene initialised (_isReady = true). // when Three.js loaded + scene initialised (_isReady = true).
// Rejects on any async failure so highway.js can revert. // Rejects on any async failure so highway.js can revert.
let _resolveReady, _rejectReady; let _resolveReady, _rejectReady;
@@ -13571,11 +13571,11 @@
}; };
} }
window.slopsmithViz_highway_3d = createFactory; window.feedBackViz_highway_3d = createFactory;
// Per-panel control descriptors (splitscreen). The palette selector was // Per-panel control descriptors (splitscreen). The palette selector was
// removed — per-string colors are set via the core "Highway String Colors" // removed — per-string colors are set via the core "Highway String Colors"
// UI, which drives both highways by named string. // UI, which drives both highways by named string.
window.slopsmithViz_highway_3d.panelControls = [ window.feedBackViz_highway_3d.panelControls = [
{ {
key: 'cameraSmoothing', key: 'cameraSmoothing',
label: 'Camera smoothing (X-pan)', label: 'Camera smoothing (X-pan)',
@@ -13618,8 +13618,8 @@
// are matched by the piano plugin instead. // are matched by the piano plugin instead.
// _canRun3D() in app.js still gates Auto from // _canRun3D() in app.js still gates Auto from
// picking us on machines without WebGL2. // picking us on machines without WebGL2.
window.slopsmithViz_highway_3d.contextType = 'webgl2'; window.feedBackViz_highway_3d.contextType = 'webgl2';
window.slopsmithViz_highway_3d.__test = { window.feedBackViz_highway_3d.__test = {
getAnalyserForBridgeTest: _bgGetAnalyser, getAnalyserForBridgeTest: _bgGetAnalyser,
readBandsForBridgeTest: _bgReadBands, readBandsForBridgeTest: _bgReadBands,
resetAnalyserBridgeForTest() { _bgBridgeKeys.clear(); _bgAudio = null; _bgAudioCore = null; _bgAudioFailedAt = 0; }, resetAnalyserBridgeForTest() { _bgBridgeKeys.clear(); _bgAudio = null; _bgAudioCore = null; _bgAudioFailedAt = 0; },
@@ -13630,12 +13630,12 @@
// sloppaks). Word boundaries (\b) keep us from accidentally matching // sloppaks). Word boundaries (\b) keep us from accidentally matching
// arrangements that merely contain these as substrings (e.g. a // arrangements that merely contain these as substrings (e.g. a
// "BasslineKeys" arrangement would otherwise match `bass`). // "BasslineKeys" arrangement would otherwise match `bass`).
window.slopsmithViz_highway_3d.matchesArrangement = function (songInfo) { window.feedBackViz_highway_3d.matchesArrangement = function (songInfo) {
const arr = (songInfo && songInfo.arrangement) || ''; const arr = (songInfo && songInfo.arrangement) || '';
return /\b(?:lead|rhythm|bass|combo|guitar)\b/i.test(arr); return /\b(?:lead|rhythm|bass|combo|guitar)\b/i.test(arr);
}; };
// No imperative register() call needed: slopsmith#272 introduced the // No imperative register() call needed: feedBack#272 introduced the
// consolidated tour menu, which discovers this plugin's tour automatically // consolidated tour menu, which discovers this plugin's tour automatically
// via /api/plugins (has_tour:true from plugin.json's tour field) and // via /api/plugins (has_tour:true from plugin.json's tour field) and
// gates relevance on whether highway_3d is the active viz. A register() // gates relevance on whether highway_3d is the active viz. A register()
+2 -2
View File
@@ -147,7 +147,7 @@
<p class="text-xs text-gray-500 mb-2"> <p class="text-xs text-gray-500 mb-2">
Upload an MP4 or WebM (&le;50&nbsp;MB). Plays muted, looped Upload an MP4 or WebM (&le;50&nbsp;MB). Plays muted, looped
in the fog band when the style above is set to in the fog band when the style above is set to
<em>Custom video</em>. Bytes stay on the slopsmith server, <em>Custom video</em>. Bytes stay on the feedBack server,
not in the browser. not in the browser.
</p> </p>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -725,7 +725,7 @@
// localStorage values are stored as UTF-16 (two bytes per // localStorage values are stored as UTF-16 (two bytes per
// character), so the on-disk footprint is ~2.67× the raw // character), so the on-disk footprint is ~2.67× the raw
// file size. localStorage quotas are typically 5 MB per // file size. localStorage quotas are typically 5 MB per
// origin and slopsmith already uses some of that for other // origin and feedBack already uses some of that for other
// settings, so a 1.5 MB raw limit (≈4 MB on disk) leaves // settings, so a 1.5 MB raw limit (≈4 MB on disk) leaves
// safe headroom; the read-back verification below catches // safe headroom; the read-back verification below catches
// remaining edge cases where the write still gets refused. // remaining edge cases where the write still gets refused.
+1 -1
View File
@@ -1,7 +1,7 @@
/** /**
* Tailwind build config for the 3D Highway plugin's OWN stylesheet. * Tailwind build config for the 3D Highway plugin's OWN stylesheet.
* *
* Slopsmith serves Tailwind as a prebuilt stylesheet and core only scans core * FeedBack serves Tailwind as a prebuilt stylesheet and core only scans core
* source at build time (constitution Principle II no Play CDN / runtime JIT). * source at build time (constitution Principle II no Play CDN / runtime JIT).
* This plugin owns its utilities so it styles correctly even when core's build * This plugin owns its utilities so it styles correctly even when core's build
* didn't scan it (it's excluded from core's content globs). It uses arbitrary * didn't scan it (it's excluded from core's content globs). It uses arbitrary
+7 -7
View File
@@ -15,10 +15,10 @@
(function () { (function () {
'use strict'; 'use strict';
window.slopsmith = window.slopsmith || {}; window.feedBack = window.feedBack || {};
if (window.slopsmithInputSetup && window.slopsmithInputSetup.version === 1) return; if (window.feedBackInputSetup && window.feedBackInputSetup.version === 1) return;
const capabilities = window.slopsmith.capabilities; const capabilities = window.feedBack.capabilities;
const DONE_KEY = (inst) => `input_setup.done.${inst}`; const DONE_KEY = (inst) => `input_setup.done.${inst}`;
const INSTRUMENTS = { const INSTRUMENTS = {
guitar: { label: 'Guitar', mode: 'audio' }, guitar: { label: 'Guitar', mode: 'audio' },
@@ -36,7 +36,7 @@
// The Web-MIDI source provider now ships built-in with the core midi-input // The Web-MIDI source provider now ships built-in with the core midi-input
// domain (static/capabilities/midi-input.js), so input_setup is a pure // domain (static/capabilities/midi-input.js), so input_setup is a pure
// consumer — it just discovers/selects/opens through `window.slopsmith.midiInput`. // consumer — it just discovers/selects/opens through `window.feedBack.midiInput`.
// ── audio-input helper (guitar/bass device context) ───────────────────── // ── audio-input helper (guitar/bass device context) ─────────────────────
async function _audioSources() { async function _audioSources() {
@@ -176,7 +176,7 @@
// Keys/drums: pick a MIDI device via midi-input and confirm a live hit. // Keys/drums: pick a MIDI device via midi-input and confirm a live hit.
async function renderMidiPanel(inst) { async function renderMidiPanel(inst) {
const mi = window.slopsmith.midiInput; const mi = window.feedBack.midiInput;
// Availability is the midi-input DOMAIN being present, not the // Availability is the midi-input DOMAIN being present, not the
// Web-MIDI browser API — the domain coordinates providers (the // Web-MIDI browser API — the domain coordinates providers (the
// built-in Web-MIDI one, plus any native/desktop adapter), so // built-in Web-MIDI one, plus any native/desktop adapter), so
@@ -249,7 +249,7 @@
// Show every source the midi-input domain surfaces — not just // Show every source the midi-input domain surfaces — not just
// the built-in Web-MIDI provider — so a native/desktop MIDI // the built-in Web-MIDI provider — so a native/desktop MIDI
// adapter registered with the domain is selectable too. // adapter registered with the domain is selectable too.
const sources = window.slopsmith.midiInput.listSources() || []; const sources = window.feedBack.midiInput.listSources() || [];
if (!sources.length) { testEl && (testEl.textContent = ''); wrap.classList.remove('hidden'); select.innerHTML = '<option>No MIDI devices found</option>'; select.disabled = true; return; } if (!sources.length) { testEl && (testEl.textContent = ''); wrap.classList.remove('hidden'); select.innerHTML = '<option>No MIDI devices found</option>'; select.disabled = true; return; }
wrap.classList.remove('hidden'); wrap.classList.remove('hidden');
select.disabled = false; select.disabled = false;
@@ -327,7 +327,7 @@
return _runWizard({ host, instruments: instruments || [] }).then((r) => { overlay.remove(); return r; }); return _runWizard({ host, instruments: instruments || [] }).then((r) => { overlay.remove(); return r; });
} }
window.slopsmithInputSetup = { window.feedBackInputSetup = {
version: 1, version: 1,
mount, mount,
launch, launch,
+12 -12
View File
@@ -1,16 +1,16 @@
# slopsmith-plugin-minigames # feedBack-plugin-minigames
The minigame framework for [Slopsmith](https://github.com/got-feedback/feedback). The minigame framework for [FeedBack](https://github.com/got-feedback/feedBack).
This plugin provides: This plugin provides:
- A **Minigames hub** screen that discovers every installed minigame plugin and lists them as tiles with leaderboards. - A **Minigames hub** screen that discovers every installed minigame plugin and lists them as tiles with leaderboards.
- A **shared profile** (XP, level, unlocks, totals) that aggregates runs across every minigame. - A **shared profile** (XP, level, unlocks, totals) that aggregates runs across every minigame.
- A JS **SDK** exposed at `window.slopsmithMinigames` that minigame plugins use to access scoring, HUD primitives, run persistence, and a scheduler — so individual minigames do not need their own DSP or backend. - A JS **SDK** exposed at `window.feedBackMinigames` that minigame plugins use to access scoring, HUD primitives, run persistence, and a scheduler — so individual minigames do not need their own DSP or backend.
## Writing a minigame ## Writing a minigame
A minigame is a standard Slopsmith plugin that: A minigame is a standard FeedBack plugin that:
1. Adds a `minigame` block to its `plugin.json`: 1. Adds a `minigame` block to its `plugin.json`:
@@ -33,7 +33,7 @@ A minigame is a standard Slopsmith plugin that:
2. On script load, registers itself with the SDK using the safe late-binding 2. On script load, registers itself with the SDK using the safe late-binding
pattern (minigame plugins may load before the SDK; the pending queue pattern (minigame plugins may load before the SDK; the pending queue
handles both orderings — the SDK drains it on init, and the handles both orderings — the SDK drains it on init, and the
`slopsmith-minigames-ready` event is an alternative for plugins that prefer `feedBack-minigames-ready` event is an alternative for plugins that prefer
event-driven registration): event-driven registration):
> **Important:** `spec.id` must exactly match the `id` field in `plugin.json`. > **Important:** `spec.id` must exactly match the `id` field in `plugin.json`.
@@ -51,20 +51,20 @@ A minigame is a standard Slopsmith plugin that:
stop: () => { /* tear down */ }, stop: () => { /* tear down */ },
}; };
if (window.slopsmithMinigames) { if (window.feedBackMinigames) {
window.slopsmithMinigames.register(spec); window.feedBackMinigames.register(spec);
} else { } else {
(window.__slopsmithMinigamesPending = window.__slopsmithMinigamesPending || []).push(spec); (window.__feedBackMinigamesPending = window.__feedBackMinigamesPending || []).push(spec);
} }
``` ```
3. Calls `window.slopsmithMinigames.end({ score, durationMs, modifiers, meta })` when the run ends. 3. Calls `window.feedBackMinigames.end({ score, durationMs, modifiers, meta })` when the run ends.
See [`slopsmith-plugin-flappy-bend`](https://github.com/got-feedback/feedback-plugin-flappy-bend) for a working example. See [`feedBack-plugin-flappy-bend`](https://github.com/got-feedback/feedBack-plugin-flappy-bend) for a working example.
## SDK reference ## SDK reference
`window.slopsmithMinigames` exposes: `window.feedBackMinigames` exposes:
- `register(spec)` — declare a minigame - `register(spec)` — declare a minigame
- `start(gameId, opts)` / `end(result)` — lifecycle - `start(gameId, opts)` / `end(result)` — lifecycle
@@ -77,4 +77,4 @@ See [`slopsmith-plugin-flappy-bend`](https://github.com/got-feedback/feedback-pl
## Dependencies ## Dependencies
- `slopsmith-plugin-notedetect` >= 1.10.0 — required for discrete/chord scoring modes (continuous mode is self-contained). - `feedBack-plugin-notedetect` >= 1.10.0 — required for discrete/chord scoring modes (continuous mode is self-contained).
+4 -4
View File
@@ -36,7 +36,7 @@ _state = {
"db_path": None, "db_path": None,
"profile_path": None, "profile_path": None,
"plugins_dir_resolver": None, "plugins_dir_resolver": None,
"log": logging.getLogger("slopsmith.plugin.minigames"), "log": logging.getLogger("feedBack.plugin.minigames"),
# fee[dB]ack v0.3.0 unified XP: when running inside core these point at the # fee[dB]ack v0.3.0 unified XP: when running inside core these point at the
# single core XP store (server.py plugin_context). XP then flows to ONE # single core XP store (server.py plugin_context). XP then flows to ONE
# store the profile badge reads. Absent when the plugin runs standalone, # store the profile badge reads. Absent when the plugin runs standalone,
@@ -282,7 +282,7 @@ def _list_minigame_plugins(force_refresh: bool = False) -> list:
"version": data.get("version"), "version": data.get("version"),
} }
# Deduplicate by plugin_id: first entry wins (resolver returns # Deduplicate by plugin_id: first entry wins (resolver returns
# SLOPSMITH_PLUGINS_DIR before the bundled siblings, so an explicit # FEEDBACK_PLUGINS_DIR before the bundled siblings, so an explicit
# override takes precedence over the in-tree snapshot — same winner # override takes precedence over the in-tree snapshot — same winner
# selection as the core plugin loader). # selection as the core plugin loader).
if plugin_id not in seen_ids: if plugin_id not in seen_ids:
@@ -328,7 +328,7 @@ def setup(app, context):
# The plugin loader doesn't currently expose a list-other-plugins helper, # The plugin loader doesn't currently expose a list-other-plugins helper,
# so derive the plugin directories from environment + conventions: # so derive the plugin directories from environment + conventions:
# 1. SLOPSMITH_PLUGINS_DIR env var (explicit override) # 1. FEEDBACK_PLUGINS_DIR env var (explicit override)
# 2. The directory that contains this plugin (plugin_self.parent) — # 2. The directory that contains this plugin (plugin_self.parent) —
# covers the common case where all plugins live in one flat dir. # covers the common case where all plugins live in one flat dir.
# 3. plugin_self.parent.parent / "plugins" — covers the layout where # 3. plugin_self.parent.parent / "plugins" — covers the layout where
@@ -336,7 +336,7 @@ def setup(app, context):
# Duplicates are removed via a seen-set keyed on resolved paths. # Duplicates are removed via a seen-set keyed on resolved paths.
def _resolve_plugin_dirs(): def _resolve_plugin_dirs():
roots = [] roots = []
env_dir = os.environ.get("SLOPSMITH_PLUGINS_DIR") env_dir = os.environ.get("FEEDBACK_PLUGINS_DIR") or os.environ.get("SLOPSMITH_PLUGINS_DIR")
if env_dir: if env_dir:
roots.append(Path(env_dir)) roots.append(Path(env_dir))
# Built-in plugins/ next to server.py (one level above this file's # Built-in plugins/ next to server.py (one level above this file's
+1 -1
View File
@@ -40,7 +40,7 @@
</div> </div>
<!-- In-game container — only visible while a minigame is running. <!-- In-game container — only visible while a minigame is running.
z-[60] sits above the Slopsmith navbar (z-50) so the game owns the z-[60] sits above the FeedBack navbar (z-50) so the game owns the
viewport during a run; the stage's own Quit button is the exit. --> viewport during a run; the stage's own Quit button is the exit. -->
<div id="mg-stage" class="hidden fixed inset-0 z-[60] bg-fb-bg/95 flex flex-col" <div id="mg-stage" class="hidden fixed inset-0 z-[60] bg-fb-bg/95 flex flex-col"
role="region" aria-labelledby="mg-stage-title"> role="region" aria-labelledby="mg-stage-title">
+23 -23
View File
@@ -1,20 +1,20 @@
// slopsmith-plugin-minigames — SDK + hub controller. // feedBack-plugin-minigames — SDK + hub controller.
// //
// This file does two things: // This file does two things:
// 1) Publishes window.slopsmithMinigames — the SDK that individual // 1) Publishes window.feedBackMinigames — the SDK that individual
// minigame plugins call (register, start/end, scoring, ui, persistence). // minigame plugins call (register, start/end, scoring, ui, persistence).
// 2) Mounts a hub UI in screen.html that lists every registered minigame // 2) Mounts a hub UI in screen.html that lists every registered minigame
// and the shared profile/leaderboards. // and the shared profile/leaderboards.
// //
// Plugin load order is alphabetical, so minigame plugins (e.g. flappy_bend) // Plugin load order is alphabetical, so minigame plugins (e.g. flappy_bend)
// load BEFORE this script. They should register via a tiny shim that queues // load BEFORE this script. They should register via a tiny shim that queues
// to `window.__slopsmithMinigamesPending` if the SDK isn't up yet — we drain // to `window.__feedBackMinigamesPending` if the SDK isn't up yet — we drain
// the queue on init and also fire `slopsmith-minigames-ready` once ready. // the queue on init and also fire `feedBack-minigames-ready` once ready.
(function () { (function () {
'use strict'; 'use strict';
if (window.slopsmithMinigames && window.slopsmithMinigames.__alive) { if (window.feedBackMinigames && window.feedBackMinigames.__alive) {
return; // hot-reload guard return; // hot-reload guard
} }
@@ -128,7 +128,7 @@
const yinD = new Float32Array(yinHalfN); const yinD = new Float32Array(yinHalfN);
const yinCmnd = new Float32Array(yinHalfN); const yinCmnd = new Float32Array(yinHalfN);
let ringWrite = 0; let ringWrite = 0;
// Desktop-engine bridge path. On slopsmith-desktop the native JUCE engine // Desktop-engine bridge path. On feedBack-desktop the native JUCE engine
// owns the input device (often an exclusive ASIO device the browser's // owns the input device (often an exclusive ASIO device the browser's
// getUserMedia can't see), so a renderer getUserMedia stream lands on the // getUserMedia can't see), so a renderer getUserMedia stream lands on the
// wrong/silent Windows-default device. When the bridge is present we pull // wrong/silent Windows-default device. When the bridge is present we pull
@@ -347,7 +347,7 @@
// Prefer the desktop engine bridge (correct, user-configured input device); // Prefer the desktop engine bridge (correct, user-configured input device);
// fall back to getUserMedia on the web build or a downlevel addon. // fall back to getUserMedia on the web build or a downlevel addon.
function start() { function start() {
const audio = window.slopsmithDesktop && window.slopsmithDesktop.audio; const audio = window.feedBackDesktop && window.feedBackDesktop.audio;
if (audio && typeof audio.getRawAudioFrame === 'function') { if (audio && typeof audio.getRawAudioFrame === 'function') {
startBridge(audio); startBridge(audio);
} else { } else {
@@ -360,7 +360,7 @@
} }
// ── scoring.createDiscrete / createChord ────────────────────────────── // ── scoring.createDiscrete / createChord ──────────────────────────────
// Both wrap window.createNoteDetector from slopsmith-plugin-notedetect. // Both wrap window.createNoteDetector from feedBack-plugin-notedetect.
// For v1 they are thin event re-emitters — minigames using them must // For v1 they are thin event re-emitters — minigames using them must
// run alongside a chart (createNoteDetector needs a highway). Chart-free // run alongside a chart (createNoteDetector needs a highway). Chart-free
// discrete scoring is out of scope until the scoring-core extraction // discrete scoring is out of scope until the scoring-core extraction
@@ -369,7 +369,7 @@
const handlers = { hit: [], miss: [], end: [] }; const handlers = { hit: [], miss: [], end: [] };
const fn = window.createNoteDetector; const fn = window.createNoteDetector;
if (typeof fn !== 'function') { if (typeof fn !== 'function') {
console.warn('[minigames] window.createNoteDetector unavailable — install slopsmith-plugin-notedetect for discrete/chord scoring.'); console.warn('[minigames] window.createNoteDetector unavailable — install feedBack-plugin-notedetect for discrete/chord scoring.');
let _unavailStopped = false; let _unavailStopped = false;
return { return {
on(event, cb) { (handlers[event] || (handlers[event] = [])).push(cb); return this; }, on(event, cb) { (handlers[event] || (handlers[event] = [])).push(cb); return this; },
@@ -736,8 +736,8 @@
container, container,
modifiers, modifiers,
// Convenience pass-through for the SDK so games don't have to // Convenience pass-through for the SDK so games don't have to
// touch window.slopsmithMinigames inside their start handler. // touch window.feedBackMinigames inside their start handler.
sdk: window.slopsmithMinigames, sdk: window.feedBackMinigames,
}); });
} catch (e) { } catch (e) {
console.error('[minigames] minigame start() threw:', e); console.error('[minigames] minigame start() threw:', e);
@@ -895,7 +895,7 @@
tile.setAttribute('aria-label', title); tile.setAttribute('aria-label', title);
const stats = perGame[spec.id] || { runs: 0, best_score: 0 }; const stats = perGame[spec.id] || { runs: 0, best_score: 0 };
// Thumbnails are served via the minigame plugin's own asset route // Thumbnails are served via the minigame plugin's own asset route
// (the Slopsmith plugin loader only serves manifest-declared files, // (the FeedBack plugin loader only serves manifest-declared files,
// so each minigame that ships extra assets must expose /assets/). // so each minigame that ships extra assets must expose /assets/).
// Thumbnails are served by the minigame plugin's own /assets/ route; // Thumbnails are served by the minigame plugin's own /assets/ route;
// not every plugin ships one, so fall back to the placeholder on 404. // not every plugin ships one, so fall back to the placeholder on 404.
@@ -957,15 +957,15 @@
listRegistered: () => Array.from(registered.values()), listRegistered: () => Array.from(registered.values()),
}; };
window.slopsmithMinigames = sdk; window.feedBackMinigames = sdk;
// Drain queue of plugins that loaded before us. // Drain queue of plugins that loaded before us.
(window.__slopsmithMinigamesPending || []).forEach(register); (window.__feedBackMinigamesPending || []).forEach(register);
window.__slopsmithMinigamesPending = null; window.__feedBackMinigamesPending = null;
window.dispatchEvent(new CustomEvent('slopsmith-minigames-ready')); window.dispatchEvent(new CustomEvent('feedBack-minigames-ready'));
// ── Wire hub render to screen lifecycle ─────────────────────────────── // ── Wire hub render to screen lifecycle ───────────────────────────────
// Slopsmith mounts plugin screens with id "plugin-<plugin_id>" and // FeedBack mounts plugin screens with id "plugin-<plugin_id>" and
// routes there via showScreen() / window.slopsmith.navigate(). // routes there via showScreen() / window.feedBack.navigate().
const SCREEN_ID = `plugin-${PLUGIN_ID}`; const SCREEN_ID = `plugin-${PLUGIN_ID}`;
// Non-scoring teardown: called when navigation happens mid-run so that // Non-scoring teardown: called when navigation happens mid-run so that
// microphone streams, timers, and stage DOM are cleaned up without submitting // microphone streams, timers, and stage DOM are cleaned up without submitting
@@ -993,8 +993,8 @@
console.info('[minigames] active session torn down (reason=' + reason + ')'); console.info('[minigames] active session torn down (reason=' + reason + ')');
} }
if (window.slopsmith && typeof window.slopsmith.on === 'function') { if (window.feedBack && typeof window.feedBack.on === 'function') {
window.slopsmith.on('screen:changed', (e) => { window.feedBack.on('screen:changed', (e) => {
const id = e && e.detail && e.detail.id; const id = e && e.detail && e.detail.id;
if (id === SCREEN_ID) { if (id === SCREEN_ID) {
renderHub(); renderHub();
@@ -1035,8 +1035,8 @@
function installNavLink() { function installNavLink() {
const navigateToHub = (e) => { const navigateToHub = (e) => {
if (e) e.preventDefault(); if (e) e.preventDefault();
if (window.slopsmith && typeof window.slopsmith.navigate === 'function') { if (window.feedBack && typeof window.feedBack.navigate === 'function') {
window.slopsmith.navigate(SCREEN_ID); window.feedBack.navigate(SCREEN_ID);
} else if (typeof window.showScreen === 'function') { } else if (typeof window.showScreen === 'function') {
window.showScreen(SCREEN_ID); window.showScreen(SCREEN_ID);
} }
@@ -1078,7 +1078,7 @@
} }
} }
installNavLink(); installNavLink();
// The slopsmith plugin loader rebuilds the dropdown when plugins // The feedBack plugin loader rebuilds the dropdown when plugins
// hot-reload — re-install on a short delay then settle. // hot-reload — re-install on a short delay then settle.
setTimeout(installNavLink, 250); setTimeout(installNavLink, 250);
setTimeout(installNavLink, 1500); setTimeout(installNavLink, 1500);
+2 -2
View File
@@ -22,14 +22,14 @@
if (!btn || !status) return; if (!btn || !status) return;
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
if (!confirm('Wipe all minigame XP, unlocks, and run history? This cannot be undone.')) return; if (!confirm('Wipe all minigame XP, unlocks, and run history? This cannot be undone.')) return;
if (!window.slopsmithMinigames?.resetProfile) { if (!window.feedBackMinigames?.resetProfile) {
status.textContent = 'Minigames SDK not loaded — reload the page and try again.'; status.textContent = 'Minigames SDK not loaded — reload the page and try again.';
return; return;
} }
btn.disabled = true; btn.disabled = true;
status.textContent = 'Wiping…'; status.textContent = 'Wiping…';
try { try {
await window.slopsmithMinigames.resetProfile(); await window.feedBackMinigames.resetProfile();
status.textContent = 'Profile reset.'; status.textContent = 'Profile reset.';
} catch (e) { } catch (e) {
status.textContent = 'Failed: ' + String(e?.message || e); status.textContent = 'Failed: ' + String(e?.message || e);
+14 -14
View File
@@ -1,22 +1,22 @@
# Slopsmith Tuner Plugin # FeedBack Tuner Plugin
<img width="290" height="362" alt="grafik" src="https://github.com/user-attachments/assets/879440e9-b680-481b-9091-ddfa73319078" /> <img width="290" height="362" alt="grafik" src="https://github.com/user-attachments/assets/879440e9-b680-481b-9091-ddfa73319078" />
A real-time guitar and bass tuner plugin for [Slopsmith](https://github.com/got-feedback/feedback). A real-time guitar and bass tuner plugin for [FeedBack](https://github.com/got-feedback/feedBack).
This plugin adds a floating "Tuner" button to the Slopsmith interface, providing a high-accuracy chromatic tuner with support for multiple presets, custom tunings, and automatic song tuning detection. This plugin adds a floating "Tuner" button to the FeedBack interface, providing a high-accuracy chromatic tuner with support for multiple presets, custom tunings, and automatic song tuning detection.
## Features ## Features
- **Real-time Pitch Detection**: Uses the YIN algorithm for robust and accurate frequency tracking. - **Real-time Pitch Detection**: Uses the YIN algorithm for robust and accurate frequency tracking.
- **Multiple Presets**: Includes common guitar and bass tunings (Standard, Drop D, DADGAD, Open G, etc.). - **Multiple Presets**: Includes common guitar and bass tunings (Standard, Drop D, DADGAD, Open G, etc.).
- **Automatic Song Tuning**: Detects and selects the correct tuning for the currently playing song in the Slopsmith player. - **Automatic Song Tuning**: Detects and selects the correct tuning for the currently playing song in the FeedBack player.
- **Manual & Auto Tracking**: Automatically estimates the closest string or allows manual selection for focused tuning. - **Manual & Auto Tracking**: Automatically estimates the closest string or allows manual selection for focused tuning.
- **Visual Feedback**: Large cents-deviation gauge, frequency display, and color-coded indicators. - **Visual Feedback**: Large cents-deviation gauge, frequency display, and color-coded indicators.
- **Custom Tunings**: Add your own tunings via note names (e.g., E2, A2) or Hz frequencies in the settings. - **Custom Tunings**: Add your own tunings via note names (e.g., E2, A2) or Hz frequencies in the settings.
- **Audio Device Selection**: Choose specific input devices and channels (Mono, Left, Right) for professional interfaces. - **Audio Device Selection**: Choose specific input devices and channels (Mono, Left, Right) for professional interfaces.
- **Themable UI**: Styled with Tailwind CSS to match your Slopsmith theme. - **Themable UI**: Styled with Tailwind CSS to match your FeedBack theme.
- **Visualizations**: Pick from different visualizations to suit your needs (Currently: Default, Strobe, Analogue Gauge, Mace Fx III, and Toilet Tuner) - **Visualizations**: Pick from different visualizations to suit your needs (Currently: Default, Strobe, Analogue Gauge, Mace Fx III, and Toilet Tuner)
## Available Visualizations ## Available Visualizations
@@ -34,18 +34,18 @@ This plugin adds a floating "Tuner" button to the Slopsmith interface, providing
## Installation ## Installation
### Download a Release ### Download a Release
1. Download one of the [Releases](https://github.com/OmikronApex/slopsmith-plugin-tuner/releases) 1. Download one of the [Releases](https://github.com/OmikronApex/feedBack-plugin-tuner/releases)
2. Extract it to your plugins folder 2. Extract it to your plugins folder
3. Restart Slopsmith 3. Restart FeedBack
### Update Manager ### Update Manager
The plugin is listed in the official plugin repository, so it can also be installed directly via the [Update Manager](https://github.com/masc0t/slopsmith-update-manager) The plugin is listed in the official plugin repository, so it can also be installed directly via the [Update Manager](https://github.com/masc0t/feedBack-update-manager)
### Git ### Git
```bash ```bash
cd /path/to/slopsmith/plugins cd /path/to/feedBack/plugins
git clone https://github.com/OmikronApex/slopsmith-plugin-tuner.git tuner git clone https://github.com/OmikronApex/feedBack-plugin-tuner.git tuner
# Restart Slopsmith (or restart your docker container) # Restart FeedBack (or restart your docker container)
docker compose restart docker compose restart
``` ```
@@ -70,7 +70,7 @@ Click the ⚙️ icon in the tuner window to access:
### Plugin Manager ### Plugin Manager
Access advanced settings via the Slopsmith Plugin Manager (Settings -> Plugins -> Tuner): Access advanced settings via the FeedBack Plugin Manager (Settings -> Plugins -> Tuner):
- **Floating Button**: Toggle the visibility of the tuner button on the main interface. - **Floating Button**: Toggle the visibility of the tuner button on the main interface.
- **Tuning Visibility**: Toggle which built-in tunings appear in your menu. - **Tuning Visibility**: Toggle which built-in tunings appear in your menu.
- **Custom Tunings**: Define your own tuning presets by entering a name and a list of notes/frequencies. - **Custom Tunings**: Define your own tuning presets by entering a name and a list of notes/frequencies.
@@ -82,7 +82,7 @@ Access advanced settings via the Slopsmith Plugin Manager (Settings -> Plugins -
## Changelog ## Changelog
### [1.3.1] - 2026-06-04 ### [1.3.1] - 2026-06-04
- JUCE bridge audio input: when running inside Slopsmith Desktop the tuner taps the engine's raw audio stream (`getRawAudioFrame`) and runs its own tuning-optimised YIN over it, falling back to the browser microphone pipeline otherwise. - JUCE bridge audio input: when running inside FeedBack Desktop the tuner taps the engine's raw audio stream (`getRawAudioFrame`) and runs its own tuning-optimised YIN over it, falling back to the browser microphone pipeline otherwise.
- Fixed octave-low / sub-harmonic pitch errors (canonical YIN absolute-threshold selection) and added octave-aware nearest-string matching. - Fixed octave-low / sub-harmonic pitch errors (canonical YIN absolute-threshold selection) and added octave-aware nearest-string matching.
- "Free Tune" is now remembered as your last tuning, so it persists across sessions instead of resetting to a preset each time. - "Free Tune" is now remembered as your last tuning, so it persists across sessions instead of resetting to a preset each time.
- Relocated visualization SVG assets to `visualization/assets/`, served via the dedicated `/api/plugins/tuner/viz-assets/` route (supersedes the 1.3.0 note about the root `assets/` directory). - Relocated visualization SVG assets to `visualization/assets/`, served via the dedicated `/api/plugins/tuner/viz-assets/` route (supersedes the 1.3.0 note about the root `assets/` directory).
@@ -93,7 +93,7 @@ Access advanced settings via the Slopsmith Plugin Manager (Settings -> Plugins -
- Added CHEF MT-3 visualization: inspired by the BOSS TU-3, featuring a 90° curved glass gauge arc, 51 tick marks, red 7-segment display, and rubber mode/brightness buttons. - Added CHEF MT-3 visualization: inspired by the BOSS TU-3, featuring a 90° curved glass gauge arc, 51 tick marks, red 7-segment display, and rubber mode/brightness buttons.
- Refactored `screen.js` into focused modules: audio pipeline extracted to `utils/audio.js`, UI layer extracted to `utils/ui.js` (shared-state factory pattern). `screen.js` reduced from ~1060 to ~300 lines. - Refactored `screen.js` into focused modules: audio pipeline extracted to `utils/audio.js`, UI layer extracted to `utils/ui.js` (shared-state factory pattern). `screen.js` reduced from ~1060 to ~300 lines.
- Normalised `DEFAULT_TUNINGS` keys to instrument keys (`guitar-6`, `bass-4`, etc.) — removes the internal group-name lookup table. - Normalised `DEFAULT_TUNINGS` keys to instrument keys (`guitar-6`, `bass-4`, etc.) — removes the internal group-name lookup table.
- Added plugin stylesheet (`assets/plugin.css`) via the Slopsmith styles contract, ensuring arbitrary Tailwind classes render correctly for runtime-installed users. - Added plugin stylesheet (`assets/plugin.css`) via the FeedBack styles contract, ensuring arbitrary Tailwind classes render correctly for runtime-installed users.
- Moved SVG assets (`Bathroom.svg`, `Plunger.svg`, `Toiletbowl.svg`) to the root `assets/` directory; removed the now-redundant custom asset route from `routes.py`. - Moved SVG assets (`Bathroom.svg`, `Plunger.svg`, `Toiletbowl.svg`) to the root `assets/` directory; removed the now-redundant custom asset route from `routes.py`.
- Moved Toilet Tuner to the end of the visualization picker list. - Moved Toilet Tuner to the end of the visualization picker list.
+1 -1
View File
@@ -25,7 +25,7 @@ def _migrate_custom_tuning(name: str, value) -> dict:
def setup(app: FastAPI, context: dict): def setup(app: FastAPI, context: dict):
config_dir = Path(context["config_dir"]) config_dir = Path(context["config_dir"])
config_file = config_dir / "tuner.json" config_file = config_dir / "tuner.json"
log = context.get("log") or logging.getLogger("slopsmith.plugin.tuner") log = context.get("log") or logging.getLogger("feedBack.plugin.tuner")
def _read() -> dict: def _read() -> dict:
defaults = { defaults = {
+25 -25
View File
@@ -1,7 +1,7 @@
// Guitar/Bass Tuner Plugin for Slopsmith // Guitar/Bass Tuner Plugin for FeedBack
(function() { (function() {
'use strict'; 'use strict';
const _TUNER_STORAGE_KEY = 'slopsmith_tuner_settings'; const _TUNER_STORAGE_KEY = 'feedBack_tuner_settings';
// ── Player sync state ───────────────────────────────────────────── // ── Player sync state ─────────────────────────────────────────────
let _onScreenChanged = null; let _onScreenChanged = null;
@@ -104,18 +104,18 @@
function _tuningIdentityKey(songInfo) { function _tuningIdentityKey(songInfo) {
if (!songInfo || !Array.isArray(songInfo.tuning) || !songInfo.tuning.length) return null; if (!songInfo || !Array.isArray(songInfo.tuning) || !songInfo.tuning.length) return null;
const ctx = (typeof window.slopsmith?.songTuningContext === 'function') const ctx = (typeof window.feedBack?.songTuningContext === 'function')
? window.slopsmith.songTuningContext(songInfo) ? window.feedBack.songTuningContext(songInfo)
: { : {
stringCount: songInfo.stringCount, stringCount: songInfo.stringCount,
arrangement: songInfo.arrangement, arrangement: songInfo.arrangement,
arrangement_smart_name: songInfo.arrangement_smart_name, arrangement_smart_name: songInfo.arrangement_smart_name,
}; };
const isBass = (typeof window.slopsmith?.isBassArrangement === 'function') const isBass = (typeof window.feedBack?.isBassArrangement === 'function')
? window.slopsmith.isBassArrangement(ctx) ? window.feedBack.isBassArrangement(ctx)
: (songInfo.arrangement || '').toLowerCase().includes('bass'); : (songInfo.arrangement || '').toLowerCase().includes('bass');
const sc = (typeof window.slopsmith?.effectiveStringCount === 'function') const sc = (typeof window.feedBack?.effectiveStringCount === 'function')
? window.slopsmith.effectiveStringCount(songInfo.tuning, ctx) ? window.feedBack.effectiveStringCount(songInfo.tuning, ctx)
: (songInfo.stringCount || songInfo.tuning.length); : (songInfo.stringCount || songInfo.tuning.length);
if (!sc || sc <= 0) return null; if (!sc || sc <= 0) return null;
const offsets = songInfo.tuning.slice(0, sc); const offsets = songInfo.tuning.slice(0, sc);
@@ -125,7 +125,7 @@
function _autoOpenSessionKey(songInfo) { function _autoOpenSessionKey(songInfo) {
if (!songInfo) return ''; if (!songInfo) return '';
const cur = window.slopsmith?.currentSong; const cur = window.feedBack?.currentSong;
const filename = (cur && cur.filename) || songInfo.filename || songInfo.title || 'unknown'; const filename = (cur && cur.filename) || songInfo.filename || songInfo.title || 'unknown';
const arr = (cur && cur.arrangementIndex != null) const arr = (cur && cur.arrangementIndex != null)
? cur.arrangementIndex ? cur.arrangementIndex
@@ -142,7 +142,7 @@
async function _maybeAutoOpenOnTuningChange() { async function _maybeAutoOpenOnTuningChange() {
if (!document.getElementById('player')?.classList.contains('active')) return; if (!document.getElementById('player')?.classList.contains('active')) return;
const songInfo = window.highway?.getSongInfo?.() || window.slopsmith?.currentSong; const songInfo = window.highway?.getSongInfo?.() || window.feedBack?.currentSong;
if (!songInfo) return; if (!songInfo) return;
const tuningKey = _tuningIdentityKey(songInfo); const tuningKey = _tuningIdentityKey(songInfo);
@@ -181,11 +181,11 @@
} }
function _installAutoOpenListeners() { function _installAutoOpenListeners() {
if (_onAutoOpenSongLoading || !window.slopsmith?.on) return; if (_onAutoOpenSongLoading || !window.feedBack?.on) return;
_onAutoOpenSongLoading = _onAutoOpenSongLoadingHandler; _onAutoOpenSongLoading = _onAutoOpenSongLoadingHandler;
_onAutoOpenSongReady = () => { _maybeAutoOpenOnTuningChange(); }; _onAutoOpenSongReady = () => { _maybeAutoOpenOnTuningChange(); };
window.slopsmith.on('song:loading', _onAutoOpenSongLoading); window.feedBack.on('song:loading', _onAutoOpenSongLoading);
window.slopsmith.on('song:ready', _onAutoOpenSongReady); window.feedBack.on('song:ready', _onAutoOpenSongReady);
} }
// ── Player sync helpers ─────────────────────────────────────────── // ── Player sync helpers ───────────────────────────────────────────
@@ -196,18 +196,18 @@
|| (onPlayer && songInfo?.tuning?.length); || (onPlayer && songInfo?.tuning?.length);
if (songInfo?.tuning?.length && wantCurrent) { if (songInfo?.tuning?.length && wantCurrent) {
_state.selectedTuningName = '_current'; _state.selectedTuningName = '_current';
const ctx = (typeof window.slopsmith?.songTuningContext === 'function') const ctx = (typeof window.feedBack?.songTuningContext === 'function')
? window.slopsmith.songTuningContext(songInfo) ? window.feedBack.songTuningContext(songInfo)
: { : {
stringCount: songInfo.stringCount, stringCount: songInfo.stringCount,
arrangement: songInfo.arrangement, arrangement: songInfo.arrangement,
arrangement_smart_name: songInfo.arrangement_smart_name, arrangement_smart_name: songInfo.arrangement_smart_name,
}; };
const isBass = (typeof window.slopsmith?.isBassArrangement === 'function') const isBass = (typeof window.feedBack?.isBassArrangement === 'function')
? window.slopsmith.isBassArrangement(ctx) ? window.feedBack.isBassArrangement(ctx)
: (songInfo.arrangement || '').toLowerCase().includes('bass'); : (songInfo.arrangement || '').toLowerCase().includes('bass');
const sc = (typeof window.slopsmith?.effectiveStringCount === 'function') const sc = (typeof window.feedBack?.effectiveStringCount === 'function')
? window.slopsmith.effectiveStringCount(songInfo.tuning, ctx) ? window.feedBack.effectiveStringCount(songInfo.tuning, ctx)
: (songInfo.stringCount || songInfo.tuning.length); : (songInfo.stringCount || songInfo.tuning.length);
_state.currentSongOffsets = songInfo.tuning.slice(0, sc); _state.currentSongOffsets = songInfo.tuning.slice(0, sc);
_state.currentSongIsBass = isBass; _state.currentSongIsBass = isBass;
@@ -378,14 +378,14 @@
_outsideClickClose = () => { if (_state.enabled) disable(); }; _outsideClickClose = () => { if (_state.enabled) disable(); };
setTimeout(() => { if (_outsideClickClose) document.addEventListener('click', _outsideClickClose, { once: true }); }, 0); setTimeout(() => { if (_outsideClickClose) document.addEventListener('click', _outsideClickClose, { once: true }); }, 0);
if (window.slopsmith && !_onScreenChanged) { if (window.feedBack && !_onScreenChanged) {
_onScreenChanged = () => { disable(); }; _onScreenChanged = () => { disable(); };
_onSongReady = () => { _onSongReady = () => {
_tunerUIApi.renderTuningOptions(); _tunerUIApi.renderTuningOptions();
if (_state.selectedTuningName === '_current') _syncCurrentTuning(); if (_state.selectedTuningName === '_current') _syncCurrentTuning();
}; };
window.slopsmith.on('screen:changed', _onScreenChanged); window.feedBack.on('screen:changed', _onScreenChanged);
window.slopsmith.on('song:ready', _onSongReady); window.feedBack.on('song:ready', _onSongReady);
} }
_state.uiContainer?.querySelector('.tuner-mic-error')?.remove(); _state.uiContainer?.querySelector('.tuner-mic-error')?.remove();
@@ -413,8 +413,8 @@
if (_outsideClickClose) { document.removeEventListener('click', _outsideClickClose); _outsideClickClose = null; } if (_outsideClickClose) { document.removeEventListener('click', _outsideClickClose); _outsideClickClose = null; }
if (_state.activeViz) { _state.activeViz.destroy(); _state.activeViz = null; } if (_state.activeViz) { _state.activeViz.destroy(); _state.activeViz = null; }
if (_state.uiContainer) { _state.uiContainer.classList.add('hidden'); _state.uiContainer.classList.remove('flex'); } if (_state.uiContainer) { _state.uiContainer.classList.add('hidden'); _state.uiContainer.classList.remove('flex'); }
if (_onScreenChanged) { window.slopsmith?.off('screen:changed', _onScreenChanged); _onScreenChanged = null; } if (_onScreenChanged) { window.feedBack?.off('screen:changed', _onScreenChanged); _onScreenChanged = null; }
if (_onSongReady) { window.slopsmith?.off('song:ready', _onSongReady); _onSongReady = null; } if (_onSongReady) { window.feedBack?.off('song:ready', _onSongReady); _onSongReady = null; }
if (window._tunerAudio) window._tunerAudio.stop(); if (window._tunerAudio) window._tunerAudio.stop();
if (_state.vizContainer) _state.vizContainer.innerHTML = ''; if (_state.vizContainer) _state.vizContainer.innerHTML = '';
if (window.tuner?.updateButtons) window.tuner.updateButtons(); if (window.tuner?.updateButtons) window.tuner.updateButtons();
@@ -426,7 +426,7 @@
).catch(e => console.warn('Tuner: badge audio resume failed:', e && e.message ? e.message : e)); ).catch(e => console.warn('Tuner: badge audio resume failed:', e && e.message ? e.message : e));
} }
if (wasEnabled && onPlayer) { if (wasEnabled && onPlayer) {
const songInfo = window.highway?.getSongInfo?.() || window.slopsmith?.currentSong; const songInfo = window.highway?.getSongInfo?.() || window.feedBack?.currentSong;
if (songInfo) _autoOpenDismissedSessionKey = _autoOpenSessionKey(songInfo); if (songInfo) _autoOpenDismissedSessionKey = _autoOpenSessionKey(songInfo);
} }
} }
+2 -2
View File
@@ -11,7 +11,7 @@
</div> </div>
<script> <script>
if (window.slopsmithDesktop && window.slopsmithDesktop.isDesktop) { if (window.feedBackDesktop && window.feedBackDesktop.isDesktop) {
document.currentScript.insertAdjacentHTML('beforebegin', ` document.currentScript.insertAdjacentHTML('beforebegin', `
<div class="flex items-center justify-between bg-dark-900/50 p-3 rounded-xl border border-gray-800/50"> <div class="flex items-center justify-between bg-dark-900/50 p-3 rounded-xl border border-gray-800/50">
<div> <div>
@@ -129,7 +129,7 @@
body: JSON.stringify(config) body: JSON.stringify(config)
}); });
if (window._tunerReloadConfig) window._tunerReloadConfig(); if (window._tunerReloadConfig) window._tunerReloadConfig();
if (opts && opts.tuningsChanged) window.slopsmith?.emit('tunings:updated'); if (opts && opts.tuningsChanged) window.feedBack?.emit('tunings:updated');
} catch (e) { console.error('Tuner settings: save failed', e); } } catch (e) { console.error('Tuner settings: save failed', e); }
} }
+1 -1
View File
@@ -86,7 +86,7 @@
async function _tryBridgeStart(audioInputMode, myGen) { async function _tryBridgeStart(audioInputMode, myGen) {
if (audioInputMode === 'browser') return false; if (audioInputMode === 'browser') return false;
var desktop = (typeof window !== 'undefined') ? window.slopsmithDesktop : null; var desktop = (typeof window !== 'undefined') ? window.feedBackDesktop : null;
if (!desktop || !desktop.isDesktop || !desktop.audio if (!desktop || !desktop.isDesktop || !desktop.audio
|| typeof desktop.audio.isAvailable !== 'function') return false; || typeof desktop.audio.isAvailable !== 'function') return false;
+21 -21
View File
@@ -149,7 +149,7 @@ window._tunerUI = function(state, actions) {
if (state.tuningSelect) state.tuningSelect.value = name; if (state.tuningSelect) state.tuningSelect.value = name;
renderStringNotes(); renderStringNotes();
actions.saveConfig(); actions.saveConfig();
window.slopsmith?.emit('tunings:updated'); window.feedBack?.emit('tunings:updated');
} catch (e) { } catch (e) {
console.error('Tuner: Failed to save custom tuning', e); console.error('Tuner: Failed to save custom tuning', e);
} }
@@ -204,18 +204,18 @@ window._tunerUI = function(state, actions) {
if (isPlayer && typeof window.highway?.getSongInfo === 'function') { if (isPlayer && typeof window.highway?.getSongInfo === 'function') {
const info = window.highway.getSongInfo(); const info = window.highway.getSongInfo();
if (info && info.tuning) { if (info && info.tuning) {
const ctx = (typeof window.slopsmith?.songTuningContext === 'function') const ctx = (typeof window.feedBack?.songTuningContext === 'function')
? window.slopsmith.songTuningContext(info) ? window.feedBack.songTuningContext(info)
: { : {
stringCount: info.stringCount, stringCount: info.stringCount,
arrangement: info.arrangement, arrangement: info.arrangement,
arrangement_smart_name: info.arrangement_smart_name, arrangement_smart_name: info.arrangement_smart_name,
}; };
const isBass = (typeof window.slopsmith?.isBassArrangement === 'function') const isBass = (typeof window.feedBack?.isBassArrangement === 'function')
? window.slopsmith.isBassArrangement(ctx) ? window.feedBack.isBassArrangement(ctx)
: (info.arrangement || '').toLowerCase().includes('bass'); : (info.arrangement || '').toLowerCase().includes('bass');
const sc = (typeof window.slopsmith?.effectiveStringCount === 'function') const sc = (typeof window.feedBack?.effectiveStringCount === 'function')
? window.slopsmith.effectiveStringCount(info.tuning, ctx) ? window.feedBack.effectiveStringCount(info.tuning, ctx)
: (info.stringCount || info.tuning.length); : (info.stringCount || info.tuning.length);
const sliced = info.tuning.slice(0, sc); const sliced = info.tuning.slice(0, sc);
const freqs = window._tunerUtils.offsetsToFreqs(sliced, isBass); const freqs = window._tunerUtils.offsetsToFreqs(sliced, isBass);
@@ -340,8 +340,8 @@ window._tunerUI = function(state, actions) {
_lastAutoTargetFreq = null; _lastAutoTargetFreq = null;
if (state.activeViz) state.activeViz.update(null, 0, 0, vizMode, null, referencePitch); if (state.activeViz) state.activeViz.update(null, 0, 0, vizMode, null, referencePitch);
_syncStringHighlight(state.manualTargetFreq); _syncStringHighlight(state.manualTargetFreq);
if (window.slopsmith && window.slopsmith.emit) { if (window.feedBack && window.feedBack.emit) {
window.slopsmith.emit('tuner:frame', { note: null, cents: 0, freq: 0, hasSignal: false }); window.feedBack.emit('tuner:frame', { note: null, cents: 0, freq: 0, hasSignal: false });
} }
return; return;
} }
@@ -384,8 +384,8 @@ window._tunerUI = function(state, actions) {
if (state.activeViz) state.activeViz.update(note, cents, displayFreq, vizMode, targetFreq, referencePitch, state.useFlats); if (state.activeViz) state.activeViz.update(note, cents, displayFreq, vizMode, targetFreq, referencePitch, state.useFlats);
if (state.freeTune) _syncStringHighlight(null); if (state.freeTune) _syncStringHighlight(null);
else _syncActiveStringFromFreq(targetFreq, isManual); else _syncActiveStringFromFreq(targetFreq, isManual);
if (window.slopsmith && window.slopsmith.emit) { if (window.feedBack && window.feedBack.emit) {
window.slopsmith.emit('tuner:frame', { note, cents, freq: displayFreq, hasSignal: true }); window.feedBack.emit('tuner:frame', { note, cents, freq: displayFreq, hasSignal: true });
} }
} }
@@ -393,7 +393,7 @@ window._tunerUI = function(state, actions) {
const btn = document.getElementById('tuner-toggle-btn'); const btn = document.getElementById('tuner-toggle-btn');
if (!btn) return; if (!btn) return;
const isPlayer = document.querySelector('.screen.active')?.id === 'player'; const isPlayer = document.querySelector('.screen.active')?.id === 'player';
if (!state.showFloatingButton || isPlayer || window.slopsmith?.isPlaying) { if (!state.showFloatingButton || isPlayer || window.feedBack?.isPlaying) {
btn.classList.add('hidden'); btn.classList.add('hidden');
} else { } else {
btn.classList.remove('hidden'); btn.classList.remove('hidden');
@@ -687,16 +687,16 @@ window._tunerUI = function(state, actions) {
}; };
const handleStop = () => updateFloatingButtonVisibility(); const handleStop = () => updateFloatingButtonVisibility();
if (window.slopsmith) { if (window.feedBack) {
window.slopsmith.on('song:play', handlePlay); window.feedBack.on('song:play', handlePlay);
window.slopsmith.on('song:pause', handleStop); window.feedBack.on('song:pause', handleStop);
window.slopsmith.on('song:ended', handleStop); window.feedBack.on('song:ended', handleStop);
window.slopsmith.on('screen:changed', (e) => { window.feedBack.on('screen:changed', (e) => {
if (e.detail.id === 'player') { handlePlay(); injectPlayerButton(); } if (e.detail.id === 'player') { handlePlay(); injectPlayerButton(); }
else handleStop(); else handleStop();
}); });
if (window.slopsmith.isPlaying || document.querySelector('.screen.active')?.id === 'player') { if (window.feedBack.isPlaying || document.querySelector('.screen.active')?.id === 'player') {
handlePlay(); handlePlay();
if (document.querySelector('.screen.active')?.id === 'player') injectPlayerButton(); if (document.querySelector('.screen.active')?.id === 'player') injectPlayerButton();
} else { } else {
@@ -710,10 +710,10 @@ window._tunerUI = function(state, actions) {
// popover). The legacy `button:last-child` anchor resolves to a NESTED // popover). The legacy `button:last-child` anchor resolves to a NESTED
// transport button in v3 and would throw on insertBefore; the slot is // transport button in v3 and would throw on insertBefore; the slot is
// always present in v3, so that anchor is only used in the classic UI. // always present in v3, so that anchor is only used in the classic UI.
const isV3 = !!(window.slopsmith && window.slopsmith.uiVersion === 'v3'); const isV3 = !!(window.feedBack && window.feedBack.uiVersion === 'v3');
let slot = null; let slot = null;
if (isV3 && window.slopsmith.ui && typeof window.slopsmith.ui.playerControlSlot === 'function') { if (isV3 && window.feedBack.ui && typeof window.feedBack.ui.playerControlSlot === 'function') {
try { const _s = window.slopsmith.ui.playerControlSlot(); if (_s instanceof Element) slot = _s; } try { const _s = window.feedBack.ui.playerControlSlot(); if (_s instanceof Element) slot = _s; }
catch (_e) { /* host slot API failure → fall back to legacy container */ } catch (_e) { /* host slot API failure → fall back to legacy container */ }
} }
const controls = slot || document.getElementById('player-controls'); const controls = slot || document.getElementById('player-controls');
@@ -1,5 +1,5 @@
/** /**
* Analogue gauge tuner visualization for the Slopsmith tuner plugin. * Analogue gauge tuner visualization for the FeedBack tuner plugin.
* *
* Contract: window['_tunerViz_analogue-gauge'](container) { update(note, cents, freq), destroy() } * Contract: window['_tunerViz_analogue-gauge'](container) { update(note, cents, freq), destroy() }
* - note: string | null (null = no signal) * - note: string | null (null = no signal)
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* CHEF MT-3 tuner visualization for the Slopsmith tuner plugin. * CHEF MT-3 tuner visualization for the FeedBack tuner plugin.
* *
* Inspired by classic chromatic pedal tuners: * Inspired by classic chromatic pedal tuners:
* - Shiny black rectangular panel with chamfered edges and corner screws * - Shiny black rectangular panel with chamfered edges and corner screws
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* Default (gauge) tuner visualization for the Slopsmith tuner plugin. * Default (gauge) tuner visualization for the FeedBack tuner plugin.
* *
* Contract: window._tunerViz_default(container) { update(note, cents, freq), destroy() } * Contract: window._tunerViz_default(container) { update(note, cents, freq), destroy() }
* - note: string | null (null = no signal) * - note: string | null (null = no signal)
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* Mace Fx III style tuner visualization for the Slopsmith tuner plugin. * Mace Fx III style tuner visualization for the FeedBack tuner plugin.
* *
* Inspired by hardware rack tuner displays: * Inspired by hardware rack tuner displays:
* - Dark navy LCD background * - Dark navy LCD background
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* Strobe tuner visualization for the Slopsmith tuner plugin. * Strobe tuner visualization for the FeedBack tuner plugin.
* *
* Contract: window._tunerViz_strobe(container) { update(note, cents, freq), destroy() } * Contract: window._tunerViz_strobe(container) { update(note, cents, freq), destroy() }
* - note: string | null (null = no signal) * - note: string | null (null = no signal)
+1 -1
View File
@@ -1,5 +1,5 @@
/** /**
* Toilet Tuner visualization for the Slopsmith tuner plugin. * Toilet Tuner visualization for the FeedBack tuner plugin.
* *
* Bathroom scene background; plunger slides left/right over the bowl based on * Bathroom scene background; plunger slides left/right over the bowl based on
* cents deviation; dips into bowl when in tune (±2 cents); wall calendar shows * cents deviation; dips into bowl when in tune (±2 cents); wall calendar shows
+1 -1
View File
@@ -79,7 +79,7 @@ from notation_lift import ( # noqa: E402
split_hands, split_hands,
) )
log = logging.getLogger("slopsmith.scripts.lift_keys_notation") log = logging.getLogger("feedBack.scripts.lift_keys_notation")
_SAFE_ID_RE = re.compile(r"[A-Za-z0-9_-]+") _SAFE_ID_RE = re.compile(r"[A-Za-z0-9_-]+")
+51 -50
View File
@@ -1,4 +1,4 @@
"""Slopsmith — FastAPI backend serving highway viewer + library.""" """FeedBack — FastAPI backend serving highway viewer + library."""
import asyncio import asyncio
import bisect import bisect
@@ -15,9 +15,10 @@ from pathlib import Path
from typing import Any, ClassVar from typing import Any, ClassVar
from logging_setup import configure_logging from logging_setup import configure_logging
from env_compat import getenv_compat
configure_logging() configure_logging()
log = logging.getLogger("slopsmith.server") log = logging.getLogger("feedBack.server")
from fastapi import Body, FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File, HTTPException, Query from fastapi import Body, FastAPI, WebSocket, WebSocketDisconnect, UploadFile, File, HTTPException, Query
from fastapi.concurrency import run_in_threadpool from fastapi.concurrency import run_in_threadpool
@@ -67,7 +68,7 @@ import xml.etree.ElementTree as ET
import structlog import structlog
from fastapi import Request from fastapi import Request
app = FastAPI(title="Slopsmith") app = FastAPI(title="FeedBack")
# Plugins that maintain session stores can register a cleanup callback here. # Plugins that maintain session stores can register a cleanup callback here.
# The demo-mode janitor calls every registered hook once per hour so stale # The demo-mode janitor calls every registered hook once per hour so stale
@@ -230,17 +231,17 @@ _DEMO_BLOCKED: list[tuple[str, re.Pattern]] = [
@app.middleware("http") @app.middleware("http")
async def _demo_mode_guard(request: Request, call_next): async def _demo_mode_guard(request: Request, call_next):
if os.environ.get("SLOPSMITH_DEMO_MODE") == "1": if getenv_compat("FEEDBACK_DEMO_MODE") or getenv_compat("FEEDBACK_DEMO_MODE") == "1":
path = request.url.path path = request.url.path
for method, pattern in _DEMO_BLOCKED: for method, pattern in _DEMO_BLOCKED:
if request.method == method and pattern.match(path): if request.method == method and pattern.match(path):
return JSONResponse({"error": "demo mode: read-only"}, status_code=403) return JSONResponse({"error": "demo mode: read-only"}, status_code=403)
response = await call_next(request) response = await call_next(request)
if request.method == "GET" and path == "/" and "slopsmith_demo_session" not in request.cookies: if request.method == "GET" and path == "/" and "feedBack_demo_session" not in request.cookies:
forwarded_proto = (request.headers.get("x-forwarded-proto") or "").split(",")[0].strip() forwarded_proto = (request.headers.get("x-forwarded-proto") or "").split(",")[0].strip()
is_secure = request.url.scheme == "https" or forwarded_proto.lower() == "https" is_secure = request.url.scheme == "https" or forwarded_proto.lower() == "https"
response.set_cookie( response.set_cookie(
"slopsmith_demo_session", str(uuid.uuid4()), "feedBack_demo_session", str(uuid.uuid4()),
max_age=86400, httponly=True, samesite="lax", max_age=86400, httponly=True, samesite="lax",
secure=is_secure, secure=is_secure,
) )
@@ -275,11 +276,11 @@ SLOPPAK_CACHE_DIR = CONFIG_DIR / "sloppak_cache"
def _env_flag(name: str) -> bool: def _env_flag(name: str) -> bool:
"""Parse a conventional boolean env flag.""" """Parse a conventional boolean env flag (honours legacy SLOPSMITH_* alias)."""
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"} return (getenv_compat(name, "") or "").strip().lower() in {"1", "true", "yes", "on"}
# Canonical Tuning-filter grouping key (slopsmith#867). tuning_name collapses # Canonical Tuning-filter grouping key (feedBack#867). tuning_name collapses
# every non-standard tuning to "Custom Tuning"; for those rows we key on the # every non-standard tuning to "Custom Tuning"; for those rows we key on the
# raw offsets so distinct customs stay distinct, while named tunings keep # raw offsets so distinct customs stay distinct, while named tunings keep
# grouping by name (stable across the offsets-column migration). Used by both # grouping by name (stable across the offsets-column migration). Used by both
@@ -391,14 +392,14 @@ class MetadataDB:
for ddl in ( for ddl in (
"ALTER TABLE songs ADD COLUMN format TEXT DEFAULT 'archive'", "ALTER TABLE songs ADD COLUMN format TEXT DEFAULT 'archive'",
"ALTER TABLE songs ADD COLUMN stem_count INTEGER DEFAULT 0", "ALTER TABLE songs ADD COLUMN stem_count INTEGER DEFAULT 0",
# slopsmith#129: per-stem filter needs the id list, not just count. # feedBack#129: per-stem filter needs the id list, not just count.
"ALTER TABLE songs ADD COLUMN stem_ids TEXT DEFAULT '[]'", "ALTER TABLE songs ADD COLUMN stem_ids TEXT DEFAULT '[]'",
# slopsmith#69 + #22: denormalized canonical tuning name + numeric # feedBack#69 + #22: denormalized canonical tuning name + numeric
# sort key (sum of offsets). The existing `tuning` text column # sort key (sum of offsets). The existing `tuning` text column
# stays — these are caches, repopulated on rescan. # stays — these are caches, repopulated on rescan.
"ALTER TABLE songs ADD COLUMN tuning_name TEXT DEFAULT ''", "ALTER TABLE songs ADD COLUMN tuning_name TEXT DEFAULT ''",
"ALTER TABLE songs ADD COLUMN tuning_sort_key INTEGER DEFAULT 0", "ALTER TABLE songs ADD COLUMN tuning_sort_key INTEGER DEFAULT 0",
# slopsmith#867: raw per-string offsets (space-joined ints) so the # feedBack#867: raw per-string offsets (space-joined ints) so the
# v3 client can render target notes and the Tuning filter can keep # v3 client can render target notes and the Tuning filter can keep
# distinct custom tunings distinct (tuning_name collapses them all # distinct custom tunings distinct (tuning_name collapses them all
# to "Custom Tuning"). Cache; repopulated on rescan. # to "Custom Tuning"). Cache; repopulated on rescan.
@@ -1582,7 +1583,7 @@ class MetadataDB:
# Manifest-allowed filter values. Whitelisted before binding so a # Manifest-allowed filter values. Whitelisted before binding so a
# malformed query string can't push arbitrary text through to SQL — # malformed query string can't push arbitrary text through to SQL —
# parameters are bound, but capping the input space is still cheap # parameters are bound, but capping the input space is still cheap
# defense-in-depth (see slopsmith#129). # defense-in-depth (see feedBack#129).
_ALLOWED_ARRANGEMENT_NAMES = {"Lead", "Rhythm", "Bass", "Combo"} _ALLOWED_ARRANGEMENT_NAMES = {"Lead", "Rhythm", "Bass", "Combo"}
# Per-smart-type list of (sql_op, sql_param) pairs appended to the SQL # Per-smart-type list of (sql_op, sql_param) pairs appended to the SQL
# name-fallback branch (key-absent smart_name). Covers legacy raw names # name-fallback branch (key-absent smart_name). Covers legacy raw names
@@ -1622,7 +1623,7 @@ class MetadataDB:
naming_mode: str = "legacy") -> tuple[str, list]: naming_mode: str = "legacy") -> tuple[str, list]:
"""Shared WHERE-clause builder for query_page / query_artists / """Shared WHERE-clause builder for query_page / query_artists /
query_stats. Returns (where_sql, params). Leading 'WHERE' is query_stats. Returns (where_sql, params). Leading 'WHERE' is
included so callers paste it directly. See slopsmith#129/#69. included so callers paste it directly. See feedBack#129/#69.
""" """
where = "WHERE title != ''" where = "WHERE title != ''"
params: list = [] params: list = []
@@ -1804,7 +1805,7 @@ class MetadataDB:
"title": "title COLLATE NOCASE", "title-desc": "title COLLATE NOCASE DESC", "title": "title COLLATE NOCASE", "title-desc": "title COLLATE NOCASE DESC",
"recent": "mtime DESC", "recent": "mtime DESC",
# Tuning sort uses musical distance from E Standard # Tuning sort uses musical distance from E Standard
# (slopsmith#22 — was alphabetical). `tuning_sort_key` is # (feedBack#22 — was alphabetical). `tuning_sort_key` is
# the sum of per-string offsets, so |sort_key| is the # the sum of per-string offsets, so |sort_key| is the
# magnitude of the down/up-tune. ABS ascending puts E # magnitude of the down/up-tune. ABS ascending puts E
# Standard (0) first, then ±2 (Drop D, F Standard), then # Standard (0) first, then ±2 (Drop D, F Standard), then
@@ -1832,7 +1833,7 @@ class MetadataDB:
"COALESCE(tuning_sort_key, 0) ASC, " "COALESCE(tuning_sort_key, 0) ASC, "
"COALESCE(tuning_name, '') COLLATE NOCASE" "COALESCE(tuning_name, '') COLLATE NOCASE"
), ),
# Year sort (slopsmith#128). Empty-year rows pushed to the # Year sort (feedBack#128). Empty-year rows pushed to the
# bottom for both directions; otherwise CAST so '2010' > # bottom for both directions; otherwise CAST so '2010' >
# '2005' rather than alphabetic. # '2005' rather than alphabetic.
"year": "(year = '') ASC, CAST(year AS INTEGER) ASC", "year": "(year = '') ASC, CAST(year AS INTEGER) ASC",
@@ -2950,7 +2951,7 @@ _scan_status = dict(_SCAN_STATUS_INIT)
_STARTUP_STATUS_INIT = { _STARTUP_STATUS_INIT = {
"running": True, "running": True,
"phase": "booting", "phase": "booting",
"message": "Starting Slopsmith server...", "message": "Starting FeedBack server...",
"current_plugin": "", "current_plugin": "",
"loaded": 0, "loaded": 0,
"total": 0, "total": 0,
@@ -3034,13 +3035,13 @@ def _make_scan_executor():
mp_ctx = multiprocessing.get_context("spawn") mp_ctx = multiprocessing.get_context("spawn")
# Default to one worker per core so CPU-bound metadata parsing uses the # Default to one worker per core so CPU-bound metadata parsing uses the
# whole machine (the point of moving to processes). # whole machine (the point of moving to processes).
# SLOPSMITH_MAX_SCAN_WORKERS (set by the Desktop launcher to cap memory # FEEDBACK_MAX_SCAN_WORKERS (set by the Desktop launcher to cap memory
# usage on low-RAM machines — e.g. 8 GB M2 MacBook Air) takes priority; # usage on low-RAM machines — e.g. 8 GB M2 MacBook Air) takes priority;
# SCAN_MAX_WORKERS is a legacy override for Docker/bare installs. # SCAN_MAX_WORKERS is a legacy override for Docker/bare installs.
# A malformed override falls back to the core count rather than crashing. # A malformed override falls back to the core count rather than crashing.
try: try:
max_workers = int( max_workers = int(
os.environ.get("SLOPSMITH_MAX_SCAN_WORKERS") getenv_compat("FEEDBACK_MAX_SCAN_WORKERS")
or os.environ.get("SCAN_MAX_WORKERS") or os.environ.get("SCAN_MAX_WORKERS")
or (os.cpu_count() or 1) or (os.cpu_count() or 1)
) )
@@ -3060,14 +3061,14 @@ def _make_scan_executor():
_BUILTIN_DIAGNOSTIC_SUBDIR = "diagnostics-builtin" _BUILTIN_DIAGNOSTIC_SUBDIR = "diagnostics-builtin"
_BUILTIN_DIAGNOSTIC_SOURCES: list[tuple[str, str]] = [ _BUILTIN_DIAGNOSTIC_SOURCES: list[tuple[str, str]] = [
( (
"slopsmith-diagnostic-basic-guitar.sloppak", "feedBack-diagnostic-basic-guitar.sloppak",
"docs/diagnostics/slopsmith-diagnostic-basic-guitar.sloppak", "docs/diagnostics/feedBack-diagnostic-basic-guitar.sloppak",
), ),
] ]
def _slopsmith_server_root() -> Path: def _feedBack_server_root() -> Path:
"""Directory containing server.py (repo root in dev; resources/slopsmith when bundled).""" """Directory containing server.py (repo root in dev; resources/feedBack when bundled)."""
return Path(__file__).resolve().parent return Path(__file__).resolve().parent
@@ -3079,7 +3080,7 @@ def _builtin_diagnostic_filename() -> str:
# Progression content (spec 010): bundled JSON under data/progression/ (paths, # Progression content (spec 010): bundled JSON under data/progression/ (paths,
# quest pools, shop catalog). Loaded lazily-once; invalid entries are logged # quest pools, shop catalog). Loaded lazily-once; invalid entries are logged
# warnings, never fatal. SLOPSMITH_PROGRESSION_DATA overrides the root (tests). # warnings, never fatal. FEEDBACK_PROGRESSION_DATA overrides the root (tests).
_progression_content: dict | None = None _progression_content: dict | None = None
_progression_content_lock = threading.Lock() _progression_content_lock = threading.Lock()
@@ -3090,8 +3091,8 @@ def _get_progression_content() -> dict:
with _progression_content_lock: with _progression_content_lock:
if _progression_content is None: if _progression_content is None:
import progression as progression_mod import progression as progression_mod
root = os.environ.get("SLOPSMITH_PROGRESSION_DATA") or ( root = getenv_compat("FEEDBACK_PROGRESSION_DATA") or (
_slopsmith_server_root() / "data" / "progression" _feedBack_server_root() / "data" / "progression"
) )
content, warnings = progression_mod.load_content(root) content, warnings = progression_mod.load_content(root)
for warning in warnings: for warning in warnings:
@@ -3115,7 +3116,7 @@ def _seed_builtin_diagnostic_sloppaks(dlc: Path | None = None) -> None:
log.debug("Builtin diagnostic seed: no DLC folder configured, skipping") log.debug("Builtin diagnostic seed: no DLC folder configured, skipping")
return return
root = _slopsmith_server_root() root = _feedBack_server_root()
dest_dir = dlc / _BUILTIN_DIAGNOSTIC_SUBDIR dest_dir = dlc / _BUILTIN_DIAGNOSTIC_SUBDIR
# Refuse a symlinked seed directory: mkdir(exist_ok=True) would accept # Refuse a symlinked seed directory: mkdir(exist_ok=True) would accept
# it and copies would land at the link target, outside the DLC tree. # it and copies would land at the link target, outside the DLC tree.
@@ -3417,7 +3418,7 @@ async def startup_events():
# phase so any frontend startup waiter that observes the lifespan also # phase so any frontend startup waiter that observes the lifespan also
# unblocks cleanly (the SSE/poll client treats only `complete` and # unblocks cleanly (the SSE/poll client treats only `complete` and
# `error` as terminal when `running` becomes false). # `error` as terminal when `running` becomes false).
if _env_flag("SLOPSMITH_SKIP_STARTUP_TASKS"): if _env_flag("FEEDBACK_SKIP_STARTUP_TASKS"):
log.info("[startup] Skipping plugin load and background scan") log.info("[startup] Skipping plugin load and background scan")
# Tests pop `server` from sys.modules across runs, but the `plugins` # Tests pop `server` from sys.modules across runs, but the `plugins`
# module is not reloaded — so LOADED_PLUGINS can carry stale entries # module is not reloaded — so LOADED_PLUGINS can carry stale entries
@@ -3432,7 +3433,7 @@ async def startup_events():
_set_startup_status( _set_startup_status(
running=False, running=False,
phase="complete", phase="complete",
message="Startup tasks skipped (SLOPSMITH_SKIP_STARTUP_TASKS).", message="Startup tasks skipped (FEEDBACK_SKIP_STARTUP_TASKS).",
error=None, error=None,
current_plugin="", current_plugin="",
loaded=0, loaded=0,
@@ -3488,7 +3489,7 @@ async def startup_events():
# Load plugins asynchronously so HTTP routes and the desktop window can # Load plugins asynchronously so HTTP routes and the desktop window can
# come up immediately while heavy plugin imports/install steps continue. # come up immediately while heavy plugin imports/install steps continue.
_sync_mode = os.environ.get("SLOPSMITH_SYNC_STARTUP", "").lower() in {"1", "true", "yes", "on"} _sync_mode = getenv_compat("FEEDBACK_SYNC_STARTUP", "").lower() in {"1", "true", "yes", "on"}
def _load_plugins_background(): def _load_plugins_background():
try: try:
@@ -3677,7 +3678,7 @@ async def startup_events():
route_setup_fn=_route_setup_on_main) route_setup_fn=_route_setup_on_main)
# Self-heal a freshly recreated container: its filesystem reset to # Self-heal a freshly recreated container: its filesystem reset to
# the image-baked sheet (in-tree plugins only), but a mounted # the image-baked sheet (in-tree plugins only), but a mounted
# SLOPSMITH_PLUGINS_DIR may carry user-installed plugins whose # FEEDBACK_PLUGINS_DIR may carry user-installed plugins whose
# classes aren't in it. Run in its OWN daemon thread so the startup # classes aren't in it. Run in its OWN daemon thread so the startup
# status can flip to "complete" immediately rather than waiting on # status can flip to "complete" immediately rather than waiting on
# the (up to 120s) Tailwind subprocess. No-op when there are no user # the (up to 120s) Tailwind subprocess. No-op when there are no user
@@ -3723,7 +3724,7 @@ async def startup_events():
threading.Thread(target=_load_plugins_background, daemon=True).start() threading.Thread(target=_load_plugins_background, daemon=True).start()
global _DEMO_JANITOR_STARTED, _DEMO_JANITOR_THREAD global _DEMO_JANITOR_STARTED, _DEMO_JANITOR_THREAD
if os.environ.get("SLOPSMITH_DEMO_MODE") == "1" and not _DEMO_JANITOR_STARTED: if getenv_compat("FEEDBACK_DEMO_MODE") or getenv_compat("FEEDBACK_DEMO_MODE") == "1" and not _DEMO_JANITOR_STARTED:
_DEMO_JANITOR_STARTED = True _DEMO_JANITOR_STARTED = True
_DEMO_JANITOR_STOP.clear() _DEMO_JANITOR_STOP.clear()
def _janitor(): def _janitor():
@@ -3830,7 +3831,7 @@ def get_version():
version = version_file.read_text().strip() version = version_file.read_text().strip()
except (OSError, UnicodeDecodeError): except (OSError, UnicodeDecodeError):
pass pass
default_source_url = "https://github.com/got-feedback/feedback" default_source_url = "https://github.com/got-feedback/feedBack"
# APP_SOURCE_URL / APP_LICENSE_URL flow straight into <a href> in the UI, # APP_SOURCE_URL / APP_LICENSE_URL flow straight into <a href> in the UI,
# so validate with urllib.parse rather than a bare prefix check — a prefix # so validate with urllib.parse rather than a bare prefix check — a prefix
# check accepts malformed values like "https://" (no host) which produce # check accepts malformed values like "https://" (no host) which produce
@@ -4514,7 +4515,7 @@ async def list_tuning_names(provider: str = "local"):
"""Distinct tuning names present in the library, with per-tuning """Distinct tuning names present in the library, with per-tuning
counts. Powers the tuning multi-select. Sorted by `tuning_sort_key` counts. Powers the tuning multi-select. Sorted by `tuning_sort_key`
so names appear in the same musical order the sort uses so names appear in the same musical order the sort uses
(slopsmith#22) — E Standard first, then nearest neighbors.""" (feedBack#22) — E Standard first, then nearest neighbors."""
library_provider = _get_library_provider(provider) library_provider = _get_library_provider(provider)
_require_library_provider_capability(library_provider, "library.read") _require_library_provider_capability(library_provider, "library.read")
return await _call_library_provider_async(library_provider, "tuning_names") return await _call_library_provider_async(library_provider, "tuning_names")
@@ -5573,7 +5574,7 @@ def save_settings(data: dict):
return {"message": ". ".join(messages) if messages else "Settings saved"} return {"message": ". ".join(messages) if messages else "Settings saved"}
# ── Settings export/import (slopsmith#113) ─────────────────────────────────── # ── Settings export/import (feedBack#113) ───────────────────────────────────
# Bumped only when the bundle JSON shape changes incompatibly. Importer # Bumped only when the bundle JSON shape changes incompatibly. Importer
# refuses anything but this exact value — version mismatches are warned # refuses anything but this exact value — version mismatches are warned
@@ -5945,7 +5946,7 @@ def _atomic_write_file(target: Path, payload: bytes):
def export_settings(): def export_settings():
"""Build a settings bundle covering server config + opted-in plugin """Build a settings bundle covering server config + opted-in plugin
server-side files. Frontend layers in `local_storage` before server-side files. Frontend layers in `local_storage` before
triggering the download. See slopsmith#113.""" triggering the download. See feedBack#113."""
import datetime import datetime
from plugins import LOADED_PLUGINS, PLUGINS_LOCK from plugins import LOADED_PLUGINS, PLUGINS_LOCK
@@ -5968,11 +5969,11 @@ def export_settings():
bundle = { bundle = {
"schema": SETTINGS_BUNDLE_SCHEMA, "schema": SETTINGS_BUNDLE_SCHEMA,
"exported_at": now.strftime("%Y-%m-%dT%H:%M:%SZ"), "exported_at": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
"slopsmith_version": _running_version(), "feedBack_version": _running_version(),
"server_config": server_config, "server_config": server_config,
"plugin_server_configs": plugin_blocks, "plugin_server_configs": plugin_blocks,
} }
filename = f"slopsmith-settings-{now.strftime('%Y-%m-%d')}.json" filename = f"feedBack-settings-{now.strftime('%Y-%m-%d')}.json"
return JSONResponse( return JSONResponse(
bundle, bundle,
headers={"Content-Disposition": f'attachment; filename="{filename}"'}, headers={"Content-Disposition": f'attachment; filename="{filename}"'},
@@ -5984,7 +5985,7 @@ def import_settings(bundle: dict):
"""Apply a previously exported settings bundle. Validates the entire """Apply a previously exported settings bundle. Validates the entire
bundle in phase 1 (no disk writes); only on full success does bundle in phase 1 (no disk writes); only on full success does
phase 2 commit each file via temp+rename. The frontend reads phase 2 commit each file via temp+rename. The frontend reads
`local_storage` itself server ignores it. See slopsmith#113.""" `local_storage` itself server ignores it. See feedBack#113."""
from plugins import LOADED_PLUGINS, PLUGINS_LOCK from plugins import LOADED_PLUGINS, PLUGINS_LOCK
if not isinstance(bundle, dict): if not isinstance(bundle, dict):
@@ -6022,7 +6023,7 @@ def import_settings(bundle: dict):
) )
warnings: list[str] = [] warnings: list[str] = []
bundle_version = bundle.get("slopsmith_version") bundle_version = bundle.get("feedBack_version")
running = _running_version() running = _running_version()
if bundle_version and bundle_version != running: if bundle_version and bundle_version != running:
warnings.append( warnings.append(
@@ -6155,7 +6156,7 @@ def import_settings(bundle: dict):
} }
# ── Diagnostic bundle export (slopsmith#166) ────────────────────────── # ── Diagnostic bundle export (feedBack#166) ──────────────────────────
# #
# One-click "Export Diagnostics" in Settings produces a redacted zip # One-click "Export Diagnostics" in Settings produces a redacted zip
# combining server logs, system info, hardware (CPU/GPU/RAM), plugin # combining server logs, system info, hardware (CPU/GPU/RAM), plugin
@@ -6179,11 +6180,11 @@ def _diag_plugins_roots() -> list[Path]:
"""Return all plugin root directories for orphan scanning. """Return all plugin root directories for orphan scanning.
Includes both the built-in ``plugins/`` directory and Includes both the built-in ``plugins/`` directory and
``SLOPSMITH_PLUGINS_DIR`` when set, so user-installed plugins and ``FEEDBACK_PLUGINS_DIR`` when set, so user-installed plugins and
orphans in the external dir are reflected in the bundle. orphans in the external dir are reflected in the bundle.
""" """
roots: list[Path] = [] roots: list[Path] = []
user_dir = os.environ.get("SLOPSMITH_PLUGINS_DIR", "").strip() user_dir = getenv_compat("FEEDBACK_PLUGINS_DIR", "").strip()
if user_dir: if user_dir:
p = Path(user_dir) p = Path(user_dir)
if p.is_dir(): if p.is_dir():
@@ -6363,7 +6364,7 @@ def export_diagnostics(payload: dict = Body(default_factory=dict)):
) )
zip_bytes, filename, _manifest = _diag_build( zip_bytes, filename, _manifest = _diag_build(
slopsmith_version=_running_version(), feedBack_version=_running_version(),
config_dir=CONFIG_DIR, config_dir=CONFIG_DIR,
dlc_dir=_get_dlc_dir(), dlc_dir=_get_dlc_dir(),
log_file=_diag_log_file(), log_file=_diag_log_file(),
@@ -6409,7 +6410,7 @@ def preview_diagnostics(
with PLUGINS_LOCK: with PLUGINS_LOCK:
plugins_snapshot = list(LOADED_PLUGINS) plugins_snapshot = list(LOADED_PLUGINS)
return _diag_preview( return _diag_preview(
slopsmith_version=_running_version(), feedBack_version=_running_version(),
config_dir=CONFIG_DIR, config_dir=CONFIG_DIR,
dlc_dir=_get_dlc_dir(), dlc_dir=_get_dlc_dir(),
log_file=_diag_log_file(), log_file=_diag_log_file(),
@@ -7076,7 +7077,7 @@ async def highway_ws(websocket: WebSocket, filename: str, arrangement: int = -1,
"audio_error": audio_error, "audio_error": audio_error,
"tuning": arr.tuning, "tuning": arr.tuning,
# Number of strings on the active arrangement # Number of strings on the active arrangement
# (slopsmith-plugin-3dhighway#7). arrangement XML / archive sources # (feedBack-plugin-3dhighway#7). arrangement XML / archive sources
# always emit `tuning` as length 6 with zero-padding for # always emit `tuning` as length 6 with zero-padding for
# unused string slots, so `len(arr.tuning)` is unreliable # unused string slots, so `len(arr.tuning)` is unreliable
# there; sloppak / GP-imported sources may instead carry # there; sloppak / GP-imported sources may instead carry
@@ -7570,7 +7571,7 @@ async def highway_ws(websocket: WebSocket, filename: str, arrangement: int = -1,
}) })
# Per-phrase difficulty data for the master-difficulty slider # Per-phrase difficulty data for the master-difficulty slider
# (slopsmith#48). Only sent when the source chart had multiple # (feedBack#48). Only sent when the source chart had multiple
# `<level>` tiers — single-level charts (GP converter, older # `<level>` tiers — single-level charts (GP converter, older
# sloppaks without phrase data) produce arr.phrases=None, and the # sloppaks without phrase data) produce arr.phrases=None, and the
# frontend treats the missing message as "slider disabled". # frontend treats the missing message as "slider disabled".
@@ -7681,9 +7682,9 @@ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
def index(): def index():
# fee[dB]ack v0.3.0: the v3 shell is now the DEFAULT at `/`. The classic v2 # fee[dB]ack v0.3.0: the v3 shell is now the DEFAULT at `/`. The classic v2
# UI remains fully available as a fallback — opt back in with # UI remains fully available as a fallback — opt back in with
# SLOPSMITH_UI=v2 (or =legacy), or hit the dedicated /v2 route below (which # FEEDBACK_UI=v2 (or =legacy), or hit the dedicated /v2 route below (which
# serves it regardless of the env var). # serves it regardless of the env var).
if os.environ.get("SLOPSMITH_UI") in ("v2", "legacy"): if getenv_compat("FEEDBACK_UI") or getenv_compat("FEEDBACK_UI") in ("v2", "legacy"):
return FileResponse(str(STATIC_DIR / "index.html")) return FileResponse(str(STATIC_DIR / "index.html"))
return FileResponse(str(STATIC_DIR / "v3" / "index.html")) return FileResponse(str(STATIC_DIR / "v3" / "index.html"))
@@ -7698,5 +7699,5 @@ def index_v3():
@app.get("/v2") @app.get("/v2")
def index_v2(): def index_v2():
# Always serve the classic v2 UI, independent of the env var, so the # Always serve the classic v2 UI, independent of the env var, so the
# fallback is reachable without flipping SLOPSMITH_UI. # fallback is reachable without flipping FEEDBACK_UI.
return FileResponse(str(STATIC_DIR / "index.html")) return FileResponse(str(STATIC_DIR / "index.html"))
+4 -4
View File
@@ -6,7 +6,7 @@
`midi-input` is a **core-owned provider-coordinator** capability domain for MIDI `midi-input` is a **core-owned provider-coordinator** capability domain for MIDI
device discovery, selection, and open/close session lifecycle — the MIDI analog device discovery, selection, and open/close session lifecycle — the MIDI analog
of `audio-input` (spec 006). It gives every MIDI consumer in Slopsmith (the of `audio-input` (spec 006). It gives every MIDI consumer in FeedBack (the
`input_setup` onboarding wizard, the `piano`/keys and `drums` plugins, and — as `input_setup` onboarding wizard, the `piano`/keys and `drums` plugins, and — as
a follow-up — note-detection's Web-MIDI provider) **one device-access boundary**: a follow-up — note-detection's Web-MIDI provider) **one device-access boundary**:
one permission prompt, one source list, one redaction boundary. one permission prompt, one source list, one redaction boundary.
@@ -68,13 +68,13 @@ shared listener session to an already-discovered source and never re-prompts.
One shared open session per source across requesters (refcounted); the provider One shared open session per source across requesters (refcounted); the provider
receives `source.close` only after the last requester releases. Live MIDI receives `source.close` only after the last requester releases. Live MIDI
message delivery (for the "play a note / hit a pad" calibration check) is exposed message delivery (for the "play a note / hit a pad" calibration check) is exposed
to in-page consumers via the public `window.slopsmith.midiInput` session handle to in-page consumers via the public `window.feedBack.midiInput` session handle
**only** — never as raw capability events or in diagnostics. **only** — never as raw capability events or in diagnostics.
### Persistence & redaction ### Persistence & redaction
Selected source persists under `slopsmith.midiInput.selectedLogicalSourceKey`. Selected source persists under `feedBack.midiInput.selectedLogicalSourceKey`.
Diagnostics (`slopsmith.midi_input.diagnostics.v1`) carry provider ids, source Diagnostics (`feedBack.midi_input.diagnostics.v1`) carry provider ids, source
ids/keys/kinds/availability, the selected key, and open-session keys; device ids/keys/kinds/availability, the selected key, and open-session keys; device
**labels are redacted** and **no raw MIDI messages** are ever included. **labels are redacted** and **no raw MIDI messages** are ever included.
+1 -1
View File
@@ -52,7 +52,7 @@ device plane stabilize independently of mapping semantics.
- **Learn mode:** open a `midi-input` session, capture the next matching event, - **Learn mode:** open a `midi-input` session, capture the next matching event,
and bind it to the pending action (the per-plugin "learn" UIs in drums today and bind it to the pending action (the per-plugin "learn" UIs in drums today
are the reference behaviour to generalise). are the reference behaviour to generalise).
- **Diagnostics:** `slopsmith.midi_control.diagnostics.v1` — mapping summaries + - **Diagnostics:** `feedBack.midi_control.diagnostics.v1` — mapping summaries +
bounded recent activations; **no raw MIDI streams, no device labels**. bounded recent activations; **no raw MIDI streams, no device labels**.
## Intended consumers (promotion trigger) ## Intended consumers (promotion trigger)
+284 -279
View File
File diff suppressed because it is too large Load Diff
+19 -19
View File
@@ -1,6 +1,6 @@
// Audio mixer — registry + popover for per-channel volume control (slopsmith#87). // Audio mixer — registry + popover for per-channel volume control (feedBack#87).
// //
// Plugins (or core) register a fader spec via window.slopsmith.audio.registerFader(spec). // Plugins (or core) register a fader spec via window.feedBack.audio.registerFader(spec).
// Each spec is the source of truth for its own value: the popover only calls // Each spec is the source of truth for its own value: the popover only calls
// getValue() to render and setValue() to commit. Persistence is the plugin's // getValue() to render and setValue() to commit. Persistence is the plugin's
// responsibility — the registry doesn't store values. // responsibility — the registry doesn't store values.
@@ -10,8 +10,8 @@
(function () { (function () {
'use strict'; 'use strict';
if (!window.slopsmith) { if (!window.feedBack) {
console.warn('[mixer] window.slopsmith missing — audio-mixer.js loaded too early'); console.warn('[mixer] window.feedBack missing — audio-mixer.js loaded too early');
return; return;
} }
@@ -24,11 +24,11 @@ let _openTimer = null;
function _audioEl() { return document.getElementById('audio'); } function _audioEl() { return document.getElementById('audio'); }
function _audioSession() { function _audioSession() {
return window.slopsmith && window.slopsmith.audioSession; return window.feedBack && window.feedBack.audioSession;
} }
function _capabilities() { function _capabilities() {
return window.slopsmith && window.slopsmith.capabilities; return window.feedBack && window.feedBack.capabilities;
} }
async function _mixCommand(command, payload) { async function _mixCommand(command, payload) {
@@ -143,7 +143,7 @@ function _applySongVolume(v) {
// routes every stem through its own master GainNode, so a.volume above is // routes every stem through its own master GainNode, so a.volume above is
// dead. Drive that master instead when the stems plugin has published its // dead. Drive that master instead when the stems plugin has published its
// hook (it clears the hook on teardown for stem-less songs). // hook (it clears the hook on teardown for stem-less songs).
const stemsSetMaster = window.slopsmith?.stems?.setMasterVolume; const stemsSetMaster = window.feedBack?.stems?.setMasterVolume;
if (typeof stemsSetMaster === 'function') { if (typeof stemsSetMaster === 'function') {
// A synchronous throw or a rejected Promise from the stems plugin hook // A synchronous throw or a rejected Promise from the stems plugin hook
// must not abort _applySongVolume before it returns / persists. The // must not abort _applySongVolume before it returns / persists. The
@@ -155,13 +155,13 @@ function _applySongVolume(v) {
// consistent with the other ignored async calls in this module. // consistent with the other ignored async calls in this module.
void Promise.resolve(stemsSetMaster(linear)) void Promise.resolve(stemsSetMaster(linear))
.then(function () { .then(function () {
_recordAudioBridge('stems.master-volume', 'window.slopsmith.stems.setMasterVolume', 'core.song', 'handled'); _recordAudioBridge('stems.master-volume', 'window.feedBack.stems.setMasterVolume', 'core.song', 'handled');
}) })
.catch(function () { .catch(function () {
_recordAudioBridge('stems.master-volume', 'window.slopsmith.stems.setMasterVolume', 'core.song', 'failed', 'Stems master volume hook rejected'); _recordAudioBridge('stems.master-volume', 'window.feedBack.stems.setMasterVolume', 'core.song', 'failed', 'Stems master volume hook rejected');
}); });
} catch (_) { } catch (_) {
_recordAudioBridge('stems.master-volume', 'window.slopsmith.stems.setMasterVolume', 'core.song', 'failed', 'Stems master volume hook threw'); _recordAudioBridge('stems.master-volume', 'window.feedBack.stems.setMasterVolume', 'core.song', 'failed', 'Stems master volume hook threw');
} }
} }
_registerAudioSessionFader({ _registerAudioSessionFader({
@@ -183,7 +183,7 @@ function _applySongVolume(v) {
_recordAudioBridge('audio-mix.song-volume', 'applySongVolume', 'core.song', 'handled'); _recordAudioBridge('audio-mix.song-volume', 'applySongVolume', 'core.song', 'handled');
// Desktop + JUCE: song audio is mixed in the native engine; HTML5 volume is ignored. // Desktop + JUCE: song audio is mixed in the native engine; HTML5 volume is ignored.
if (window._juceMode) { if (window._juceMode) {
const setGain = window.slopsmithDesktop?.audio?.setGain; const setGain = window.feedBackDesktop?.audio?.setGain;
if (typeof setGain === 'function') { if (typeof setGain === 'function') {
// Same dual guard as the stems hook above: the try/catch covers a // Same dual guard as the stems hook above: the try/catch covers a
// synchronous throw from setGain, the .catch() covers a rejected IPC. // synchronous throw from setGain, the .catch() covers a rejected IPC.
@@ -502,17 +502,17 @@ function _init() {
_btnEl = document.getElementById('btn-mixer'); _btnEl = document.getElementById('btn-mixer');
_popoverEl = document.getElementById('mixer-popover'); _popoverEl = document.getElementById('mixer-popover');
_registerSongFader(); _registerSongFader();
if (window.slopsmith && window.slopsmith.on) { if (window.feedBack && window.feedBack.on) {
window.slopsmith.on('screen:changed', _onScreenChanged); window.feedBack.on('screen:changed', _onScreenChanged);
window.slopsmith.on('audio-mix:fader-value-changed', () => { if (_open) _renderPopover(); }); window.feedBack.on('audio-mix:fader-value-changed', () => { if (_open) _renderPopover(); });
window.slopsmith.on('audio-mix:fader-unavailable', () => { if (_open) _renderPopover(); }); window.feedBack.on('audio-mix:fader-unavailable', () => { if (_open) _renderPopover(); });
window.slopsmith.on('audio-mix:participant-registered', () => { if (_open) _renderPopover(); }); window.feedBack.on('audio-mix:participant-registered', () => { if (_open) _renderPopover(); });
window.slopsmith.on('audio-mix:participant-removed', () => { if (_open) _renderPopover(); }); window.feedBack.on('audio-mix:participant-removed', () => { if (_open) _renderPopover(); });
} }
window.dispatchEvent(new Event('slopsmith:audio:ready')); window.dispatchEvent(new Event('feedBack:audio:ready'));
} }
window.slopsmith.audio = Object.assign(window.slopsmith.audio || {}, { window.feedBack.audio = Object.assign(window.feedBack.audio || {}, {
registerFader, unregisterFader, getFaders, registerFader, unregisterFader, getFaders,
openMixer, closeMixer, toggleMixer, openMixer, closeMixer, toggleMixer,
applySongVolume: _applySongVolume, applySongVolume: _applySongVolume,
+18 -18
View File
@@ -1,4 +1,4 @@
// Slopsmith capability registry and dispatcher. // FeedBack capability registry and dispatcher.
(function () { (function () {
'use strict'; 'use strict';
@@ -30,8 +30,8 @@
}; };
} }
function _ensureSlopsmithEventBus() { function _ensureFeedBackEventBus() {
const existing = window.slopsmith && typeof window.slopsmith === 'object' ? window.slopsmith : null; const existing = window.feedBack && typeof window.feedBack === 'object' ? window.feedBack : null;
const hasEventTarget = existing const hasEventTarget = existing
&& typeof existing.addEventListener === 'function' && typeof existing.addEventListener === 'function'
&& typeof existing.removeEventListener === 'function' && typeof existing.removeEventListener === 'function'
@@ -53,12 +53,12 @@
bus.off = function (event, fn, options) { bus.off = function (event, fn, options) {
this.removeEventListener(event, fn, options); this.removeEventListener(event, fn, options);
}; };
window.slopsmith = bus; window.feedBack = bus;
return bus; return bus;
} }
_ensureSlopsmithEventBus(); _ensureFeedBackEventBus();
if (window.slopsmith.capabilities && window.slopsmith.capabilities.version === 1) return; if (window.feedBack.capabilities && window.feedBack.capabilities.version === 1) return;
const VALID_ROLES = new Set([ const VALID_ROLES = new Set([
'owner', 'coordinator', 'provider', 'executor', 'observer', 'requester', 'transformer', 'handler', 'owner', 'coordinator', 'provider', 'executor', 'observer', 'requester', 'transformer', 'handler',
@@ -209,7 +209,7 @@
const VALID_SETTING_TYPES = new Set(['toggle', 'range', 'select']); const VALID_SETTING_TYPES = new Set(['toggle', 'range', 'select']);
// Normalize per-instance control descriptors (slopsmith#849) declared on a // Normalize per-instance control descriptors (feedBack#849) declared on a
// capability. Lenient (drops malformed entries rather than failing the whole // capability. Lenient (drops malformed entries rather than failing the whole
// declaration): the server-side validator in plugins/__init__.py already // declaration): the server-side validator in plugins/__init__.py already
// strict-checks the /api/plugins manifest path; this keeps runtime // strict-checks the /api/plugins manifest path; this keeps runtime
@@ -661,7 +661,7 @@
lifecycle: 'plugin-defined', lifecycle: 'plugin-defined',
label: 'Plugin-defined', label: 'Plugin-defined',
tone: 'info', tone: 'info',
summary: 'Declared by a plugin or test fixture rather than registered as a core Slopsmith domain.', summary: 'Declared by a plugin or test fixture rather than registered as a core FeedBack domain.',
}; };
} }
@@ -863,12 +863,12 @@
_notifySubscribers(event, detail); _notifySubscribers(event, detail);
_notifySubscribers(`${capabilityName}:${event}`, detail); _notifySubscribers(`${capabilityName}:${event}`, detail);
try { try {
if (window.slopsmith && typeof window.slopsmith.emit === 'function') { if (window.feedBack && typeof window.feedBack.emit === 'function') {
window.slopsmith.emit(`${capabilityName}:${event}`, detail); window.feedBack.emit(`${capabilityName}:${event}`, detail);
window.slopsmith.emit('capability:event', detail); window.feedBack.emit('capability:event', detail);
} else { } else {
window.dispatchEvent(new CustomEvent(`${capabilityName}:${event}`, { detail })); window.dispatchEvent(new CustomEvent(`${capabilityName}:${event}`, { detail }));
window.dispatchEvent(new CustomEvent('slopsmith:capability:event', { detail })); window.dispatchEvent(new CustomEvent('feedBack:capability:event', { detail }));
} }
} catch (err) { } catch (err) {
console.warn('[capabilities] event dispatch failed:', err); console.warn('[capabilities] event dispatch failed:', err);
@@ -1238,7 +1238,7 @@
if (!source || typeof source !== 'object') return 'legacy-runtime'; if (!source || typeof source !== 'object') return 'legacy-runtime';
const candidate = source.source || source.pluginId || source.owner || source.requester || source.providerId || source.id; const candidate = source.source || source.pluginId || source.owner || source.requester || source.providerId || source.id;
if (candidate) return String(candidate); if (candidate) return String(candidate);
const activePluginId = window.slopsmith && (window.slopsmith._loadingPluginId || window.slopsmith._activePluginId); const activePluginId = window.feedBack && (window.feedBack._loadingPluginId || window.feedBack._activePluginId);
return activePluginId ? String(activePluginId) : 'legacy-runtime'; return activePluginId ? String(activePluginId) : 'legacy-runtime';
} }
@@ -1413,7 +1413,7 @@
function snapshotDiagnostics() { function snapshotDiagnostics() {
const pipelineSummaries = inspect(); const pipelineSummaries = inspect();
const snapshot = { const snapshot = {
schema: 'slopsmith.capabilities.diagnostics.v1', schema: 'feedBack.capabilities.diagnostics.v1',
pipelines: pipelineSummaries, pipelines: pipelineSummaries,
participants: pipelineSummaries.flatMap(pipeline => Array.isArray(pipeline.participants) ? pipeline.participants : []), participants: pipelineSummaries.flatMap(pipeline => Array.isArray(pipeline.participants) ? pipeline.participants : []),
recentDecisions: recentDecisions.slice(), recentDecisions: recentDecisions.slice(),
@@ -1447,14 +1447,14 @@
capabilitiesChangeScheduled = false; capabilitiesChangeScheduled = false;
const detail = { timestamp: _now() }; const detail = { timestamp: _now() };
try { try {
window.dispatchEvent(new CustomEvent('slopsmith:capabilities:changed', { detail })); window.dispatchEvent(new CustomEvent('feedBack:capabilities:changed', { detail }));
} catch (_err) { /* capability diagnostics must not break runtime behavior */ } } catch (_err) { /* capability diagnostics must not break runtime behavior */ }
}); });
} }
function _contributeDiagnostics() { function _contributeDiagnostics() {
if (contributing) return; if (contributing) return;
const diagnostics = window.slopsmith && window.slopsmith.diagnostics; const diagnostics = window.feedBack && window.feedBack.diagnostics;
if (diagnostics && typeof diagnostics.contribute === 'function') { if (diagnostics && typeof diagnostics.contribute === 'function') {
contributing = true; contributing = true;
try { diagnostics.contribute('capabilities', snapshotDiagnostics()); } try { diagnostics.contribute('capabilities', snapshotDiagnostics()); }
@@ -1494,7 +1494,7 @@
recordUserOverride, recordUserOverride,
}; };
window.slopsmith.capabilities = api; window.feedBack.capabilities = api;
registerParticipant('core', { registerParticipant('core', {
diagnostics: { diagnostics: {
@@ -1533,7 +1533,7 @@
}, },
}); });
try { try {
window.dispatchEvent(new CustomEvent('slopsmith:capabilities:ready', { detail: api })); window.dispatchEvent(new CustomEvent('feedBack:capabilities:ready', { detail: api }));
_notifySubscribers('registered', { capability: '*', pluginId: 'core', timestamp: _now() }); _notifySubscribers('registered', { capability: '*', pluginId: 'core', timestamp: _now() });
} catch (_) {} } catch (_) {}
})(); })();

Some files were not shown because too many files have changed in this diff Show More