From f16ea641afaf77df85c99d23d85eba36ce67c41e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 31 May 2026 00:41:28 +0100 Subject: [PATCH] Embed primitive timeline facts in IPA trials --- docs/ipa-vocal-tract-roadmap.md | 28 +++- src/AquaSynth.Core/IpaTrialResults.cs | 14 +- .../PrimitiveTimelineFactExtractor.cs | 157 ++++++++++++++++++ src/AquaSynth.Faust/IpaTrialOrchestrator.cs | 49 +++++- state/evidence.jsonl | 1 + state/map.yaml | 2 + state/memory.json | 12 +- state/scratch.md | 16 ++ state/spine.yaml | 3 +- .../IpaTrialOrchestratorTests.cs | 31 +++- tools/IpaTrialWorker/Program.cs | 100 +++++++++++ 11 files changed, 392 insertions(+), 21 deletions(-) create mode 100644 src/AquaSynth.Core/PrimitiveTimelineFactExtractor.cs diff --git a/docs/ipa-vocal-tract-roadmap.md b/docs/ipa-vocal-tract-roadmap.md index 4e74511..a0d6636 100644 --- a/docs/ipa-vocal-tract-roadmap.md +++ b/docs/ipa-vocal-tract-roadmap.md @@ -640,8 +640,9 @@ and stores typed `IpaTrialResult` records in `ipa-trial-results.cc`. The `.cc` store is per-record CultCache MessagePack data, not a hand-written JSON ledger. `IpaTrialResult` records include hypothesis text, candidate patch URI, reference/candidate artifacts, primitive timeline URI, metrics, evaluator -summary, verdict, known contamination, and timing receipts. This is the trial -result database that later hypothesis workers consume. +summary, verdict, known contamination, timing receipts, and extracted primitive +timeline facts. This is the trial result database that later hypothesis workers +consume. The first five seed trials were run locally on 2026-05-29 under `artifacts/parity/ipa-trials/20260529T214127955/five-seed-trials`. The first @@ -672,13 +673,14 @@ workers to hand-edit evidence: against the PT fixtures, and upserts typed `IpaTrialResult` records. - `search` is the semantic retrieval surface for the `.cc` store. It expands speech-control vocabulary such as vowel/nasal/fricative/stop, indexes trial - summaries plus referenced local artifact snippets into Qdrant with Ollama - embeddings when the local vector stack is available, ranks records with - vector and metric-aware evidence bias, and writes a compact markdown result - set. If Qdrant or Ollama is down, it falls back to the lexical/metric ranker - and names the fallback in the report. + summaries, embedded primitive timeline facts, and referenced local artifact + snippets into Qdrant with Ollama embeddings when the local vector stack is + available, ranks records with vector and metric-aware evidence bias, and + writes a compact markdown result set. If Qdrant or Ollama is down, it falls + back to the lexical/metric ranker and names the fallback in the report. - `show` drills into one trial or candidate with full metrics, artifacts, - known contamination, hypothesis, and evaluator summary. + known contamination, hypothesis, extracted primitive timeline facts, and + evaluator summary. - `dump` remains an audit escape hatch, not the normal agent memory path. The default local vector organ mirrors VoidBot's retrieval pattern: stable @@ -701,6 +703,16 @@ as stop, nasal, fricative, or vowel is also present. This keeps `mix-p` useful for plosive seams without letting every mixed candidate impersonate stop evidence. +Primitive timeline facts are now part of the CultCache record itself, not only +CSV attachments. The extractor records contact closure/opening/reservoir/release +peaks and release block, source pressure/flow peaks, branch admittance/exchanged +flow, radiation boundary/output peaks, path area/delay, path energy, and +passivity ratios. `search`, `show`, `dump`, and trial summary CSVs surface the +release/radiation/passivity witnesses directly. A hypothesis about stops, +fricatives, nasals, vowels, or radiation is not strong evidence unless it can +point to these facts or explicitly say that the required primitive witness is +missing. The machine is allowed to be ugly; it is not allowed to be mysterious. + `tools/run-ipa-trial-loop.ps1` orchestrates the outer loop with external `codex exec` workers. Each round searches accumulated trial memory, asks a hypothesis worker to write a batch of candidates, scores those candidates diff --git a/src/AquaSynth.Core/IpaTrialResults.cs b/src/AquaSynth.Core/IpaTrialResults.cs index 4c9fbe5..25b7f92 100644 --- a/src/AquaSynth.Core/IpaTrialResults.cs +++ b/src/AquaSynth.Core/IpaTrialResults.cs @@ -28,7 +28,19 @@ public sealed record IpaTrialResult( [property: Key(16)] string EvaluationSummary, [property: Key(17)] string Verdict, [property: Key(18)] string[] KnownLies, - [property: Key(19)] SpeechTimingReceipt[] TimingReceipts); + [property: Key(19)] SpeechTimingReceipt[] TimingReceipts, + [property: Key(20)] PrimitiveTimelineFact[] TimelineFacts); + +[MessagePackObject] +public sealed record PrimitiveTimelineFact( + [property: Key(0)] string Name, + [property: Key(1)] string Primitive, + [property: Key(2)] string Signal, + [property: Key(3)] float Value, + [property: Key(4)] string Unit, + [property: Key(5)] int BlockStart, + [property: Key(6)] int BlockEnd, + [property: Key(7)] string Summary); public static class IpaTrialResultCultCacheStore { diff --git a/src/AquaSynth.Core/PrimitiveTimelineFactExtractor.cs b/src/AquaSynth.Core/PrimitiveTimelineFactExtractor.cs new file mode 100644 index 0000000..0cdfc27 --- /dev/null +++ b/src/AquaSynth.Core/PrimitiveTimelineFactExtractor.cs @@ -0,0 +1,157 @@ +using System.Globalization; + +namespace AquaSynth.Dsl; + +public static class PrimitiveTimelineFactExtractor +{ + public static IReadOnlyList Extract(IReadOnlyList timeline) + { + if (timeline.Count == 0) + { + return []; + } + + var facts = new List + { + Fact( + "timeline_blocks", + "timeline", + "block_count", + timeline.Select(sample => sample.Block).Distinct().Count(), + "blocks", + timeline.Min(sample => sample.Block), + timeline.Max(sample => sample.Block), + "Observed primitive timeline block count.") + }; + + AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "opening", "contact_min_opening", "normalized", Min); + AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "opening", "contact_mean_opening", "normalized", Mean); + AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "opening", "contact_closed_blocks", "blocks", ClosedBlocks); + AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "reservoir", "contact_reservoir_peak", "pressure", MaxAbs); + AddSignalFacts(facts, timeline, primitivePrefix: "contact:", signal: "released_flow", "contact_release_peak", "flow", MaxAbs); + AddPeakBlockFacts(facts, timeline, "contact:", "released_flow", "contact_release_peak_block"); + + AddSignalFacts(facts, timeline, primitivePrefix: "source:", signal: "load_pressure", "source_load_pressure_peak", "pressure", MaxAbs); + AddSignalFacts(facts, timeline, primitivePrefix: "source:", signal: "flow", "source_flow_peak", "flow", MaxAbs); + AddPeakBlockFacts(facts, timeline, "source:", "flow", "source_flow_peak_block"); + + AddSignalFacts(facts, timeline, primitivePrefix: "branch:", signal: "admittance", "branch_admittance_peak", "admittance", MaxAbs); + AddSignalFacts(facts, timeline, primitivePrefix: "branch:", signal: "exchanged_flow", "branch_exchanged_flow_peak", "flow", MaxAbs); + + AddSignalFacts(facts, timeline, primitivePrefix: "radiation:", signal: "boundary_flow", "radiation_boundary_flow_peak", "flow", MaxAbs); + AddSignalFacts(facts, timeline, primitivePrefix: "radiation:", signal: "output", "radiation_output_peak", "amplitude", MaxAbs); + AddSignalFacts(facts, timeline, primitivePrefix: "radiation:", signal: "output", "radiation_output_mean_abs", "amplitude", MeanAbs); + AddPeakBlockFacts(facts, timeline, "radiation:", "output", "radiation_output_peak_block"); + + AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "area", "path_mean_area", "area", Mean); + AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "delay_samples", "path_mean_delay_samples", "samples", Mean); + AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "energy_in", "path_energy_in_sum", "energy", Sum); + AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "energy_out", "path_energy_out_sum", "energy", Sum); + AddSignalFacts(facts, timeline, primitivePrefix: "path:", signal: "passivity_ratio", "path_passivity_max", "ratio", Max); + + return facts + .Where(fact => float.IsFinite(fact.Value)) + .OrderBy(fact => fact.Name, StringComparer.Ordinal) + .ThenBy(fact => fact.Primitive, StringComparer.Ordinal) + .ThenBy(fact => fact.BlockStart) + .ToArray(); + } + + private static void AddSignalFacts( + List facts, + IReadOnlyList timeline, + string primitivePrefix, + string signal, + string name, + string unit, + Func, float> reducer) + { + foreach (var group in Matching(timeline, primitivePrefix, signal).GroupBy(sample => sample.Primitive, StringComparer.OrdinalIgnoreCase)) + { + var samples = group.ToArray(); + if (samples.Length == 0) + { + continue; + } + + var value = reducer(samples); + facts.Add(Fact( + name, + group.Key, + signal, + value, + unit, + samples.Min(sample => sample.Block), + samples.Max(sample => sample.Block), + Summary(name, group.Key, signal, value, unit))); + } + } + + private static void AddPeakBlockFacts( + List facts, + IReadOnlyList timeline, + string primitivePrefix, + string signal, + string name) + { + foreach (var group in Matching(timeline, primitivePrefix, signal).GroupBy(sample => sample.Primitive, StringComparer.OrdinalIgnoreCase)) + { + var peak = group + .OrderByDescending(sample => Math.Abs(sample.Value)) + .ThenBy(sample => sample.Block) + .FirstOrDefault(); + if (peak is null) + { + continue; + } + + facts.Add(Fact( + name, + peak.Primitive, + signal, + peak.Block, + "block", + peak.Block, + peak.Block, + Summary(name, peak.Primitive, signal, peak.Block, "block"))); + } + } + + private static IEnumerable Matching( + IReadOnlyList timeline, + string primitivePrefix, + string signal) => + timeline.Where(sample => + sample.Primitive.StartsWith(primitivePrefix, StringComparison.OrdinalIgnoreCase) && + sample.Signal.Equals(signal, StringComparison.OrdinalIgnoreCase) && + float.IsFinite(sample.Value)); + + private static PrimitiveTimelineFact Fact( + string name, + string primitive, + string signal, + float value, + string unit, + int blockStart, + int blockEnd, + string summary) => + new(name, primitive, signal, value, unit, blockStart, blockEnd, summary); + + private static string Summary(string name, string primitive, string signal, float value, string unit) => + $"{name} {primitive} {signal} {value.ToString("0.######", CultureInfo.InvariantCulture)} {unit}"; + + private static float Min(IReadOnlyList samples) => samples.Min(sample => sample.Value); + + private static float Max(IReadOnlyList samples) => samples.Max(sample => sample.Value); + + private static float Mean(IReadOnlyList samples) => samples.Average(sample => sample.Value); + + private static float Sum(IReadOnlyList samples) => samples.Sum(sample => sample.Value); + + private static float MaxAbs(IReadOnlyList samples) => samples.Max(sample => Math.Abs(sample.Value)); + + private static float MeanAbs(IReadOnlyList samples) => samples.Average(sample => Math.Abs(sample.Value)); + + private static float ClosedBlocks(IReadOnlyList samples) => + samples.Count(sample => sample.Value <= 0.2f); +} diff --git a/src/AquaSynth.Faust/IpaTrialOrchestrator.cs b/src/AquaSynth.Faust/IpaTrialOrchestrator.cs index 08d7012..4f33bf4 100644 --- a/src/AquaSynth.Faust/IpaTrialOrchestrator.cs +++ b/src/AquaSynth.Faust/IpaTrialOrchestrator.cs @@ -237,7 +237,8 @@ public static class IpaTrialOrchestrator 0, Confidence(metrics), "Local worker generated the candidate patch, rendered Faust audio, compared log-mel/articulation evidence, and wrote CultCache trial result data.") - ])); + ], + PrimitiveTimelineFactExtractor.Extract(ReadTimeline(candidate.TimelinePath)).ToArray())); } } @@ -328,6 +329,7 @@ public static class IpaTrialOrchestrator { new("gesture_score", AgentGestureScore(mapped.target.Target, patch, timeline), .25f) }; + var timelineFacts = PrimitiveTimelineFactExtractor.Extract(timeline).ToArray(); string verdict; string evaluation; var artifacts = new List @@ -394,7 +396,8 @@ public static class IpaTrialOrchestrator 0, Confidence(metrics), "Local worker rendered an external Codex-authored patch, compared log-mel/articulation evidence, and wrote CultCache trial result data.") - ])); + ], + timelineFacts)); } await IpaTrialResultCultCacheStore.UpsertResultsAsync(storePath, results).ConfigureAwait(false); @@ -607,7 +610,7 @@ public static class IpaTrialOrchestrator private static string SummaryCsv(IReadOnlyList results) { var builder = new StringBuilder(); - builder.AppendLine("trial_id,target_set_id,candidate_id,reference_id,verdict,gesture_score,log_mel_cosine,log_mel_distance,audio_score,articulation_score,rms_ratio"); + builder.AppendLine("trial_id,target_set_id,candidate_id,reference_id,verdict,gesture_score,log_mel_cosine,log_mel_distance,audio_score,articulation_score,rms_ratio,contact_release_peak,radiation_output_peak,path_passivity_max"); foreach (var result in results) { builder.Append(Escape(result.TrialId)); @@ -630,7 +633,13 @@ public static class IpaTrialOrchestrator builder.Append(','); builder.Append(Metric(result, "articulation_score")); builder.Append(','); - builder.AppendLine(Metric(result, "rms_ratio")); + builder.Append(Metric(result, "rms_ratio")); + builder.Append(','); + builder.Append(TimelineFact(result, "contact_release_peak")); + builder.Append(','); + builder.Append(TimelineFact(result, "radiation_output_peak")); + builder.Append(','); + builder.AppendLine(TimelineFact(result, "path_passivity_max")); } return builder.ToString(); @@ -680,6 +689,38 @@ public static class IpaTrialOrchestrator (result.Metrics.FirstOrDefault(metric => metric.Name == name)?.Value ?? 0) .ToString("0.######", CultureInfo.InvariantCulture); + private static string TimelineFact(IpaTrialResult result, string name) => + (result.TimelineFacts? + .Where(fact => fact.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + .Select(fact => fact.Value) + .DefaultIfEmpty(0) + .Max() ?? 0) + .ToString("0.######", CultureInfo.InvariantCulture); + + private static IReadOnlyList ReadTimeline(string path) + { + if (!File.Exists(path)) + { + return []; + } + + var samples = new List(); + foreach (var line in File.ReadLines(path).Skip(1)) + { + var parts = line.Split(','); + if (parts.Length < 4 || + !int.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var block) || + !float.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) + { + continue; + } + + samples.Add(new ProbeTimelineSample(block, parts[1], parts[2], value)); + } + + return samples; + } + private static string SafeName(string value) => new(value.Select(character => char.IsLetterOrDigit(character) ? character : '_').ToArray()); diff --git a/state/evidence.jsonl b/state/evidence.jsonl index aae8123..2569adc 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -243,3 +243,4 @@ {"date":"2026-05-29","area":"ipa-trial-prompt-optimization","lesson":"Five prompt optimization passes showed that live external-agent retrieval is too slow and noisy for science unless tightly bounded. The sharpest hypothesizer smoke used a fixed six-receipt packet and a reasoning-only boundary, then scored evidence by contrast pairs, concrete show metrics, primitive timeline excerpts, class-consistent references, owner sentences, micro-sweep plans, and novelty gates. Production prompts now require those witnesses before treating a DSL seam as actionable."} {"date":"2026-05-30","area":"ipa-trial-vector-retrieval","lesson":"The IPA trial semantic retrieval organ now uses the local full-ass stack: CultCache `.cc` records remain truth, while `IpaTrialWorker index/search` derives Qdrant points with stable SHA-256 UUIDs and Ollama `qwen3-embedding:0.6b` vectors. Search reports include vector scores, metric facts, evidence tags, contrast candidates, artifact snippets, and show commands, with lexical fallback only when the vector stack is unavailable.","evidence":"tools/IpaTrialWorker/Program.cs; artifacts/parity/ipa-trial-worker-smoke/20260529T2204/qdrant-index-smoke.md; artifacts/parity/ipa-trial-worker-smoke/20260529T2204/qdrant-search-stops.md"} {"date":"2026-05-30","area":"ipa-trial-retrieval-tuning","lesson":"Semantic retrieval smokes showed raw vector similarity was useful but too permissive: class queries could leak through generic weak-articulation language, and mixed-set records could impersonate every class because their target set contains all phonemes. `IpaTrialWorker search` now adds phonetic class-affinity scoring, candidate-specific class ownership for mixed lanes, demotes mixed/generalization to a secondary transfer hint when a real class is present, and only tags dressing when the record itself mentions dressing/FM/AM/envelope/helper evidence.","evidence":"artifacts/parity/ipa-trial-worker-smoke/20260529T2204/retrieval-tuning-20260530-pass1; artifacts/parity/ipa-trial-worker-smoke/20260529T2204/retrieval-tuning-20260530-pass4; tools/IpaTrialWorker/Program.cs"} +{"date":"2026-05-31","area":"ipa-trial-primitive-timeline-facts","lesson":"IPA trial evidence must carry primitive phase witnesses inside the CultCache record, not only as CSV artifacts or prose claims. `PrimitiveTimelineFactExtractor` turns `ProbeTimelineReport` samples into contact release, source flow, branch admittance, radiation output, path energy, and passivity facts; `IpaTrialWorker` indexes and reports those facts through search/show/dump so hypothesis agents can cite release/radiation/passivity evidence directly.","evidence":"src/AquaSynth.Core/PrimitiveTimelineFactExtractor.cs; src/AquaSynth.Core/IpaTrialResults.cs; tools/IpaTrialWorker/Program.cs; tests/AquaSynth.Dsl.Tests/IpaTrialOrchestratorTests.cs; artifacts/parity/ipa-timeline-fact-smoke/20260531T233217641"} diff --git a/state/map.yaml b/state/map.yaml index 05c27ea..01160c0 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -60,6 +60,7 @@ modules: primitive_vocal_reference_report: src/AquaSynth.Core/PrimitiveReferenceReport.cs ipa_gesture_experiment: src/AquaSynth.Core/IpaGestureExperiment.cs ipa_trial_results: src/AquaSynth.Core/IpaTrialResults.cs + ipa_trial_timeline_facts: src/AquaSynth.Core/PrimitiveTimelineFactExtractor.cs ipa_trial_orchestrator: src/AquaSynth.Faust/IpaTrialOrchestrator.cs ipa_trial_worker: tools/IpaTrialWorker ipa_trial_external_loop: tools/run-ipa-trial-loop.ps1 @@ -121,6 +122,7 @@ boundaries: - chained utterance-embedding-to-synth-driver backpropagation that returns synth input gradients and updates the utterance encoder from downstream speech loss - external-gradient speech backpropagation entry point for rendered loss surfaces - distributed speech render request/result/checkpoint CultCache contracts for multi-machine Faust rendering and local backpropagation + - extracted primitive timeline facts embedded in IPA trial-result CultCache records for contact release, source flow, branch admittance, radiation output, path energy, and passivity evidence - CultMesh speech render/scoring worker orchestration with payload manifests, peer/lease admission receipts, worker assignment, result collection, and central checkpoint application; workers return gradients and receipts, while one authority trainer owns optimizer updates and checkpoint commits - optional eSpeak NG generated ground-truth fixture for supervised utterance-embedding and synth-driver gradient descent - Faust-rendered speech loss surface in AquaSynth.Faust, using rendered audio analysis and finite-difference controller-output gradients diff --git a/state/memory.json b/state/memory.json index 647c60d..ab867a6 100644 --- a/state/memory.json +++ b/state/memory.json @@ -59,7 +59,7 @@ "Faust-generated C# candidate rendering", "speech timing/confidence receipts for render requests, results, and checkpoints", "CultMesh speech render/scoring orchestration with payload manifests, peer/lease admission receipts, worker assignment, result collection, and central checkpoint application while the authority trainer owns optimizer steps and checkpoint commits", - "frozen IPA gesture experiment rounds and local render trials with candidate scripts, primitive timelines, gesture-layer metrics, manifests, evidence JSONL, metric summaries, candidate clusters, science briefs, WAV artifacts, evaluator reports, CultCache .cc trial-result records, and Qdrant/Ollama-derived semantic retrieval indexes over those records and local artifacts", + "frozen IPA gesture experiment rounds and local render trials with candidate scripts, primitive timelines, extracted primitive timeline facts, gesture-layer metrics, manifests, evidence JSONL, metric summaries, candidate clusters, science briefs, WAV artifacts, evaluator reports, CultCache .cc trial-result records, and Qdrant/Ollama-derived semantic retrieval indexes over those records and local artifacts", "script scoring", "preset catalogs", "Faust emission and compiler wrapper" @@ -92,10 +92,10 @@ "Surge XT for modern hybrid synth modulation", "Cardinal/VCV-style patches for modular cable/rate semantics" ], - "rule": "For speech, add schema fields and learned-control lanes only when the parity harness has a loss surface that can exercise them. For reference synths, add DSL abstractions only when rebuilds prove the missing invariant. Parsing alone is inventory, not evidence of expressivity. Pink Trombone should be decomposed into reusable low-level Aqua primitives before audio golf, not copied as a bespoke clone or inflated into PT-shaped modules. After the 2026-05-29 proprioception correction, collapse graph-vocal lowering into a small set of instrumented physical primitives before more utterance golf. Gesture motion belongs in ControlSurface/ControlSpline timeline ownership, not primitive-local smoothing; cut any generated contact/port spline that invents motion not supported by primitive reference timelines. Public clients author gestures through ControlSurfaceCatalog plus ControlSplineTimeline and stream surface control dictionaries; higher-level gesture templates and phoneme_gesture are only spline emitters. IPA descriptors seed which anatomical surfaces move, while local datasets golf numeric spline defaults and patch voice code. Training reports must separate gesture_score, clean_vocal_score, and full_parity_score so full-patch recording-condition helpers cannot impersonate articulation. Frozen IPA gesture rounds own candidate scripts, primitive timelines, gesture metrics, manifests, evidence receipts, metric summaries, candidate clusters, and science briefs; `IpaTrialResult` CultCache records own rendered trial outcomes. External Codex IPA exploration must query the `.cc` store through the worker's semantic `search`/`show` organ, backed by Qdrant/Ollama vector retrieval when available; Qdrant is a derived index over CultCache and local artifacts, not a second truth. Search reports expose class focus/match and use phonetic class-affinity so agents do not treat generic weak-articulation language as cross-class evidence. Crude full-store dumps are audit fallback, not agent memory. Statistical science workers and audio parity stages consume those bundles without rewriting gesture evidence. If gesture scores rise but rendered audio is silent, fix the primitive source/tract owner before adding full-patch dressing." + "rule": "For speech, add schema fields and learned-control lanes only when the parity harness has a loss surface that can exercise them. For reference synths, add DSL abstractions only when rebuilds prove the missing invariant. Parsing alone is inventory, not evidence of expressivity. Pink Trombone should be decomposed into reusable low-level Aqua primitives before audio golf, not copied as a bespoke clone or inflated into PT-shaped modules. After the 2026-05-29 proprioception correction, collapse graph-vocal lowering into a small set of instrumented physical primitives before more utterance golf. Gesture motion belongs in ControlSurface/ControlSpline timeline ownership, not primitive-local smoothing; cut any generated contact/port spline that invents motion not supported by primitive reference timelines. Public clients author gestures through ControlSurfaceCatalog plus ControlSplineTimeline and stream surface control dictionaries; higher-level gesture templates and phoneme_gesture are only spline emitters. IPA descriptors seed which anatomical surfaces move, while local datasets golf numeric spline defaults and patch voice code. Training reports must separate gesture_score, clean_vocal_score, and full_parity_score so full-patch recording-condition helpers cannot impersonate articulation. Frozen IPA gesture rounds own candidate scripts, primitive timelines, gesture metrics, manifests, evidence receipts, metric summaries, candidate clusters, and science briefs; `IpaTrialResult` CultCache records own rendered trial outcomes plus extracted primitive timeline facts for contact release, source flow, branch admittance, radiation output, path energy, and passivity. External Codex IPA exploration must query the `.cc` store through the worker's semantic `search`/`show` organ, backed by Qdrant/Ollama vector retrieval when available; Qdrant is a derived index over CultCache and local artifacts, not a second truth. Search reports expose class focus/match and use phonetic class-affinity plus timeline-fact text so agents do not treat generic weak-articulation language as cross-class evidence. Crude full-store dumps are audit fallback, not agent memory. Statistical science workers and audio parity stages consume those bundles without rewriting gesture evidence. If gesture scores rise but rendered audio is silent, fix the primitive source/tract owner before adding full-patch dressing." }, "ipa_vocal_tract_path": { - "status": "This is now the active project center. Roadmap and research notes align the curriculum around structured utterance metadata -> learned utterance embedding -> learned synth automation -> vocal tract/envelope/LFO/filter/spectral controls -> rendered audio loss. The first Core contracts still defend the boundary: PhoneticIntent is the Weksa/language-owned handoff, ArticulatoryPlan is the AquaSynth realization layer, VocalTractMorphology declares anatomy capabilities, ArticulatoryConstraintReport is the host-visible veto surface, and VocalTractPlanResult is the tiny end-to-end proof artifact for host branching. IPA is an authoring input only: tokenization and feature mapping lower into language phonology profiles, timed articulatory gestures, morphology-specific tract/excitation curves, and Faust DSP. The staged support plan starts with tokenization and vowels, then core pulmonic consonants, expanded human IPA, airstream/phonation, and Weksa/alien morphologies. Optional eSpeak NG tests provide the first generated speech workout when ESPEAK_NG or espeak-ng is installed; eSpeak is intelligibility/timing/log-mel pressure, not anatomy truth. AquaSynth owns the learned utterance embedding encoder and synth driver: speech text embeddings plus prosody/emphasis hints plus Ghostlight/Epiphany-shaped character state vectors compress into utterance embeddings, and phonetic features plus that embedding feed a packed-buffer native C# backprop/Adam mapper that predicts tract, AM/FM, LFO, filter, and mel spectral-envelope controls. SpeechBackpropagationPipeline chains the two models: synth-driver target loss returns gradients with respect to its input, slices the utterance-embedding gradient, and updates the utterance encoder from downstream speech loss. The generated ground-truth training fixture renders eSpeak syllables, extracts log-mel evidence, and trains that chained pipeline toward eSpeak-seeded mel/control targets. CultMesh speech training now has payload manifests, peer/lease admission receipts, worker assignment, result collection, and central checkpoint application; workers do not own model updates. Real remote artifact chunk transfer and process supervision remain CultMesh infrastructure surfaces, not AquaSynth workaround territory. IpaGestureExperiment writes frozen descriptor/variant rounds with candidate scripts, primitive timelines, gesture metrics, manifests, and evidence JSONL, then analyzes them into metric summaries, score-band clusters, and a science brief. IpaTrialOrchestrator now writes local five-seed IPA render trials, accepts externally authored candidate scripts through RunCandidateScriptsAsync, writes WAV/report artifacts, evaluator reports, and `ipa-trial-results.cc` CultCache records. `tools/IpaTrialWorker` is the public worker tool for seed/score/search/show/index over the store; search now auto-upserts Qdrant/Ollama evidence chunks and returns vector-backed reports, while `tools/run-ipa-trial-loop.ps1` dispatches external Codex hypothesis/evaluator workers around five five-phoneme target sets. The latest seed run improved voiced vowels/nasals after primitive `SourcePort` lowering gained an oscillatory carrier; plosive closure/release remains the next weak owner.", + "status": "This is now the active project center. Roadmap and research notes align the curriculum around structured utterance metadata -> learned utterance embedding -> learned synth automation -> vocal tract/envelope/LFO/filter/spectral controls -> rendered audio loss. The first Core contracts still defend the boundary: PhoneticIntent is the Weksa/language-owned handoff, ArticulatoryPlan is the AquaSynth realization layer, VocalTractMorphology declares anatomy capabilities, ArticulatoryConstraintReport is the host-visible veto surface, and VocalTractPlanResult is the tiny end-to-end proof artifact for host branching. IPA is an authoring input only: tokenization and feature mapping lower into language phonology profiles, timed articulatory gestures, morphology-specific tract/excitation curves, and Faust DSP. The staged support plan starts with tokenization and vowels, then core pulmonic consonants, expanded human IPA, airstream/phonation, and Weksa/alien morphologies. Optional eSpeak NG tests provide the first generated speech workout when ESPEAK_NG or espeak-ng is installed; eSpeak is intelligibility/timing/log-mel pressure, not anatomy truth. AquaSynth owns the learned utterance embedding encoder and synth driver: speech text embeddings plus prosody/emphasis hints plus Ghostlight/Epiphany-shaped character state vectors compress into utterance embeddings, and phonetic features plus that embedding feed a packed-buffer native C# backprop/Adam mapper that predicts tract, AM/FM, LFO, filter, and mel spectral-envelope controls. SpeechBackpropagationPipeline chains the two models: synth-driver target loss returns gradients with respect to its input, slices the utterance-embedding gradient, and updates the utterance encoder from downstream speech loss. The generated ground-truth training fixture renders eSpeak syllables, extracts log-mel evidence, and trains that chained pipeline toward eSpeak-seeded mel/control targets. CultMesh speech training now has payload manifests, peer/lease admission receipts, worker assignment, result collection, and central checkpoint application; workers do not own model updates. Real remote artifact chunk transfer and process supervision remain CultMesh infrastructure surfaces, not AquaSynth workaround territory. IpaGestureExperiment writes frozen descriptor/variant rounds with candidate scripts, primitive timelines, gesture metrics, manifests, and evidence JSONL, then analyzes them into metric summaries, score-band clusters, and a science brief. IpaTrialOrchestrator now writes local five-seed IPA render trials, accepts externally authored candidate scripts through RunCandidateScriptsAsync, writes WAV/report artifacts, evaluator reports, extracted primitive timeline facts, and `ipa-trial-results.cc` CultCache records. `tools/IpaTrialWorker` is the public worker tool for seed/score/search/show/index over the store; search now auto-upserts Qdrant/Ollama evidence chunks including timeline-fact text and returns vector-backed reports, while `tools/run-ipa-trial-loop.ps1` dispatches external Codex hypothesis/evaluator workers around five five-phoneme target sets. The latest seed run improved voiced vowels/nasals after primitive `SourcePort` lowering gained an oscillatory carrier; plosive closure/release remains the next weak owner.", "lesson": "Do not build a complete IPA parser before the tract can say basic syllables. The durable machine is tokenizer -> feature mapper -> phonology profile -> gesture planner -> morphology -> tract plan -> Faust renderer, with dependency-injected seams and mocks at each boundary. Weksa should use IPA-like phonetic input without letting human IPA semantics own alien anatomy. Anatomy has veto power: if a morphology lacks bilabial closure or another required capability, AquaSynth reports that failure instead of smearing over it.", "next": "Start the next speech gradient descent pass from Weksa's emitted v0.1 seed batch at `E:\\Projects\\weksa\\examples\\speech-training\\tiny-panphon-v0.1\\batch.json`: 1024 bge-m3 semantic speech-text floats, variable-length PanPhon sequence evidence, 256 AquaSynth-owned phonetic-realization embedding floats from `aquasynth.panphon_sequence_encoder.v0.1`, 32 prosody/emphasis floats, 64 projected character-state floats, and 64 AquaSynth utterance-embedding output floats. Keep semantic text embeddings and phonetic realization separate. Next tighten the parity harness: load that batch into AquaSynth training records, render tiny utterance fixtures, sweep controller outputs through compiled Faust controls, score log-mel/timing/envelope loss, record per-stage decision time/latency/budget/confidence receipts, backpropagate into the utterance encoder plus synth driver, and connect Stage 0/1 IPA token spans, vowel feature bundles, and human baseline tract-area targets to that loop. For distributed work, use RunCultMeshTrainingStep as the boring authority path: admitted leased workers return gradients/results, then the trainer applies centrally. Do not attempt federated averaging until this path is exercised against real Faust-rendered speech jobs." }, @@ -121,9 +121,9 @@ "binding_rule": "Parameter binding uses field-site references such as `lpf=@/macro/brightness` and `/opgraphs/0/operators/2/level`; the field owns meaning and the parameter owns the runtime value. Avoid target-routing semantics unless a new graph authority is deliberately mapped." }, "last_verified": { - "command": "dotnet build tools\\IpaTrialWorker\\IpaTrialWorker.csproj --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false -v minimal; retrieval smokes under artifacts\\parity\\ipa-trial-worker-smoke\\20260529T2204\\retrieval-tuning-20260530-pass{1..4}; powershell -NoProfile -ExecutionPolicy Bypass -File tools\\run-ipa-trial-loop.ps1 -Rounds 0 -SkipLocalTrialRun", - "result": "IPA trial retrieval now indexes CultCache trial records and local artifact snippets into Docker Qdrant using local Ollama qwen3-embedding:0.6b vectors. Build passes. Retrieval tuning smokes improved class-specific evidence: stop queries surface stop lanes plus mix-p before off-class records, vowel queries surface vowel lanes plus mix-a/u, nasal queries surface nasal lanes first, and dressing tags no longer appear from accidental artifact tokens. The trial loop skeleton still passes after adding index boundaries and skip-index agent search commands.", - "commit": "pending ipa qdrant retrieval" + "command": "dotnet build tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false -v minimal; dotnet build tools\\IpaTrialWorker\\IpaTrialWorker.csproj --no-restore --disable-build-servers -p:UseSharedCompilation=false -p:BuildInParallel=false -v minimal; dotnet test tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-build --filter \"FullyQualifiedName~IpaTrialOrchestratorTests\" -v minimal; IpaTrialWorker one-candidate timeline fact score/show/search smoke", + "result": "IPA trial records now embed extracted primitive timeline facts. The worker indexes, searches, shows, dumps, and summarizes contact release, radiation output, and path passivity witnesses from the .cc record itself. Focused build/test pass, old-store show compatibility passes, and fresh smoke under artifacts\\parity\\ipa-timeline-fact-smoke\\20260531T233217641 stores release/radiation/passivity facts and retrieves them through Qdrant/Ollama search.", + "commit": "pending ipa primitive timeline facts" } } diff --git a/state/scratch.md b/state/scratch.md index cc097f6..7cd32cf 100644 --- a/state/scratch.md +++ b/state/scratch.md @@ -2,6 +2,22 @@ Current slice: +- Added primitive timeline facts to IPA trial results as first-class CultCache + evidence. `IpaTrialResult` now stores extracted `PrimitiveTimelineFact[]` + values from `ProbeTimelineReport`: contact opening/closed blocks/reservoir + peak/release peak/release block, source load pressure and flow peaks, branch + admittance/exchanged flow, radiation boundary/output peaks, path area/delay, + energy, and passivity ratios. `IpaTrialWorker search/show/dump` indexes and + reports those facts alongside metrics and artifact snippets, and trial + summary CSVs now include release/radiation/passivity columns. Verified with + a one-candidate smoke under + `artifacts/parity/ipa-timeline-fact-smoke/20260531T233217641`: stored facts + include `contact_closed_blocks=12`, `contact_release_peak=0.020328`, + `contact_release_peak_block=11`, `radiation_output_peak=0.008015`, and + `path_passivity_max=1`. Qdrant/Ollama search for stop release/radiation + evidence returned through the vector organ with `release-facts` and + `passivity-facts` tags. + - Upgraded the IPA trial retrieval organ from lexical metric search to a Qdrant/Ollama vector index modeled after VoidBot. `IpaTrialWorker search` now builds evidence chunks from each `IpaTrialResult`, includes metric text, diff --git a/state/spine.yaml b/state/spine.yaml index 14f9c6a..ed9b1a7 100644 --- a/state/spine.yaml +++ b/state/spine.yaml @@ -30,6 +30,7 @@ current_mechanism: zyn_reference_source: external/zynaddsubfx pink_trombone_reference_source: external/pink-trombone-mod primitive_vocal_timeline: src/AquaSynth.Core/ProbeTimelineReport.cs + primitive_timeline_fact_extractor: src/AquaSynth.Core/PrimitiveTimelineFactExtractor.cs primitive_vocal_reference_report: src/AquaSynth.Core/PrimitiveReferenceReport.cs zyn_upstream_instrument_bank: external/zynaddsubfx/instruments zyn_reference_build_script: tools/zyn-reference/build-zyn-reference.ps1 @@ -145,7 +146,7 @@ live_invariants: - IPA training scoring contract is documented. Reports must keep `gesture_score`, `clean_vocal_score`, and `full_parity_score` separate. Gesture score measures descriptor-to-surface coverage, motion direction, spline contour/timing, primitive timeline consequences, and optional external articulation landmarks. Full parity patches may use FM/AM/modulators/envelopes/filters, auxiliary animated voices, and recording-condition helpers, but those helpers cannot count as articulation evidence unless gesture and clean-vocal scores move coherently. - `IpaGestureExperiment.WriteRound` is the first frozen batch harness for IPA hypothesis work. It writes `manifest.yaml`, candidate `.aqua` scripts, primitive `ProbeTimelineReport` CSVs, gesture-layer `metrics.csv`, and per-candidate `evidence.jsonl` under a caller-provided artifact root. `IpaGestureExperiment.AnalyzeRound` writes `analysis/metric-summary.csv`, `analysis/candidate-clusters.csv`, and `analysis/science-brief.md` for the science-worker handoff. This harness owns frozen descriptor/spline/primitive evidence only; clean vocal identity, full spectrogram parity, optimizer checkpoints, and worker orchestration stay downstream. Focused verification: `PatchScriptTests` pass 93/93. - `IpaTrialOrchestrator.RunAsync` is the first local render/scoring worker for IPA trials. It generates candidate patches through `IpaGestureExperiment`, renders them through Faust, compares against local PT fixture references with `AudioAnalyzer`, writes WAV/report artifacts, and stores typed `IpaTrialResult` records in `ipa-trial-results.cc`. The five-seed trial run at `tests/AquaSynth.Dsl.Tests/bin/Debug/net10.0/artifacts/parity/ipa-trials/20260529T214127955/five-seed-trials` showed voiced vowels/nasals became audible only after primitive `SourcePort` lowering gained a true oscillatory voiced carrier; sibilants are pressure, plosives remain weak. Verification: opt-in IPA trial test passed, and focused `PatchScriptTests|IpaTrialResultsRoundTripThroughCultCache` pass 94/94. - - External IPA exploration now has a worker/tool boundary. `tools/IpaTrialWorker` owns `seed`, `score`, `search`, `show`, and audit-only `dump` commands over the typed `.cc` trial-result store; `score` renders external Codex-authored `.aqua` candidates through `IpaTrialOrchestrator.RunCandidateScriptsAsync`, while `search` is the semantic retrieval organ for hypothesis agents. `search` now follows VoidBot's Qdrant shape: `.cc` records plus referenced local artifacts become evidence chunks, local Ollama `qwen3-embedding:0.6b` produces 1024-dim vectors, Qdrant collection `aquasynth_ipa_trial_results` stores stable SHA-256 UUID points with trial/candidate payload metadata, and the report surfaces vector scores, excerpts, tags, metric facts, contrast candidates, `class_focus`, per-hit `class_match`, and `show` commands. Qdrant is a derived index; CultCache remains truth, and lexical/metric search is fallback when the vector stack is unavailable. Retrieval tuning added phonetic class-affinity scoring and candidate-specific mixed-lane ownership so `mix-p` can support stop seams without every mixed candidate matching stop evidence. `tools/run-ipa-trial-loop.ps1` orchestrates external `codex exec` hypothesis/evaluator workers around that store. The active trial shape is five target sets of five phoneme lanes each: vowels, nasals/approximants, fricatives, stops, and mixed generalization. Candidate names are exactly `____.aqua`, with lowercase kebab-case family ids. Three dry external Codex smoke passes refined hypothesizer/evaluator prompts into exact 25-lane output, retrieval receipts, claim audits, evidence-quality ledgers/verdicts, and fail-closed evaluator compliance. A later five-pass prompt optimization showed the best science comes from fixed receipt packets plus constrained reasoning; production prompts now require contrast pairs, `show` metric values, primitive timeline excerpts or explicit `missing`, class-consistent reference matrices, `X owns Y so Z remains true` owner sentences, at least five micro-sweep perturbations before a seam is actionable, and a novelty gate rejecting loudness/dressing-only families as loss-landscape novelty. Verification: `IpaTrialWorker` builds, Qdrant/Ollama index/search smoke reads the seed store, and the loop skeleton runs with `-Rounds 0 -SkipLocalTrialRun`. + - External IPA exploration now has a worker/tool boundary. `tools/IpaTrialWorker` owns `seed`, `score`, `search`, `show`, and audit-only `dump` commands over the typed `.cc` trial-result store; `score` renders external Codex-authored `.aqua` candidates through `IpaTrialOrchestrator.RunCandidateScriptsAsync`, while `search` is the semantic retrieval organ for hypothesis agents. `IpaTrialResult` records now embed extracted primitive timeline facts from `ProbeTimelineReport` for contact closure/reservoir/release, source load/flow, branch admittance/exchanged flow, radiation boundary/output, path area/delay/energy, and passivity; `search`, `show`, `dump`, and summary CSV output surface release/radiation/passivity witnesses directly from the `.cc` record. `search` follows VoidBot's Qdrant shape: `.cc` records plus timeline facts and referenced local artifacts become evidence chunks, local Ollama `qwen3-embedding:0.6b` produces 1024-dim vectors, Qdrant collection `aquasynth_ipa_trial_results` stores stable SHA-256 UUID points with trial/candidate payload metadata, and the report surfaces vector scores, excerpts, tags, metric facts, timeline facts, contrast candidates, `class_focus`, per-hit `class_match`, and `show` commands. Qdrant is a derived index; CultCache remains truth, and lexical/metric search is fallback when the vector stack is unavailable. Retrieval tuning added phonetic class-affinity scoring and candidate-specific mixed-lane ownership so `mix-p` can support stop seams without every mixed candidate matching stop evidence. `tools/run-ipa-trial-loop.ps1` orchestrates external `codex exec` hypothesis/evaluator workers around that store. The active trial shape is five target sets of five phoneme lanes each: vowels, nasals/approximants, fricatives, stops, and mixed generalization. Candidate names are exactly `____.aqua`, with lowercase kebab-case family ids. Three dry external Codex smoke passes refined hypothesizer/evaluator prompts into exact 25-lane output, retrieval receipts, claim audits, evidence-quality ledgers/verdicts, and fail-closed evaluator compliance. A later five-pass prompt optimization showed the best science comes from fixed receipt packets plus constrained reasoning; production prompts now require contrast pairs, `show` metric values, primitive timeline facts/excerpts or explicit `missing`, class-consistent reference matrices, `X owns Y so Z remains true` owner sentences, at least five micro-sweep perturbations before a seam is actionable, and a novelty gate rejecting loudness/dressing-only families as loss-landscape novelty. Verification: focused builds/tests pass, old-store show compatibility passes, and a fresh one-candidate smoke under `artifacts/parity/ipa-timeline-fact-smoke/20260531T233217641` proves release/radiation/passivity facts persist and retrieve through Qdrant/Ollama. - TurbulenceJet now includes a release-timed burst-noise component in addition to the deterministic pressure pulse, but a stronger trial barely moved papa; the source is not the main remaining articulation bottleneck. Graph radiation now mixes 30% raw boundary flow with 70% high-passed flow after local admittance weighting, down from the over-bright 15/85 blend. Latest radiation/source artifact under `artifacts/parity/pink-trombone-utterance-logmel/20260527T174236049`: mama/papa/thrombosis cosine 0.6578/0.7692/0.5585, rmsRatio 3.0006/2.6153/2.3488, articulation 0.1437/0.1173/0.1191. Static `closure-release` under `artifacts/parity/pink-trombone-logmel/20260527T174139661` has rmsRatio 0.9900 and speechBandRatio 0.0842. This is still not speaking; next pressure is true tract resonance/passive scattering and pressure storage, not more burst gain. - Same-path two-port area discontinuities now emit `graph_area_energy_in_*` and `graph_area_energy_out_*` probes alongside the three-port branch `graph_connection_energy_*` probes. Use these to inspect passivity and formant-body failures at the scatter sites rather than judging only final output loudness. - `FaustExportOptions.DebugProbeUi` wraps graph probe signals in Faust `vbargraph` UI controls under `/debug/...` without changing audio output arity. Native streaming Faust patches now separate writable controls from read-only probe zones and can read/snapshot probe values after `ProcessBlock`; use this for acoustic graph passivity/radiation inspection, leave it off for normal parity renders. diff --git a/tests/AquaSynth.Dsl.Tests/IpaTrialOrchestratorTests.cs b/tests/AquaSynth.Dsl.Tests/IpaTrialOrchestratorTests.cs index 2af68e6..718e57e 100644 --- a/tests/AquaSynth.Dsl.Tests/IpaTrialOrchestratorTests.cs +++ b/tests/AquaSynth.Dsl.Tests/IpaTrialOrchestratorTests.cs @@ -32,7 +32,8 @@ public sealed class IpaTrialOrchestratorTests "Weak but measurable spectrogram pressure.", "pressure", ["fixture reference is not an IPA sample"], - [new SpeechTimingReceipt("ipa-trial-render-score", "2026-05-29T12:00:00.0000000Z", 3, 20, .5f, "test")]); + [new SpeechTimingReceipt("ipa-trial-render-score", "2026-05-29T12:00:00.0000000Z", 3, 20, .5f, "test")], + [new PrimitiveTimelineFact("contact_release_peak", "contact:contact", "released_flow", .12f, "flow", 2, 2, "release witness")]); await IpaTrialResultCultCacheStore.UpsertResultsAsync(store, [result]); var loaded = await IpaTrialResultCultCacheStore.ReadResultsAsync(store); @@ -42,6 +43,7 @@ public sealed class IpaTrialOrchestratorTests Assert.Equal("candidate-a", single.CandidateId); Assert.Contains(single.Metrics, metric => metric.Name == "log_mel_cosine"); Assert.Contains(single.KnownLies, lie => lie.Contains("not an IPA sample", StringComparison.Ordinal)); + Assert.Contains(single.TimelineFacts, fact => fact.Name == "contact_release_peak" && fact.Value > 0); } finally { @@ -49,6 +51,33 @@ public sealed class IpaTrialOrchestratorTests } } + [Fact] + public void PrimitiveTimelineFactsExtractPhaseWitnesses() + { + var timeline = new[] + { + new ProbeTimelineSample(0, "contact:contact", "opening", .1f), + new ProbeTimelineSample(1, "contact:contact", "opening", .8f), + new ProbeTimelineSample(0, "contact:contact", "reservoir", .4f), + new ProbeTimelineSample(1, "contact:contact", "released_flow", .3f), + new ProbeTimelineSample(0, "source:folds", "flow", .2f), + new ProbeTimelineSample(1, "branch:velopharynx", "admittance", .15f), + new ProbeTimelineSample(1, "radiation:mouth", "output", .7f), + new ProbeTimelineSample(0, "path:oral_path", "passivity_ratio", .99f), + new ProbeTimelineSample(1, "path:oral_path", "energy_in", .5f), + new ProbeTimelineSample(1, "path:oral_path", "energy_out", .45f), + }; + + var facts = PrimitiveTimelineFactExtractor.Extract(timeline); + + Assert.Contains(facts, fact => fact.Name == "contact_closed_blocks" && fact.Value == 1); + Assert.Contains(facts, fact => fact.Name == "contact_release_peak" && Math.Abs(fact.Value - .3f) < .0001f); + Assert.Contains(facts, fact => fact.Name == "contact_release_peak_block" && fact.Value == 1); + Assert.Contains(facts, fact => fact.Name == "branch_admittance_peak" && Math.Abs(fact.Value - .15f) < .0001f); + Assert.Contains(facts, fact => fact.Name == "radiation_output_peak" && Math.Abs(fact.Value - .7f) < .0001f); + Assert.Contains(facts, fact => fact.Name == "path_passivity_max" && Math.Abs(fact.Value - .99f) < .0001f); + } + [Fact] public async Task IpaTrialOrchestratorWritesFiveSeedTrialsWhenEnabled() { diff --git a/tools/IpaTrialWorker/Program.cs b/tools/IpaTrialWorker/Program.cs index 9909da9..4afd23d 100644 --- a/tools/IpaTrialWorker/Program.cs +++ b/tools/IpaTrialWorker/Program.cs @@ -769,6 +769,7 @@ static Dictionary DocumentFields(IpaTrialResult result, s ["evaluation"] = new(2.5f, Tokenize(result.EvaluationSummary)), ["known_lies"] = new(2.0f, Tokenize(string.Join(' ', result.KnownLies))), ["metrics"] = new(2.0f, Tokenize(MetricText(result))), + ["timeline_facts"] = new(2.5f, Tokenize(TimelineFactText(result))), ["artifacts"] = new(1.75f, Tokenize(ArtifactText(result, store))), ["timing"] = new(1.25f, Tokenize(TimingText(result))) }; @@ -799,6 +800,40 @@ static string MetricText(IpaTrialResult result) return builder.ToString(); } +static string TimelineFactText(IpaTrialResult result) +{ + var builder = new StringBuilder(); + foreach (var fact in result.TimelineFacts ?? []) + { + builder.Append(fact.Name).Append(' '); + builder.Append(fact.Primitive).Append(' '); + builder.Append(fact.Signal).Append(' '); + builder.Append(fact.Value.ToString("0.######", CultureInfo.InvariantCulture)).Append(' '); + builder.Append(fact.Unit).Append(' '); + builder.Append("blocks ").Append(fact.BlockStart).Append(' ').Append(fact.BlockEnd).Append(' '); + builder.Append(fact.Summary).Append(' '); + if (fact.Name.Contains("release", StringComparison.OrdinalIgnoreCase)) + { + builder.Append("closure release burst plosive stop "); + } + if (fact.Name.Contains("branch", StringComparison.OrdinalIgnoreCase)) + { + builder.Append("nasal velum admittance branch "); + } + if (fact.Name.Contains("radiation", StringComparison.OrdinalIgnoreCase)) + { + builder.Append("radiation lip output flow "); + } + if (fact.Name.Contains("passivity", StringComparison.OrdinalIgnoreCase) || + fact.Name.Contains("energy", StringComparison.OrdinalIgnoreCase)) + { + builder.Append("passivity energy check "); + } + } + + return builder.ToString(); +} + static string TimingText(IpaTrialResult result) => string.Join(' ', result.TimingReceipts.Select(receipt => $"{receipt.StageId} {receipt.Notes} latency {receipt.LatencyMilliseconds:0.###} budget {receipt.BudgetMilliseconds:0.###} confidence {receipt.Confidence:0.###}")); @@ -896,6 +931,12 @@ static IEnumerable BuildEvidenceChunks(IReadOnlyList fact.Name, StringComparer.OrdinalIgnoreCase)) + { + text.AppendLine($"{fact.Name} {fact.Primitive} {fact.Signal} {fact.Value.ToString("0.######", CultureInfo.InvariantCulture)} {fact.Unit} blocks {fact.BlockStart}-{fact.BlockEnd} {fact.Summary}"); + } + text.AppendLine("hypothesis"); text.AppendLine(result.Hypothesis); text.AppendLine("evaluation"); @@ -937,6 +978,14 @@ static string[] EvidenceTags(IpaTrialResult result, SearchDocument document) { tags.Add("timeline"); } + if ((result.TimelineFacts ?? []).Any(fact => fact.Name.Contains("release", StringComparison.OrdinalIgnoreCase))) + { + tags.Add("release-facts"); + } + if ((result.TimelineFacts ?? []).Any(fact => fact.Name.Contains("passivity", StringComparison.OrdinalIgnoreCase))) + { + tags.Add("passivity-facts"); + } if (MentionsDressing(result)) { tags.Add("dressing"); @@ -1001,6 +1050,16 @@ static string SearchReport( builder.Append(Metric(result, "articulation_score")); builder.Append(", rmsRatio="); builder.AppendLine(Metric(result, "rms_ratio")); + builder.Append(" timeline: releasePeak="); + builder.Append(TimelineFact(result, "contact_release_peak")); + builder.Append(", releaseBlock="); + builder.Append(TimelineFact(result, "contact_release_peak_block")); + builder.Append(", branchAdmittance="); + builder.Append(TimelineFact(result, "branch_admittance_peak")); + builder.Append(", radiationOutput="); + builder.Append(TimelineFact(result, "radiation_output_peak")); + builder.Append(", passivityMax="); + builder.AppendLine(TimelineFact(result, "path_passivity_max")); builder.Append(" hypothesis: "); builder.AppendLine(result.Hypothesis); builder.Append(" evaluation: "); @@ -1068,6 +1127,33 @@ static string DetailReport(string store, IpaTrialResult result) builder.AppendLine(metric.Weight.ToString("0.######", CultureInfo.InvariantCulture)); } + builder.AppendLine(); + builder.AppendLine("## Primitive Timeline Facts"); + foreach (var fact in (result.TimelineFacts ?? []).OrderBy(fact => fact.Name, StringComparer.OrdinalIgnoreCase)) + { + builder.Append("- "); + builder.Append(fact.Name); + builder.Append(": "); + builder.Append(fact.Value.ToString("0.######", CultureInfo.InvariantCulture)); + builder.Append(' '); + builder.Append(fact.Unit); + builder.Append(" / "); + builder.Append(fact.Primitive); + builder.Append(' '); + builder.Append(fact.Signal); + builder.Append(" / blocks "); + builder.Append(fact.BlockStart); + builder.Append('-'); + builder.Append(fact.BlockEnd); + if (!string.IsNullOrWhiteSpace(fact.Summary)) + { + builder.Append(" / "); + builder.Append(fact.Summary); + } + + builder.AppendLine(); + } + builder.AppendLine(); builder.AppendLine("## Artifacts"); foreach (var artifact in result.Artifacts) @@ -1124,6 +1210,12 @@ static string StoreReport(string store, IReadOnlyList results) builder.Append(Metric(result, "articulation_score")); builder.Append(", rmsRatio="); builder.Append(Metric(result, "rms_ratio")); + builder.Append(", releasePeak="); + builder.Append(TimelineFact(result, "contact_release_peak")); + builder.Append(", radiationOutput="); + builder.Append(TimelineFact(result, "radiation_output_peak")); + builder.Append(", passivityMax="); + builder.Append(TimelineFact(result, "path_passivity_max")); builder.AppendLine(); builder.Append(" patch: "); builder.AppendLine(result.CandidatePatchUri); @@ -1146,6 +1238,14 @@ static string Metric(IpaTrialResult result, string name) => (result.Metrics.FirstOrDefault(metric => metric.Name == name)?.Value ?? 0) .ToString("0.######", CultureInfo.InvariantCulture); +static string TimelineFact(IpaTrialResult result, string name) => + (result.TimelineFacts? + .Where(fact => fact.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + .Select(fact => fact.Value) + .DefaultIfEmpty(0) + .Max() ?? 0) + .ToString("0.######", CultureInfo.InvariantCulture); + static IpaTrialResult? ContrastCandidate(IpaTrialResult result, IReadOnlyList allResults) { var sameTarget = allResults