Implemented in the current working tree. This document records the implemented plan and the verification gates used for the realtime STS checkpoint.
Provider contract checked on 2026-06-23 against the official ElevenLabs API docs:
POST /v1/single-use-token/realtime_scribe returns a 15-minute one-use token.wss://api.elevenlabs.io/v1/speech-to-text/realtime accepts client auth through the token query parameter, requires model_id, supports commit_strategy=manual, and receives input_audio_chunk messages with commit.POST /v1/text-to-speech/:voice_id/stream returns streaming audio and supports output_format.Add an account-synced Realtime Speech-to-Speech settings toggle across iOS, Android, and web with two modes:
Standard API: the current default workflow.Realtime Speech: streams microphone audio to ElevenLabs Realtime STT, commits the transcript on Stop, then generates speech through SpeakTrue’s protected TTS path.This is not only a UI switch. It requires a shared settings contract, Supabase session and finalization functions, client streaming code, artifact compatibility, usage accounting, and automatic fallback to the existing batch workflow.
Realtime Speech-to-Speech settings toggle.scribe_v2_realtime.public.user_settings so iOS, Android, and web converge on one account setting.tts-generate function for TTS streaming by correcting the provider endpoint selection when responseMode === "stream"./api/speech-to-speech auth/runtime envelope remains the compatibility baseline.Add:
alter table public.user_settings
add column if not exists sts_workflow_mode text not null default 'batch';
alter table public.user_settings
drop constraint if exists user_settings_sts_workflow_mode_valid;
alter table public.user_settings
add constraint user_settings_sts_workflow_mode_valid
check (sts_workflow_mode in ('batch', 'realtime'));
Update backend/supabase/migrations/001_core_schema.sql if the bootstrap schema snapshot is still being kept current.
user-settings-get and user-settings-update must return and accept:
{
"settings": {
"sts_workflow_mode": "batch"
}
}
Invalid or missing values normalize to batch.
Create POST /functions/v1/sts-realtime-session.
Request body:
{
"client_platform": "ios",
"expected_duration_ms": 30000,
"audio_format": "pcm_16000",
"language_code": "en"
}
Response body:
{
"session_id": "uuid",
"provider": "elevenlabs",
"token": "sutkn_...",
"token_type": "realtime_scribe",
"model_id": "scribe_v2_realtime",
"websocket_url": "wss://api.elevenlabs.io/v1/speech-to-text/realtime",
"expires_in_seconds": 900,
"commit_strategy": "manual",
"audio_format": "pcm_16000"
}
Server behavior:
status = 'created', platform, requested duration, max duration, provider key source, and redacted diagnostics.POST /v1/single-use-token/realtime_scribe.Create POST /functions/v1/sts-realtime-finalize.
Request body:
{
"session_id": "uuid",
"committed_transcript": "Hello world.",
"audio_bytes_sent": 960000,
"duration_ms": 30000,
"tts": {
"voice_id": "CwhRBWXzGAHq8TQ4Fs17",
"model_id": "eleven_multilingual_v2",
"language_code": "en",
"stability": 0.9,
"similarity": 0.85,
"speed": 0.75,
"style_exaggeration": 0.65,
"speaker_boost": true,
"response_mode": "artifact"
}
}
Response body must match the current canonical artifact shape:
{
"success": true,
"artifact_id": "uuid",
"artifact_path": "users/<user-id>/generated/<artifact-id>.mp3",
"audio_path": "https://signed-url",
"download_path": "https://signed-url",
"transcribed_text": "Hello world.",
"clip_metadata": {
"source": "sts-realtime",
"stt_model": "scribe_v2_realtime",
"tts_model": "eleven_multilingual_v2"
}
}
Server behavior:
audio_bytes_sent and duration_ms by the session limits.stt and TTS usage through the existing tts-generate accounting path.tts-generate.finalized only after artifact persistence succeeds.Add a migration for public.sts_realtime_sessions:
id uuid primary key default gen_random_uuid()user_id uuid not null references auth.users(id) on delete cascadeprovider text not null default 'elevenlabs'client_platform text not nullmodel_id text not null default 'scribe_v2_realtime'audio_format text not null default 'pcm_16000'commit_strategy text not null default 'manual'expected_duration_ms integermax_duration_ms integer not nullaudio_bytes_sent bigintcommitted_transcript textartifact_path textstatus text not null default 'created' check (status in ('created', 'streaming', 'finalized', 'failed', 'expired'))provider_key_source texterror_code textcreated_at timestamptz not null default timezone('utc', now())updated_at timestamptz not null default timezone('utc', now())expires_at timestamptz not nullEnable RLS. Users may select their own session rows if needed for diagnostics, but inserts/updates should be service-role only through Edge Functions.
backend/supabase/migrations/<timestamp>_add_sts_workflow_mode.sql.public.user_settings.sts_workflow_mode text not null default 'batch'.batch and realtime.backend/supabase/migrations/001_core_schema.sql if the bootstrap schema remains current.user-settings-get and user-settings-update handler, index, and tests.backend/supabase/migrations/<timestamp>_create_sts_realtime_sessions.sql.backend/supabase/functions/sts-realtime-session/index.ts.backend/supabase/functions/sts-realtime-session/handler.ts.backend/supabase/functions/sts-realtime-session/handler_test.ts.backend/supabase/config.toml.POST /v1/single-use-token/realtime_scribe.backend/supabase/functions/sts-realtime-finalize/index.ts.backend/supabase/functions/sts-realtime-finalize/handler.ts.backend/supabase/functions/sts-realtime-finalize/handler_test.ts.backend/supabase/functions/tts-generate/index.ts.responseMode === "stream", call POST /v1/text-to-speech/:voice_id/stream.full, buffered, and artifact behavior.ios/SpeakTrue/UserSettings.swift to add STSWorkflowMode, default batch, a UserDefaults key, and a published property.ios/SpeakTrue/UserSettingsSyncService.swift to decode, upsert, apply, default, and missing-column diagnose sts_workflow_mode.ios/SpeakTrue/SettingsTabView.swift to add a Realtime Speech-to-Speech toggle in the STT/STS area.ios/SpeakTrue/AIProxyService.swift to add sts-realtime-session, sts-realtime-finalize, and streaming/finalization helpers.ios/SpeakTrue/STSViewModel.swift to branch on userSettings.stsWorkflowMode; leave the existing batch path intact.ios/SpeakTrue/RealtimeSTTService.swift using AVAudioEngine plus URLSessionWebSocketTask to send base64 PCM chunks, receive partial/committed transcript messages, and commit on Stop.CURRENT_PROJECT_VERSION for concrete iOS app code changes.android/app/src/main/java/com/speaktrue/features/settings/data/SettingsRepository.kt to add stsWorkflowMode, serializer mapping, sanitizer, dirty-field tracking, sync patching, and synced response application.android/app/src/main/java/com/speaktrue/features/settings/ui/SettingsScreen.kt or SettingsSections.kt to add the workflow toggle.android/app/src/main/java/com/speaktrue/features/sts/presentation/STSViewModel.kt to branch between batch and realtime.android/app/src/main/java/com/speaktrue/features/sts/presentation/STSViewModel.kt to expose realtime progress, fallback messages, and artifact state.android/app/src/main/java/com/speaktrue/features/sts/data/RealtimeSTSRepository.kt for session token retrieval, WebSocket STT streaming, TTS stream/artifact request, and finalization.web/python-web-app/templates/partials/index_slow/settings.html to add the workflow control in the Speech-to-Text settings panel, with Speech-to-Speech wording in the label.web/python-web-app/static/js/index_settings.js to load and save sts_workflow_mode.web/python-web-app/src/services/user_settings_service.py to include sts_workflow_mode in strict edge allowlists, edge payload mapping, and local fallback behavior.web/python-web-app/static/js/index_core.js so submitSTSForm() chooses batch or realtime./api/speech-to-speech as the batch path and preserve its current artifact response contract.web/python-web-app/src/routes/stt.py for realtime session and finalize so legacy web remains behind the existing Flask runtime/auth envelope.batch as the database and local default.STS_REALTIME_ENABLED=false as a server-side kill switch for disabling realtime session/finalize functions without a database rollback.backend/supabase/migrations/<timestamp>_add_sts_workflow_mode.sqlbackend/supabase/migrations/<timestamp>_create_sts_realtime_sessions.sqlbackend/supabase/migrations/001_core_schema.sqlbackend/supabase/config.tomlbackend/supabase/functions/user-settings-get/handler.tsbackend/supabase/functions/user-settings-get/index.tsbackend/supabase/functions/user-settings-get/handler_test.tsbackend/supabase/functions/user-settings-update/handler.tsbackend/supabase/functions/user-settings-update/index.tsbackend/supabase/functions/user-settings-update/handler_test.tsbackend/supabase/functions/tts-generate/index.tsbackend/supabase/functions/tts-generate/handler_test.tsbackend/supabase/functions/sts-realtime-session/index.tsbackend/supabase/functions/sts-realtime-session/handler.tsbackend/supabase/functions/sts-realtime-session/handler_test.tsbackend/supabase/functions/sts-realtime-finalize/index.tsbackend/supabase/functions/sts-realtime-finalize/handler.tsbackend/supabase/functions/sts-realtime-finalize/handler_test.tsios/SpeakTrue/UserSettings.swiftios/SpeakTrue/UserSettingsSyncService.swiftios/SpeakTrue/SettingsTabView.swiftios/SpeakTrue/AIProxyService.swiftios/SpeakTrue/STSViewModel.swiftios/SpeakTrue/RealtimeSTTService.swiftios/SpeakTrue.xcodeproj/project.pbxprojios/SpeakTrueTests/android/app/src/main/java/com/speaktrue/features/settings/data/SettingsRepository.ktandroid/app/src/main/java/com/speaktrue/features/settings/ui/SettingsScreen.ktandroid/app/src/main/java/com/speaktrue/features/settings/ui/SettingsSections.ktandroid/app/src/main/java/com/speaktrue/features/sts/data/STSOrchestrator.ktandroid/app/src/main/java/com/speaktrue/features/sts/presentation/STSViewModel.ktandroid/app/src/main/java/com/speaktrue/features/sts/data/RealtimeSTSRepository.ktandroid/app/src/test/web/python-web-app/templates/partials/index_slow/settings.htmlweb/python-web-app/static/js/index_settings.jsweb/python-web-app/static/js/index_core.jsweb/python-web-app/src/services/user_settings_service.pyweb/python-web-app/src/routes/stt.pyweb/python-web-app/tests/Use focused checks per slice, then run broader gates only when crossing contracts.
npx -y deno@latest test \
backend/supabase/functions/user-settings-get/handler_test.ts \
backend/supabase/functions/user-settings-update/handler_test.ts \
backend/supabase/functions/sts-realtime-session/handler_test.ts \
backend/supabase/functions/sts-realtime-finalize/handler_test.ts \
backend/supabase/functions/tts-generate/handler_test.ts
bash scripts/coverage_backend_contracts.sh
bash scripts/regression_migration_paths.sh
xcodebuild test -project ios/SpeakTrue.xcodeproj -scheme SpeakTrue -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:SpeakTrueTests/SettingsContractsTests
xcodebuild test -project ios/SpeakTrue.xcodeproj -scheme SpeakTrue -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:SpeakTrueTests/STTAndSTSViewModelTests
./android/gradlew -p android :app:testDebugUnitTest --tests 'com.speaktrue.features.settings.data.SettingsRepositoryTest' --tests 'com.speaktrue.features.sts.presentation.STSViewModelTest'
./android/gradlew -p android :app:lintDebug
web/python-web-app/venv/bin/pytest \
web/python-web-app/tests/test_user_settings_service.py \
web/python-web-app/tests/api/test_stt.py -q
Add or update tests so they prove:
sts_workflow_mode defaults to batch, rejects invalid values, and syncs round-trip across each client.batch./stream endpoint when responseMode === "stream".git diff --check
git status --short
STS_REALTIME_ENABLED=false on the Supabase realtime functions.backend/supabase/migrations/001_core_schema.sqlbackend/supabase/functions/tts-generate/index.tsios/SpeakTrue/STSViewModel.swiftandroid/app/src/main/java/com/speaktrue/features/sts/data/STSOrchestrator.ktweb/python-web-app/static/js/index_core.js and web/python-web-app/src/routes/stt.py