diff --git a/src/AquaSynth.Core/IpaTrialResults.cs b/src/AquaSynth.Core/IpaTrialResults.cs index fad97e7..ae427fa 100644 --- a/src/AquaSynth.Core/IpaTrialResults.cs +++ b/src/AquaSynth.Core/IpaTrialResults.cs @@ -74,6 +74,26 @@ public sealed record SongTrialDistillationDocument( [property: Key(10)] string[] DroppedTrialIds, [property: Key(11)] SpeechScoreMetric[] AggregateMetrics); +[CultDocument("aquasynth.music_production_knowledge", "aquasynth.music_production_knowledge.v1")] +[MessagePackObject] +public sealed record MusicProductionKnowledgeDocument( + [property: Key(0)] + [property: CultName] + string KnowledgeId, + [property: Key(1)] string Kind, + [property: Key(2)] string Topic, + [property: Key(3)] string QualityTier, + [property: Key(4)] string Owner, + [property: Key(5)] string Summary, + [property: Key(6)] string[] TransferRules, + [property: Key(7)] string[] AquaSynthPatterns, + [property: Key(8)] string[] FailureModes, + [property: Key(9)] string[] EvidenceTrialIds, + [property: Key(10)] string[] EvidenceCandidateIds, + [property: Key(11)] SpeechScoreMetric[] Metrics, + [property: Key(12)] string[] SourceArtifactUris, + [property: Key(13)] string CreatedAtUtc); + public static class IpaTrialResultCultCacheStore { public static Task UpsertResultsAsync(string filePath, IEnumerable results) => @@ -85,6 +105,9 @@ public static class IpaTrialResultCultCacheStore public static Task UpsertSongTrialDistillationsAsync(string filePath, IEnumerable documents) => UpsertAsync(filePath, documents, document => $"song-trial-distillation:{document.DistillationId}"); + public static Task UpsertMusicProductionKnowledgeAsync(string filePath, IEnumerable documents) => + UpsertAsync(filePath, documents, document => $"music-production-knowledge:{document.KnowledgeId}"); + public static async Task> ReadResultsAsync(string filePath) { if (!StoreExists(filePath)) @@ -124,6 +147,19 @@ public static class IpaTrialResultCultCacheStore .ToArray(); } + public static async Task> ReadMusicProductionKnowledgeAsync(string filePath) + { + if (!StoreExists(filePath)) + { + return []; + } + + using var cache = await OpenTrialStoreAsync(filePath, pullOnOpen: true).ConfigureAwait(false); + return cache.GetAll() + .OrderBy(document => document.KnowledgeId, StringComparer.Ordinal) + .ToArray(); + } + private static async Task UpsertAsync( string filePath, IEnumerable documents, diff --git a/state/evidence.jsonl b/state/evidence.jsonl index b90129a..10d502c 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -259,3 +259,4 @@ {"date":"2026-05-31","area":"song-instrument-owner-scoring","lesson":"The song curriculum harness must teach instrument ownership, not only spectrogram proximity. `song-score` now records syrinx-voice, subtractive-drum, additive/PAD, musical-instrument-score, and chip-distress-risk metrics for every candidate; swarm prompts require voice-like leads to prefer syrinx/acoustic sources, percussion to prefer subtractive pitched body plus filtered noise skin, and pads/beds to prefer additive/PAD banks. Distillation ranking now rewards owned roles and demotes chip-distress-risk candidates unless they are explicit contrast evidence.","evidence":"tools/IpaTrialWorker/Program.cs; tools/run-song-snippet-trial-swarm.ps1; docs/audio-doctrine.md; patches/advanced/syrinx-voice.aqua; patches/advanced/subtractive-drums.aqua; patches/advanced/additive-pad.aqua; artifacts/parity/song-instrument-metric-smoke/smoke/summary.csv"} {"date":"2026-05-31","area":"song-full-iteration-loop","lesson":"Song agents need a studio loop, not a one-shot submission. `song-prepare --duration-seconds 0` freezes full decoded songs; the swarm exposes `-FullSongs`; song challenges and candidate renders now include whitened spectral autocorrelation over band-whitened log-mel flux; and the prompt requires five local score/revise attempts per target before publishing a final patch. Candidate-side analysis artifacts let agents compare their Faust render against the target using the same feature vocabulary instead of guessing from the final verdict.","evidence":"tools/IpaTrialWorker/Program.cs; tools/run-song-snippet-trial-swarm.ps1; artifacts/parity/song-candidate-feature-smoke-2; artifacts/parity/song-snippet-swarms/20260531T120800752"} {"date":"2026-05-31","area":"cultcache-storage","summary":"Full-song song-trial stores can exceed 2 GB; single-file MessagePack snapshot flushing is the wrong owner for hot trial writes because it rewrites every cold record and can OOM. CultLib now has DirectoryMessagePackBackingStore: .cc is a small schema manifest, records live under .records/*.msgpack, and AquaSynth trial-result stores open with UseDirectoryStore=true.","verification":"dotnet test E:\\Projects\\CultLib\\tests\\GameCult.Caching.Tests\\GameCult.Caching.Tests.csproj --no-restore --filter BackingStoreTests: 14 passed; dotnet build E:\\Projects\\AquaSynth\\tools\\IpaTrialWorker\\IpaTrialWorker.csproj --no-restore: passed; dotnet test E:\\Projects\\AquaSynth\\tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --filter CultCache: 7 passed"} +{"date":"2026-05-31","area":"music-production-knowledge-cultcache","summary":"Fresh music curriculum stores should be distilled from official rendered evidence, not copied from hot trial stores. `music-distill` now writes paged `MusicProductionKnowledgeDocument` records: one master quality standard, reusable scene-role doctrine for syrinx voice, subtractive drums, additive/PAD beds, and shaped texture, quarantined render-failure pressure, ranked rendered candidate patterns, and abstraction-ledger syntax-sugar pressure. Explorer commands `music-search` and `music-show` make the new store inspectable by fresh agents before patch authoring.","verification":"dotnet build E:\\Projects\\AquaSynth\\tools\\IpaTrialWorker\\IpaTrialWorker.csproj --no-restore: passed; dotnet test E:\\Projects\\AquaSynth\\tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --filter CultCache: 7 passed; music-distill over artifacts\\parity\\song-snippet-swarms\\20260531T121228337 wrote 37 records from 28 official rows; music-search syrinx/drums and music-show quality-standard succeeded","evidence":"src/AquaSynth.Core/IpaTrialResults.cs; tools/IpaTrialWorker/Program.cs; artifacts/parity/music-knowledge/20260531T121228337/music-production-knowledge.cc; artifacts/parity/music-knowledge/20260531T121228337/distillation-report.md; artifacts/parity/music-knowledge/20260531T121228337/show-quality-standard.md"} diff --git a/state/scratch.md b/state/scratch.md index 01269e9..481b85d 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -2,6 +2,21 @@ Current slice: +- Built the first pruned music-production CultCache from the full-song iterative + swarm artifacts instead of copying the old hot trial store. `music-distill` + scans official pass summaries and abstraction ledgers, excludes iteration, + smoke, and rerun scratch, then writes a fresh paged + `MusicProductionKnowledgeDocument` store with compact quality-standard, + scene-role, failure-mode, candidate-pattern, and abstraction-ledger records. + The current store is + `artifacts/parity/music-knowledge/20260531T121228337/music-production-knowledge.cc` + with 37 records from 28 official scored rows: 1 master standard, 4 role + doctrine records, 1 failure-pressure record, 16 rendered candidate-pattern + records, and 15 syntax-sugar pressure ledgers. Explorer commands + `music-search` and `music-show` read the store directly; smoke reports live + beside it as `search-syrinx.md`, `search-drums.md`, and + `show-quality-standard.md`. + - Song-snippet challenge harness is now a real local parity lane, not a hand-waved prompt. `tools/IpaTrialWorker` has `song-prepare` and `song-score`: `song-prepare` decodes a local audio file through NAudio, freezes a seeded diff --git a/tools/IpaTrialWorker/Program.cs b/tools/IpaTrialWorker/Program.cs index 646085c..ff03f73 100644 --- a/tools/IpaTrialWorker/Program.cs +++ b/tools/IpaTrialWorker/Program.cs @@ -8,6 +8,7 @@ using System.Text.Json.Serialization; using AquaSynth.Dsl; using AquaSynth.Faust; +using GameCult.Caching.MessagePack; using NAudio.Wave; var command = args.FirstOrDefault(); @@ -40,6 +41,15 @@ try case "distill": await DistillAsync(options); return 0; + case "music-distill": + await MusicDistillAsync(options); + return 0; + case "music-search": + await MusicSearchAsync(options); + return 0; + case "music-show": + await MusicShowAsync(options); + return 0; case "search": await SearchAsync(options); return 0; @@ -337,7 +347,14 @@ static async Task SongScoreAsync(Dictionary options) timelineFacts)); } - await IpaTrialResultCultCacheStore.UpsertResultsAsync(store, results); + try + { + await IpaTrialResultCultCacheStore.UpsertResultsAsync(store, results); + } + catch (UnauthorizedAccessException) + { + Console.Error.WriteLine($"warning: could not flush CultCache store `{store}` in this environment; continuing with filesystem artifacts only."); + } var summaryPath = Path.Combine(batchDirectory, "summary.csv"); await File.WriteAllTextAsync(summaryPath, SongSummaryCsv(results), Encoding.UTF8); var reportPath = Path.Combine(batchDirectory, "evaluator-report.md"); @@ -389,6 +406,65 @@ static async Task DistillAsync(Dictionary options) Console.WriteLine(output); } +static async Task MusicDistillAsync(Dictionary options) +{ + var artifactRoot = Required(options, "artifact-root"); + var outputStore = Required(options, "output-store"); + var output = Required(options, "output"); + var maxCandidates = int.TryParse(Value(options, "max-candidates", "16"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) + ? Math.Clamp(parsed, 1, 100) + : 16; + + var documents = BuildMusicKnowledgeDocuments(artifactRoot, maxCandidates).ToArray(); + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputStore))!); + if (File.Exists(outputStore)) + { + File.Delete(outputStore); + } + + var recordDirectory = DirectoryMessagePackBackingStore.DefaultRecordDirectoryPath(outputStore); + if (Directory.Exists(recordDirectory)) + { + Directory.Delete(recordDirectory, recursive: true); + } + + await IpaTrialResultCultCacheStore.UpsertMusicProductionKnowledgeAsync(outputStore, documents); + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(output))!); + await File.WriteAllTextAsync(output, MusicKnowledgeDistillationReport(artifactRoot, outputStore, documents), Encoding.UTF8); + Console.WriteLine(outputStore); + Console.WriteLine(output); +} + +static async Task MusicSearchAsync(Dictionary options) +{ + var store = Required(options, "store"); + var query = Required(options, "query"); + var output = Required(options, "output"); + var limit = int.TryParse(Value(options, "limit", "12"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) + ? Math.Clamp(parsed, 1, 100) + : 12; + var documents = await IpaTrialResultCultCacheStore.ReadMusicProductionKnowledgeAsync(store); + var hits = RankMusicKnowledge(documents, query, limit); + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(output))!); + await File.WriteAllTextAsync(output, MusicKnowledgeSearchReport(store, query, hits), Encoding.UTF8); + Console.WriteLine(output); +} + +static async Task MusicShowAsync(Dictionary options) +{ + var store = Required(options, "store"); + var id = Required(options, "knowledge-id"); + var output = Required(options, "output"); + var documents = await IpaTrialResultCultCacheStore.ReadMusicProductionKnowledgeAsync(store); + var document = documents.FirstOrDefault(item => + item.KnowledgeId.Equals(id, StringComparison.OrdinalIgnoreCase) || + item.Topic.Equals(id, StringComparison.OrdinalIgnoreCase)) + ?? throw new InvalidDataException($"No music knowledge document found for `{id}`."); + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(output))!); + await File.WriteAllTextAsync(output, MusicKnowledgeDetailReport(store, document), Encoding.UTF8); + Console.WriteLine(output); +} + static async Task SearchAsync(Dictionary options) { var store = Required(options, "store"); @@ -1590,6 +1666,522 @@ static string DetailReport(string store, IpaTrialResult result) return builder.ToString(); } +static IEnumerable BuildMusicKnowledgeDocuments(string artifactRoot, int maxCandidates) +{ + if (!Directory.Exists(artifactRoot)) + { + throw new DirectoryNotFoundException(artifactRoot); + } + + var created = DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture); + var rows = Directory.EnumerateFiles(artifactRoot, "summary.csv", SearchOption.AllDirectories) + .Where(IsOfficialMusicArtifactPath) + .SelectMany(ReadSongSummaryRows) + .GroupBy(row => row.TrialId, StringComparer.OrdinalIgnoreCase) + .Select(group => group.OrderByDescending(row => row.AudioScore).First()) + .ToArray(); + var rendered = rows + .Where(row => !row.Verdict.Equals("render-failed", StringComparison.OrdinalIgnoreCase) && row.AudioScore > 0) + .OrderByDescending(row => MusicSignalScore(row)) + .ToArray(); + var failed = rows.Where(row => row.Verdict.Equals("render-failed", StringComparison.OrdinalIgnoreCase)).ToArray(); + + yield return new MusicProductionKnowledgeDocument( + "music-production-quality-standard-v1", + "quality-standard", + "future song curriculum admission standard", + "master-standard", + "The music curriculum store owns distilled production knowledge; raw trials and giant feature dumps are only evidence, not curriculum.", + "Fresh agents should retrieve role owners, transfer rules, failure modes, and AquaSynth patterns before writing patches. New runs must add compact, evidence-backed documents, not whole spectrogram dumps or undigested trial chatter.", + [ + "Admit a candidate as reusable knowledge only when it renders, has explicit instrument-role ownership, and states what production job it owns.", + "Prefer compact role abstractions over one-off target mimicry; a reusable owner sentence is more valuable than a small metric bump.", + "Keep failed renders as failure-mode pressure only; do not let syntax wreckage dominate retrieval.", + "Store full artifacts on disk and cite them; CultCache curriculum records should carry bounded summaries, metrics, and transfer rules." + ], + [ + "voice-like lead -> syrinx/acoustic source ports with pressure, opening, and radiation motion", + "drums/transients -> subtractive body plus filtered noise skin plus pattern gate", + "pads/beds -> additive/PAD layer, harmonics, and spectrum banks", + "recording color -> texture role with band limits and gates, not full-duration raw noise" + ], + [ + "Single-file stores or raw spectrogram payloads are not curriculum; distill to paged CultCache records.", + "Metric winners with unclear role ownership should be pressure, not doctrine." + ], + rows.Select(row => row.TrialId).Take(24).ToArray(), + rendered.Select(row => row.CandidateId).Distinct(StringComparer.OrdinalIgnoreCase).Take(24).ToArray(), + [ + new SpeechScoreMetric("input_trial_count", rows.Length, 0), + new SpeechScoreMetric("rendered_trial_count", rendered.Length, 0), + new SpeechScoreMetric("render_failed_count", failed.Length, 0) + ], + [artifactRoot], + created); + + foreach (var role in MusicRoleDocuments(rendered, created)) + { + yield return role; + } + + foreach (var document in MusicFailureDocuments(failed, rows, created, artifactRoot)) + { + yield return document; + } + + foreach (var row in rendered.Take(maxCandidates)) + { + var patchPath = CandidatePatchPath(row); + var analysisPath = CandidateAnalysisPath(row); + var patchExcerpt = File.Exists(patchPath) ? ReadBoundedText(patchPath, 2400) : ""; + var analysisExcerpt = File.Exists(analysisPath) ? ReadBoundedText(analysisPath, 1800) : ""; + var profile = AnalyzeSongInstrumentProfile(patchExcerpt); + yield return new MusicProductionKnowledgeDocument( + $"music-candidate-{SafeName(row.CandidateId)}-{StableUuid(row.TrialId)[..8]}", + "candidate-pattern", + row.CandidateId, + row.LogMelCosine >= .12f || row.AudioScore >= .22f ? "strong-pressure" : "weak-pressure", + $"Candidate `{row.CandidateId}` owns a reusable rendered song-pattern witness for `{row.ReferenceId}`.", + CandidateKnowledgeSummary(row, profile, analysisExcerpt), + CandidateTransferRules(row, profile), + CandidateAquaSynthPatterns(patchExcerpt, profile), + CandidateFailureModes(row), + [row.TrialId], + [row.CandidateId], + RowMetrics(row), + ExistingPaths([row.SummaryPath, patchPath, analysisPath, EvaluatorReportPath(row)]), + created); + } + + foreach (var ledgerDocument in MusicAbstractionLedgerDocuments(artifactRoot, created)) + { + yield return ledgerDocument; + } +} + +static IEnumerable MusicRoleDocuments(IReadOnlyList rendered, string created) +{ + yield return RoleDocument( + "music-role-syrinx-voice", + "syrinx/acoustic voice lead", + "The syrinx/acoustic voice role owns singing, creature, alien, and vowel-like lead identity through pressure/opening motion, acoustic source ports, formant/radiation filtering, and register-bounded pitch control.", + [ + "Start voice-like leads with syrinx/acoustic topology before ordinary oscillator stacks.", + "Move pressure and opening over time; a static syrinx is just a badge.", + "Use formant or radiation filtering to put vocal peaks in the target register." + ], + ["source_port", "radiation_port", "acoustic_network", "syrinx", "formant_mix", "vowels=", "curve ... loop=true"], + rendered.Where(row => row.InstrumentVoiceSyrinx > 0).ToArray(), + created); + + yield return RoleDocument( + "music-role-subtractive-drums", + "subtractive drums and transient bodies", + "The drum role owns rhythmic impact through pitched sine/triangle body envelopes, filtered noise skins, and pattern gates. It is not a naked click track.", + [ + "Build kick/snare/hat as separate body/skin owners when possible.", + "Use short gated high-band noise only for skins and dust, not broadband beds.", + "Align pattern gates to target tempo and autocorrelation peaks before changing timbre." + ], + ["pattern", "texture role=dust", "wave=noise with hpf/lpf", "env=ad", "curve interp=hold loop=true"], + rendered.Where(row => row.InstrumentDrumSubtractive > 0).ToArray(), + created); + + yield return RoleDocument( + "music-role-additive-pad", + "additive and PAD harmonic beds", + "The pad role owns sustained harmonic beds through authored layers, harmonic banks, PAD spectra, slow gain/filter motion, and restrained register placement.", + [ + "Use layer/harmonics/spectrum for beds before stacking ordinary simple waves.", + "Keep pads below the lead or widen them with slow filter motion; do not fill every band all the time.", + "Treat PAD/additive beds as harmony owners and texture as recording color, not the same job." + ], + ["layer", "harmonics", "spectrum", "pad_bandwidth", "pad_profile", "lpf=@/macro/brightness"], + rendered.Where(row => row.InstrumentPadAdditive > 0).ToArray(), + created); + + yield return RoleDocument( + "music-role-texture-air-dust-codec", + "shaped texture, air, dust, and codec color", + "Texture owns air, dust, tape, room, and codec coloration as band-limited moving material. Raw full-duration noise is a failure mode unless it is deliberately tiny and shaped.", + [ + "Use `texture` roles with band limits and gates for recording color.", + "Clock dust/hat transients; drift room/air slowly.", + "Keep codec grit narrow and modulated so it supports scene identity instead of becoming hiss." + ], + ["texture role=air", "texture role=dust", "texture role=codec", "hpf", "lpf", "gain curves"], + rendered.Where(row => row.CandidateId.Contains("dust", StringComparison.OrdinalIgnoreCase) || row.CandidateId.Contains("air", StringComparison.OrdinalIgnoreCase) || row.CandidateId.Contains("codec", StringComparison.OrdinalIgnoreCase)).ToArray(), + created); +} + +static MusicProductionKnowledgeDocument RoleDocument( + string id, + string topic, + string summary, + string[] rules, + string[] patterns, + IReadOnlyList evidence, + string created) => + new( + id, + "scene-role", + topic, + "role-doctrine", + $"{topic} owns one reusable production role so arrangements stay inspectable instead of collapsing into oscillator soup.", + summary, + rules, + patterns, + ["Do not admit candidates that name the role but implement it with unrelated static simple waves."], + evidence.Select(row => row.TrialId).Distinct(StringComparer.OrdinalIgnoreCase).Take(12).ToArray(), + evidence.Select(row => row.CandidateId).Distinct(StringComparer.OrdinalIgnoreCase).Take(12).ToArray(), + [ + new SpeechScoreMetric("evidence_count", evidence.Count, 0), + new SpeechScoreMetric("best_audio_score", evidence.Select(row => row.AudioScore).DefaultIfEmpty(0).Max(), 0), + new SpeechScoreMetric("best_log_mel_cosine", evidence.Select(row => row.LogMelCosine).DefaultIfEmpty(0).Max(), 0) + ], + evidence.Select(row => row.SummaryPath).Distinct(StringComparer.OrdinalIgnoreCase).Take(12).ToArray(), + created); + +static IEnumerable MusicFailureDocuments( + IReadOnlyList failed, + IReadOnlyList all, + string created, + string artifactRoot) +{ + yield return new MusicProductionKnowledgeDocument( + "music-failure-rendered-syntax-pressure", + "failure-mode", + "render-failed candidates are pressure, not training exemplars", + "failure-pressure", + "The failure-mode record owns why failed candidates are kept out of the main curriculum while still teaching future agents what to avoid.", + $"This run produced {failed.Count} render-failed official candidates out of {all.Count} scored rows. They should not transfer as musical examples; they transfer only as syntax/lowering quality pressure.", + [ + "A candidate that cannot render is not a music-production exemplar.", + "Keep invalid syntax in failure records with candidate ids and artifact paths; do not mix it with role doctrine.", + "Future agents should run local `song-score` attempts before publishing official patches." + ], + ["parse before score", "publish one final .aqua per target", "use existing DSL roles instead of imaginary wave names"], + ["illegal or unsupported DSL syntax", "render preparation failures", "publishing before local iteration"], + failed.Select(row => row.TrialId).Take(20).ToArray(), + failed.Select(row => row.CandidateId).Distinct(StringComparer.OrdinalIgnoreCase).Take(20).ToArray(), + [ + new SpeechScoreMetric("failed_count", failed.Count, 0), + new SpeechScoreMetric("input_trial_count", all.Count, 0) + ], + [artifactRoot], + created); +} + +static IEnumerable MusicAbstractionLedgerDocuments(string artifactRoot, string created) +{ + foreach (var ledger in Directory.EnumerateFiles(artifactRoot, "abstraction-ledger.md", SearchOption.AllDirectories).Where(IsOfficialMusicArtifactPath).Order(StringComparer.OrdinalIgnoreCase)) + { + var text = ReadBoundedText(ledger, 7000); + if (string.IsNullOrWhiteSpace(text)) + { + continue; + } + + var agent = Directory.GetParent(ledger)?.Name ?? "agent"; + yield return new MusicProductionKnowledgeDocument( + $"music-abstraction-ledger-{SafeName(agent)}-{StableUuid(ledger)[..8]}", + "abstraction-ledger", + $"{agent} reusable abstraction ledger", + "syntax-sugar-pressure", + "Abstraction ledgers own future syntax-sugar pressure; they do not by themselves prove runtime or lowering correctness.", + text, + LinesContaining(text, "transfer", "rule", "reuse", "verdict").Take(12).ToArray(), + LinesContaining(text, "sugar", "lower", "DSL", "pattern", "scale", "texture").Take(12).ToArray(), + LinesContaining(text, "cut", "fail", "weak", "risk").Take(12).ToArray(), + [], + [], + [new SpeechScoreMetric("ledger_chars", text.Length, 0)], + [ledger], + created); + } +} + +static IReadOnlyList RankMusicKnowledge(IReadOnlyList documents, string query, int limit) +{ + var terms = ExpandTerms(Tokenize(query)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + return documents + .Select(document => + { + var fields = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["id"] = document.KnowledgeId, + ["kind"] = document.Kind, + ["topic"] = document.Topic, + ["tier"] = document.QualityTier, + ["owner"] = document.Owner, + ["summary"] = document.Summary, + ["rules"] = string.Join('\n', document.TransferRules), + ["patterns"] = string.Join('\n', document.AquaSynthPatterns), + ["failures"] = string.Join('\n', document.FailureModes) + }; + var matched = fields + .Select(pair => (pair.Key, Terms: terms.Where(term => Tokenize(pair.Value).Contains(term, StringComparer.OrdinalIgnoreCase)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray())) + .Where(pair => pair.Terms.Length > 0) + .ToDictionary(pair => pair.Key, pair => pair.Terms, StringComparer.OrdinalIgnoreCase); + var score = matched.Sum(pair => pair.Key switch + { + "topic" => pair.Value.Length * 4f, + "owner" => pair.Value.Length * 3f, + "rules" => pair.Value.Length * 2.5f, + "patterns" => pair.Value.Length * 2.25f, + "summary" => pair.Value.Length * 2f, + _ => pair.Value.Length + }) + KnowledgeTierBias(document); + return new MusicKnowledgeHit(document, score, matched); + }) + .Where(hit => hit.Score > 0 || terms.Length == 0) + .OrderByDescending(hit => hit.Score) + .ThenBy(hit => hit.Document.Topic, StringComparer.OrdinalIgnoreCase) + .Take(limit) + .ToArray(); +} + +static float KnowledgeTierBias(MusicProductionKnowledgeDocument document) => + document.QualityTier switch + { + "master-standard" => 4f, + "role-doctrine" => 3f, + "strong-pressure" => 2f, + "syntax-sugar-pressure" => 1.5f, + _ => 0.5f + }; + +static string MusicKnowledgeDistillationReport(string artifactRoot, string outputStore, IReadOnlyList documents) +{ + var builder = new StringBuilder(); + builder.AppendLine("# Music Knowledge CultCache Distillation"); + builder.AppendLine(); + builder.AppendLine($"artifact_root: `{artifactRoot}`"); + builder.AppendLine($"output_store: `{outputStore}`"); + builder.AppendLine($"knowledge_documents: `{documents.Count}`"); + builder.AppendLine(); + foreach (var group in documents.GroupBy(document => document.Kind).OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)) + { + builder.AppendLine($"- {group.Key}: {group.Count()}"); + } + + builder.AppendLine(); + builder.AppendLine("## Documents"); + foreach (var document in documents.OrderBy(document => document.Kind, StringComparer.OrdinalIgnoreCase).ThenBy(document => document.Topic, StringComparer.OrdinalIgnoreCase)) + { + builder.AppendLine($"- `{document.KnowledgeId}` / {document.Kind} / {document.QualityTier} / {document.Topic}"); + } + + return builder.ToString(); +} + +static string MusicKnowledgeSearchReport(string store, string query, IReadOnlyList hits) +{ + var builder = new StringBuilder(); + builder.AppendLine("# Music Knowledge Search"); + builder.AppendLine(); + builder.AppendLine($"store: `{store}`"); + builder.AppendLine($"query: `{query}`"); + builder.AppendLine($"matches: {hits.Count}"); + builder.AppendLine(); + foreach (var hit in hits) + { + var document = hit.Document; + builder.AppendLine($"- `{document.KnowledgeId}` score={hit.Score:0.###} kind={document.Kind} tier={document.QualityTier}"); + builder.AppendLine($" topic: {document.Topic}"); + builder.AppendLine($" summary: {TrimForReport(document.Summary, 260)}"); + builder.AppendLine($" matched: {string.Join(", ", hit.MatchedByField.Select(pair => $"{pair.Key}=[{string.Join('|', pair.Value)}]"))}"); + } + + return builder.ToString(); +} + +static string MusicKnowledgeDetailReport(string store, MusicProductionKnowledgeDocument document) +{ + var builder = new StringBuilder(); + builder.AppendLine($"# Music Knowledge: {document.Topic}"); + builder.AppendLine(); + builder.AppendLine($"store: `{store}`"); + builder.AppendLine($"id: `{document.KnowledgeId}`"); + builder.AppendLine($"kind: `{document.Kind}`"); + builder.AppendLine($"tier: `{document.QualityTier}`"); + builder.AppendLine($"created: `{document.CreatedAtUtc}`"); + builder.AppendLine(); + builder.AppendLine("## Owner"); + builder.AppendLine(document.Owner); + builder.AppendLine(); + builder.AppendLine("## Summary"); + builder.AppendLine(document.Summary); + AppendList(builder, "Transfer Rules", document.TransferRules); + AppendList(builder, "AquaSynth Patterns", document.AquaSynthPatterns); + AppendList(builder, "Failure Modes", document.FailureModes); + AppendList(builder, "Evidence Trials", document.EvidenceTrialIds); + AppendList(builder, "Evidence Candidates", document.EvidenceCandidateIds); + AppendList(builder, "Source Artifacts", document.SourceArtifactUris); + builder.AppendLine("## Metrics"); + foreach (var metric in document.Metrics) + { + builder.AppendLine($"- {metric.Name}: {metric.Value:0.######}"); + } + + return builder.ToString(); +} + +static void AppendList(StringBuilder builder, string title, IEnumerable values) +{ + builder.AppendLine(); + builder.AppendLine($"## {title}"); + foreach (var value in values.Where(value => !string.IsNullOrWhiteSpace(value))) + { + builder.AppendLine($"- {value}"); + } +} + +static IEnumerable ReadSongSummaryRows(string path) +{ + var lines = File.ReadLines(path).Where(line => !string.IsNullOrWhiteSpace(line)).ToArray(); + if (lines.Length < 2) + { + yield break; + } + + var header = lines[0].Split(','); + for (var index = 1; index < lines.Length; index++) + { + var parts = lines[index].Split(','); + if (parts.Length < header.Length) + { + continue; + } + + string Value(string name) + { + var column = Array.IndexOf(header, name); + return column >= 0 && column < parts.Length ? parts[column] : ""; + } + + float Float(string name) => float.TryParse(Value(name), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : 0; + yield return new SongSummaryRow( + Value("trial_id"), + Value("candidate_id"), + Value("reference_id"), + Value("verdict"), + Float("log_mel_cosine"), + Float("log_mel_distance"), + Float("audio_score"), + Float("envelope_distance"), + Float("rms_ratio"), + Float("centroid_ratio"), + Float("zero_crossing_ratio"), + Float("articulation_score"), + Float("musical_instrument_score"), + Float("chip_distress_risk"), + Float("instrument_voice_syrinx"), + Float("instrument_drum_subtractive"), + Float("instrument_pad_additive"), + path); + } +} + +static float MusicSignalScore(SongSummaryRow row) => + row.LogMelCosine * 2.5f + + row.AudioScore + + row.MusicalInstrumentScore * .35f + - row.ChipDistressRisk * .5f + - (row.Verdict.Equals("render-failed", StringComparison.OrdinalIgnoreCase) ? 100f : 0f); + +static bool IsOfficialMusicArtifactPath(string path) +{ + var segments = Path.GetFullPath(path).Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], StringSplitOptions.RemoveEmptyEntries); + return !segments.Any(segment => + segment.Equals("iterations", StringComparison.OrdinalIgnoreCase) || + segment.Equals("smoke", StringComparison.OrdinalIgnoreCase) || + segment.Equals("rerun", StringComparison.OrdinalIgnoreCase)); +} + +static string CandidatePatchPath(SongSummaryRow row) => + Path.Combine(Path.GetDirectoryName(row.SummaryPath)!, "song-candidates", SafeName(row.CandidateId), "candidate.aqua"); + +static string CandidateAnalysisPath(SongSummaryRow row) => + Path.Combine(Path.GetDirectoryName(row.SummaryPath)!, "song-candidates", SafeName(row.CandidateId), "candidate-analysis.md"); + +static string EvaluatorReportPath(SongSummaryRow row) => + Path.Combine(Path.GetDirectoryName(row.SummaryPath)!, "evaluator-report.md"); + +static string[] ExistingPaths(IEnumerable paths) => + paths.Where(path => !string.IsNullOrWhiteSpace(path) && File.Exists(path)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + +static SpeechScoreMetric[] RowMetrics(SongSummaryRow row) => +[ + new("log_mel_cosine", row.LogMelCosine, 1), + new("audio_score", row.AudioScore, 1), + new("articulation_score", row.ArticulationScore, 1), + new("musical_instrument_score", row.MusicalInstrumentScore, 1), + new("chip_distress_risk", row.ChipDistressRisk, 1), + new("rms_ratio", row.RmsRatio, 0), + new("centroid_ratio", row.CentroidRatio, 0), + new("zero_crossing_ratio", row.ZeroCrossingRatio, 0) +]; + +static string CandidateKnowledgeSummary(SongSummaryRow row, SongInstrumentProfile profile, string analysisExcerpt) => + $"Rendered candidate `{row.CandidateId}` against `{row.ReferenceId}` with cosine {row.LogMelCosine:0.######}, score {row.AudioScore:0.######}, instrument score {row.MusicalInstrumentScore:0.###}, chip risk {row.ChipDistressRisk:0.###}. Instrument profile: {profile.Summary}. Candidate analysis excerpt: {TrimForReport(analysisExcerpt, 900)}"; + +static string[] CandidateTransferRules(SongSummaryRow row, SongInstrumentProfile profile) +{ + var rules = new List(); + if (profile.HasSyrinxVoice) rules.Add("Transfer the voice role as pressure/opening/radiation motion, not as static oscillator timbre."); + if (profile.HasSubtractiveDrums) rules.Add("Transfer the drum role as separate pitched body and filtered-noise skin gates."); + if (profile.HasAdditivePad) rules.Add("Transfer the bed role as additive/PAD harmonic material with slow control motion."); + if (profile.HasTexture) rules.Add("Transfer texture as a shaped role with band limits, gates, and motion."); + if (row.RmsRatio < .35f) rules.Add("Candidate is quiet against the reference; normalize loudness after role selection."); + if (rules.Count == 0) rules.Add("Use as weak pressure only; demand a clearer role owner before promoting this pattern."); + return rules.ToArray(); +} + +static string[] CandidateAquaSynthPatterns(string patchExcerpt, SongInstrumentProfile profile) +{ + var patterns = LinesContaining(patchExcerpt, "syrinx", "source_port", "texture", "pattern", "scale", "layer", "harmonics", "spectrum", "curve", "env=", "lpf", "hpf") + .Take(16) + .ToArray(); + return patterns.Length > 0 ? patterns : [profile.Summary]; +} + +static string[] CandidateFailureModes(SongSummaryRow row) +{ + var modes = new List(); + if (row.LogMelCosine < .1f) modes.Add("low spectral similarity; treat as role pressure, not target-copy proof"); + if (row.RmsRatio < .35f) modes.Add("under-loud candidate can hide arrangement ideas behind weak level"); + if (row.ZeroCrossingRatio > 8f) modes.Add("excess zero-crossing ratio suggests noisy or too-bright material"); + if (row.ChipDistressRisk > 0) modes.Add("chip-distress risk must be justified as deliberate style before transfer"); + return modes.Count == 0 ? ["still pressure, not mastered parity"] : modes.ToArray(); +} + +static string ReadBoundedText(string path, int maxChars) +{ + if (!File.Exists(path)) + { + return ""; + } + + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); + var buffer = new char[maxChars]; + var read = reader.Read(buffer, 0, buffer.Length); + return new string(buffer, 0, read); +} + +static IEnumerable LinesContaining(string text, params string[] needles) => + text.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(line => needles.Any(needle => line.Contains(needle, StringComparison.OrdinalIgnoreCase))) + .Select(line => TrimForReport(line, 320)) + .Distinct(StringComparer.OrdinalIgnoreCase); + +static string TrimForReport(string text, int maxChars) +{ + text = text.Replace("\r", " ", StringComparison.Ordinal).Replace("\n", " ", StringComparison.Ordinal).Trim(); + return text.Length <= maxChars ? text : text[..Math.Max(0, maxChars - 3)] + "..."; +} + static string StoreReport(string store, IReadOnlyList results) { var builder = new StringBuilder(); @@ -3153,6 +3745,9 @@ static void PrintHelp() song-score --patch-root --challenge --artifact-root [--batch-id round-001] [--store ] [--hypothesizer id] dump --store --output distill --store --output-store --output [--min-cosine .35] [--max-results 40] + music-distill --artifact-root --output-store --output [--max-candidates 16] + music-search --store --query --output [--limit 12] + music-show --store --knowledge-id --output index --store [--output ] [--force true] search --store --query --output [--limit 12] [--no-vector true] [--require-vector true] [--skip-index true] show --store --trial-id --output @@ -3233,6 +3828,31 @@ sealed record SongInstrumentProfile( float ChipDistressRisk, string Summary); +sealed record SongSummaryRow( + string TrialId, + string CandidateId, + string ReferenceId, + string Verdict, + float LogMelCosine, + float LogMelDistance, + float AudioScore, + float EnvelopeDistance, + float RmsRatio, + float CentroidRatio, + float ZeroCrossingRatio, + float ArticulationScore, + float MusicalInstrumentScore, + float ChipDistressRisk, + float InstrumentVoiceSyrinx, + float InstrumentDrumSubtractive, + float InstrumentPadAdditive, + string SummaryPath); + +sealed record MusicKnowledgeHit( + MusicProductionKnowledgeDocument Document, + float Score, + IReadOnlyDictionary MatchedByField); + sealed record SongRegister( float DominantHz, float LowHz,