adapters/PBDRagdollAgentAdapter


brain/ExperienceBuffer


brain/NetworkArchitecture

Summary: Layer class - represents a single neural network layer


Summary: Network Architecture Definitions Layer configurations and hyperparameters for Policy and Value networks


brain/PolicyNetwork


brain/ValueNetwork


brain/trainers/PPOTrainer


config/curriculum

Summary: Curriculum Learning Stages Progressive training stages from standing to full locomotion


config/hyperparameters

Summary: Neural Network Hyperparameters Configuration for Policy and Value networks, training parameters


config/rewards

Summary: Compute reward for current state


computeReward

computeReward(state, config = REWARD_WEIGHTS.standing)

Summary: Compute reward for current state


Summary: Reward Function Weights Configurable reward components for different training objectives


core/CurriculumManager


core/MotionMatchingTeacher


MotionMatchingTeacher

class MotionMatchingTeacher extends MotionMatcher


core/MotorController

Summary: Motor Controller Converts neural network actions into physics impulses


core/ObservationBuilder

Summary: Observation Builder Converts ragdoll sensor data into neural network input


core/RagdollController


core/RewardFunction


index


llm/DeviceProfiler

Summary: DeviceProfiler.js - WebGPU capability and LLM feasibility profiler. This module is intentionally browser-native and dependency-free. It can use the kernel's shared GPU device when available, or probe navigator.gpu directly for standalone AGI tests.


llm/formats/GGUFParser


llm/formats/GGUFTensorReader


llm/formats/LLMPersistenceContracts

Summary: Accept a legacy bare conversation and return the canonical durable v1 envelope.


prepareConversation

prepareConversation(input, expectedId = null)


llm/formats/ModelPackageReader


llm/formats/ModelPackageWriter


llm/formats/Quantization


inferQuantization

inferQuantization(tensors = [], metadata = {})


llm/formats/TensorShardIndex

Summary: Accept legacy bare indexes and return a canonical, independently cloned v1 document.


UnsupportedTensorShardIndexVersionError

class UnsupportedTensorShardIndexVersionError extends Error


llm/index


llm/inference/CacheGovernor


planAdaptivePreload

planAdaptivePreload(tensors = [], options = {})


adaptiveEvictionScore

adaptiveEvictionScore(name, entry = {}, options = {})


llm/inference/CpuTensorRunner


llm/inference/GenerationLoop


llm/inference/GpuPrimitiveRunner


mapAsyncWithTimeout

mapAsyncWithTimeout(buffer, mode, timeoutMs, message)


llm/inference/GpuRuntimeResources


llm/inference/GraphCompiler


llm/inference/InferenceEngine


llm/inference/KVCache


llm/inference/LocalGGUFExecutor


llm/inference/PagedAttention


llm/inference/QuantizedMatVecRunner


createKQuantParityProbeData

createKQuantParityProbeData(quantization, { rows = SUBGROUP_PROBE_ROWS } = {})


llm/inference/StaticMemoryPlanner


llm/inference/TransformerExecutor


llm/inference/WeightCache


llm/kernels/KernelRegistry


llm/kernels/PipelineUtils


createComputePipelineChecked

async createComputePipelineChecked(device, module, { label, name = label, entryPoint = 'main' } = {})


assertShaderCompiled

async assertShaderCompiled(module, name = 'shader')


llm/kernels/shaders/attention_decode


llm/kernels/shaders/attention_decode_legacy


llm/kernels/shaders/attention_prefill_legacy


llm/kernels/shaders/attention_prefill_tiled


llm/kernels/shaders/dequant_row_dense


llm/kernels/shaders/dequant_row_k_quant


llm/kernels/shaders/dequant_row_q4_0


llm/kernels/shaders/dequant_row_q8_0


llm/kernels/shaders/embedding_row_gather


llm/kernels/shaders/matmul_k_quant


shader

shader({ quant = 'Q4_K', workgroupSize = WORKGROUP_SIZE } = {})


llm/kernels/shaders/matmul_q4


llm/kernels/shaders/matmul_q4_0


shader({ workgroupSize = WORKGROUP_SIZE } = {})


llm/kernels/shaders/matvec_dense


shader({ format = 'F32', workgroupSize = WORKGROUP_SIZE } = {})


llm/kernels/shaders/matvec_k_quant


shader({ quant = 'Q4_K', workgroupSize = WORKGROUP_SIZE, useSubgroups = false } = {})


llm/kernels/shaders/matvec_q4_0


llm/kernels/shaders/matvec_q8_0


llm/kernels/shaders/rmsnorm_fused


shader({ hiddenSize = 1536, epsilon = '0.000001', workgroupSize = 256 } = {})


llm/kernels/shaders/rmsnorm_residual_fused


llm/kernels/shaders/rope_batch


llm/kernels/shaders/rope_fused


llm/kernels/shaders/rope_kv_append


llm/kernels/shaders/rope_table


llm/kernels/shaders/sampling_topk_candidates


llm/kernels/shaders/sampling_topk_reduce


llm/kernels/shaders/sampling_topk_topp


llm/kernels/shaders/sampling_topk_topp_legacy


llm/kernels/shaders/swiglu_fused


shader({ activation = 'gelu_pytorch_tanh' } = {})


llm/kernels/shaders/tied_embedding_logits


llm/kernels/shaders/vector_ops


llm/runtime/LLMRuntime


llm/runtime/LLMSession


llm/runtime/LogFormat


logRuntimeEvent

logRuntimeEvent(event, data = {}, options = {})


llm/runtime/ModelRegistry


llm/runtime/PromptTemplate


llm/runtime/RuntimePerformanceMonitor


llm/runtime/Sampler


llm/runtime/Tokenizer


llm/runtime/TokenizerWorkerClient


llm/runtime/ToolBridge


llm/storage/AtomicJSON

Summary: Publish JSON through a verified staging file while retaining the previous revision until the replacement move succeeds.


replaceJSONCopyOnWrite

async replaceJSONCopyOnWrite(store, path, value, { validate = null, label = 'JSON record' } = {})


llm/storage/Checksum


checksumBytes

async checksumBytes(bytes, algorithm = 'SHA-256')


checksumString

async checksumString(text, algorithm = 'SHA-256')


llm/storage/ModelCache


llm/storage/ModelHandleStore


ModelHandleStoreError

class ModelHandleStoreError extends Error


prepareModelHandleRecord

prepareModelHandleRecord(value, expectedModelId = null)


llm/storage/OPFSStore


llm/storage/VFSConversationStore


llm/workers/LLMLoadPlannerWorker


llm/workers/LLMTokenizerWorker


loader/ModelLoader


modelLoaderVertexDedupHash32(posBits, pOff, uvBits = null, uOff = 0)


network/TrainingStateChannel

Summary: Bind AGI Studio telemetry to the standard State Channel stack. The training runtime is authoritative; same-origin tabs need no setup, and callers hosted inside WebGPU OS may pass NetworkDriver to publish over mesh.


particle_voice/articulatory/BreathReservoirState

Summary: BreathReservoirState.js — Phase 2 Voice Box state schema (`v0`). "Lung pressure, compliance, target subglottal pressure" per `ARCHITECTURE.md` §4 — the JS-side state container wrapping `nn/kernels/articulatory/breath_reservoir.js`'s per-instance recurrence (`pressure[n] = pressure[n-1] + (target[n] - pressure[n-1]) * lagRate`, proven against `risk/BreathReservoir.js` (R6A)). This module owns the `lagRate` derivation from `compliance`/`sampleRate` (the caller-computed value that kernel's docstring says it expects pre-computed, not recomputed in-shader) and the timestamped state container the kernel's `initial_pressure`/`pressure_out` buffers round-trip through across chunks — it does not itself dispatch the kernel (that is `ParticleTract.js`'s job once a real model exists to drive it).


lagRateFromCompliance

Summary: `lagRate = 1 - exp(-1/(compliance*sampleRate))` — matches `risk/BreathReservoir.js` (R6A)'s proven formula exactly; `breath_reservoir.js`'s own docstring requires this be computed by the caller, not the kernel.


createBreathReservoirState

createBreathReservoirState({ sampleIndex, pressure, compliance, targetSubglottalPressure })


stepBreathReservoirState

Summary: Advance one `BreathReservoirState` by one sample using the exact same recurrence `breath_reservoir.js`/`risk/BreathReservoir.js` implement — the CPU-side single-sample equivalent, useful for tests and for any non-GPU fallback path, not a replacement for the GPU kernel's chunked dispatch.


particle_voice/articulatory/FormantSolver

Summary: FormantSolver.js — Phase 3 ParticleVoice articulatory calibration. Measures the resonances a Kelly-Lochbaum area profile actually produces, and inverts that: given target formants, searches for an area profile that hits them. ## Why this exists Vowel identity in speech is carried almost entirely by **F1 and F2**. An area profile invented from articulatory adjectives ("high", "back", "rounded") produces *some* formants, but nothing forces them to be the RIGHT ones — and if they are wrong, every vowel collapses toward a neutral timbre and the speech is unintelligible no matter how clean the pipeline is. That was the state before this module: `ArticulationHead`'s height/backness → area mapping was plausible but uncalibrated. Defining vowels by their measured formant targets and SOLVING for the geometry inverts the dependency: the targets are citable data (Peterson & Barney 1952 / Hillenbrand et al. 1995 male averages, whose vowel set maps exactly onto `spec/PhonemeSet-v0.md`'s ten monophthongs), and the geometry becomes a derived quantity that stays correct if the section count or sample rate changes. ## The response is measured with the SAME recurrence the GPU runs `impulseResponse()` reimplements `tract_waveguide.js`'s scattering recurrence on the CPU rather than using an analytic transfer function. That is deliberate: an analytic formula would be a second model that could disagree with the kernel, and then a "calibrated" vowel would not actually be calibrated for the thing doing the synthesis. In particular this omits the lip first-difference, matching `ParticleTract`'s `radiation_difference = 0` (its source is already a flow derivative).


impulseResponse

impulseResponse(areas, { glottalReflection, lipReflection, responseLength, wallLoss = 1 })

Summary: Impulse response of the lossless KL chain, mirroring `tract_waveguide.js`'s recurrence (and, like `ParticleTract`, WITHOUT the lip first difference).


findFormants

findFormants(areas, sampleRate, options = {})

Summary: Resonance frequencies of an area profile, lowest first.


profileFromParams

Summary: Build the profile this solver searches over. `constrictionWidth` exists because a single control point is not enough for high front vowels. `shapedAreaProfile` interpolates LINEARLY between control points, so one narrow point produces a smooth V — a tract that tapers and re-widens with no distinct cavities. /i/ needs the opposite: a genuinely SHORT front cavity (F2 = 2290 Hz implies c/(4*F2) ~ 3.7 cm) sharply separated from a long back cavity. Giving the constriction a WIDTH creates that two-cavity structure, and with it F2 values a single point cannot reach (an earlier 3-point version topped out at 1889 Hz against IY's 2290 Hz target, 333 cents low, which made IY and IH nearly identical in F2). A width of 0 degenerates to the original 3-point shape, so the parametrisation strictly extends the old one rather than replacing it.


fitVowelAreas

fitVowelAreas(target, numSections, sampleRate, options = {})

Summary: Search for an area profile whose first two resonances match `target`. Error is measured in the LOG-frequency domain, because formant perception is roughly logarithmic: being 100 Hz off matters far more at F1 = 270 than at F2 = 2290, and a linear error would happily sacrifice F1 accuracy to shave a little off F2. Exhaustive over a coarse grid, then a local refinement pass. Exhaustive rather than gradient-based on purpose: the map from geometry to formants is non-monotonic (moving a constriction can swap which cavity dominates F2), so a descent method lands in whichever basin it started in.


fitTargets

fitTargets(targets, numSections, sampleRate, options = {})

Summary: Fit every entry in a formant-target table. Slow (a full grid per entry) — intended for a calibration tool/test, not a hot path.


fitAllVowels

fitAllVowels(numSections, sampleRate, options = {})


fitAllApproximants

fitAllApproximants(numSections, sampleRate, options = {})


tractLengthFor

Summary: The tract length a section count represents, for reporting alongside a fit.


Summary: Peterson & Barney (1952) male averages, Hz. Their vowel set — heed, hid, head, had, hod, hawed, hood, who'd, hud, heard — is exactly this project's ten monophthongs, so no mapping or interpolation is needed. F3 is carried for `ER` only, where the unusually LOW F3 is the defining rhotic cue; for the others F1/F2 determine identity.


Summary: The solved geometry for each vowel, produced by `fitAllVowels()` and baked here so the runtime does not pay for the search (a full grid per vowel takes tens of seconds). `tests/particle-voice/vowel-formants.html` re-verifies this table against the targets cheaply on every run, so it cannot silently go stale if the section count or sample rate changes — the check fails and the table is re-fitted. Fitted at **32 sections / 64 kHz, two constrictions, wall loss included**. Measured F1/F2 vs target, worst case **21 cents** across all ten: IY 270/2291   IH 390/1989   EH 530/1844   AE 661/1724   AH 640/1190 ER 490/1350   AA 730/1090   AO 569/850    UH 438/1019   UW 300/867 Three structural changes got here, none of them tuning: 1. **Doubling the resolution** (16 -> 32 sections, via 32 -> 64 kHz) made a short front cavity representable, which is what front vowels need. IY's F2 went 1926 -> 2211 Hz on its own. 2. **A second constriction** fixed the back rounded vowels, which a single narrowing simply could not reach: AO's F2 was 1130 Hz against an 840 Hz target (513 cents) and no better solution existed anywhere in the search space, because a low F2 requires a long back cavity AND lip rounding acting together. Worst error 513 -> 60 cents. 3. **Including WALL LOSS in the calibration**, which improved it again (60 -> 21 cents). That is not a coincidence: loss gives the formants realistic bandwidth, and damped peaks are better separated and more reliably located than the near-singular peaks of a lossless tube. It also means the fit is now against the same damped tube that renders. Every area respects `MIN_VOWEL_AREA_CM2`, so no vowel is realised with a constriction tight enough to whistle or hiss.


Summary: Formant targets for the four approximants, Hz. These are consonants only in their distribution — acoustically /w j r l/ are "vowel-like consonants" (semivowels), and a generic `MANNER_AREA` constriction cannot produce any of them. That is why they were inaudible as themselves: a single 0.85 cm^2 narrowing at a place position yields something vowel-shaped and roughly neutral, so /r/, /l/, /w/ and /y/ all collapsed into the same glide. Treating them like vowels — targets first, geometry solved — is the only way they become distinct. Values from the acoustic-phonetics literature (Espy-Wilson's prevocalic measurements; the Delaware ASEL synthesis tables): /w/ 250/750   — an "extreme /u/": lower F1 and F2 than the vowel /r/ 350/1000  — F1/F2 like a central rounded vowel... /l/ 400/1075  — ...nearly the same F1/F2 as /r/ /y/ 300/2450  — an "extreme /i/" Note the F1/F2 values for /r/ and /l/ are nearly identical. **F3 is what separates them**, almost entirely: /r/'s F3 falls below 2000 Hz (often close to F2, which is its single most salient property), while /l/'s sits at or above 2500 Hz. `f3` is carried here for that reason, and `f3Weight` marks where matching it actually matters — for /r/ and /l/ an F3 error is not a refinement, it is the difference between the two phonemes.


Summary: Solved approximant geometry, same provenance and staleness guard as `VOWEL_AREA_PARAMS`. Fitted at 32 sections / 64 kHz, with the second constriction AND wall loss (both must match `DEFAULT_TRACT_CONFIG`, or this calibrates a different tube than the one that renders). Measured F1/F2/F3 against target: W 249/852/2113  (250/750/2500) R 337/1137/1845 (350/1000/1500) L 398/1075/2556 (400/1075/2550) Y 299/2402/2688 (300/2450/2600) L is near-exact on all three and Y is within 50 Hz on F1/F2. The decisive property holds: **R's F3 sits at 1845 Hz and L's at 2556 Hz**, a 711 Hz separation, so the one cue that distinguishes /r/ from /l/ genuinely exists. Before calibration both were a generic manner constriction with NO F3 contrast at all — literally the same sound, which is why the user could hear neither. The second constriction is what made this reachable. With a single one, R's F3 was 2166 Hz (not rhotic at all), W's F2 was 1091 against 750, and Y's F2 was 1541 against 2450. R's F3 is still ~345 Hz above target, the remaining gap being the sublingual cavity that a 1-D area function cannot represent at all.


Summary: Smallest cross-section, in cm^2, a VOWEL is allowed anywhere in the tract. Without this the search is free to buy formant accuracy with impossible geometry: an unconstrained fit put AA's constriction at 0.15 cm^2, which is TIGHTER than `ArticulationHead`'s fricative constant (0.14). Two things are wrong with that. Physically, a channel that narrow generates turbulence at speech airflow rates — it would be a fricative, not a vowel, and a vowel that hisses is not the vowel you asked for. Numerically, the extreme area ratio drives junction reflection coefficients toward +-1, giving absurdly high-Q resonances that ring. 0.35 cm^2 sits just above the turbulence onset region while still allowing the tight constrictions high vowels genuinely need (measured vowel minima run from ~0.3 cm^2 for /i/ upward).


particle_voice/articulatory/NeuralPhysiologyState

Summary: NeuralPhysiologyState.js — Phase 2 Voice Box state schema (`v0`). The model's predicted per-frame physiology state, per `ARCHITECTURE.md` §4: four control heads plus an uncertainty head, version `0x00010000`. This is a `v0` working definition of each head's field set — the heads themselves are named and required by the plan, but their exact field composition was not otherwise specified anywhere in the frozen `spec/` documents, so the fields chosen here are deliberately the smallest set that maps DIRECTLY onto an already-built, already-tested GPU kernel's input parameters (not invented ahead of a consumer): - `RespiratoryDriveHead.targetSubglottalPressure` → drives `BreathReservoirState`/`nn/kernels/articulatory/breath_reservoir.js`. - `GlottalStateHead` (`t0Samples`/`teSamples`/`tpSamples`/`taSamples`/ `ee`) → the exact parameter names `nn/kernels/articulatory/glottal_lf.js` takes per instance. - `TractGestureHead.areas` → the area profile `nn/kernels/articulatory/tract_transfer.js`/`tract_waveguide.js` consume. - `ArticulationEventHead` (`constrictionIndex`/`constrictionAmplitude`/ `nasalCoupling`) → drives `constriction_noise.js`/`nasal_junction.js` injection points (the actual multi-port coupling math those two kernels' docstrings still flag as deferred — this head only carries the control values, not a coupling implementation). - `UncertaintyHead.confidence` → a single scalar per-frame confidence, the minimal representation until a real model exists to justify a richer (e.g. per-head covariance) shape. Like every other `v0` document in this project, this field set may still change before a `v1` freeze — it is not a claim that these are the final head shapes, only that they are usable today and traceable to real kernel inputs rather than speculative. Per `ARCHITECTURE.md` §4's timestamping rule, every state object here carries the absolute sample counter it was produced at (`sampleIndex`), never a chunk-relative index.


createNeuralPhysiologyState

createNeuralPhysiologyState({ sampleIndex, respiratoryDrive, glottalState, tractGesture, articulationEvent = null, uncertainty = { confidence: 1 }, })


particle_voice/articulatory/ParticleTract

Summary: ParticleTract.js — Phase 2 ParticleVoice Voice Box orchestrator. The GPU-resident articulatory synthesizer: composes the already-verified Phase 2 articulatory kernels into audible speech, one fixed-size chunk at a time, with all cross-chunk state persisted ON THE GPU (no CPU round trip mid-pipeline). This is the production counterpart of R6's pure-CPU `risk/VoiceBoxWaveguideProbe.js`, and deliberately reproduces the same four controllable behaviors that probe proved: stable vowels, nasal coupling, closure/release/frication, and pitch sweeps. Signal graph per chunk (7 GPU passes + 4 state copies, ONE submit): glottal_lf ────────────────► voicing ──┐ ├─ signal_add ─► source_flow ─┐ constriction_noise(asp seed) ─► asp ────┘                            │ ▼ constriction_noise(fric seed) ─► frication ──────► tract_waveguide (oral) │         │ radiated ◄─────────┘         └──► nasal_tap │                                │ │            nasal_junction ◄────┘ │                   │ └── signal_add ◄── radiated │ ▼  final PCM Design decisions worth knowing: - **Frication is injected at the constriction, not the glottis.** The frication noise buffer goes to `tract_waveguide.js`'s `constriction_noise` input and is added to the right-going wave at `constrictionIndex`, so only the cavity in FRONT of the constriction filters it. Mixing it into `source_flow` instead would make every fricative sound like a whisper. Aspiration, which really is glottal, IS mixed into `source_flow`. - **No `lip_radiation` pass.** `tract_waveguide.js` already applies the `outputRaw - prevOutputRaw` radiation first-difference internally (exactly as R6's `step()` does). Running `lip_radiation.js` on its output would differentiate twice and produce a thin, bass-less buzz. That kernel exists for callers holding an undifferentiated signal. - **Fixed `chunkSamples`.** `tract_waveguide.js`/`nasal_junction.js` pack their radiated signal and persisted state into one buffer whose internal offsets depend on `num_samples`, so a varying chunk size would move every offset and invalidate the pre-built bind groups. A fixed chunk size is also what a real-time audio bridge wants anyway (`streaming/VoiceChunker.js` schedules in fixed frame buckets), so it is required here rather than worked around. - **Reflection coefficients are computed CPU-side** each chunk via `risk/KellyLochbaumWaveguide.js`'s proven `reflectionCoefficients`, matching `tract_waveguide.js`'s stated contract. Changing `areas` between chunks is how closure/release is articulated. **Visualization is optional and structurally incapable of changing the PCM.** `ARCHITECTURE.md` §4 requires visualization to be "bit-identically disable-able without changing rendered PCM". Here that is not a runtime convention but a structural property: when `visualFramesPerChunk` is 0 (the default) the visualization buffers, pipeline and dispatches are never created at all, and when it is enabled every visualization buffer is WRITE-ONLY from the acoustic path's perspective — the field is copied OUT of `tract_waveguide`/`nasal_junction`'s already-final packed outputs, and nothing in the visualization path is ever read back into the acoustic path. `visual_state_decimate.js` makes the same guarantee at kernel level (it has no read_write access to anything the acoustic path reads). Known limitations, stated honestly rather than papered over: - `constrictionIndex` and `nasalCoupling` are DISPATCH-WIDE (they live in the kernels' uniform structs, not per-instance storage arrays), so all instances of one `ParticleTract` share them. Per-instance variation would require promoting both to storage buffers. They are therefore top-level controls here, not per-instance ones, so the API does not imply a capability that does not exist. - The nasal tap is a read-only observation of the oral tract, not a rigorous 3-port scattering junction — the oral tract is not loaded by the nasal branch. This matches R6 exactly and is noted in `tract_waveguide.js`'s docstring too. - The visualization field carries the THREE per-sample solver signals the kernels actually expose (oral radiated, nasal tap, nasal radiated), not one channel per tube section. `ARCHITECTURE.md` §4 describes the richer per-section field, but `tract_waveguide.js` does not emit a `[section, sample]` field — only its radiated signal, final state and the single tapped section. Adding one would mean a `numSections * chunkSamples` output block (~512 KB per chunk at 32 sections / 1024 samples) allocated whether or not visualization is on. That is a deliberate follow-up, not an oversight; the three channels here are enough to drive a ribbon/heatmap of what the solver is actually radiating.


sectionsForLength

sectionsForLength(lengthM, sampleRate, speedOfSound = SPEED_OF_SOUND_M_PER_S)


lengthForSections

lengthForSections(numSections, sampleRate, speedOfSound = SPEED_OF_SOUND_M_PER_S)


Summary: Boundary coefficients are R6's proven values. The SECTION COUNTS are not — they are derived from physics, and R6's were wrong. ## Section count is not a free parameter: it sets the tract LENGTH In this waveguide a wave advances one section per sample (`right[i] -> right[i+1]`), so each section is one sample of travel: sectionLength = c / sampleRate tractLength   = numSections * c / sampleRate The count is therefore *determined* by the sample rate and the length you want. At 32 kHz with c = 340 m/s a section is 1.06 cm, so a 17.5 cm adult vocal tract needs **16 sections**, not 32. R6 used 32 sections at 32 kHz, which models a **34 cm** tube — twice human length. Nothing errors: the model is a perfectly valid tube, just the wrong one. Since a quarter-wave resonator's formants are `(2k-1)*c/(4L)`, doubling L puts EVERY formant an octave too low — F1 at ~260 Hz instead of ~500 Hz. That is not a subtle detune; it moves the formants out of the range speech sounds occupy, which is audible as muffled, "underwater", vowel-less noise no matter how clean the rest of the pipeline is. It was measured directly (F1 261 Hz, implied length 32.6 cm) by the uniform-tube calibration check in `tests/particle-voice/voicebox-audio.html`, which now guards it. Note the literature's 2x trap: a HALF-sample-delay Kelly-Lochbaum variant has `sectionLength = c/(2*sampleRate)`, for which 32 sections at 32 kHz WOULD be right (that is why published models quote e.g. 44 segments at 44.1 kHz). This implementation is the full-sample form, so the full-sample relation applies. The calibration test asserts the resulting length empirically rather than trusting either convention.


Summary: The per-sample solver signals the visualization field carries, in channel order. Exported so a renderer indexes channels by name instead of hard-coding 0/1/2.


Summary: LF shape defaults. `openQuotient` matches R6's `createGlottalSource({ openQuotient: 0.6 })`; the other two are LF-specific ratios with no R6 equivalent (R6 used the raised-cosine "LF-lite" shape that has no Tp/Ta at all).


particle_voice/articulatory/PhysiologyFeedbackState

Summary: PhysiologyFeedbackState.js — Phase 2 Voice Box state schema (`v0`). `ARCHITECTURE.md` §4's `PhysiologyFeedbackState`: "a reduced/decimated feedback signal from the acoustic solve back toward the model". The JS-side timestamped container for one `nn/kernels/articulatory/physiology_feedback_reduce.js` dispatch's output — per-instance (mean, RMS) pairs, one pair per fixed-size frame — distinct from `TractStateSnapshot.js`'s multi-channel visualization field (`visual_state_decimate.js`'s output): this shape is what the model's control loop reads back, not what a rig/visual system reads. Every consumer of this state should treat it as internal-physiology feedback, not a real acoustic measurement — matching `spec/ActivityContract-v0.md` §8's `GeneratedPhysiologyEnergy` / `RenderedOutputEnergy` / `ObservedInputEnergy` three-domain separation (this is `GeneratedPhysiologyEnergy`'s reduced form, never to be confused with a microphone-observed or rendered-PCM-measured energy value).


createPhysiologyFeedbackState

createPhysiologyFeedbackState({ sampleIndex, numFrames, mean, rms })


particle_voice/articulatory/TractStateSnapshot

Summary: TractStateSnapshot.js — Phase 2 Voice Box state schema (`v0`). `ARCHITECTURE.md` §4's `TractStateSnapshot`: "low-rate timestamped state for rigs/visualization, decimated from the per-sample waveguide state". The JS-side timestamped container for one `nn/kernels/articulatory/visual_state_decimate.js` dispatch's output — a `[channel, frame]` box-averaged field (one channel per tube section, per that kernel's docstring), NOT the model-feedback (mean, RMS) shape `PhysiologyFeedbackState.js` wraps (`physiology_feedback_reduce.js`'s output) — the two are read by different consumers (a rig/visual system here, the model's control loop there) and were kept as separate kernels for exactly this reason (see `visual_state_decimate.js`'s own docstring). Per `ARCHITECTURE.md` §4's requirement that visualization "must be bit-identically disable-able without changing rendered PCM", this container holds ONLY derived/decimated data for display — nothing here is ever read back into the acoustic path, matching `visual_state_decimate.js`'s structural read-only guarantee.


createTractStateSnapshot

createTractStateSnapshot({ sampleIndex, numChannels, numFrames, field })


channelSeries

Summary: Reads channel `channel`'s decimated time series out of a snapshot's flat `[channel, frame]` field — matches `visual_state_decimate.js`'s `(inst*num_channels+ch)*num_frames+frame` packing (single-instance view; the caller selects `channel`/`instance` slicing upstream of this helper).


particle_voice/articulatory/VoiceAnatomyProfile

Summary: VoiceAnatomyProfile.js — Phase 2 Voice Box state schema (`v0`). `ARCHITECTURE.md` §4's `CharacterAnatomyState`: "persistent per-character morphology (equivalent role to `PRVC-v0`'s `formantScale`/pitch-range calibration, but for non-cloned/authored characters rather than captured voices)". This is the STATIC per-voice data that stays constant across frames — the rest-position oral tube area profile, optional nasal branch area profile, and the three boundary reflection coefficients `nn/kernels/articulatory/tract_waveguide.js`/`nasal_junction.js` require (`glottalReflection`/`lipReflection`/`nostrilReflection`, all magnitude `< 1` for BIBO stability per `risk/KellyLochbaumWaveguide.js` (R6)) — as distinct from `NeuralPhysiologyState`'s `TractGestureHead`, which is the per-FRAME predicted deviation/gesture applied on top of this rest profile, not a replacement for it. `risk/VoiceBoxWaveguideProbe.js` (R6/R6A) already proved out `NUM_SECTIONS = 32`/`NASAL_SECTIONS = 16`, `GLOTTAL_REFLECTION = 0.85`, `LIP_REFLECTION = -0.9`, `NOSTRIL_REFLECTION = -0.85` as one working anatomy — `defaultVoiceAnatomyProfile()` below reuses those exact proven values as the default rather than inventing new ones.


createVoiceAnatomyProfile

createVoiceAnatomyProfile({ oralAreas, nasalAreas = null, glottalReflection = DEFAULT_GLOTTAL_REFLECTION, lipReflection = DEFAULT_LIP_REFLECTION, nostrilReflection = DEFAULT_NOSTRIL_REFLECTION, formantScale = 1, pitchRangeHz = { min: 80, max: 400 }, } = {})


defaultVoiceAnatomyProfile

Summary: The R6/R6A-proven default anatomy (uniform tube, no nasal coupling) — a reasonable starting profile for an authored (non-cloned) character, not a claim of acoustic realism.


particle_voice/articulatory/WaveguideAcousticState

Summary: WaveguideAcousticState.js — Phase 2 Voice Box state schema (`v0`). "The R6 `KellyLochbaumWaveguide.js` tract's per-sample output (radiated signal + nasal tap)" per `ARCHITECTURE.md` §4 — the JS-side timestamped container for one dispatched chunk's worth of `nn/kernels/articulatory/tract_waveguide.js` (`radiated`, plus the persisted `finalRight`/`finalLeft`/`finalPrevOutput` continuation state that kernel's packed output layout produces) and, optionally, a coupled `nn/kernels/articulatory/nasal_junction.js` chunk's own radiated output. This container does not itself dispatch either kernel or compute `nasalTapSignal` (`tract_waveguide.js`'s own docstring notes the nasal tap read-out is still deferred there) — it only fixes the shape a caller that DOES dispatch those kernels should hand back, so downstream consumers (the audio bridge, `physiology_feedback_reduce.js`, `visual_state_decimate.js`) have one stable state shape to depend on regardless of which kernel(s) produced it.


createWaveguideAcousticState

createWaveguideAcousticState({ sampleIndex, numSamples, radiated, finalRight, finalLeft, finalPrevOutput, nasalRadiated = null })


nextChunkInitialState

Summary: Builds the next chunk's initial-state inputs (`tract_waveguide.js`'s `initial_right`/`initial_left`/`initial_prev_output` bindings) from a previous chunk's `WaveguideAcousticState` — the exact cross-chunk continuity `tests/particle-voice/phase2-dsp.html`'s `tract_waveguide` test verifies at the kernel level.


Summary: G2PModel.js — Phase 3 ParticleVoice text frontend. Grapheme-to-phoneme fallback for words `PronunciationLexicon.js` does not cover, plus `textToPhonemeSequence()` — the deterministic lowering `spec/VoicePlan-v0.md` §4 describes, which turns raw text into the `{ phonemeIds, stress, punctuationContext }` triple the model consumes. ## It is rules, not a network — deliberately The plan calls for a "minimal `G2PModel.js`" in Phase 3, and this is a **deterministic, context-sensitive letter-to-sound rule set**, not a trained model. That is the honest choice at this stage: a neural G2P needs a pronunciation corpus to train against, and this project vendors no external data (CMUdict included). Rules give reproducible output today, and the class boundary here is what a learned model would later slot into without touching callers. The name matches the plan's; the implementation does not pretend to be learned. ## Accuracy expectations, stated plainly English orthography is not a function of spelling alone, so a rule set of this size gets the regular cases right and a meaningful minority wrong (`read` has two pronunciations; `bread` breaks the `ea → IY` rule; stress placement in long words is genuinely lexical). That is exactly why `PronunciationLexicon.js` exists and is consulted FIRST — every word whose spelling actively lies belongs there. G2P's job is to make unseen words (character names, invented words, rare vocabulary) pronounceable and stable, not to be a dictionary. ## Rule format Each rule is `[leftContext, target, rightContext, phonemes]`, tried in order at each position, first match winning. Rules are authored longest-target-first per letter so digraphs beat single letters. The context pattern language is deliberately tiny — three metacharacters, no regex — so a rule's behaviour is obvious by inspection: `#` word boundary (start of a left context, end of a right context) `C` exactly one consonant letter `V` exactly one vowel letter anything else, a literal letter Stress is assigned afterwards by syllable heuristic (see `assignStressHeuristic`), because stress is a property of the whole word, not of any single grapheme.


graphemesToPhonemeSymbols

Summary: Apply the rule set to a spelling.


assignStressHeuristic

Summary: Place a single primary stress by syllable heuristic. Documented as a heuristic because English stress is genuinely lexical — the lexicon is the place to be exact. - Suffixes `-tion/-sion/-cial/-tial/-cian` pull stress onto the syllable immediately before them (na-TION-al → "NA-tion", in-for-MA-tion). - `-ity/-ety/-ify/-ogy/-omy` pull stress two syllables back. - Otherwise stress the first syllable, unless the word opens with a normally-unstressed prefix and has more than one syllable, in which case stress the second.


textToPhonemeSequence

textToPhonemeSequence(normalized, { g2p = new G2PModel()

Summary: Deterministic lowering of normalized text into `spec/VoicePlan-v0.md` §4's `PhonemeSequence`. All three arrays are the same length, one entry per phoneme instance including structural tokens, exactly as that spec requires. Structural insertion follows `spec/PhonemeSet-v0.md` §4's Effect column: a phrase's `structural` token (`SP` for comma-level, `SIL` for terminals) is appended after its words, and a paragraph boundary additionally emits `BREATH` before its `SIL` (§4: "longer SIL, breath event likely").


Summary: Letter-to-sound rules, grouped by first letter of `target`. Order within a group IS the priority order.


particle_voice/frontend/PhonemeSet

Summary: PhonemeSet.js — Phase 3 ParticleVoice text frontend. The executable form of `spec/PhonemeSet-v0.md`: the English phoneme inventory, its per-entry articulatory feature vectors, the stress scale, and the punctuation-context tags. This module is the SINGLE SOURCE OF TRUTH every other Phase 3 component reads — `PronunciationLexicon.js` validates against it, `G2PModel.js` targets it, `PhonemeEncoder.js` sizes its embedding table from `PHONEME_COUNT`, and `ArticulationHead.js` consumes the feature vectors to bias `NeuralPhysiologyState`. ## Corrections to `spec/PhonemeSet-v0.md` Encoding the spec surfaced four places where its §1 tables (the actual data) disagree with its §1 summary line or its §2 enum declarations. The tables are treated as normative and the spec has been corrected to match; each is recorded here because a reader comparing the two must know which way the discrepancy was resolved. 1. **Inventory size.** §1 claimed "24 vowel symbols + 24 consonant symbols + 3 structural tokens = 51" and `u8` IDs `0..50`. The tables actually list **10 monophthongs + 5 diphthongs = 15 vowels**, so the real total is **42** entries, IDs `0..41`. "24 vowel symbols" appears to be an accidental duplication of the consonant count. This matters concretely: it sets `PhonemeEncoder.js`'s embedding table size and the legal `phonemeIds` range in `spec/VoicePlan-v0.md` §4, both of which were also stated as 51/`0..50` and are now corrected. 2. **Vowel height values.** §2 declares `height: low | mid | high`, but §1's table also uses `low-mid` (`AO`) and `high-mid` (`UH`). All five values are kept (see `HEIGHT`); the narrower §2 enum was the error. 3. **`ER`'s rounded column** contains `rhotic`, which is not a rounded-ness value at all. Modeled here as `rounded: false` plus a separate `rhotic: true` flag, since rhoticity is an independent articulatory property (it shapes the area profile rather than the lip aperture). 4. **`W`'s place** is `bilabial-velar` in §1's table, which is not one of §2's `place` values. Modeled as `place: 'bilabial'` with `secondaryPlace: 'velar'`, preserving the information without inventing an enum member the spec does not define. Structural tokens (`SIL`/`SP`/`BREATH`) carry `place: null`, `manner: null`, `voiced: false` and are identified by `category === 'structural'` — deliberately NOT given invented enum values, since §2's `place`/`manner` enums describe articulated speech sounds only.


STRESS

Summary: §3: per-vowel-instance stress, separate from the phoneme symbol.


Summary: §4: punctuation-context tag ids, emitted per phoneme by `TextNormalizer.js`.


HEIGHT

Summary: Includes §1's `low-mid`/`high-mid`, which §2's three-value enum omitted (correction 2).


Summary: §2: place → normalized position (0 = glottis, 1 = lips) of the narrowest point, fed to `shapedAreaProfile`'s control points. Provisional estimates per the spec's own §6 — NOT measured against vocal-tract MRI data.


PHONEMES

Summary: The inventory, in `spec/PhonemeSet-v0.md` §1 table order. **Array index IS the `u8` phoneme id** — appending is safe, reordering is a breaking change to every trained embedding table.


Summary: 42 — sizes `PhonemeEncoder.js`'s embedding table. See correction 1.


Summary: Highest legal `u8` phoneme id (41).


particle_voice/frontend/PronunciationLexicon

Summary: PronunciationLexicon.js — Phase 3 ParticleVoice text frontend. Word → phoneme-sequence dictionary, checked entry-by-entry against `PhonemeSet.js` at construction time. Sits between `TextNormalizer.js` and `G2PModel.js`: a lexicon hit is authoritative, and only words with no entry fall through to G2P's letter-to-sound rules. ## ⚠ Stress digits are the INVERSE of CMUdict Pronunciation strings use CMUdict's familiar `SYMBOL+digit` shape (`HH AH L OW2`), but the digit follows `spec/PhonemeSet-v0.md` §3: **0 = unstressed, 1 = SECONDARY, 2 = PRIMARY** CMUdict is the other way round (`1` = primary, `2` = secondary). Copying CMUdict strings verbatim would therefore silently swap primary and secondary stress on every polysyllabic word — audible as wrong prosody, not as an error. This module cannot detect that mistake for you, so the convention is stated here, in `spec/PhonemeSet-v0.md` §3, and enforced by `MAX_STRESS`-range validation only. ## Authoring rules (validated, not merely documented) - Every symbol must exist in `PhonemeSet.js`. A typo throws at construction, naming the word — never at synthesis time. - A stress digit is legal ONLY on a vowel. Spec §3 defines stress as a per-vowel-instance property, so `S2` is rejected outright rather than quietly ignored. - Omitting the digit on a vowel means unstressed (0). This keeps reduced function words (`the` = `DH AH`) readable. - **At most one** primary stress per word. Not *exactly* one: genuinely reduced function words (`the`, `a`, `of`) carry no stress at all, and forcing a primary onto them would produce robotically over-stressed speech. ## Scope `CORE_LEXICON` is a deliberately curated core, NOT a full dictionary. It covers high-frequency function/content words plus — the reason a lexicon exists at all — words whose spelling actively lies about their pronunciation (`one`, `through`, `women`, `said`, `colonel`, `choir`, `island`), which any letter-to-sound rule set gets wrong. CMUdict is not vendored: this project takes no external data dependencies, and the long tail is `G2PModel.js`'s job. Callers add domain vocabulary (character names, place names) via `add()`/`addAll()`.


parsePronunciation

parsePronunciation(pronunciation, wordLabel = '<unknown>')

Summary: Parse one `SYMBOL+digit` pronunciation string.


Summary: Spoken letter names, for spelling out initialisms. `TextNormalizer.js` flags all-caps tokens with `wasAllCaps` but deliberately does not decide whether they are words or initialisms; `spellOut()` here is the mechanism for the latter.


Summary: Curated core lexicon. Stress digits: 2 = PRIMARY (see the warning above). Grouped by why each entry is here.


particle_voice/frontend/TextNormalizer

Summary: TextNormalizer.js — Phase 3 ParticleVoice text frontend. Turns raw author/LLM text into speakable words plus the per-phrase punctuation-context tags `spec/PhonemeSet-v0.md` §4 defines, ready for `PronunciationLexicon.js`/`G2PModel.js` to lower into phoneme ids. Output shape (`normalizeText`): { raw: string, phrases: [{ words: [{ text, wasAllCaps }], boundary: PUNCTUATION_CONTEXT id,   // §4 tag for THIS phrase's end structural: 'SP' | 'SIL' | null,    // token to insert at the boundary breathLikely: boolean,              // §4: paragraph breaks only }] } One tag per phrase (not per phoneme): `spec/VoicePlan-v0.md` §4's per-phoneme `punctuationContext` array is produced downstream by repeating a phrase's tag across the phonemes its words expand into, which is exactly what that spec means by "repeats within a phrase, changes at boundaries". ## Ordering matters Abbreviation expansion and decimal-point protection run BEFORE phrase splitting, because both consume `.` characters that would otherwise be mistaken for sentence terminators — `Dr. Smith` is one phrase, not two, and `3.14` is one number, not `3` then `14`. Getting this order wrong is the classic text-frontend bug, so it is enforced by construction here rather than left to a later cleanup pass. ## Deliberate non-guesses Two things this module refuses to infer, because guessing wrong is worse than not trying: - **Years.** `1999` could be a year ("nineteen ninety nine") or a quantity ("one thousand nine hundred ninety nine"), and `1500 dollars` proves the quantity reading is often right. Bare integers are ALWAYS read as cardinals; `expandYear()` is exported for callers that actually know the context (e.g. a date field), rather than being applied by a heuristic that would silently corrupt prices and counts. - **Acronym pronunciation.** `NASA` is said as a word, `USA` letter by letter, and nothing in the surface form distinguishes them. All-caps tokens are passed through with `wasAllCaps: true` so `PronunciationLexicon.js` gets first refusal and `G2PModel.js` can fall back to spelling out — the decision belongs where the pronunciation knowledge lives, not here. Dates and clock times are likewise out of scope for `v0` (both are format-ambiguous: `3/4` is a date in one locale and a fraction in another). They are documented as unhandled rather than half-handled.


expandYear

Summary: Read a 4-digit year the way speech does. NOT applied automatically — see this module's "deliberate non-guesses" note; callers that know a number is a year opt in explicitly.


normalizeCharacters

Summary: Unicode/typography normalization: NFKC, curly quotes and dashes to ASCII, whitespace collapsed (paragraph breaks preserved as `\n\n`).


ABBREVIATIONS

Summary: Expanded before phrase splitting, so their trailing `.` never reads as a sentence end. Keys are matched case-insensitively without the period.


Summary: Standalone symbol → word. Applied per token, so `&` between words works but `AT&T` is left to the lexicon.


particle_voice/lab/ManualArticulator

Summary: ManualArticulator.js — Phase 3 ParticleVoice lab. Turns direct articulator controls (the Voice Box demo's sliders) into the SAME physiology-stream shape `ArticulationHead.predict()` produces, so `ParticleVoiceModel.renderPhysiology()` accepts it unchanged. ## Why this is a module and not inline demo code The plan requires the live demo to have "manual articulator mode + neural mode; same `VisualAirflowState`". If manual mode built its own control path — or worse, called `ParticleTract` directly — the two modes would diverge: different smoothing, different validation, a different visualization source. By emitting the identical stream shape, manual mode goes through exactly the same `renderPhysiology()` → `ParticleTract` → `SafetyLimiter` path, and the visualization comes from the same solver snapshots. That makes "same VisualAirflowState" structural rather than a promise, and it means this logic is testable headlessly instead of only by clicking. ## Controls Deliberately the physical knobs, not phonemes — the point of manual mode is to drive the tract directly: - `f0Hz`, `voiced` — glottal source - `constrictionPosition` (0 = glottis, 1 = lips) and `constrictionArea` — one moving constriction, the single most expressive articulator - `pharynxArea`, `lipArea` — the tube's two ends - `nasalCoupling` — velum - `fricationAmplitude` — turbulence at the constriction - `pressure` — subglottal drive A held pose is rendered as a constant stream; `interpolatePoses()` produces a glide between two poses, which is how the demo can sweep a diphthong or a closure/release gesture without hand-authoring every frame.


normalizePose

Summary: Validate and clamp a partial pose into a complete one.


interpolatePoses

Summary: Linear blend between two poses; `voiced` switches at the midpoint since it is boolean.


poseAreaProfile

Summary: Area profile for one pose, using the same 3-control-point `shapedAreaProfile` shape `ArticulationHead` uses for vowels.


Summary: Slider metadata for the demo UI, kept beside the pose it describes so the two cannot drift apart.


particle_voice/lab/TractSagittalView

Summary: TractSagittalView.js — mid-sagittal renderer for the vocal tract area function. Draws what the solver is actually doing, as an anatomical cross-section: the pharynx rising from the glottis, the bend behind the tongue, the oral cavity, the teeth and the lips. The tract's `areas` array IS the picture — section `i` is a slice of tube, and its cross-sectional area is the gap between the tongue surface and the roof of the mouth at that point. ## Why a sagittal view rather than a plot of the numbers A line plot of 32 areas is readable but not *diagnosable*: it does not tell you whether a shape is a plausible mouth. Errors in this project have repeatedly been of exactly that kind — a tract twice the correct length, a "stop" whose closure never closed, a constriction so tight it would whistle. All of those are obvious at a glance in an anatomical view and invisible in a spectrum. ## Geometry The centreline follows an arc, matching how the real tract bends ~90 degrees behind the tongue: the glottis end points up, the lip end points forward. Each section is drawn radially across that arc, with the OUTER boundary fixed (the rigid palate, teeth and rear pharyngeal wall) and the INNER boundary displaced by the section's diameter (the tongue and jaw, which are what move). Diameter, not area, sets the drawn width: a tube's diameter goes as the SQUARE ROOT of its area, so plotting area directly would exaggerate open sections and make near-closures look tighter than they are.


TractSagittalView

Summary: Renders one frame of tract geometry. Stateless by design: it takes the geometry and draws it, so the same view can be driven by `ArticulationHead`'s per-frame stream, by `ManualArticulator`'s live sliders, or by a single static profile from `FormantSolver`, with no notion of playback inside it.


LANDMARKS

Summary: Anatomical landmarks as a fraction of tract length from the glottis, used only for labels and the teeth marker.


particle_voice/model/ArticulationHead

Summary: ArticulationHead.js — Phase 3 ParticleVoice model. Per-frame streams → `NeuralPhysiologyState` (`articulatory/NeuralPhysiologyState.js`): the four control heads plus uncertainty that `ParticleTract` actually consumes. This is where `spec/PhonemeSet-v0.md` §2's articulatory feature vectors finally reach the vocal tract — place becomes a constriction position, manner becomes a constriction degree, height/backness/rounded become a vowel area profile. ## Rule-based, mapping features to geometry Like `ProsodyPlanner`, this is a deterministic feature→geometry mapping, not a trained head. `spec/PhonemeSet-v0.md` §6 already flags its place-position percentages as "provisional estimates ... not measured against real vocal-tract MRI data", and the vowel profiles here are the same kind of estimate: a 3-control-point `shapedAreaProfile` whose constriction position comes from backness, constriction area from height, and lip area from rounding. They are ordered correctly (high vowels narrower than low, front constrictions further forward than back) and produce distinguishable formants — verified in Phase 2 to be what makes a vowel a vowel rather than a buzz — but they are not fitted to measured data. ## Two constraints that are NOT stylistic 1. **`ee` must be strictly positive, even when unvoiced.** It is tempting to set the glottal excitation to 0 on a voiceless frame. That breaks `glottal_lf.js`: its Newton solve for `alpha` computes `areaPrime = (-ee / sin(wg*te)) * (...)`, so `ee = 0` makes the derivative exactly 0 and the update `alpha -= (area - target) / 0` evaluates to **NaN**, which then propagates through the whole waveguide. `NeuralPhysiologyState`'s own validator independently requires `ee > 0`. Voiceless frames therefore use a small positive `UNVOICED_EE` — inaudible, but numerically safe. 2. **Areas are SMOOTHED across frames, per-manner.** Snapping the area profile to a new target at a phoneme boundary is a step change in the waveguide's reflection coefficients, which radiates as a click — the precise artifact Phase 2's exit gate spent effort eliminating. Each manner gets its own time constant because the right smoothing speed is phonetically real, not a tuning knob: a stop's closure and release ARE fast (smearing them destroys the plosive), while a vowel-to-vowel transition is genuinely slow. A single global constant would either smear plosives or click on vowels. ## Output shape `predict()` returns COMPACT typed arrays (one row per frame), not an array of frozen `NeuralPhysiologyState` objects: a few seconds of speech is hundreds of frames, and materializing hundreds of frozen nested objects per utterance would be wasteful when the consumer reads them once, in order. `physiologyStateAt(result, frame)` builds the real validated schema object on demand, so the schema is genuinely exercised (and validated) rather than bypassed.


physiologyStateAt

physiologyStateAt(result, frame, { glottalReflection, lipReflection } = {})

Summary: Materialize one frame of a `predict()` result as a validated `NeuralPhysiologyState`. Built on demand rather than eagerly — see the header's "output shape" note — so the schema and its validators are genuinely exercised without allocating hundreds of frozen objects per utterance.


Summary: Small but strictly positive glottal excitation for voiceless frames. See constraint 1 in the header: 0 would make `glottal_lf.js`'s Newton solve produce NaN.


Summary: Fallback constriction position (0 = glottis, 1 = lips) by vowel backness, and cross-section by height. These are NO LONGER the primary path for the ten monophthongs — see `vowelProfile()`. They were an articulatory-adjective mapping ("front vowels constrict further forward", "high vowels constrict more tightly") that is directionally right but produces whatever formants it happens to produce. Since vowel identity is carried almost entirely by F1/F2, "plausible geometry" is not good enough: uncalibrated profiles pull every vowel toward a neutral timbre, which is heard as speech-like noise with no decipherable vowels. Retained only as the fallback for any vowel with no entry in `VOWEL_AREA_PARAMS`, so adding a phoneme to `PhonemeSet` cannot crash synthesis before it has been calibrated.


Summary: Constriction LENGTH by manner, as a fraction of tract length. A constriction specified as a single control point does not survive `shapedAreaProfile`'s linear interpolation: the profile tapers to the target only exactly AT that point, and since the point rarely lands on a section centre, the realised minimum is far wider than requested. Measured, a stop asking for 0.02 cm^2 actually reached **0.138** — nearly 7x too open. That is not a cosmetic error: a stop whose closure never closes produces neither the silence nor the release burst that identify it, and stops are six of the most frequent consonants in English. The effect got worse when the section count was corrected from 32 to 16, because coarser sections smear a point further. Giving the constriction a length makes it span whole sections, so it genuinely reaches the target area. The values are anatomical: a stop's occlusion covers roughly 1.5-2 cm of the tract, a fricative channel is comparable, and an approximant's narrowing is longer and gentler. At 17 cm / 16 sections one section is 1.06 cm, so 0.10 (1.7 cm) is about 1.6 sections — enough to hold the target rather than merely touch it.


Summary: Constriction cross-section by consonant manner — the "degree" half of spec §2's place/manner pair.


Summary: Smoothing time constant for the glottal excitation amplitude. `ee` switches between `voicedEe` (1.0) and `UNVOICED_EE` (1e-3) — a factor of a THOUSAND, i.e. a 60 dB step. Applied instantaneously that is a hard click at every voicing transition, and an English sentence has one at every voiceless consonant; several per second turns into a sputter that swamps the speech. Real vocal folds cannot change amplitude discontinuously either, so smoothing here is physical as well as necessary. ~18 ms is fast enough to keep a voiceless stop sounding unvoiced (its closure alone is ~25 ms) while removing the step.


Summary: Area-smoothing time constants in ms, by manner — the speed of the ARTICULATOR, nothing else. ## These were far too slow, and every stop in every utterance was a glide The previous values (stop 25 ms, vowel 50 ms) were justified by the claim that "a stop at 25 ms still reaches ~94% of its closure within a typical 70 ms stop, so the plosive is preserved". That reasoning is wrong, and wrong in a way worth recording: it measures the PERCENTAGE OF DISTANCE travelled, but what decides whether a stop is a stop is the ABSOLUTE area reached. Going 94% of the way from a vowel's ~3.0 cm^2 to a 0.02 cm^2 closure leaves **0.18 cm^2** — which is not a closure at all, it is a narrow approximant. So the tract never actually blocked, no stop ever produced silence or a release burst, and all six English stops came out as vowel-like glides. Perceptually the whole utterance collapses toward a string of semivowels, which is exactly the reported "yeah yeah yeah". For a target near zero, percentage-of-distance and absolute-value diverge arbitrarily far. This is the same relative-versus-absolute confusion that produced the earlier bad thresholds in this project. ## Why they can now be as fast as the phonetics actually requires These constants USED to be doing two jobs: modelling articulator inertia AND preventing the per-chunk reflection-coefficient step from clicking. The second job is gone — `tract_waveguide.js` now interpolates `junction_k` per sample, so geometry is continuous no matter how fast the frame targets move. That frees these to be purely phonetic. O'Connor et al. (1957) found listeners hear a formant transition as a STOP below ~50 ms and as a SEMIVOWEL above it, so a stop's gesture must complete well inside 50 ms. At the 10 ms frame interval, tau = 8 ms gives alpha 0.71, reaching 0.026 cm^2 (a real closure) within 50 ms. Approximants stay slower: the same literature has /w j r l/ holding their formant loci for 30-50 ms, i.e. genuinely gliding rather than snapping.


Summary: Stop RELEASE BURST. A plosive is two acoustic events, not one: a silent closure, then a sharp transient when the occlusion opens and the pressure built up behind it escapes. Only the closure was being modelled, so stops had the right silence and then simply faded back into the next vowel. The burst is a primary place cue — much of what distinguishes /p/ from /t/ from /k/ lives in its spectrum — so without it stops are detectable but not identifiable. Pink Trombone models the same thing, adding a transient whenever a diameter goes from zero to positive; this is the equivalent, expressed through the turbulence channel that already exists rather than as a new kernel input, since a burst IS a brief broadband excitation at the constriction. Detection uses the SMOOTHED area, not the phoneme label: what matters is that the tract actually opened, which is the same reason the closure test asserts realised rather than target geometry. `RELEASE_AREA_CM2` is the threshold below which the tract counts as closed, matching the closure the test requires.


Summary: Burst decay. ~8 ms puts the transient plus its aspiration in the 10-30 ms range measured for real plosive releases.


particle_voice/model/LengthRegulator

Summary: LengthRegulator.js — Phase 3 ParticleVoice model. Expands per-PHONEME encoder features and prosody into per-FRAME streams, and — because `spec/VoicePlan-v0.md` §5 explicitly assigns it here — handles resampling between the 100 Hz plan frame rate and whatever rate the consumer runs at (the Activity path is 8 ms/frame = 125 Hz, `ActivityContract-v0` §1; the two "are not required to be equal; any resampling between them is `LengthRegulator.js`'s job, not assumed away here"). ## F0 is interpolated, features are held These two streams are expanded differently, on purpose: - **F0 is linearly interpolated** between phoneme centres. Holding one value per phoneme would produce a pitch STAIRCASE, and a discontinuous F0 is audible as a click or a warble at every phoneme boundary — the exact artifact the Phase 2 exit gate spent effort eliminating from the waveguide. Interpolating between centres (not edges) means each phoneme's nominal pitch is actually reached at its midpoint. - **Feature vectors are HELD** across a phoneme's frames, not interpolated. They are a learned representation, and blending two phonemes' embeddings produces a vector that means neither. Holding is the standard FastSpeech-style length regulation and is what the downstream `ArticulationHead` expects. - **The voiced flag is held** — it is boolean; interpolating it would invent half-voiced frames that no source model can honour. ## Where it runs CPU-side. The encoder's features are produced on GPU, so using them here costs a readback — but the consumer (`ArticulationHead` → `ParticleTract`) needs per-chunk control values in JS anyway (`areas`, `f0Hz`), so the readback is not avoided by moving this to GPU, only relocated. A GPU `length_regulate` gather kernel is a straightforward follow-up IF profiling later shows the feature upload dominating; the condition that would justify it is stated so the decision is revisitable rather than forgotten.


regulateLength

regulateLength({ durationFrames, f0Hz, voiced, features = null, featureDim = 0, maxFrames = 1 << 20 })

Summary: Expand per-phoneme streams to per-frame streams. durationFrames: ArrayLike<number>, f0Hz: ArrayLike<number>, voiced: ArrayLike<number>, features?: ArrayLike<number>, featureDim?: number, maxFrames?: number, }} input


resampleFrames

resampleFrames(regulated, fromRateHz, toRateHz, featureDim = 0)

Summary: Resample per-frame streams from one frame rate to another — the job `spec/VoicePlan-v0.md` §5 assigns to this module for crossing between the 100 Hz plan rate and the 125 Hz Activity rate. F0 is linearly interpolated (it is continuous); voicing and the phoneme index use NEAREST-neighbour, because both are categorical and interpolating them would invent values that do not exist (a half-voiced frame, or a fractional phoneme id).


particle_voice/model/ParticleVoiceModel

Summary: ParticleVoiceModel.js — Phase 3 ParticleVoice model. Wires the whole Phase 3 path into one call: **text → PCM**. TextNormalizer → G2P/lexicon → PhonemeSequence → PhonemeEncoder (GPU, optional) → ProsodyPlanner → LengthRegulator → ArticulationHead → NeuralPhysiologyState stream → ParticleTract (GPU acoustic solver) → SafetyLimiter → PCM ## Why there is no separate `SourceVocoder` in Phase 3 The plan lists "`SourceVocoder.js` basic harmonic + noise + waveguide output". In Phase 3 that IS `ParticleTract`: it already runs `glottal_lf` (harmonic source), two `constriction_noise` streams (aspiration at the glottis, frication at the constriction) and the Kelly-Lochbaum waveguide, with lip radiation applied internally. Adding a `SourceVocoder` wrapper that only forwarded to it would be a layer with no content. Phase 4's full source-filter Fourier vocoder (bounded spectral residual, MPD/MRD refinement, IFFT + WOLA) is where that module earns its own file — and the seam for it is `renderPhysiology()`, which is already separate from the text frontend. ## Frame rate vs chunk rate: the decimation that matters `ArticulationHead` emits physiology at the 100 Hz plan frame rate (10 ms), but `ParticleTract.renderChunk()` accepts **one control set per chunk** — one area profile, one f0, one constriction index. At 32 kHz with 1024-sample chunks a chunk is 32 ms, so roughly 3 physiology frames fall inside each one. Something has to reduce them, and the choice is audible: - **Areas are AVERAGED** across the frames in a chunk. Picking one frame (say the first) would throw away the smoothing `ArticulationHead` deliberately applied and reintroduce exactly the reflection-coefficient step it exists to prevent. - **F0 is averaged** for the same reason: a chunk-rate pitch staircase is audible as a warble. - **Constriction index is taken by MAJORITY**, not averaged — it is a position index, and the mean of "junction 20" and "junction 6" is junction 13, a place no phoneme asked for. - **Voicing/amplitudes are averaged**, which naturally cross-fades a voiced→voiceless transition across the chunk rather than switching hard. This decimation is the one place a Phase 3 discontinuity could still enter, so it is concentrated in `_decimateChunk()` and tested directly. ## Breath reservoir `BreathReservoirState`'s one-pole lag is applied CPU-side per chunk to turn `ArticulationHead`'s target subglottal pressure into an actual pressure with inertia, and that pressure scales the glottal excitation. This uses the same `lagRateFromCompliance` formula the GPU `breath_reservoir` kernel expects pre-computed, so the two agree; running it here rather than on GPU avoids a readback for a single scalar per chunk. Documented as a deliberate simplification: a per-SAMPLE reservoir would need the kernel.


particle_voice/model/PhonemeEncoder

Summary: PhonemeEncoder.js — Phase 3 ParticleVoice model. Phoneme ids → contextual per-token feature vectors, entirely on GPU: a `PHONEME_COUNT`-row embedding table followed by two Conv1D blocks, each with SiLU, and a final RMSNorm. This is the first Phase 3 component that runs the Phase 1 ParticleNN kernels in composition. Pipeline (7 dispatches, ONE submit per `encode()`): ids [T] → embedding_f16   → [T, D]   sequence-major → transpose_2d    → [D, T]   channel-major  (see "layout" below) → conv1d_w8a16    → [H, T] → silu_f16        → [H, T] → conv1d_w8a16    → [O, T] → silu_f16        → [O, T] → transpose_2d    → [T, O]   sequence-major → rmsnorm_f16     → [T, O]   per-token normalization ## Layout: why two transposes are load-bearing The Phase 1 kernels disagree about which axis is major, and each is right for itself: `embedding_f16` gathers rows so it emits **sequence-major** `[numIds, embedDim]`; `conv1d_w8a16` slides a kernel along time so it reads **channel-major** `[inChannels, seqLen]`; `rmsnorm_f16` normalizes rows, so per-token normalization needs sequence-major again. Composing them without the axis swaps would not error — it would silently convolve across the feature axis and produce scrambled activations. `core/transpose_2d.js` was added for exactly this, and the two transposes here are the minimum: SiLU is elementwise and therefore layout-agnostic, so both convolutions run back-to-back in channel-major before a single swap back for the norm. ## Weights are placeholders, and this file says so No Phase 3 training exists yet, so `createDeterministicEncoderWeights()` produces **seeded pseudorandom INT8 weights**. They make the pipeline runnable, shape-correct and reproducible end-to-end; they do NOT make it meaningful. The encoder's output is a deterministic function of its input, which is what Phase 3's downstream components need in order to be built and tested — but nothing here has learned anything about phonetics. Real weights arrive with training; the `weights` argument is the seam. ## Quantization Conv weights are INT8 with a per-output-channel scale, matching `conv1d_w8a16.js`'s packed layout exactly (4 int8 per `u32`, flattened `(inCh, tap)` index, `inChannels * kernelWidth` a multiple of 4 — validated in the constructor rather than left to fail as garbage output).


Summary: Pack int8 weights into `u32`s, 4 per word, matching `conv1d_w8a16.js`'s documented `(inCh, tap)` flattening.


createDeterministicEncoderWeights

createDeterministicEncoderWeights(config = {}, seed = 1337)

Summary: Build a complete, reproducible placeholder weight set. See the header: this is scaffolding so the pipeline can be exercised, not a trained model.


Summary: Small by design: Phase 3 is a "tiny deterministic single-speaker model", not a production encoder.


particle_voice/model/ProsodyPlanner

Summary: ProsodyPlanner.js — Phase 3 ParticleVoice model. Predicts per-phoneme **duration**, **F0** and a **voiced flag** from a `PhonemeSequence` (`spec/VoicePlan-v0.md` §4), at the plan frame rate of 100 Hz / 10 ms per frame that spec fixes. ## Rule-based, and the plan says "minimal" This is a deterministic rule model, not a trained one: durations come from per-manner base values scaled by stress and boundary position, and F0 from a declination line with stress excursions and a terminal contour selected by punctuation context. Phase 3's goal is "a tiny deterministic single-speaker model", and a learned duration/F0 predictor needs aligned speech data this project does not have. The numbers below are therefore **plausible phonetics, not measured values** — they are ordered correctly relative to each other (stops shorter than fricatives, diphthongs longer than monophthongs, stressed longer than unstressed), which is what makes speech intelligible, but they are not fitted to any corpus. `plan()` takes an optional `overrides` bag so a caller can retune without editing this file, and the class boundary is where a learned predictor slots in. ## What it deliberately does not do - **No per-phoneme F0 contour inside a phoneme.** One target per phoneme; smoothing into a continuous per-frame curve is `LengthRegulator.js`'s job, because that is where frame counts exist. Emitting a flat value per phoneme here and interpolating there avoids the audible pitch STEP that a naive per-phoneme hold would produce. - **No accent/emotion conditioning.** `spec/VoicePlan-v0.md` §3's factorized conditioning channels exist, but wiring them is Phase 4+; pretending to condition on them now would be fake.


Summary: `spec/VoicePlan-v0.md` §5: the plan frame rate is 100 Hz (10 ms/frame).


Summary: Base durations in MILLISECONDS by articulatory manner. Relative ordering is the part that matters (see header); absolute values are plausible defaults.


Summary: Structural-token durations, keyed by the punctuation context they sit at (spec §4's Effect column: comma-level is a short pause, terminals are full silences, a paragraph is longer still).


particle_voice/nn/autodiff/BackwardGraphCompiler

Summary: BackwardGraphCompiler.js — Phase 1 ParticleNN autodiff STUB. **Not wired into anything until Phase 11.** Walks a `ParticleTape`'s recorded forward ops in reverse and builds a `GradientGraph` node per op that has a registered `BackwardRuleRegistry` rule (ops without one are skipped, not silently faked with an identity/zero gradient). Refuses to compile anything if the registry is completely empty — the honest current state (no backward rules exist yet for any real kernel) rather than returning a plausible-looking but meaningless empty graph.


particle_voice/nn/autodiff/BackwardRuleRegistry

Summary: BackwardRuleRegistry.js — Phase 1 ParticleNN autodiff STUB. **Not wired into anything until Phase 11.** Maps a forward op name (from `KernelRegistry.js`'s allowlist, e.g. `matmul_w8a16`) to a backward rule: a function that, given the recorded forward op's inputs/outputs/params plus the gradient(s) flowing into its outputs, produces the gradient(s) with respect to its inputs. No rules are registered for any real kernel yet — that is genuinely Phase 11 work (differentiating through INT8 quantization and FP16 rounding correctly is a real design problem, not something to hand-wave here) — this file only fixes the registry shape.


particle_voice/nn/autodiff/GradientGraph

Summary: GradientGraph.js — Phase 1 ParticleNN autodiff STUB. **Not wired into anything until Phase 11.** The backward-pass analogue of `ParticleGraph.js`: nodes are backward-rule applications rather than forward kernels, gradient-tensor names rather than activation-tensor names. Reuses `ParticleGraph`'s DAG/topological-sort machinery directly (a gradient graph's dependency structure is a plain DAG exactly like a forward graph's — no different algorithm is needed) rather than duplicating it, and adds `accumulate()` for the one thing a gradient graph needs that a forward graph never does: multiple edges producing a gradient for the *same* tensor must sum, not overwrite (a tensor used as input to two or more forward ops receives a gradient contribution from each one).


GradientGraph

class GradientGraph extends ParticleGraph


particle_voice/nn/autodiff/ParticleTape

Summary: ParticleTape.js — Phase 1 ParticleNN autodiff STUB. **Not wired into anything until Phase 11** (browser training), per the plan's explicit phasing. This establishes the recording-tape contract a future forward pass will write to and `BackwardGraphCompiler.js` will read from — it is NOT exercised by any real training loop yet (R7's training spike used its own hand-wired `TinyAutodiff.js`, deliberately separate from this production module). Records forward-pass operations (op name, input tensor names, output tensor names, and any params the backward rule will need) in execution order, reverse-mode style.


particle_voice/nn/core/ParticleGraph

Summary: ParticleGraph.js — Phase 1 ParticleNN core. The uncompiled neural compute graph a model author builds: named nodes with an `op` (must be in `PRNN-v0.md` §3's operator allowlist) and named input/output tensor slots. `PRNNCompiler.js` (Phase 2+) lowers a `ParticleGraph` into a `PRNN-v0` §8 execution plan / `ParticleProgram`; this file only builds and validates the DAG itself (topological sort, cycle/dangling-reference detection) — it does not compile or dispatch anything.


particle_voice/nn/core/ParticleKernel

Summary: ParticleKernel.js — Phase 1 ParticleNN core. Kernel specialization: resolves a `KernelRegistry` entry + concrete params (`useShaderF16`, kernel-specific options like `eps`) into one ready-to-compile unit — WGSL source, entry point, workgroup size, and the explicit bind-group-layout entries `PipelineCache.js`/`KernelValidation.js` proved necessary for `f16` kernels (`DeviceContract-v0.md` §8).


particle_voice/nn/core/ParticleModel

Summary: ParticleModel.js — Phase 1 ParticleNN core. A loaded model container: a compiled `ParticleProgram` plus its weight memory (`WeightPageStore`/`WeightBindWindow`/`WeightPageUploader`) and `.prnn` header metadata, as one object. Deliberately thin scaffolding at Phase 1 — no real model exists yet (`PRNNReader.js`/`PRNNCompiler.js` that would actually produce one of these from a `.prnn` file are Phase 2+ work); this fixes the shape that loader will hand to callers.


particle_voice/nn/core/ParticleProgram

Summary: ParticleProgram.js — Phase 1 ParticleNN core. A compiled dispatch sequence: `ParticleGraph`'s topological order lowered into an ordered list of `{ nodeId, kernel: ParticleKernel, inputs, outputs }` steps, one `ParticleKernel` per graph node. `compileAll()` builds every step's GPU pipeline via a shared `PipelineCache` up front (so first dispatch has no compile-stall); actually running the steps each frame is `NeuralExecutor.js`'s job, not this file's.


particle_voice/nn/core/ParticleTensor

Summary: ParticleTensor.js — Phase 1 ParticleNN core. GPU-resident tensor with explicit shape/strides/dtype/quant metadata. Mirrors `agi/tensor/Tensor.js`'s ownership/readback/dispose conventions (reference only, not imported at runtime — the plan requires ParticleNN to stay decoupled from the LLM runtime) but adds the metadata ParticleVoice actually needs: multiple dtypes (`f32`/`f16`/`i8`/`u32`) and optional per-block INT8 quantization scale/zero data alongside the raw buffer.


ParticleTensor

Summary: `quant`, when present, describes INT8 block quantization for `dtype: 'i8'` tensors: `{ scheme: 'int8-block', blockSize: u32, scales: Float32Array }` — one scale per `ceil(elementCount / blockSize)` block, dequantized as `value = i8Value * scales[blockIndex]` (zero-point-free, symmetric quantization; PRNN-v0's `dequant_int8` kernel implements this exact scheme — see `nn/kernels/quantization/dequant_int8.js`).


contiguousStrides

Summary: Row-major contiguous strides for `shape` (element units, not bytes).


wrapReadbackBuffer

Summary: Wrap a raw readback `ArrayBuffer` in the correct typed-array view for `dtype`. Shared by `ParticleTensor` and `ParticleTensorView` so readback interpretation lives in exactly one place.


particle_voice/nn/core/ParticleTensorView

Summary: ParticleTensorView.js — Phase 1 ParticleNN core. A zero-copy view over a parent `ParticleTensor`'s GPU buffer: a different logical shape/strides/byte-offset into the SAME underlying buffer, with no `device.queue` copy and no new buffer allocation. Used for things like binding a single weight page's sub-range, or reading one layer's slice out of a stacked activation tensor, without a `copyBufferToBuffer` pass.


particle_voice/nn/kernels/KernelRegistry

Summary: KernelRegistry.js — Phase 1 ParticleNN kernel infrastructure. Maps kernel name -> `shader(params)` generator, mirroring `agi/llm/kernels/KernelRegistry.js`'s design (reference only, not imported — same LLM-runtime-decoupling rule as `PipelineUtils.js`). Each generator here also declares `inputCount` (how many read-only storage bindings it expects before the single read_write output binding) so `createShaderModule`'s caller can build the matching explicit bind-group layout required for `useShaderF16` kernels (see `DeviceContract-v0.md` §8 / `KernelValidation.js`'s `createExplicitLayoutPipeline`) without re-deriving each kernel's binding count by hand.


particle_voice/nn/kernels/ShaderComposer

Summary: ShaderComposer.js — Phase 1 ParticleNN kernel infrastructure. WGSL chunk composition with dependency resolution: register named chunks (each optionally depending on other chunks), then compose an ordered, deduplicated concatenation for a kernel body — so e.g. `int8_pack`'s `unpack_int8` is emitted exactly once even if two requested chunks both depend on it, and always appears before any chunk/body that calls it.


createDefaultShaderComposer

Summary: Pre-registers the Phase 1 WGSL chunk set (`math_common`, `int8_pack`, `complex_math`) so a kernel generator can immediately `compose(['math_common', 'int8_pack'], body)` without registering each chunk itself. `f16_utils.js` has no WGSL fragment (it is a host-side-only JS<->f16 bit-conversion utility, per its own module doc) so it is not registered here.


particle_voice/nn/kernels/ShaderDiagnosticFormatter

Summary: ShaderDiagnosticFormatter.js — Phase 1 ParticleNN kernel infrastructure. Formats `GPUCompilationMessage[]` (from `module.getCompilationInfo()`) into human-readable diagnostics, optionally resolving each message's line number through a `WGSLSourceMap` to name which chunk it came from — a strict superset of `PipelineUtils.js`'s `assertShaderCompiled` error text (which only reports line:col + message, no chunk attribution).


formatCompilationMessages

formatCompilationMessages(messages, { sourceMap = null } = {})


formatDiagnosticReport

formatDiagnosticReport(kernelName, messages, options = {})


formatErrorsOnly

formatErrorsOnly(kernelName, messages, options = {})

Summary: Only the `type === 'error'` messages, formatted — the subset that should fail a build/test, distinct from warnings/info the caller may want to log but not fail on.


particle_voice/nn/kernels/WGSLSourceMap

Summary: WGSLSourceMap.js — Phase 1 ParticleNN kernel infrastructure. Generated-WGSL-line -> originating-chunk mapping. Kernels compose WGSL from named chunks (`ShaderComposer.js`) plus their own body; when the GPU compiler reports an error at line N, this turns that bare line number back into "which named chunk (or the kernel's own body) that line came from" — the JS source itself isn't line-mapped (these are template literals, not a separate compilation step), but chunk attribution is usually enough to know where to look.


particle_voice/nn/kernels/WgslPreprocessor

Summary: WgslPreprocessor.js — Phase 1 ParticleNN kernel infrastructure. Tagged template `wgsl\`...\`` supporting `#if`/`#else`/`#endif` and `#for`/`#endfor` directives, evaluated at kernel-generation time (trusted JS call sites only — this is code generation for the kernel author, not a user-input template engine, so a plain `Function` eval for `#if` expressions is an acceptable, deliberate choice here). Directive syntax (each directive must be alone on its own line): #if <jsExpression>      // evaluated with no closure — pre-interpolate #else                   //   values via `${...}` before the directive #endif                  //   line if the condition depends on a variable #for <name> in <start>..<end>   // half-open range, start/end integers #endfor                          // body may reference `#{name}` (a // literal placeholder distinct from // `${}`, since `${}` is already // resolved by template-literal // substitution before this runs)


preprocessWgsl

Summary: Process `#if`/`#else`/`#endif` and `#for`/`#endfor` directives in a flat WGSL source string, line by line.


wgsl

Summary: Tagged template: joins `strings`/`values` per normal template-literal semantics, then runs `preprocessWgsl` over the result.


particle_voice/nn/kernels/activation/silu_f16

Summary: activation/silu_f16.js — Phase 1 ParticleNN kernel. Elementwise SiLU (`x * sigmoid(x)`), composed from `chunks/math_common.js`'s `siluScalar` rather than re-deriving the sigmoid inline.


siluF16Shader({ useShaderF16 = false } = {})


particle_voice/nn/kernels/articulatory/breath_reservoir

Summary: breath_reservoir.js — Phase 2 ParticleNN articulatory kernel. First-order lag filter: `pressure[n] = pressure[n-1] + (target[n] - pressure[n-1]) * lag_rate`, matching `risk/BreathReservoir.js` (R6A)'s proven recurrence exactly (`lag_rate = 1 - exp(-1/(compliance*sampleRate))` is computed by the caller and passed in, not recomputed here). A first-order IIR recurrence is inherently sequential in time, but each of up to N simultaneous voice/capsule *instances* is fully independent — `workgroup_id.x` selects the instance, and a single thread per workgroup walks that instance's whole chunk sequentially (cheap: a chunk is a few hundred samples, and GPU dispatch overhead dominates at that size anyway, so there is nothing to gain from parallelizing within one instance's strictly-sequential recurrence).


particle_voice/nn/kernels/articulatory/constriction_noise

Summary: constriction_noise.js — Phase 2 ParticleNN articulatory kernel. Turbulence/frication noise generator for a chosen internal tract junction (fricatives, plosive bursts) — deliberately a SEPARATE kernel from `speech/noise_source.js` rather than reusing it directly, because the two have genuinely different shapes: `noise_source.js` generates one flat, constant-amplitude signal for the whole dispatch (aspiration noise mixed at the glottis, `risk/GlottalSource.js` (R6)'s `aspirationLevel`), while frication noise needs a per-sample, per-instance TIME-VARYING amplitude envelope (driven by how tightly a constriction is formed, which changes continuously as an articulator moves) and per-instance start index/seed so N simultaneous voices/characters never share correlated turbulence. Sharing the same underlying hash rather than inventing a second one is still correct reuse: `noise_at()` below is the identical two-round Murmur-style avalanche hash as `noise_source.js`/ `risk/GlottalSource.js`'s `noiseAt` (same technique as `StreamingContinuitySource.js`), just applied per-instance with an amplitude that varies per output sample instead of being a single dispatch-wide uniform scalar. `tract_waveguide.js` does not (yet) consume this kernel's output at a `constrictionIndex` internally (its own docstring flags that as deferred, matching R6's `step()`'s own `constrictionIndex`/ `constrictionNoiseSample` option) — this kernel only produces the turbulence signal; injecting it into a specific internal tract junction is a `ParticleTract` orchestration concern for a later phase, once the model that decides the constriction's time-varying amplitude envelope exists. Three spectral shapes are supported: - `direct` `noise_at()` (flat/white-ish), the default reference; - `lowpass` block-interpolated `noise_at()` for glottal aspiration, giving the noise a predominantly low-frequency, breathy character rather than a hissy fricative one; - `derivative` first-differenced `noise_at()` (blue, +6 dB/octave) for oral frication and plosive bursts, where the high-frequency emphasis makes consonants like /s/ and /t/ pop without changing the vowel source.


constrictionNoiseShader

constrictionNoiseShader({ spectralShape = 'direct', lowpassHold = 32 } = {})


particle_voice/nn/kernels/articulatory/glottal_lf

Summary: glottal_lf.js — Phase 2 ParticleNN articulatory kernel. The full Liljencrants-Fant (LF) glottal flow DERIVATIVE model, replacing `speech/harmonic_source.js`'s raised-cosine "LF-lite" stand-in (`risk/GlottalSource.js` (R6)'s own docstring calls that shape a deliberate simplification and names `articulatory/glottal_lf.js` as the real implementation it stands in for). Per period, given control instants Te (glottal closure / main excitation, 0 < Te < T0), Tp (peak-flow instant, 0 < Tp < Te), Ta (return-phase time constant), and Ee (excitation strength), the classic two-segment LF waveform is: E(t) = E0 * exp(alpha*t) * sin(wg*t)                            0<=t<Te E(t) = -(Ee/(eps*Ta)) * (exp(-eps*(t-Te)) - exp(-eps*(T0-Te)))  Te<=t<T0 with `wg = pi/Tp`. `E0`, `alpha`, and `eps` are NOT free parameters — they are pinned by three physical constraints this file's author derived and verified algebraically (rather than recalling a textbook formula from memory, to avoid presenting an unverified transcendental equation as fact): 1. Boundary match at Te: E(Te-) = -Ee, giving `E0 = -Ee / (exp(alpha*Te) * sin(wg*Te))`. 2. `eps` solves `eps*Ta = 1 - exp(-eps*(T0-Te))` (the standard LF return-phase time-constant equation) via Newton-Raphson with the exact analytic derivative `f(eps)=eps*Ta-1+exp(-eps*(T0-Te))`, `f'(eps)=Ta-(T0-Te)*exp(-eps*(T0-Te))`, started at `eps0=1/Ta`. 3. `alpha` solves the area-balance constraint (glottal flow is periodic, so its derivative integrates to zero over one full period): substituting constraint 1 into the closed-form `int[0,Te] E0*exp(alpha*t)*sin(wg*t) dt` (standard `exp(at)sin(bt)` antiderivative) and the closed-form return-phase integral (simplified using constraint 2) gives `Area(alpha) = -Ee/sin(wg*Te) * N(alpha)/D(alpha)` where `N(alpha) = alpha*sin(wg*Te) - wg*cos(wg*Te) + wg*exp(-alpha*Te)` and `D(alpha) = alpha^2 + wg^2`; this is solved for `Area(alpha) = Ee/eps - Ee*(T0-Te)*exp(-eps*(T0-Te))/(eps*Ta)` via Newton-Raphson using the exact analytic derivative of `N/D` (quotient rule from `N'(alpha) = sin(wg*Te) - wg*Te*exp(-alpha*Te)`, `D'(alpha) = 2*alpha`), started at `alpha0 = 0` (a safe, singularity- free starting point: `D(0) = wg^2 != 0`). Both Newton solves run for a fixed, generous iteration count (8 each) — cheap scalar math, and every thread in this kernel redoes the same per-instance solve redundantly rather than solving once and broadcasting through workgroup memory: unlike `tract_waveguide.js`'s per-sample recurrence, LF generation has NO cross-sample dependency at all (like `harmonic_source.js`/`noise_source.js`), so the simplest correct design is one thread per (instance, sample) output element, matching those kernels' flat-parallel convention exactly. If per-period solve cost ever matters at scale, hoisting it into its own once-per-instance kernel (mirroring `tract_transfer.js`'s role for `tract_waveguide.js`) is a straightforward follow-up, not a design change. Every control instant is expressed in SAMPLES (not seconds), matching `harmonic_source.js`'s `period_samples`/`open_samples`/`start_phase` convention and this kernel's own absolute-sample-counter continuity requirement across chunk boundaries.


solveLfShapeParameters

solveLfShapeParameters({ t0, te, tp, ta, ee })

Summary: CPU-side reference implementation of the exact same closed-form/Newton- Raphson math the WGSL kernel below runs — used by the parity test and documented here as the canonical derivation any future GPU-side per-instance solve kernel (see docstring) must also match bit-for-bit in structure (same iteration count, same starting values).


evaluateLfSample

evaluateLfSample(cyclePos, { t0, te, ta, ee, wg, eps, alpha, e0 })

Summary: CPU-side reference for a single sample's LF value, given already-solved shape parameters — matches the WGSL kernel's per-sample branch exactly.


particle_voice/nn/kernels/articulatory/lip_radiation

Summary: lip_radiation.js — Phase 2 ParticleNN articulatory kernel. Idealized lip-radiation filter: a first difference, `radiated[n] = raw[n] - raw[n-1]`, matching `risk/KellyLochbaumWaveguide.js` (R6)'s `step()` exactly (`radiated = outputRaw - prevOutputRaw`) — the differentiating characteristic of radiation from an open end. Despite being defined via a one-sample recurrence, this is embarrassingly parallel: `raw[n-1]` for `n > 0` is just the previous element of the *same* input buffer (already fully computed, no accumulation), and only `n = 0` needs externally-supplied state (`prev_last_sample`, the previous chunk's final raw sample) — so every output sample can be computed independently with no sequential loop at all.


particle_voice/nn/kernels/articulatory/nasal_junction

Summary: nasal_junction.js — Phase 2 ParticleNN articulatory kernel. Production nasal branch: a second, independent Kelly-Lochbaum tube chain generalizing risk/KellyLochbaumWaveguide.js (R6)'s proven createNasalBranch().step() to run a whole sample chunk times N instances on GPU, using the exact same one-thread-per-section / double-buffered-workgroup-memory / sequential-workgroupBarrier-per-sample structure as tract_waveguide.js (see that file's docstring for the full parallelization rationale, not repeated here). Differences from tract_waveguide.js's oral tract, matching R6's createNasalBranch() exactly: - The near end has no reflection term at all: nextRight[0] = inputFlow (R6's oral tract instead does sourceFlow + glottalReflection * left[0]) — the nasal branch is driven directly by a coupled fraction of an oral-tract tap signal, not its own glottal source. - The far end uses a single nostrilReflection coefficient (R6's lipReflection equivalent). `radiation_difference` selects whether the first-difference radiation filter is applied: 1.0 for R6's flow-domain parity path, 0.0 for ParticleTract's LF-derivative-domain production path. This kernel does not itself compute the oral-tract tap signal fed into inputFlow — that is tract_waveguide.js's nasal_tap output block, and inputFlow is an ordinary per-instance per-sample input buffer here, matching every other Phase 2 kernel's convention of not silently reaching into another kernel's state. input_gain is the velum/nasal COUPLING fraction. R6's createNasalBranch() has no such parameter because its caller pre-multiplied the tap signal (nasalCoupling * nasalTapSignal) before calling step(). On GPU that would force either a whole extra scaling kernel or a CPU round-trip in the middle of an otherwise GPU-resident chunk, so the scalar lives here instead — exactly where this file's own docstring already says the branch is "driven by a coupled fraction of an oral-tract tap signal". input_gain = 1.0 reproduces R6's step() bit-for-bit, which is what the parity test in phase2-dsp.html pins.


nasalJunctionOutputLayout

Summary: Shared flat-output-buffer layout — mirrors tractWaveguideOutputLayout() exactly (same three persisted-state pieces plus the radiated signal), kept as its own function per kernel so a future layout change to one tract type can never silently affect the other.


particle_voice/nn/kernels/articulatory/physiology_feedback_reduce

Summary: physiology_feedback_reduce.js — Phase 2 ParticleNN articulatory kernel. Reduces a high-rate (32 kHz per `ARCHITECTURE.md` §3) per-sample waveguide/tract signal (e.g. `tract_waveguide.js`'s radiated output, or a raw right/left traveling-wave channel) down to one (mean, RMS) pair per fixed-size frame — the `PhysiologyFeedbackState` `ARCHITECTURE.md` §4 names as "a reduced/decimated feedback signal from the acoustic solve back toward the model", so the model's control-rate loop (100 Hz per `ARCHITECTURE.md` §3) can see a compact summary of what the acoustic solver actually produced last frame without reading back and processing the full high-rate PCM itself. `samples_per_frame` is expected to be one of the plan's fixed legal chunk buckets (first 12/16/24 frames, steady 32/48/64 frames at the activity frame size, or a caller-chosen block size for this specific feedback path) — this kernel does not itself enforce bucket membership, it just requires `num_samples == num_frames * samples_per_frame` exactly (no partial trailing frame), matching every other Phase 2 kernel's expectation that framing/bucketing is decided by the caller (`stft.js`/`overlap_add.js` take the same stance). One thread per (instance, frame) pair does its own small sequential reduction over `samples_per_frame` samples — deliberately not a parallel-reduction tree, since typical frame sizes here (tens to a few hundred samples) are small enough that per-thread sequential summation is simpler and cheaper than the synchronization a tree reduction would need, the same reasoning `breath_reservoir.js`'s per-instance single-thread recurrence gives for its own necessarily-sequential loop (this kernel's reduction isn't sequential for correctness, just small enough that a loop is the pragmatic choice).


physiologyFeedbackReduceOutputLayout

Summary: Packed-output layout: [mean block (num_instances*num_frames)] then [rms block (num_instances*num_frames)] — kept as its own function so the WGSL offsets and any JS-side reader stay in lockstep, matching `tractWaveguideOutputLayout()`'s precedent.


particle_voice/nn/kernels/articulatory/signal_add

Summary: signal_add.js — Phase 2 ParticleNN articulatory kernel. Elementwise sum of two equal-shaped signals: `out[i] = a[i] + b[i]`. `ParticleTract.js` needs exactly this twice, at the two points its signal graph merges: 1. **Glottal source mix** — `voicing` (`glottal_lf.js`) + `aspiration` (`constriction_noise.js` with a slowly-varying envelope) into the `source_flow` that `tract_waveguide.js` injects at the glottis. 2. **Output mix** — the oral tract's `radiated` signal plus the nasal branch's `radiated` signal (`nasal_junction.js`, already scaled by its own `input_gain` coupling), producing the final PCM. Note that FRICATION is deliberately NOT a third term here. Turbulence belongs at the constriction, not the glottis, so it is fed to `tract_waveguide.js`'s `constriction_noise` input and injected at `constriction_index` — only the cavity in front of the constriction then filters it, which is what makes a fricative sound like a fricative rather than a whisper. That is why this kernel takes two inputs and not three. This kernel exists because nothing in the registry could do it. There was no elementwise-add op, and `speech/crossfade.js` — the only existing two-signal combiner — is NOT a mixer: it sweeps its ratio across the buffer (`t = i/(length-1)`), making it a one-shot transition. Using it here would ramp the voicing/aspiration balance from 0 to 1 across every chunk. There are no gain inputs, deliberately: every amplitude this could scale is already a first-class parameter of the kernel that PRODUCES the term — `glottal_lf.js`'s per-instance `ee`, `constriction_noise.js`'s per-sample `amplitude_envelope`, and `nasal_junction.js`'s `input_gain`. Duplicating that control here would invite the two copies to disagree. Flat-parallel with no cross-sample dependency (one thread per output element), matching `lip_radiation.js`/`constriction_noise.js`. Instance layout is implicit: the caller passes `total_elements = num_instances * num_samples` and both inputs share the same `[num_instances, num_samples]` row-major shape, so no per-instance indexing is needed at all.


particle_voice/nn/kernels/articulatory/tract_transfer

Summary: tract_transfer.js — Phase 2 ParticleNN articulatory kernel. Converts a per-instance tube cross-sectional area profile into Kelly- Lochbaum junction reflection coefficients — the exact formula `risk/KellyLochbaumWaveguide.js` (R6)'s proven `reflectionCoefficients()` uses: `k[i] = (A[i] - A[i+1]) / (A[i] + A[i+1] + 1e-6)`. `tract_waveguide.js`'s own docstring notes junction coefficients are "computed CPU-side by the caller... not recomputed here", matching `mel_filterbank.js`'s precedent for a fixed-shape precomputation — that is still correct for a STATIC area profile. This kernel exists for the case the plan's `NeuralPhysiologyState.TractGestureHead` implies: an area profile predicted fresh every control frame (100 Hz per `ARCHITECTURE.md` §3), which would otherwise force a GPU-to-CPU-to-GPU round trip every frame just to run one elementwise division — exactly the per-frame readback stall the plan's Production Contract and `engine/core/gpu/VGPUReadbackQueue.js`-style staged-readback convention are meant to avoid. Keeping the whole `TractGestureHead -> junction_k -> tract_waveguide.js` path on GPU is what "single owned execution path" requires once area profiles become per-frame instead of per-utterance. Purely elementwise across `(instance, junction)` pairs — no inter-junction dependency at all (unlike `tract_waveguide.js`'s per-sample recurrence), so this is one thread per output element with no workgroup memory or barriers needed.