From 5f46fb73fcc50691d89b123663e9ef8c9ca4097f Mon Sep 17 00:00:00 2001 From: zai-bot Date: Sat, 7 Nov 2026 03:45:21 +0000 Subject: [PATCH] 3e7e93ec-7a7c-4e6c-85be-68b1e1c2a9d2 --- db/custom.db.seed | Bin 307200 -> 307200 bytes prisma/schema.prisma | 4 +- src/app/api/presets/route.ts | 84 +- src/app/page.tsx | 12 +- src/components/ava/audio-source.ts | 66 +- src/components/ava/visual-stage.tsx | 4 +- src/lib/ava/embedded-presets.ts | 1435 +-------------------------- src/lib/ava/presets.ts | 244 +---- src/lib/store.ts | 2 +- worklog.md | 35 + 10 files changed, 137 insertions(+), 1749 deletions(-) diff --git a/db/custom.db.seed b/db/custom.db.seed index 0bfb762e87a5490af3ada440d5d0f02979de0f10..922425bad0de7d96250cfbc158edb181b24f7a22 100755 GIT binary patch delta 3487 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 E0pvz1asU7T diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a1d82af..577d22c 100755 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,7 +8,7 @@ datasource db { } // A preset script file living somewhere in the folder tree. -// folderPath is the full path from the virtual root, e.g. "root/embedded/Synth-C". +// folderPath is the full path from the virtual root, e.g. "root/embedded". // Names are unique per folder (files in a directory). // approx = true marks embedded presets that are generated stand-ins rather // than ports of the original's code (the original relies on effects the @@ -45,7 +45,7 @@ model Settings { bandConfig String? beatConfig String? // Version stamp of the embedded preset library; when it changes, the - // root/embedded subtree is re-seeded from BUILTIN + EMBEDDED_LIBRARY. + // root/embedded subtree is re-seeded from EMBEDDED_LIBRARY. embeddedSeed String? updatedAt DateTime @updatedAt } diff --git a/src/app/api/presets/route.ts b/src/app/api/presets/route.ts index f6e9e24..ab5f1bf 100755 --- a/src/app/api/presets/route.ts +++ b/src/app/api/presets/route.ts @@ -1,23 +1,17 @@ import { NextRequest, NextResponse } from 'next/server' import { db } from '@/lib/db' -import { BUILTIN_PRESETS } from '@/lib/ava/presets' import { EMBEDDED_LIBRARY, EMBEDDED_SEED_VERSION } from '@/lib/ava/embedded-presets' import { ensureFolderRows } from '@/lib/server/preset-tree' import { EMBEDDED_ROOT, ROOT, USER_ROOT } from '@/lib/preset-paths' export const dynamic = 'force-dynamic' -/** The structural backbone of the tree, created on first run. */ -const BASE_FOLDERS = [ - ROOT, - EMBEDDED_ROOT, - USER_ROOT, - 'root/embedded/Synth-C', - 'root/embedded/Amp Picks', - 'root/embedded/Dynamics', - 'root/embedded/Envision', - 'root/embedded/Zai-Magic', -] +/** + * The structural backbone of the tree, created on first run. The embedded + * branch has no subfolders (v7.19.0 curation) — "Beat Matrix" lives directly + * under root/embedded. + */ +const BASE_FOLDERS = [ROOT, EMBEDDED_ROOT, USER_ROOT] /** * Version-gated seed + INTEGRITY SELF-HEAL for the embedded library. @@ -26,18 +20,13 @@ const BASE_FOLDERS = [ * sees, so GET verifies it on every request: * 1. version gate — EMBEDDED_SEED_VERSION changes trigger a full re-seed * (library was regenerated); - * 2. fingerprint — the embedded subtree must exactly match - * BUILTIN + EMBEDDED (folder + name). Any drift — a preset renamed or - * deleted through pre-hardening endpoints, a row injected straight into - * the DB — re-seeds the subtree automatically. + * 2. fingerprint — the embedded subtree must exactly match EMBEDDED + * (folder + name). Any drift — a preset renamed or deleted through + * pre-hardening endpoints, a row injected straight into the DB — + * re-seeds the subtree automatically. */ function libraryFingerprint(): string { - const items = [ - ...BUILTIN_PRESETS.map((p) => `root/embedded/Synth-C/${p.name}`), - ...EMBEDDED_LIBRARY.map( - (p) => `${p.folder.replace('root/embedded/root/embedded/', 'root/embedded/')}/${p.name}`, - ), - ] + const items = EMBEDDED_LIBRARY.map((p) => `${p.folder}/${p.name}`) return items.sort().join('|') } @@ -72,30 +61,20 @@ async function ensureSeeded(): Promise { ], }, }) + // the curated library has NO subfolders — drop any stale category + // folder rows (Synth-C / Amp Picks / Dynamics / Envision / Zai-Magic) + await db.folder.deleteMany({ where: { path: { startsWith: 'root/embedded/' } } }) await db.preset.createMany({ - data: [ - ...BUILTIN_PRESETS.map((p) => ({ - name: p.name, - author: p.author, - description: p.description, - content: p.content, - originalFile: p.originalFile ?? null, - isBuiltIn: true, - approx: false, - folderPath: 'root/embedded/Synth-C', - })), - ...EMBEDDED_LIBRARY.map((p) => ({ - name: p.name, - author: p.author, - description: p.description, - content: p.content, - originalFile: p.originalFile ?? null, - isBuiltIn: true, - approx: p.approx ?? false, - // tolerate old converter output that recorded the download nesting - folderPath: p.folder.replace('root/embedded/root/embedded/', 'root/embedded/'), - })), - ], + data: EMBEDDED_LIBRARY.map((p) => ({ + name: p.name, + author: p.author, + description: p.description, + content: p.content, + originalFile: p.originalFile ?? null, + isBuiltIn: true, + approx: p.approx ?? false, + folderPath: p.folder, + })), }) await db.settings.upsert({ where: { id: 'main' }, @@ -114,6 +93,12 @@ async function ensureSeeded(): Promise { * the embedded library, folders outside the structural backbone — is purged * here so no visitor can ever alter what other visitors see. Runs on every * GET; it's two indexed deleteMany calls on a small SQLite table. + * + * v7.19.0: the embedded branch ships NO subfolders, so the folder purge now + * keeps only the three backbone rows and deletes everything else (stale + * category folders included). User folders never exist server-side — the + * "root/user created" tree lives in each visitor's browser — so nothing of + * any visitor's is touched. */ async function purgeVisitorJunk(): Promise { await db.preset.deleteMany({ @@ -126,14 +111,7 @@ async function purgeVisitorJunk(): Promise { }, }) await db.folder.deleteMany({ - where: { - NOT: [ - { path: ROOT }, - { path: EMBEDDED_ROOT }, - { path: USER_ROOT }, - { path: { startsWith: 'root/embedded/' } }, - ], - }, + where: { path: { notIn: [ROOT, EMBEDDED_ROOT, USER_ROOT] } }, }) } diff --git a/src/app/page.tsx b/src/app/page.tsx index 48c9767..67f9e14 100755 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -102,19 +102,17 @@ function useInitialLoad() { const beat = readStoredBeatConfig() if (beat) store.applyBeatConfig(beat) - // startup script: saved default (this browser) → v7.2 default preset - // (Zai-Magic "Bass Bloom" — the megabuf particle bloom that remembers - // every beat's hue) → any Zai-Magic preset → template + // startup script: saved default (this browser) → the embedded default + // preset (v7.19.0: "Beat Matrix" — the 16-band beat-reactive spectrum + // wall, the single preset of the curated embedded library) → template let text: string | null = lsGetString('ava.defaultScript') const usedSavedDefault = text !== null && text.trim() !== '' let startupPreset: (typeof presetsList)[number] | undefined if (!text) { startupPreset = presetsList.find( - (p) => p.folderPath === 'root/embedded/Zai-Magic' && p.name === 'Bass Bloom', - ) ?? - presetsList.find((p) => p.folderPath === 'root/embedded/Zai-Magic') ?? - presetsList[0] + (p) => p.folderPath === 'root/embedded' && p.name === 'Beat Matrix', + ) ?? presetsList[0] text = startupPreset?.content ?? null } if (!text) text = STARTER_TEMPLATE diff --git a/src/components/ava/audio-source.ts b/src/components/ava/audio-source.ts index bbc87cc..93669a8 100755 --- a/src/components/ava/audio-source.ts +++ b/src/components/ava/audio-source.ts @@ -320,17 +320,18 @@ function fileErrorMessage(err: unknown): string { } // --------------------------------------------------------------------------- -// v7.2 showcase defaults — Bass Bloom + the Zai-Magic club mix +// Showcase defaults (v7.19.0) — "Beat Matrix" over the demo track // --------------------------------------------------------------------------- -/** The read-only folder that ships the hand-authored Zai-Magic presets. */ -const ZAI_MAGIC_FOLDER = 'root/embedded/Zai-Magic' +/** The read-only folder of the curated embedded library (one preset: + * "Beat Matrix" — the single shipped preset, directly under root/embedded). */ +const EMBEDDED_FOLDER = 'root/embedded' // --------------------------------------------------------------------------- // v7.11 — the visitor's own selection vs. the welcome showcase // --------------------------------------------------------------------------- -/** The script text this visit booted with (saved default / Bass Bloom / +/** The script text this visit booted with (saved default / Beat Matrix / * template). Any later difference = the visitor's own material. */ let bootScriptSnapshot = '' @@ -366,18 +367,18 @@ export function visitorHasOwnSelection(): boolean { /** * Music just started but no preset is active (the visitor never picked one — * e.g. they are running their own untitled draft): load the default preset, - * Zai-Magic "Bass Bloom", so something beat-reactive is on screen the moment - * audio arrives. Engine-only — the editor's unsaved text is never touched - * (the same courtesy random switching pays). v7.11: a visitor with their OWN - * material (custom draft / saved default) is left completely alone — the - * forced default used to replace their running script. + * the embedded "Beat Matrix", so something beat-reactive is on screen the + * moment audio arrives. Engine-only — the editor's unsaved text is never + * touched (the same courtesy random switching pays). v7.11: a visitor with + * their OWN material (custom draft / saved default) is left completely + * alone — the forced default used to replace their running script. */ function ensureDefaultPreset(): void { const s = useAvaStore.getState() if (s.activePresetId !== null) return if (visitorHasOwnSelection()) return const preset = s.presets.find( - (p) => p.folderPath === ZAI_MAGIC_FOLDER && p.name === 'Bass Bloom', + (p) => p.folderPath === EMBEDDED_FOLDER && p.name === 'Beat Matrix', ) if (!preset) return const v = validateScript(preset.content) @@ -390,30 +391,31 @@ function ensureDefaultPreset(): void { } /** - * The Demo button is the showcase: point the preset tree at Zai-Magic (and - * expand it so the visitor sees what is playing), then start the sequence — - * v7.4: every Zai-Magic preset IN ORDER, one every 20 seconds, wrapping - * around, for the one play-through (Sequence, not Random — the demo pass - * should walk the whole family, not repeat picks). v7.11: only ever called - * for a visit that made no choice of their own (see visitorHasOwnSelection). + * The Demo button is the showcase: point the preset tree at the embedded + * library (and expand it so the visitor sees what is playing). v7.19.0: the + * curated library is a single preset ("Beat Matrix"), so there is nothing + * to cycle — no mix timer is engaged; the track simply plays under the + * beat-reactive wall, and any mix the visitor starts later is scoped to + * the folder by the selection. v7.11: only ever called for a visit that + * made no choice of their own (see visitorHasOwnSelection). */ function startShowcaseMix(): void { const s = useAvaStore.getState() - if (s.folders.includes(ZAI_MAGIC_FOLDER)) { - s.selectFolder(ZAI_MAGIC_FOLDER) // also scopes the mix to the folder - s.toggleFolder(ZAI_MAGIC_FOLDER, true) + if (s.folders.includes(EMBEDDED_FOLDER)) { + s.selectFolder(EMBEDDED_FOLDER) // also scopes any later mix to the folder + s.toggleFolder(EMBEDDED_FOLDER, true) } - s.setRandomIntervalSec(20) - s.setSequenceMode(true) // mutual exclusion unticks Random if it was on } /** * v7.3/v7.4: the demo is a single pass — when it ends (natural play-through, * Stop button, or switching to another source) unwind the showcase state: - * the Demo flow force-checked the mix mode (Sequence), so it gets unchecked - * again. The visitor's own selection (the last preset that was running) - * stays put. v7.11: only called when the showcase actually engaged — a - * visitor whose own Random/Sequence mix was already running keeps it. + * the Demo flow used to force-check the mix mode (Sequence), so it gets + * unchecked again. v7.19.0 no longer engages a timer, but the unwind stays + * as a safety net for any flags that did get set. The visitor's own + * selection (the last preset that was running) stays put. v7.11: only + * called when the showcase actually engaged — a visitor whose own + * Random/Sequence mix was already running keeps it. */ function retireDemoShowcase(): void { const s = useAvaStore.getState() @@ -473,7 +475,7 @@ export function useAudioSource() { const index = readDemoNext() try { await engine.audio.startDemo(index) - // v7.11: the welcome showcase (Bass Bloom + Zai-Magic Sequence) only + // v7.11: the welcome showcase (Beat Matrix over the demo track) only // engages for a visit that made NO choice of their own — no preset // loaded (double-click / Load / a copy), no custom script material in // the editor, no own mix running. Everyone else keeps their selection: @@ -492,17 +494,17 @@ export function useAudioSource() { if (reason === 'ended' || reason === 'stop') writeDemoNext(index + 1) } syncSource() - // showcase flow (v7.2/v7.4/v7.11): nothing active and nothing of the - // visitor's own → Bass Bloom, then the Zai-Magic sequence takes over — - // every preset in order, 20 s each, for this one play-through. With - // the visitor's own selection, BOTH are skipped (ensureDefaultPreset - // holds its own guard as well). + // showcase flow (v7.2/v7.11, reworked v7.19.0): nothing active and + // nothing of the visitor's own → Beat Matrix on screen; the tree points + // at the embedded library for the rest of the pass. With the visitor's + // own selection, the showcase is skipped (ensureDefaultPreset holds its + // own guard as well). ensureDefaultPreset() if (showcase) startShowcaseMix() toast({ title: 'Demo track playing', description: showcase - ? `${demoTrackTitle(index)} (${index + 1} of ${DEMO_TRACK_COUNT}) · Zai-Magic preset sequence -- every preset in order, 20 s each, one play-through.` + ? `${demoTrackTitle(index)} (${index + 1} of ${DEMO_TRACK_COUNT}) · "Beat Matrix" is running -- the built-in beat-reactive wall, straight from the embedded library.` : `${demoTrackTitle(index)} (${index + 1} of ${DEMO_TRACK_COUNT}) · your selection stays active -- the demo never touches your preset or the editor.`, }) } catch (err) { diff --git a/src/components/ava/visual-stage.tsx b/src/components/ava/visual-stage.tsx index d1b575b..8728144 100755 --- a/src/components/ava/visual-stage.tsx +++ b/src/components/ava/visual-stage.tsx @@ -110,8 +110,8 @@ export function VisualStage({ className }: { className?: string }) {
{/* v7.2 showcase entry point -- the shimmering Demo Track - button: one click starts the music AND the Zai-Magic - club mix (random preset every 20 s) */} + button: one click starts the music AND the embedded + "Beat Matrix" showcase (v7.19.0: single-preset library) */}