Standalone single-deploy Modal packages for each stack - #12
Conversation
Repackage the Modal deployment so each self-hosted stack is a self-contained Modal App deployed with one `modal deploy` — no start_asr step, no modal.Dict address handshake, no cross-package dependency. Uses Modal Servers (@app.server) with sibling URLs resolved via Server.from_name at startup. - sync/modal_app.py -> aai-sync-u3pro - streaming/modal_app_universal_3_5_pro.py -> aai-streaming-u3pro - streaming/modal_app_english_multilang.py -> aai-streaming-english-multilang Fixes carried over from the #11 review: autoscaling ASR Server instead of a 24h-capped Sandbox; no 1h WebSocket session ceiling; encrypted ASR hop via h2_enabled + AAI_USE_SECURE_CHANNEL_TO_ASR_SERVICE (encrypted_ports alone negotiates no ALPN); unauthenticated=False by default (Modal proxy auth); fate-shared vendor processes; env-overridable sync audio limits. Verified end to end on L40S (single deploy each): sync 152-word transcript, autoscaled to 181x realtime at concurrency 8; streaming u3pro real turns over the TLS h2 ASR hop; english+multilang routing both models through the nginx Lb. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
sample_sync.py (POST /transcribe), sample_streaming.py (live realtime turns, model selection for the u3pro and english/multilang stacks), and a README. Both support --concurrency / --load for a quick input-load sweep and Modal proxy-auth headers. Verified live against all three deployed stacks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
The turn counter was appended only inside the live-printing branch, so --load sessions always reported 0 turns even though turns arrived (first-turn latency was correct). Track end_of_turn regardless of live; gate only printing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
…eaming_modal_stack/
Consolidate each stack's Modal artifacts into a self-contained top-level dir:
sync_modal_stack/ modal_app.py, sample_sync.py, README.md
streaming_modal_stack/ modal_app_universal_3_5_pro.py,
modal_app_english_multilang.py, sample_streaming.py, README.md
Each new dir carries its own deploy/verify/auth/teardown README. The compose
READMEs (sync/, streaming/) keep a one-line pointer instead of the full Modal
section, and the root README points at the two new dirs. No code behavior
changes; comment cross-references updated to the new paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
The self-hosted-streaming-api v1.0.1 image carries the handshake-logging fix (DeepLearning #19523: peer-aborted WebSocket handshakes log at WARNING, not ERROR). Bump streaming-api and self-hosted-streaming-asr-universal-3-5-pro to release-v1.0.1 in the u3pro stack via per-image tags; the license-and-usage-proxy has no v1.0.1 and stays on v1.0.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
Fresh deploy + verification (v1.0.1 bump)Tore down all three apps and redeployed from scratch off this branch in the dlange Modal workspace (L40S), then verified:
Deploys used the reorganized layout ( Note: the english/multilang stack still uses |
Adversarial review of PR #12 (8 findings). Fixes: - [major] english/multilang ASR MAX_OPEN_STREAMS + target_concurrency 32 -> 48 (compose parity; avoids a ~50% GPU over-provision at scale). - [major] streaming README Verify pointed at the bundled example client, which cannot send Modal proxy-auth headers and 401s on the default deploy; point it at sample_streaming.py with --modal-key/--modal-secret and note the caveat. - [minor] sync/streaming README Verify curls now send Modal-Key/Modal-Secret (they 401'd on the default proxy-auth deploy). - [minor] nginx Lb -> ASR hop now verifies the backend cert (grpc_ssl_verify on + ca-certificates trusted store), not just encrypts. - [minor] fate-share reaper no longer reports a clean shutdown as a crash: a module-level _stopping event, set by @modal.exit stop(), distinguishes an intentional teardown from an unexpected vendor exit (all three apps). - [minor] sample_streaming.py no longer crashes formatting first_turn_s when a session yields no word-bearing turns (prints n/a). - [minor/question] english/multilang now pins streaming-api via its own API_TAG. streaming-api bumped to release-v1.0.1 everywhere it is referenced (u3pro + english/multilang Modal stacks, streaming/.env.example, root README) so both streaming stacks carry the handshake-logging fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
…only for verification) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
…ectivity) Verifying the backend cert (finding 4) fails against Modal's edge: nginx cannot build the chain and errors "unable to get local issuer certificate", marking the ASR backend down so every session 3005s (confirmed live). Revert to encrypt-only (grpc_ssl_server_name on) and document the residual limitation in-place, the finding's sanctioned alternative. The backend is unauthenticated by design; co-location or i6pn is the real fix, per the README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
Adversarial review fixes + streaming-api v1.0.1 everywhere — applied & verifiedRan an adversarial review (14 raw findings → 3 refuted → 8 confirmed) and addressed all of them:
[minor] LB→ASR cert verification ( Re-verified from scratch (dlange workspace, L40S): sync (26.9x realtime), u3pro v1.0.1 (12 turns), english + multilingual with the 48-cap and encrypt-only LB (8 / 14 turns). Handshake fix confirmed on both v1.0.1 streaming APIs — u3pro 42/42 and english/multilang 33/33 |
|
nice! From organization perspective can we add folders like streaming/modal streaming/docker streaming/sagemaker etc? I can see us adding more variations here and I dont think top level makes sense |
Per review feedback, nest deployment variants under each service dir instead of top-level *_modal_stack dirs, so more targets (e.g. sagemaker) slot in cleanly: sync/docker/ (compose stack, moved from sync/) sync/modal/ (moved from sync_modal_stack/) streaming/docker/ (compose stack + nginx, moved from streaming/) streaming/modal/ (moved from streaming_modal_stack/) Relative links and paths updated throughout (root README layout + pointers, docker READMEs' ../README.md -> ../../README.md and modal pointers, modal READMEs' example paths -> ../docker/example, sample-script docstrings, and the cross-file comment references). No code behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL3rQMpGXvmMHRJiffWMF1
|
Good call — done. Reorganized so each service groups its deployment targets, and the top-level I moved the existing compose stacks into |
| curl -F 'audio=@../docker/example/example_audio_file.wav;type=audio/wav' \ | ||
| -F 'config={"language_code":"en"};type=application/json' \ | ||
| -H "Modal-Key: $MODAL_KEY" -H "Modal-Secret: $MODAL_SECRET" \ | ||
| -H 'Authorization: any-non-empty-value' \ |
There was a problem hiding this comment.
Exposed secret in sync/modal/README.md - low severity
Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More Info
bgotthold-aai
left a comment
There was a problem hiding this comment.
Approving this because I think it looks good overall.
Should we still add a .env for model to get the registry and tag out of code?
Runs each self-hosted stack on Modal's serverless GPUs. Each stack is a self-contained Modal App deployed with a single
modal deploy: the app defines its servers, and the API resolves its ASR and license-proxy URLs from the same App at startup, so one command brings up and wires the whole stack.Three self-contained packages
sync_modal_stack/modal_app.pyaai-sync-u3proSyncApi(L40S),LicenseProxy(CPU)streaming_modal_stack/modal_app_universal_3_5_pro.pyaai-streaming-u3proStreamingApi(CPU),Asr(L40S),LicenseProxy(CPU)streaming_modal_stack/modal_app_english_multilang.pyaai-streaming-english-multilangStreamingApi(CPU),Lb(nginx),AsrEnglish(L40S),AsrMultilang(L40S),LicenseProxy(CPU)Each directory also ships a README and a sample client (
sample_sync.py/sample_streaming.py) that prints live output.Design
@app.server). The ASR runs as an autoscaling Server (min_containers=1,target_concurrencyset toMAX_OPEN_STREAMS), health-gated ongrpc_health_probe; WebSocket sessions are unbounded.modal.Server.from_name(...).get_url()resolves the ASR and proxy URLs from the same App at container start, so no address has to be wired in by hand.h2_enabled=Truemakes Modal's TLS edge advertise ALPN h2, so the API's default-TLS gRPC client connects withAAI_USE_SECURE_CHANNEL_TO_ASR_SERVICE=True. (encrypted_portsalone negotiates no ALPN and fails withmissing selected ALPN property;h2_ports/h2_enabledis required.)StreamingApi/SyncApishipunauthenticated=False(Modal proxy auth, enforced on the WebSocket upgrade);AAI_REQUIRE_MODAL_AUTH=0opts into a throwaway public test endpoint.MAX_AUDIO_DURATION_MS, etc.) read from the environment with the compose defaults as fallback.Lbthat routes thex-model-versiongRPC metadata (en-default/ml-default) to the two backends.streaming-apiand the Universal-3.5 Pro ASR are pinned torelease-v1.0.1(the API image logs peer-aborted handshakes at WARNING rather than ERROR); the license-and-usage-proxy stays onrelease-v1.0.0.Security note
Modal has no private inter-container network by default, so the internal hops (
StreamingApito the ASR /Lb, and toLicenseProxy) cross Modal's public TLS edge rather than a private bridge. The gRPC hop is encrypted, but the backends and proxy areunauthenticated=Truebecause the API dials them server-side and cannot attach Modal auth headers: their URLs are unguessable but public. Suitable for evaluation; co-locate the API and ASR in one container, or use Modal'si6pnprivate network, before exposing the backends to untrusted traffic.Validated live (Modal, L40S)
Deployed from scratch and torn down:
universal-streaming-englishanduniversal-streaming-multilingualreturn real turns, each routed by the nginxLbto its own L40S backend.Proxy is connected and license is validand SIGKILL on failure; an invalid license makes the proxy exit 1 at startup.usage-tracker.assemblyai.com/v1/usagewithstatus_code=200.opening handshake failed(from L4 health probes and aborted clients) logs at WARNING, not ERROR.🤖 Generated with Claude Code