Spark voice surface

AI voice listen/speak (STT/TTS), reviewer, coder (codifer), copier, written voice models, and PSTN (off by default). Asm: asm/voice_ops.s. Companions: ./spark-stt-tts, ./spark-pstn-dial.

Whole-model map (behaviors + ears/eyes/speaking + thinking): Model aspects.

Voice ask (dump / binary Q&A): ears → SPARK_BC context → TinyCoder or dump facts → speak — VOICE_ASK.md (./spark-ask, ./spark-speak-ask). No OpenBin login.

Spark stays a generic language; optional vendor voice ids are config only.

Syntax

listen "input.wav" -> transcript
        listen -> user # --live: mic (arecord)
        speak "Hello" -> "out.wav"
        speak reply -> "out.wav" # text from last_val
        speak with model NAME

        voice review PATH_OR_ID -> report
        voice code request "..." -> spark_or_asm
        voice copy from SRC to DST -> voice_model
        voice model write NAME spec { ... } -> path
        voice model load NAME -> model

        voice pstn status -> st
        voice pstn dial "+1…" -> call
        voice pstn hangup "CALL_ID"
        

Live STT / TTS (listen / speak)

--dry-run / make test stay offline (stub transcript + tiny WAV marker). --live forks ./spark-stt-tts — real WAV I/O, mic, synth, or HTTP vendors when explicitly gated on.

Mode Behavior
Dry Stub transcript; write_stub_wav honors speak … -> "path" (default spark-out.wav)
Live listen file Open RIFF/WAVE; sidecar .intent.txt / .txt, or SPARK_STT_CMD, or local openai-whisper, or HTTP if net on
Live listen bare --mic → arecord (S16_LE 16 kHz mono); then same STT path
Live speak Built-in PCM synthesizer → real 16-bit WAV; or SPARK_TTS_CMD; or HTTP if net on; optional aplay

Network vendors — OFF by default

Gate Enables
SPARK_STT_NET=1 HTTP STT via SPARK_STT_URL
SPARK_TTS_NET=1 HTTP TTS via SPARK_TTS_URL
SPARK_SPEECH_NET=1 Both STT + TTS net

URL set without the gate → exit 2 (fail closed; no silent vendor call). Bearer: SPARK_STT_KEY / SPARK_TTS_KEY / OPENAI_API_KEY (never printed).

make # builds spark-stt-tts
        ./spark --dry-run examples/voice_turn.spark # offline
        ./spark --live examples/voice_live.spark # local sidecar + synth

        # Optional vendor (explicit):
        export SPARK_SPEECH_NET=1
        export SPARK_STT_URL=http://127.0.0.1:9/v1/audio/transcriptions
        export SPARK_TTS_URL=http://127.0.0.1:9/v1/audio/speech
        

Local engines without vendors: SPARK_STT_CMD / SPARK_TTS_CMD (shell; %i input path, %o output path), or openai-whisper (tiny.en default; SPARK_WHISPER_MODEL; SPARK_STT_WHISPER=0 to disable). Play after speak: SPARK_TTS_PLAY=1 or companion --play.

Reviewer

Opens wav/ogg path (fixture: examples/fixtures/audio/sample_review.wav). Asm parses RIFF/WAVE + PCM: sample rate, channels, bits, peak, silence frames, clip hits, crude SNR hint. Emits structured JSON (byte_parsed:true) — not fake metrics. Optional intent consistency uses dry STT stub vs fixture intent.

./spark --dry-run examples/voice_reviewer.spark
        

Coder (codifer)

voice code request "…" writes real out/voice_codegen.spark (or .s notes if request mentions .s) that calls listen / classify / ask / speak — runnable by the VM.

./spark --dry-run examples/voice_coder.spark
        

Copier → written voice model

Extracts PCM feature coeffs into:

out/voice_models/<name>/manifest.json
        out/voice_models/<name>/features.bin
        

Written model documents timbre/prosody params Spark understands. Optional brand_voice_id in manifest (vendor TTS profile id) is config only — not a live PSTN path.

Vendor neural TTS clones sit outside this artifact; the written model is fully loadable without stubbing “copied”.

./spark --dry-run examples/voice_copy.spark
        

Overlay speak on a text-only model

model train … method "spark_reply_pack" stores spoken scripts in replies.json even when the base has no voice. Spark speak reply uses the pack; this is not neural TTS / voice-GPU training. Inventable speak lines still need sot_ref (see MODEL_TRAINING.md).

./spark --dry-run examples/model_train_reply.spark
        

Written AI voice models

voice model write NAME spec { … } -> path
        voice model load NAME -> model
        speak with model NAME
        

Templates: templates/voice_models/. Artifacts under out/voice_models/.

Voice easy train (owned heads)

Piece-of-cake path for training owned STT/TTS heads (tiny CI or large opt-in). Not a vendor neural clone.

./spark-voice easy --dry --device auto
        ./spark-voice easy --scale large --device auto # opt-in
        

Docs: Voice easy → /docs/voice-easy.html.

PSTN — capability on, default OFF

Gate Required for live dial
spark.toml [pstn] enabled=false documented default
Env SPARK_PSTN=1 yes
CLI --pstn-live (with --live) yes
Allowlist Placeholders +15555550100 / +15555550101, or SPARK_PSTN_ALLOW
Telnyx TELNYX_API_KEY, TELNYX_CONNECTION_ID, SPARK_PSTN_FROM

Carrier divert guard

While a carrier-divert guard file (e.g. SPARK_PSTN_GUARD_JSON) has mode=failover with applied_dids, live dial to those guarded DIDs is refused (exit 4). Documented placeholders stay allowlisted. Operator override (only when explicitly enabled): SPARK_PSTN_OVERRIDE_DIVERT=1.

Also honors SPARK_PSTN_CARRIER_DIVERT / SPARK_PSTN_REFUSE_LIVE_INJECT in combination with the ledger map.

Dry-run / make test

--dry-run never forks spark-pstn-dial. Emits claimed:false. make test / make voice-test prove default-off.

Enable recipe

# 1) Env + Telnyx
        export SPARK_PSTN=1
        export SPARK_PSTN_FROM=+1YOUR_TN
        export TELNYX_API_KEY=…
        export TELNYX_CONNECTION_ID=…

        # 2) Live Spark mode + PSTN flag (not dry-run)
        ./spark --live --pstn-live examples/voice_pstn.spark

        # Dial only allowlisted placeholders unless SPARK_PSTN_ALLOW lists more
        # voice pstn dial "+15555550100" -> call
        

Default proof

./spark --dry-run examples/voice_pstn.spark # claimed:false
        ./spark-pstn-dial --to +15555550100 # exit ≠ 0 (no gates)
        

Files

Path Role
asm/voice_ops.s review/code/copy/model/pstn
tools/voice/spark_pstn_dial.c gated Telnyx dial/hangup
examples/voice_*.spark dry-run demos
templates/voice_models/ manifest skeleton
spark.toml [pstn] enabled=false

Tiny vs large (voice / weight play)

Voice model write/copy and weight playgrounds follow the same scale table as spark-coder + factory scale fixtures:

Scale Default How
tiny CI / dry demos Stub WAV, tiny STT (tiny.en), tiny coder/SGD fixtures
large Opt-in local ./spark-code train --scale large / make spark-sgd-proof-scale; consumer GPU optional

Spark train runs on CPU (default) or a consumer GPU. Larger stubs still measurement only. Details: Spark coder · Train loop.

Production gaps (not sold as telephony)

Spark voice is a language/demo surface, not a production call center.

Not shipped as production: barge-in; end-of-utterance / silence; latency budgets; call state machine (transfer / hold / hangup / voicemail / DTMF / STT retry); named STT/TTS/telephony adapter contracts; multi-call concurrency model.

Until those land, keep voice optional / gated on the marketing site. See ADOPTION_BAR.md, ROADMAP.md, and Model aspects.