|
Download docs/03-data-model.md from salgadev/jam-buddy: direct link, hf CLI and curl.
- Browser
- Download file 11.9 kB
-
https://huggingface.co/spaces/salgadev/jam-buddy/resolve/main/docs/03-data-model.md
- Command line
-
hf download hf://spaces/salgadev/jam-buddy/docs/03-data-model.md
-
curl -L -o 03-data-model.md https://huggingface.co/spaces/salgadev/jam-buddy/resolve/main/docs/03-data-model.md
11.9 kB
| # 03 — Data Model | |
| ## Overview | |
| PatternTalk's core data is small and human-editable. Most of it lives in JSON files under `apps/web/data/` and `data/training/`. | |
| ``` | |
| Prompt (user voice/text) | |
| │ | |
| ▼ | |
| ParsedRequest ◄── onomatopoeia.json + prompt grammar | |
| │ | |
| ▼ | |
| Pattern (loaded from patterns/*.json, expanded to bars + tempo) | |
| │ | |
| ▼ | |
| MIDI events | |
| │ | |
| ▼ | |
| .mid file | |
| ``` | |
| ## Prompt grammar | |
| User input is freeform text, but parses into a structured `ParsedRequest`. | |
| ```typescript | |
| interface ParsedRequest { | |
| // Source pattern | |
| onomatopoeia?: string; // e.g. "tupatupatupa" | |
| patternId?: string; // e.g. "d-beat", "blast-traditional" | |
| patternName?: string; // e.g. "D-Beat", user-friendly | |
| // Length | |
| bars: number; // default 4 | |
| beats?: number; // override pattern beats per bar | |
| // Tempo | |
| tempo?: number; // explicit BPM | |
| tempoSource: "prompt" | "reaper" | "audio" | "default"; | |
| tempoDefault: number; // 120 if nothing else | |
| // Time signature | |
| timeSignature: { | |
| numerator: number; // default 4 | |
| denominator: number; // default 4 | |
| }; | |
| // Cymbal/voice hints | |
| cymbal?: { | |
| type: "crash" | "ride" | "china" | "hihat-open" | "hihat-closed" | "splash"; | |
| pattern?: "8ths" | "quarters" | "bell" | "wash" | "upstrokes"; | |
| }; | |
| // Accent overrides | |
| accents?: ("1" | "and-of-2" | "3" | "and-of-4" | string)[]; | |
| // Style modifiers | |
| feel?: "straight" | "swing" | "half-time" | "double-time"; | |
| intensity?: "soft" | "medium" | "brutal" | "brutal-max"; | |
| } | |
| ``` | |
| ## Onomatopoeia mapping | |
| `apps/web/data/onomatopoeia.json` is the lookup table the parser checks first. | |
| ```json | |
| { | |
| "version": 1, | |
| "entries": [ | |
| { | |
| "id": "skank-beat", | |
| "patterns": ["tupatupatupa", "tupa-tupa-tupa", "chka-chka-chka"], | |
| "patternId": "skank", | |
| "defaultCymbal": { "type": "hihat-open", "pattern": "upstrokes" }, | |
| "confidence": 0.9, | |
| "notes": "Ska-style offbeat hi-hat upstrokes." | |
| }, | |
| { | |
| "id": "blast-beat", | |
| "patterns": ["krrk-krrk-krrk", "BLAM-BLAM-BLAM", "krkrkrkrk"], | |
| "patternId": "blast-traditional", | |
| "defaultIntensity": "brutal", | |
| "confidence": 0.85, | |
| "notes": "Traditional blast beat. Kick-snare alternation." | |
| }, | |
| { | |
| "id": "boom-bap", | |
| "patterns": ["boom-bap", "boom-bap-boom-bap"], | |
| "patternId": "hiphop-basic", | |
| "confidence": 0.95 | |
| }, | |
| { | |
| "id": "china-accent", | |
| "patterns": ["tss", "chka", "BLAM"], | |
| "patternId": "china-accent", | |
| "defaultCymbal": { "type": "china", "pattern": "wash" } | |
| }, | |
| { | |
| "id": "ride-bell", | |
| "patterns": ["ding-ding-ding", "ding-ding-ding-ding"], | |
| "patternId": "ride-bell-8ths", | |
| "defaultCymbal": { "type": "ride", "pattern": "bell" } | |
| } | |
| ] | |
| } | |
| ``` | |
| ### Matching logic | |
| 1. Normalize input: lowercase, strip punctuation, collapse whitespace | |
| 2. Phonetic match: Soundex or simple Levenshtein against entries | |
| 3. Syllable count: "tupatupatupa" = 4 syllables → likely 8th-note hi-hat | |
| 4. Confidence threshold: 0.7 to auto-map, lower to ask user | |
| ### User-defined onomatopoeias | |
| Users can add their own mappings via the UI. Stored in `localStorage` per device. | |
| ```typescript | |
| interface UserOnomatopoeia { | |
| phrase: string; | |
| patternId: string; | |
| cymbal?: CymbalHint; | |
| createdAt: number; | |
| } | |
| ``` | |
| ## Pattern templates | |
| `apps/web/data/patterns/*.json` — one file per pattern. Hand-authored. **This is your moat.** | |
| ```typescript | |
| interface PatternTemplate { | |
| id: string; // unique slug | |
| name: string; // "D-Beat" | |
| description: string; // screenreader-friendly text | |
| tags: string[]; // searchable | |
| genre: "metal" | "rock" | "punk" | "pop" | "jazz" | "funk" | "latin" | "hiphop"; | |
| defaultTempo: number; // BPM | |
| defaultBars: number; // how many bars this template covers | |
| swingRatio?: number; // 0.0 = straight, 0.33 = shuffle | |
| hits: Hit[]; | |
| } | |
| interface Hit { | |
| position: number; // 0.0 to 1.0, fraction of one bar | |
| limb: Limb; | |
| velocity: number; // 0-127 MIDI velocity | |
| accent?: boolean; // visual + audio emphasis | |
| } | |
| type Limb = | |
| | "kick" // right foot | |
| | "snare" // left hand (default) or right hand (cross-handed) | |
| | "hihat" // right hand (default) or left hand (cross-handed) | |
| | "hihat-open" // same as hihat but with open hi-hat sample | |
| | "ride" | |
| | "ride-bell" | |
| | "crash" | |
| | "china" | |
| | "splash" | |
| | "tom-1" // highest | |
| | "tom-2" | |
| | "tom-3" // lowest floor tom | |
| | "cross-stick"; // rim click | |
| ``` | |
| ### Example: D-Beat | |
| `apps/web/data/patterns/d-beat.json`: | |
| ```json | |
| { | |
| "id": "d-beat", | |
| "name": "D-Beat", | |
| "description": "D-beat pattern at 180 BPM. Kick plays on every beat with 8th-note doubles, snare hits on 2 and 4, ride bell plays steady 8ths throughout. Common in Discharge, Entombed, and Scandinavian crust punk.", | |
| "tags": ["metal", "punk", "discharge", "entombed", "crust", "fast"], | |
| "genre": "metal", | |
| "defaultTempo": 180, | |
| "defaultBars": 1, | |
| "swingRatio": 0, | |
| "hits": [ | |
| { "position": 0.0, "limb": "kick", "velocity": 110 }, | |
| { "position": 0.125, "limb": "kick", "velocity": 80 }, | |
| { "position": 0.25, "limb": "snare", "velocity": 100 }, | |
| { "position": 0.375, "limb": "kick", "velocity": 80 }, | |
| { "position": 0.5, "limb": "kick", "velocity": 110 }, | |
| { "position": 0.625, "limb": "kick", "velocity": 80 }, | |
| { "position": 0.75, "limb": "snare", "velocity": 100 }, | |
| { "position": 0.875, "limb": "kick", "velocity": 80 }, | |
| { "position": 0.0, "limb": "ride-bell", "velocity": 70 }, | |
| { "position": 0.125, "limb": "ride-bell", "velocity": 70 }, | |
| { "position": 0.25, "limb": "ride-bell", "velocity": 70 }, | |
| { "position": 0.375, "limb": "ride-bell", "velocity": 70 }, | |
| { "position": 0.5, "limb": "ride-bell", "velocity": 70 }, | |
| { "position": 0.625, "limb": "ride-bell", "velocity": 70 }, | |
| { "position": 0.75, "limb": "ride-bell", "velocity": 70 }, | |
| { "position": 0.875, "limb": "ride-bell", "velocity": 70 } | |
| ] | |
| } | |
| ``` | |
| ### Pattern library — starting list | |
| **Metal (priority, 8 patterns):** | |
| - `d-beat.json` — D-beat | |
| - `blast-traditional.json` — traditional blast (kick-snare) | |
| - `blast-hammer.json` — hammer-and-tongs blast | |
| - `blast-hyper.json` — hyperblast (double kick 16ths) | |
| - `half-time-metal.json` — Mastodon/Baroness half-time | |
| - `djent-polyrhythm.json` — Meshuggah-style 4-over-3 kick | |
| - `doom-slow.json` — Sleep/Electric Wizard doom | |
| - `groove-metal.json` — mid-tempo groove metal | |
| **Punk/Hardcore (4 patterns):** | |
| - `punk-rock.json` — Ramones/Misfits 4-on-floor | |
| - `hardcore.json` — Black Flag/Converge | |
| - `skank.json` — ska skank beat (hi-hat upstrokes) | |
| - `reggae-one-drop.json` — reggae | |
| **Rock/Pop (4 patterns):** | |
| - `rock-basic.json` — AC/DC/Beatles standard rock | |
| - `rock-half-time.json` — half-time shuffle | |
| - `pop-groove.json` — modern pop | |
| - `country-train.json` — train beat | |
| **Other genres (4 patterns):** | |
| - `bossa-nova.json` — bossa nova | |
| - `jazz-swing.json` — jazz ride swing | |
| - `funk-new-orleans.json` — NOLA funk | |
| - `hiphop-basic.json` — boom-bap | |
| **Target: 18–20 patterns before hackathon, more as time permits.** | |
| ## Pattern engine | |
| Lives in `apps/web/lib/patterns/engine.ts`. | |
| ### Inputs | |
| - `PatternTemplate` | |
| - `bars: number` | |
| - `tempoBpm: number` | |
| - `timeSignature: { numerator, denominator }` | |
| - Optional `cymbal` override (replaces the default cymbal) | |
| - Optional `accents` (boosts velocity on specified positions) | |
| - Optional `feel` modifier | |
| ### Process | |
| ``` | |
| 1. Load template | |
| 2. Expand to requested bars: | |
| - If template.defaultBars === requested bars: use as-is | |
| - Else: repeat the pattern N times, optionally vary velocity (humanize) | |
| 3. Apply cymbal override: | |
| - If cymbal specified: replace ride/hihat hits with cymbal type | |
| - Velocity scaled: cymbal typically quieter than snare | |
| 4. Apply accents: | |
| - Find positions matching accent specs | |
| - Boost velocity by +20 | |
| 5. Apply feel modifier: | |
| - "swing": delay 8th notes on the "and" by swingRatio | |
| - "half-time": halve the effective tempo of snare/hihat | |
| - "double-time": double it | |
| 6. Convert positions (0.0-1.0) to absolute ticks at tempoBpm | |
| 7. Return array of MIDI events | |
| ``` | |
| ### Output | |
| ```typescript | |
| interface MidiEvent { | |
| tick: number; // absolute tick position | |
| limb: Limb; | |
| velocity: number; // 0-127 | |
| duration: number; // ticks (typically 1-10 for drums) | |
| } | |
| ``` | |
| ## MIDI generation | |
| `apps/web/lib/midi/generator.ts` uses `@tonejs/midi`. | |
| ```typescript | |
| import { Midi } from "@tonejs/midi"; | |
| function eventsToMidi(events: MidiEvent[]): Uint8Array { | |
| const midi = new Midi(); | |
| midi.header.setTempo(events.bpm); // bpm on master track | |
| midi.header.timeSignature = [4, 4]; | |
| const drums = midi.addTrack(); | |
| drums.name = "PatternTalk Drums"; | |
| for (const ev of events) { | |
| drums.addNote({ | |
| midi: limbToGeneralMidi(ev.limb), | |
| ticks: ev.tick, | |
| durationTicks: ev.duration, | |
| velocity: ev.velocity / 127, | |
| }); | |
| } | |
| return midi.toArray(); | |
| } | |
| function limbToGeneralMidi(limb: Limb): number { | |
| // GM Drum Map | |
| const map: Record<Limb, number> = { | |
| "kick": 36, | |
| "snare": 38, | |
| "cross-stick": 37, | |
| "hihat": 42, | |
| "hihat-open": 46, | |
| "ride": 51, | |
| "ride-bell": 53, | |
| "crash": 49, | |
| "china": 52, | |
| "splash": 55, | |
| "tom-1": 50, | |
| "tom-2": 47, | |
| "tom-3": 45, | |
| }; | |
| return map[limb]; | |
| } | |
| ``` | |
| ## Audio sample prompts | |
| When PatternTalk calls the audio service, the prompt includes both the pattern context and a style hint: | |
| ```typescript | |
| interface AudioPrompt { | |
| pattern: string; // pattern name | |
| limb?: Limb; // specific sample to generate | |
| styleHints: string[]; // ["brutal drums", "18-inch china", "trashy"] | |
| durationSeconds: number; // 1-5 for one-shots, 5-15 for loops | |
| intensity: "soft" | "medium" | "brutal" | "brutal-max"; | |
| loraAdapter?: string; // "brutal-drums" by default | |
| } | |
| ``` | |
| Examples: | |
| - Limb-specific one-shot: `{ pattern: "D-Beat", limb: "china", styleHints: ["trashy", "18-inch", "panic-attack"], durationSeconds: 3, intensity: "brutal-max" }` | |
| - Loop preview: `{ pattern: "D-Beat", styleHints: ["brutal drums", "tight snare"], durationSeconds: 8, intensity: "brutal" }` | |
| ## Training data manifest | |
| `data/training/manifest.yaml` describes every audio file used to train the brutal-drum LoRA. | |
| ```yaml | |
| version: 1 | |
| lora: | |
| name: patterntalk-brutal-drums | |
| base_model: stable-audio-3-small | |
| training_steps: 1500 | |
| learning_rate: 1e-4 | |
| rank: 32 | |
| samples: | |
| - id: kick-001 | |
| path: oneshots/kick-tight.wav | |
| category: kick | |
| tags: [tight, clicky, triggered] | |
| source: original-recording | |
| license: CC-BY | |
| duration_seconds: 1.2 | |
| - id: snare-discharge-001 | |
| path: oneshots/snare-trashy.wav | |
| category: snare | |
| tags: [trashy, snappy, crust] | |
| source: freesound.org/user-x | |
| license: CC-BY | |
| duration_seconds: 0.8 | |
| - id: loop-dbeat-discharge-style | |
| path: loops/dbeat-180.wav | |
| category: loop | |
| tags: [d-beat, 180bpm, discharge-style] | |
| source: original-recording | |
| license: CC-BY | |
| duration_seconds: 4.0 | |
| bpm: 180 | |
| ``` | |
| **License discipline:** All samples must be CC0, CC-BY, or original recordings. No unlicensed material. Documented in the manifest, surfaced in the model card. | |
| ## What we're explicitly not modeling (yet) | |
| - User accounts (out of scope) | |
| - Cloud-saved pattern libraries (localStorage only) | |
| - Pattern remixing/chaining | |
| - Time-signature patterns other than 4/4 (the parser supports it but templates are 4/4) | |
| - Polymeter / metric modulation | |
| - Genre mixing (one pattern per request, not hybrids) | |
| These are all reasonable post-hackathon extensions. | |