# Theme Generation Engines This note documents the first generation experiments to run after the corpus audit. There are now two engines sharing the same corpus loader, symbolization, rhythmic constraints, MIDI/ABC export, and report writer: - `markov`: a small variable-order Markov model using `vo_regular_bp` regular constraints and soft endpoint weights. - `transformer`: a tiny key-relative decoder-only transformer using the same symbolic vocabulary and constrained sampling surface. The unified runner is: ```bash python3 scripts/run_theme_generation.py markov python3 scripts/run_theme_generation.py transformer ``` The old Markov command remains available as a wrapper: ```bash python3 scripts/run_vo_regular_baseline.py ``` The transformer wrapper is: ```bash python3 scripts/run_transformer_baseline.py ``` The transformer engine requires PyTorch in the active Python environment. The local baseline run used: ```bash python3 -m venv .venv .venv/bin/pip install torch numpy mido .venv/bin/python scripts/run_theme_generation.py transformer \ --samples 6 \ --length 24 \ --steps 500 \ --batch-size 64 \ --block-size 64 \ --d-model 96 \ --nhead 4 \ --layers 3 \ --feedforward 192 \ --temperature 0.95 \ --top-k 16 \ --output-dir outputs/transformer_baseline \ --write-abc ``` ## Available Corpus Tables The generated SQLite database is `audit/themes_audit.sqlite`. Important tables: - `themes`: catalog metadata, ABC/MIDI paths, key signature, meter, length, and note-level summary features. - `notes`: one row per MIDI note event, sorted by `start_tick` and `note_index`. - `theme_descriptions` and `theme_text_fts`: local generated search text. - `endpoint_analysis`: first/last note and first-salient endpoint statistics. - `key_modulation_analysis`: key-signature coverage, accidental counts, and heuristic local-key drift candidates. ## Key Findings Used By The Model The corpus contains 9,824 parsed themes and 196,679 MIDI note events. Key metadata: - Each theme has a single explicit key signature. - No MIDI file contains multiple key-signature events. - No ABC file contains inline `[K:...]` key changes. - Minor mode is not encoded reliably; `abc_key` should be interpreted as a key-signature/root feature, not a full tonal label. Endpoint regularities: - Last note is the notated tonic/root in only 16.8% of themes. - Last note is tonic or dominant in 35.3% of themes. - Last note is in the tonic triad in 54.5% of themes. - The first-salient-to-last pitch-class relation favors intervals 0, 7, and 5. Implication: endpoint constraints should be soft priors, not hard cadence rules. ## Initial Symbolization Start with a deliberately small vocabulary: ```text symbol = (relative_pitch_class, duration_value) ``` where: ```text relative_pitch_class = (pitch_class - key_root) mod 12 ``` The initial version should keep the original duration labels from `notes`, possibly filtered to the common values: - `16th` - `eighth` - `quarter` - `half` - `dotted eighth` - `dotted quarter` - `eighth triplet` - `16th triplet` Rare duration labels can either be mapped to the nearest common class or kept only if they occur above a threshold. This transposes every theme into a common key-relative space and gives enough data for variable-order contexts. The symbolization deliberately removes register. Both the Markov and transformer engines therefore generate relative pitch classes, not concrete octaves. The shared output layer realizes generated pitch classes into MIDI pitches for playback and notation; generated samples currently use a treble-friendly C4-C6 range so ABC, MIDI, MusicXML, and Verovio previews do not drift into unreadable ledger-line registers. A later model can make register/octave a first-class prediction target if we want the generators to learn that behavior directly. ## Markov Training Model Train an order-stack variable-order Markov model over these symbols. Recommended first settings: ```text max_order = 4 min_context_count = 2 backoff = longest available context with nonzero feasible future mass ``` The generated sequence length can first be controlled in notes, e.g. 16, 24, or 32 notes. A later version should control length in beats/bars through a duration tracking acceptor. ## Transformer Training Model Train a deliberately small decoder-only transformer over the same symbol stream. The implementation is in `theme_generation/engines/transformer.py` and uses: ```text token stream = START, symbol_1, symbol_2, ... symbol = (relative_pitch_class, duration_value) ``` Recommended first settings: ```text block_size = 64 d_model = 96 nhead = 4 num_layers = 3 steps = 800 top_k = 16 temperature = 1.0 ``` This is intentionally small enough to make overfitting visible and iteration cheap. Once PyTorch is installed, a tiny smoke run is: ```bash python3 scripts/run_theme_generation.py transformer \ --samples 2 \ --length 16 \ --steps 20 \ --output-dir outputs/transformer_smoke ``` ## Regular / Weighted Constraints The `vo_regular_bp` acceptor should encode constraints that are finite-state: 1. Allowed symbol vocabulary. 2. Optional maximum repetition of the same relative pitch class. 3. Optional maximum number of chromatic relative pitch classes. 4. Optional accumulated duration / bar length target. 5. Boundary weights for initial and terminal relative degrees. For the first note-count baseline, the weighted DFA state can be as small as: ```text state = position ``` and the transition weight can be: ```text if position == 0: return start_degree_weight[relative_pitch_class] if position == length - 1: return terminal_degree_weight[relative_pitch_class] return 1.0 ``` If we want the first-salient-to-last relation, augment the DFA state: ```text state = (position, first_salient_relative_pc_or_none) ``` and apply the endpoint relation weight on the last transition: ```text endpoint_weight[first_salient_relative_pc][last_relative_pc] ``` This compiles the non-local relation into regular state memory. ## Soft Priors Use empirical degree distributions from `endpoint_analysis`, with smoothing. Start weights: ```text P(first_salient_degree) ``` End weights: ```text P(last_degree) ``` Optional relation weights: ```text P(last_relative_pc | first_salient_relative_pc) ``` These should be temperature-scaled so the Markov model still has room to choose idiomatic continuations. ## First Experiment The first runnable Markov experiment: 1. Load `themes` and `notes` from `audit/themes_audit.sqlite`. 2. Build key-relative sequences of `(relative_pitch_class, duration_value)`. 3. Train a max-order Markov count model. 4. Generate samples of fixed note length with endpoint degree soft weights. 5. Convert generated samples to MIDI in a chosen output key. 6. Write a small report with sampled sequences and model diagnostics. The default generation vocabulary should be rhythmically conservative but not overly square: allow durations down to a 16th note, require ordinary durations to fall on a 16th-note grid, and also allow regular triplet durations. This keeps ordinary short notes, dotted-eighth figures, and eighth-triplets. Triplets should be constrained to complete beat-aligned groups; isolated or phase-shifted triplets can make MIDI notation importers quantize surrounding material as awkward septuplets or nontuplets. This should be compiled into the finite-state acceptor, using beat phase and triplet-group state, rather than handled by generate-and-test filtering. Suggested Markov outputs: ```text outputs/vo_regular_baseline/ generated_*.mid report.md ``` Suggested transformer outputs: ```text outputs/transformer_baseline/ generated_*.mid generated_*.abc report.md ``` ABC export can still be useful for debugging, but should be opt-in rather than part of the default generation output. ## Open Questions - Should length be controlled by note count or by total duration? - Should rare chromatic degrees be allowed freely, penalized, or modeled as a separate chromatic decoration layer? - Should endpoint priors be global, meter-specific, composer-specific, or genre-specific? - Should local key drift candidates be excluded from training the simplest key-relative model? The simplest useful answer is: start global, note-count based, and key-relative; then listen.