This contract is for native and web engineers implementing the generated speech to Soundboard save workflow. After reading it, a caller should be able to generate speech, retain the returned artifact reference, save that artifact to Soundboard, and branch correctly on structured failures without inspecting legacy planning artifacts.
The workflow has two HTTP operations:
POST /api/text-to-speech generates a reusable speech artifact.POST /api/soundboard/save-clip saves that generated artifact into a Soundboard category.Privileged storage writes remain server-side. Browser and native clients only forward artifact metadata returned by the generation operation; they do not write directly to object storage.
The Flask web routes described below require the signed-in user’s Supabase bearer token. This September 4 web ownership change does not alter native Supabase Edge Function contracts. Web playback and download URLs must be fetched with the bearer token, then rendered/downloaded as browser Blob URLs; never put the token in the URL. Only approved same-origin media endpoints receive that token.
SUPABASE_EDGE_STRICT=true remains the default, but category and clip management
always require a verified bearer identity and the per-user Supabase database
index, including when strict mode is off. New objects use
users/<verified-user>/soundboard/<category-uuid>/; reads follow validated stored
paths so category/clip moves do not relocate existing media. Both adapters use
the same ownership contract; application startup remains Supabase-only.
There is no fallback to global categories or unindexed shared objects.
Existing shared data requires an explicit ownership mapping before any separate data migration; see
docs/ops/WEB_IOS_SECURITY_REMEDIATION.md for verification and rollout limits.
POST /api/text-to-speech
{
"text": "Text to speak",
"voice": "voice-id",
"model": "eleven_multilingual_v2",
"stability": 0.8,
"similarity": 0.7,
"speed": 0.9,
"style": 0.2,
"speaker_boost": true
}
Required:
text: non-empty string.Optional fields fall back to server defaults when omitted. Numeric tuning fields must be parseable as numbers.
HTTP status: 200
{
"success": true,
"status": 200,
"artifact_id": "generated-id",
"artifact_path": "speech/user-id/generated-id.mp3",
"audio_path": "/api/generated-media?path=speech/user-id/generated-id.mp3",
"download_path": "/api/generated-media?download=1&path=speech/user-id/generated-id.mp3"
}
Client rules:
success: true, numeric status: 200, artifact_id, artifact_path, audio_path, and download_path as required.artifact_path when saving; keep audio_path for playback and compatibility.Generation snapshots and local-model fields follow Saved clip metadata.
POST /api/soundboard/save-clip
{
"category": "Lecture1",
"text": "Text used to generate the speech",
"artifact_path": "speech/user-id/generated-id.mp3",
"artifact_id": "generated-id",
"audio_path": "/api/generated-media?path=speech/user-id/generated-id.mp3",
"download_path": "/api/generated-media?download=1&path=speech/user-id/generated-id.mp3",
"format": "mp3",
"bitrate_kbps": 192,
"normalize": true
}
Required:
category: target Soundboard category path.text: original generated text.artifact_path: canonical generated artifact path from the generation response.Compatibility:
audio_path is accepted by the server, but new callers should send artifact_path.artifact_id and download_path are forwarded for diagnostics and continuity; they are not privileged storage credentials.HTTP status: 200
{
"success": true,
"message": "Clip and text saved to Lecture1 category in storage",
"clip": {
"name": "Text_to_speak",
"filename": "Text_to_speak_1712345678.mp3",
"url": "https://storage.example/soundboard/Lecture1/Text_to_speak_1712345678.mp3",
"text_url": "https://storage.example/soundboard/Lecture1/Text_to_speak_1712345678.txt",
"text_filename": "Text_to_speak_1712345678.txt",
"category": "Lecture1",
"timestamp": 1712345678
}
}
Client rules:
success: true, clip.filename, and clip.category as required.Generation and save failures use the same structured envelope:
{
"error": "Human-readable failure message",
"error_code": "SPEECH_ARTIFACT_NOT_FOUND",
"operation": "save-clip-to-soundboard",
"status": 404,
"retryable": false,
"details": {
"phase": "audio_upload",
"backend": "supabase"
}
}
Required fields:
error: human-readable message.error_code: stable machine-readable code.operation: tts-generate or save-clip-to-soundboard.status: numeric status for Flask speech workflow errors; Supabase Edge provider errors may include provider_status separately.retryable: boolean retry guidance.Details rules:
details is optional and bounded.400, retryable: false.401/403, retryable: false.404, retryable: false.500 or 502, usually retryable: true.artifact_path is a server-relative generated speech path such as speech/<user-id>/<id>.mp3. Its sidecar owner is assigned by the server, not trusted from client metadata.audio_path identifies the bearer-protected /api/generated-media endpoint. Fetch it with authentication and use the resulting Blob for playback; a direct <audio src> cannot send the bearer header.latestTTSSpeechArtifact and block save.The legacy web runtime exposes:
window.latestTTSSpeechArtifact: the current reusable generated artifact or null.window.__ttsSoundboardWorkflowTestHooks.getState(): latest artifact, generation diagnostic, save diagnostic, and saving flag.Diagnostics include generation/save phase, operation, error code, status, retryability, message, and bounded details where available.
artifact_path, artifact_id, audio_path, and download_path together.artifact_path and original text when saving to Soundboard.error_code, operation, status, and retryable; do not parse prose messages.Run these checks before changing the contract:
node web/python-web-app/tests/js/tts_soundboard_workflow_runtime_check.mjs
web/python-web-app/venv/bin/pytest web/python-web-app/tests/api/test_speech_contract_hardening.py web/python-web-app/tests/api/test_tts_soundboard_workflow_target.py web/python-web-app/tests/api/test_tts.py web/python-web-app/tests/api/test_soundboard_supabase_mode.py
deno test backend/supabase/functions/tts-generate/handler_test.ts
deno test backend/supabase/functions/soundboard-save-generated/handler_test.ts
The graph rebuild command keeps code navigation artifacts current after implementation changes: