Weksa/tools/emit_tiny_panphon_handoffs.py
2026-05-21 10:21:48 +01:00

381 lines
10 KiB
Python

#!/usr/bin/env python3
"""Emit tiny v0.1 utterance handoff fixtures for AquaSynth speech training.
This is intentionally not a Weksa runtime. It is a deterministic artifact
emitter for the first crawl-stage curriculum: simple IPA syllables with the
PanPhon 22-feature basis, fixed-width Weksa/Epiphany input vectors, and pending
AquaSynth-owned learned outputs.
"""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "examples" / "speech-training" / "tiny-panphon-v0.1"
VECTORS = OUT / "vectors"
SCHEMA_VERSION = "weksa.utterance_embedding_handoff.v0.1"
DECIDED_AT_UTC = "2026-05-20T00:00:00Z"
FEATURE_ORDER = [
"syl",
"son",
"cons",
"cont",
"delrel",
"lat",
"nas",
"strid",
"voi",
"sg",
"cg",
"ant",
"cor",
"distr",
"lab",
"hi",
"lo",
"back",
"round",
"velaric",
"tense",
"long",
]
PROSODY_HINT_LABELS = [
"mean_pitch_pressure",
"pitch_rise",
"pitch_fall",
"pitch_range",
"loudness_pressure",
"loudness_attack",
"loudness_release",
"speaking_rate",
"articulation_precision",
"pause_pressure",
"phrase_final_lengthening",
"stress_contrast",
"clippedness",
"breathiness",
"creak_pressure",
"smoothness",
"warmth",
"threat",
"urgency",
"hesitation",
"irony_dryness",
"tenderness",
"contempt",
"command_force",
"question_contour",
"exclamation_pressure",
"secrecy_low_projection",
"ritual_formality_pressure",
"playful_pressure",
"fatigue",
"emotional_containment",
"reserved",
]
CHARACTER_STATE_LABELS = [
"valence",
"arousal",
"dominance",
"urgency",
"anger",
"despair",
"sadness",
"fear",
"anxiety",
"disgust",
"contempt",
"annoyance",
"dismissal",
"flippancy",
"playfulness",
"irony",
"tenderness",
"warmth",
"joy",
"excitement",
"fatigue",
"guardedness",
"confidence",
"shame",
"pride",
"threat",
"secrecy",
"hesitation",
"emotionalContainment",
"thoughtPressure",
"reactionIntensity",
"commandForce",
"currentLoad",
"initiativeHeat",
"pendingTurn",
"cooldownPressure",
"reactionBias",
"interruptThreshold",
"initiativeSpeed",
"voiceBubbly",
"voiceDryness",
"voiceFormality",
"voiceDirectness",
"voiceIntensity",
"voiceWarmth",
"voicePrecision",
"voiceRitualRegister",
"presentationPushiness",
"presentationSelfFocus",
"presentationPlay",
"presentationCareDemand",
"behaviorWorkDrive",
"behaviorBoundaryDefense",
"behaviorPurityDrive",
"behaviorPatience",
"stableAgreeableness",
"stableConscientiousness",
"stableNeuroticism",
"stableOpenness",
"organizationCoherence",
"organizationRigidity",
"slowTraitReserved62",
"slowTraitReserved63",
"slowTraitReserved64",
]
PHONE_FEATURES = {
"a": {
"syl": 1,
"son": 1,
"cons": -1,
"cont": 1,
"voi": 1,
"lo": 1,
"round": -1,
"tense": -1,
},
"p": {
"syl": -1,
"son": -1,
"cons": 1,
"cont": -1,
"voi": -1,
"lab": 1,
},
"t": {
"syl": -1,
"son": -1,
"cons": 1,
"cont": -1,
"voi": -1,
"ant": 1,
"cor": 1,
},
"k": {
"syl": -1,
"son": -1,
"cons": 1,
"cont": -1,
"voi": -1,
"back": 1,
},
"s": {
"syl": -1,
"son": -1,
"cons": 1,
"cont": 1,
"strid": 1,
"voi": -1,
"ant": 1,
"cor": 1,
},
"m": {
"syl": -1,
"son": 1,
"cons": 1,
"cont": -1,
"nas": 1,
"voi": 1,
"lab": 1,
},
}
UTTERANCES = [
("open-vowel-a", "a", ["a"], "open low vowel baseline"),
("bilabial-pa", "pa", ["p", "a"], "voiceless bilabial stop into open vowel"),
("alveolar-ta", "ta", ["t", "a"], "voiceless alveolar stop into open vowel"),
("velar-ka", "ka", ["k", "a"], "voiceless velar stop into open vowel"),
("sibilant-sa", "sa", ["s", "a"], "voiceless alveolar sibilant into open vowel"),
("nasal-ma", "ma", ["m", "a"], "voiced bilabial nasal into open vowel"),
]
def feature_vector(phone: str) -> list[float]:
features = PHONE_FEATURES[phone]
return [float(features.get(name, 0)) for name in FEATURE_ORDER]
def frame(phone: str, index: int, count: int) -> dict:
is_vowel = PHONE_FEATURES[phone].get("syl") == 1
duration_ms = 180 if is_vowel else 95
return {
"index": index,
"phone": phone,
"features": feature_vector(phone),
"markers": {
"stress": 1.0 if is_vowel else 0.0,
"length": 0.0,
"tone": 0.0,
"boundary": 1.0 if index == count - 1 else 0.0,
"duration_ms": duration_ms,
},
}
def write_json(path: Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def packet(packet_id: str, ipa: str, phones: list[str], note: str) -> dict:
prosody_values = [0.0] * 32
prosody_values[7] = -0.15
prosody_values[8] = 0.35
prosody_values[11] = 0.2
prosody_values[15] = 0.25
return {
"schema_version": SCHEMA_VERSION,
"packet_id": f"tiny-panphon-v0.1/{packet_id}",
"source_output_ref": "examples/speech-training/tiny-panphon-v0.1/README.md",
"speaker_agent_id": "neutral-human-baseline",
"text": {
"spoken_text": ipa,
"normalized_text": ipa,
"channel_note": "Pure IPA curriculum seed; semantic text embedding is intentionally zero.",
},
"inputs": {
"speech_text_embedding": {
"model_id": "bge-m3:latest",
"dimensionality": 1024,
"values_ref": "vectors/zero-semantic-1024.json",
},
"panphon_sequence": {
"model_id": "panphon.v0.1.seed",
"source": "hand-authored seed phones for AquaSynth crawl-stage parity tests",
"ipa": ipa,
"feature_order": FEATURE_ORDER,
"frames": [frame(phone, i, len(phones)) for i, phone in enumerate(phones)],
},
"prosody_emphasis_hints": {
"dimensionality": 32,
"labels": PROSODY_HINT_LABELS,
"values": prosody_values,
},
"character_state_vector": {
"source": "epiphany.agent_utterance_state.v0 neutral baseline",
"schema_ref": "E:/Projects/EpiphanyAgent/schemas/agent-utterance-state-schema.md",
"dimensionality": 64,
"values_ref": "vectors/neutral-character-64.json",
},
},
"phonetic_realization_embedding": {
"owner": "AquaSynth",
"model_id": "aquasynth.panphon_sequence_encoder.v0.1",
"dimensionality": 256,
"status": "pending_aquasynth_training_or_encoding",
"values_ref": None,
},
"utterance_embedding": {
"owner": "AquaSynth",
"model_id": "aquasynth.utterance_embedding_encoder.v0.1",
"version": None,
"training_set": "tiny-panphon-v0.1",
"dimensionality": 64,
"status": "pending_aquasynth_training_or_encoding",
"values_ref": None,
},
"audit_projection": {
"semantic_preservation": "No lexical semantics; preserve the requested IPA sound shape.",
"emphasis": "Single syllable baseline stress where a vowel exists.",
"prosody": "Neutral, slow, precise, no expressive contour.",
"character_pressure": "Neutral speaker baseline.",
"delivery_shape": note,
},
"training_refs": {
"positive_examples": [
{
"kind": "reference_synth_prompt",
"engine": "eSpeak NG or equivalent baseline speech oracle",
"text": ipa,
}
],
"negative_examples": [],
},
"trace": {
"pipeline_receipts": [
{
"stage": "weksa_emit_tiny_panphon_handoff",
"decided_at_utc": DECIDED_AT_UTC,
"latency_budget_ms": 10,
"observed_latency_ms": 0,
"confidence": 0.72,
}
],
"notes": [
"This fixture is a crawl-stage training seed, not a full English lowering artifact.",
"The schema uses PanPhon's 22-feature basis; these seed values are hand-authored for the six seed phones until Weksa wires a canonical PanPhon dependency.",
"AquaSynth owns the learned 256-float phonetic realization embedding and 64-float utterance embedding.",
],
"uncertainties": [
"Feature values should be regenerated from the canonical PanPhon library before broad phoneme coverage.",
"Durations are seed timing hints, not measured reference audio.",
],
},
}
def emit() -> None:
write_json(
VECTORS / "zero-semantic-1024.json",
{
"model_id": "bge-m3:latest",
"dimensionality": 1024,
"values": [0.0] * 1024,
"reason": "Pure IPA fixture; semantic text channel is intentionally silent.",
},
)
write_json(
VECTORS / "neutral-character-64.json",
{
"source": "epiphany.agent_utterance_state.v0 neutral baseline",
"dimensionality": 64,
"labels": CHARACTER_STATE_LABELS,
"values": [0.0] * 64,
},
)
packet_refs = []
for packet_id, ipa, phones, note in UTTERANCES:
path = OUT / f"{packet_id}.json"
write_json(path, packet(packet_id, ipa, phones, note))
packet_refs.append(path.name)
write_json(
OUT / "batch.json",
{
"schema_version": "weksa.utterance_handoff_batch.v0.1",
"batch_id": "tiny-panphon-v0.1",
"packet_schema_version": SCHEMA_VERSION,
"packet_refs": packet_refs,
"purpose": "Initial AquaSynth parity curriculum for simple IPA sounds.",
},
)
if __name__ == "__main__":
emit()