From 117897b83af40ea95e702e56ef81a42e9ec1721c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 19 May 2026 15:33:51 +0100 Subject: [PATCH] Add vocal tract plan result proof --- docs/audio-doctrine.md | 2 + docs/ipa-vocal-tract-roadmap.md | 7 + src/AquaSynth.Core/VocalTractModel.cs | 142 ++++++++++++++++++ state/evidence.jsonl | 1 + state/memory.json | 4 +- .../VocalTractModelTests.cs | 92 ++++++++++++ 6 files changed, 246 insertions(+), 2 deletions(-) diff --git a/docs/audio-doctrine.md b/docs/audio-doctrine.md index 0180d64..ab0d10f 100644 --- a/docs/audio-doctrine.md +++ b/docs/audio-doctrine.md @@ -150,6 +150,8 @@ The boundary must stay split into three inspectable records: - `ArticulatoryPlan`: AquaSynth-owned gestures and tract/source targets. - `ArticulatoryConstraintReport`: host-visible diagnostics when morphology cannot produce the requested articulation. +- `VocalTractPlanResult`: the host reaction surface that either carries an + accepted plan or a rejection with diagnostic codes. Anatomy has veto power. If a beaked morphology cannot form a bilabial closure, the planner must report the missing capability instead of silently substituting diff --git a/docs/ipa-vocal-tract-roadmap.md b/docs/ipa-vocal-tract-roadmap.md index a9f26b9..ea34f9f 100644 --- a/docs/ipa-vocal-tract-roadmap.md +++ b/docs/ipa-vocal-tract-roadmap.md @@ -32,6 +32,13 @@ Three handoffs must stay visible in code, docs, tests, and host reports: morphology cannot form a requested articulation, the report names the phone, source event, missing capability, and severity. +The first proof artifact is `VocalTractPlanResult`: it packages the intent, +morphology, constraint report, optional articulatory plan, and +`VocalTractHostReaction`. A host can reject `/pa/` for a beaked morphology by +reading `missing_bilabial_capability`, or it can accept `/a/` for a human +baseline and inspect the emitted glottal-source and vowel-area gestures. This is +not audio yet. It is the first visible branch where the machine refuses to lie. + Those records are the first defense against liar-slurry: no parser, planner, renderer, or host should need to infer whether IPA intent, physical anatomy, or DSP output owns a decision. diff --git a/src/AquaSynth.Core/VocalTractModel.cs b/src/AquaSynth.Core/VocalTractModel.cs index 1ec34f8..72f1496 100644 --- a/src/AquaSynth.Core/VocalTractModel.cs +++ b/src/AquaSynth.Core/VocalTractModel.cs @@ -186,6 +186,148 @@ public sealed record ArticulatoryPlan( IReadOnlyList Gestures, ArticulatoryConstraintReport Report); +public enum VocalTractPlanStatus +{ + Accepted, + Rejected +} + +public enum VocalTractHostReactionKind +{ + RenderPlan, + RejectIntent +} + +public sealed record VocalTractHostReaction( + VocalTractHostReactionKind Kind, + string Summary, + IReadOnlyList DiagnosticCodes); + +public sealed record VocalTractPlanResult( + VocalTractPlanStatus Status, + PhoneticIntent Intent, + VocalTractMorphology Morphology, + ArticulatoryConstraintReport Report, + VocalTractHostReaction HostReaction, + ArticulatoryPlan? Plan = null) +{ + public bool Accepted => Status == VocalTractPlanStatus.Accepted; +} + +public static class VocalTractPlanner +{ + public static VocalTractPlanResult Plan(PhoneticIntent intent, VocalTractMorphology morphology) + { + var report = VocalTractConstraintEvaluator.Evaluate(intent, morphology); + if (!report.Accepted) + { + var codes = report.Diagnostics.Select(diagnostic => diagnostic.Code).Distinct(StringComparer.Ordinal).ToArray(); + var firstError = report.Diagnostics.First(diagnostic => diagnostic.Severity == ArticulatoryDiagnosticSeverity.Error); + var reaction = new VocalTractHostReaction( + VocalTractHostReactionKind.RejectIntent, + $"Rejected `{intent.Id}` for morphology `{morphology.Id}`: {firstError.Code} on `{firstError.Ipa}`.", + codes); + return new VocalTractPlanResult(VocalTractPlanStatus.Rejected, intent, morphology, report, reaction); + } + + var gestures = BuildGestures(intent); + var plan = new ArticulatoryPlan($"{intent.Id}:{morphology.Id}:plan", intent, morphology, gestures, report); + var acceptedReaction = new VocalTractHostReaction( + VocalTractHostReactionKind.RenderPlan, + $"Accepted `{intent.Id}` for morphology `{morphology.Id}` with {gestures.Count} articulatory gesture(s).", + []); + return new VocalTractPlanResult(VocalTractPlanStatus.Accepted, intent, morphology, report, acceptedReaction, plan); + } + + private static IReadOnlyList BuildGestures(PhoneticIntent intent) + { + var gestures = new List(); + foreach (var phoneticEvent in intent.Events) + { + if (phoneticEvent.Features.Phonation != Phonation.Voiceless) + { + gestures.Add(new ArticulatoryGesture( + $"{phoneticEvent.Id}:glottal", + phoneticEvent.Id, + ArticulatoryGestureKind.GlottalSource, + phoneticEvent.StartSeconds, + phoneticEvent.DurationSeconds, + "glottis", + phoneticEvent.Prosody.Intensity)); + } + + switch (phoneticEvent.Features.Manner) + { + case PhoneticManner.Vowel: + gestures.Add(new ArticulatoryGesture( + $"{phoneticEvent.Id}:vowel-area", + phoneticEvent.Id, + ArticulatoryGestureKind.TractAreaTarget, + phoneticEvent.StartSeconds, + phoneticEvent.DurationSeconds, + VowelTarget(phoneticEvent.Features), + 1)); + break; + case PhoneticManner.Stop: + gestures.Add(new ArticulatoryGesture( + $"{phoneticEvent.Id}:closure", + phoneticEvent.Id, + ArticulatoryGestureKind.Closure, + phoneticEvent.StartSeconds, + phoneticEvent.DurationSeconds * 0.7, + phoneticEvent.Features.Place.ToString(), + 1)); + gestures.Add(new ArticulatoryGesture( + $"{phoneticEvent.Id}:release", + phoneticEvent.Id, + ArticulatoryGestureKind.Release, + phoneticEvent.StartSeconds + phoneticEvent.DurationSeconds * 0.7, + phoneticEvent.DurationSeconds * 0.3, + phoneticEvent.Features.Place.ToString(), + 1)); + break; + case PhoneticManner.Fricative: + case PhoneticManner.LateralFricative: + gestures.Add(new ArticulatoryGesture( + $"{phoneticEvent.Id}:constriction", + phoneticEvent.Id, + ArticulatoryGestureKind.Constriction, + phoneticEvent.StartSeconds, + phoneticEvent.DurationSeconds, + phoneticEvent.Features.Place.ToString(), + 0.85f)); + gestures.Add(new ArticulatoryGesture( + $"{phoneticEvent.Id}:turbulence", + phoneticEvent.Id, + ArticulatoryGestureKind.Turbulence, + phoneticEvent.StartSeconds, + phoneticEvent.DurationSeconds, + phoneticEvent.Features.Place.ToString(), + phoneticEvent.Prosody.Intensity)); + break; + case PhoneticManner.Nasal: + gestures.Add(new ArticulatoryGesture( + $"{phoneticEvent.Id}:nasal", + phoneticEvent.Id, + ArticulatoryGestureKind.NasalCoupling, + phoneticEvent.StartSeconds, + phoneticEvent.DurationSeconds, + "velum", + 1)); + break; + } + } + + return gestures; + } + + private static string VowelTarget(PhoneticFeatures features) + { + var rounded = features.Rounded ? "rounded" : "unrounded"; + return $"vowel:{features.Height.ToString().ToLowerInvariant()}:{features.Backness.ToString().ToLowerInvariant()}:{rounded}"; + } +} + public static class VocalTractConstraintEvaluator { public static ArticulatoryConstraintReport Evaluate(PhoneticIntent intent, VocalTractMorphology morphology) diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 8ebd379..f2de18b 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -106,3 +106,4 @@ {"date":"2026-05-19","evidence":"Added docs/ipa-vocal-tract-roadmap.md with staged IPA support from tokenization/vowels through pulmonic consonants, expanded IPA, airstream/phonation, and Weksa alien morphologies; linked it from docs/reference-synth-roadmap.md and state.","lesson":"AquaSynth should treat IPA as an authoring input for phonetic intent, not as the internal synth model. The planned machine is tokenizer -\u003e feature mapper -\u003e phonology profile -\u003e gesture planner -\u003e morphology -\u003e tract plan -\u003e Faust renderer, growing supported IPA only as tract/excitation physics earn each category.","topic":"ipa-vocal-tract-roadmap"} {"date":"2026-05-19","topic":"vocal-tract-boundary-contracts","lesson":"Defend the Weksa/AquaSynth tract boundary with three real bones: PhoneticIntent for language-owned phones/features/timing, ArticulatoryPlan for AquaSynth-owned gesture realization, and ArticulatoryConstraintReport for host-visible anatomy vetoes. Morphology capabilities must reject impossible phones such as bilabials for a beaked speaker instead of silently substituting liar-slurry.","evidence":"Added VocalTractModel.cs contracts and VocalTractModelTests.cs coverage for inspectable Weksa intent, bilabial veto on a beaked morphology, and acceptance on a human baseline; updated audio doctrine and IPA roadmap boundary section."} +{"date":"2026-05-19","evidence":"Added VocalTractPlanner.Plan, VocalTractPlanResult, VocalTractHostReaction, rejection coverage for /pa/ against a beaked morphology, and accepted /a/ coverage for the human baseline gesture path.","lesson":"The first vocal-tract proof artifact is VocalTractPlanResult: host-visible branching around one intent and one morphology. Rejected morphology constraints carry diagnostic codes and no plan; accepted vowel intents emit inspectable glottal-source and tract-area gestures before any renderer exists.","topic":"vocal-tract-plan-result-proof"} diff --git a/state/memory.json b/state/memory.json index ecc5986..08dbfb3 100644 --- a/state/memory.json +++ b/state/memory.json @@ -86,9 +86,9 @@ "rule": "Add DSL abstractions only when reference rebuilds prove the missing invariant. Do not consume references that cannot be backed by parity tests; parsing alone is inventory, not evidence of expressivity." }, "ipa_vocal_tract_path": { - "status": "Roadmap drafted for an IPA-to-vocal-tract lane, and the first Core contracts now defend the boundary: PhoneticIntent is the Weksa/language-owned handoff, ArticulatoryPlan is the AquaSynth realization layer, VocalTractMorphology declares anatomy capabilities, and ArticulatoryConstraintReport is the host-visible veto surface. 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.", + "status": "Roadmap drafted for an IPA-to-vocal-tract lane, and the first Core contracts now 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.", "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": "Implement Stage 0/1 fixtures: IPA token spans, vowel feature bundles, human baseline vowel tract-area targets, and a tiny render proof before expanding consonants." + "next": "Implement Stage 0/1 fixtures: IPA token spans, vowel feature bundles, human baseline vowel tract-area targets, and a tiny audible vowel render proof before expanding consonants." }, "dx7_reference_path": { "status": "DX7 SysEx parser extracts voice edit buffers, packed voices, packed 32-voice banks, six operators, envelopes, algorithm, feedback, oscillator sync, LFO, transpose, voice name, checksum validation, ReferencePatch features, algorithm topology from the reverse-engineered OPS algorithm ROM table, envelope-to-ADSR approximations, measured carrier output amplitude, isolated two-op modulation route scale, summed/cascaded route scale, calibrated feedback amount table, ratio detune lowering, voice transpose lowering, fixed-frequency operator lowering, ROM COM output compensation cut, operator key/rate scaling at the effective played note, topology-aware route lowering, staged operator envelopes with per-segment curves, raw and block-interpolated DX7 envelope tracing, applied-envelope curved lowering with optional sustain floor, two AquaSynth DSL rebuild attempts, first-class OperatorGraph support with graph-level vibrato, public-domain SysEx fixture coverage, dexed-py reference rendering, project-authored Dexed patch rendering, AquaSynth candidate rendering through Faust-generated C#, one thresholded rendered parity test for analog1.syx voice 13, a hard PRC SYNTH1 parity probe with listening WAV artifacts and log-mel <= 0.145, project-authored algorithm-8 summed-stack/cascade/summed-pair parity, rendered community parity for Mooger, Piano Bass, RES SYNTH1, and ANLGSYN 1 with a sustained high-band gate, a MELLOWSOLO pressure artifact test, and renderable delayed operator feedback.", diff --git a/tests/AquaSynth.Dsl.Tests/VocalTractModelTests.cs b/tests/AquaSynth.Dsl.Tests/VocalTractModelTests.cs index 588f2ba..f5f8a4d 100644 --- a/tests/AquaSynth.Dsl.Tests/VocalTractModelTests.cs +++ b/tests/AquaSynth.Dsl.Tests/VocalTractModelTests.cs @@ -108,4 +108,96 @@ public sealed class VocalTractModelTests Assert.True(report.Accepted); Assert.Empty(report.Diagnostics); } + + [Fact] + public void BeakBilabialPlanResultIsHostActionable() + { + var result = VocalTractPlanner.Plan( + new PhoneticIntent( + "beak-pa", + "weksa", + "/pa/", + [ + new PhoneticEvent( + "phone-p", + "p", + new PhoneticFeatures( + PhoneticManner.Stop, + PhoneticPlace.Bilabial, + Phonation.Voiceless), + DurationSeconds: 0.06), + new PhoneticEvent( + "phone-a", + "a", + new PhoneticFeatures( + PhoneticManner.Vowel, + Height: VowelHeight.Open, + Backness: VowelBackness.Front), + StartSeconds: 0.06, + DurationSeconds: 0.14) + ]), + new VocalTractMorphology( + "beak", + "Beaked speaker", + 22.0f, + 44, + [ + ArticulatoryCapability.OralTract, + ArticulatoryCapability.PulmonicPressure, + ArticulatoryCapability.Voicing, + ArticulatoryCapability.AlveolarConstriction, + ArticulatoryCapability.VelarConstriction + ])); + + Assert.False(result.Accepted); + Assert.Null(result.Plan); + Assert.Equal(VocalTractHostReactionKind.RejectIntent, result.HostReaction.Kind); + Assert.Contains("missing_bilabial_capability", result.HostReaction.DiagnosticCodes); + Assert.Contains("phone-p", result.Report.Diagnostics.Select(diagnostic => diagnostic.EventId)); + Assert.Contains("`p`", result.HostReaction.Summary); + } + + [Fact] + public void AcceptedVowelPlanEmitsInspectableGestures() + { + var result = VocalTractPlanner.Plan( + new PhoneticIntent( + "human-a", + "ipa", + "/a/", + [ + new PhoneticEvent( + "phone-a", + "a", + new PhoneticFeatures( + PhoneticManner.Vowel, + Height: VowelHeight.Open, + Backness: VowelBackness.Front), + DurationSeconds: 0.2, + Prosody: new PhoneticProsody(Intensity: 0.75f)) + ]), + new VocalTractMorphology( + "human", + "Human baseline", + 17.0f, + 44, + [ + ArticulatoryCapability.OralTract, + ArticulatoryCapability.PulmonicPressure, + ArticulatoryCapability.Voicing + ])); + + Assert.True(result.Accepted); + Assert.Equal(VocalTractHostReactionKind.RenderPlan, result.HostReaction.Kind); + var plan = Assert.IsType(result.Plan); + Assert.Empty(plan.Report.Diagnostics); + Assert.Contains(plan.Gestures, gesture => + gesture.Kind == ArticulatoryGestureKind.GlottalSource && + gesture.SourceEventId == "phone-a" && + gesture.Amount == 0.75f); + Assert.Contains(plan.Gestures, gesture => + gesture.Kind == ArticulatoryGestureKind.TractAreaTarget && + gesture.SourceEventId == "phone-a" && + gesture.Target == "vowel:open:front:unrounded"); + } }