feat: persist raw audio to object storage#1
Merged
Conversation
…ntation Introduces the ObjectStore interface abstracting Put/Delete/DeletePrefix over GCS and local filesystem, with a LocalStore implementation backed by the local filesystem and full TDD test coverage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements GCSStore backed by cloud.google.com/go/storage v1.61.3, satisfying the ObjectStore interface with Put, Delete, DeletePrefix, and Close. Uses Application Default Credentials. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…udioURL Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nscription Fires a goroutine in the voice branch of endTurn that uploads raw audio to object storage and records the URL on the message row, using context.Background so the upload is not cancelled when the request context ends. Adds OTel tracing via a package-level tracer. Introduces persistMessageWithID to share the pre-generated UUID between the upload goroutine and the DB insert. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6cfd3c7 to
cd2e8ee
Compare
No need to close pool on storage failure if pool hasn't been created yet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use context.WithoutCancel(ctx) instead of context.Background() so the upload span is a child of the request trace, not an orphan. Still detached from cancellation so upload completes after request ends. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Explicit lifecycle — no type assertions. LocalStore and MockStore get no-op Close(), GCSStore already had it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add AudioFormat field to WSMessage (defaults to "webm" for backward compat) - Frontend sends audio_format explicitly - Conductor uses msg.AudioFormat for storage key, content type, and transcription - Remove persistMessageWithID indirection: generate messageID at top of endTurn, single persistMessage with explicit ID param Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename AudioFormat to AudioMIME on WSMessage (default: "audio/webm") - Add AudioExt() method to derive extension from MIME type - Conductor uses msg.AudioMIME for GCS content-type, msg.AudioExt() for storage key and Whisper format param - Revert v0 frontend changes — v0 is a separate codebase Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0de58a4 to
c8dcf52
Compare
…/webm mime.ExtensionsByType returns empty for audio/webm and audio/wav. Simple strings.Cut on the MIME subtype is reliable for our use case. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
497a7d6 to
6c20f81
Compare
btc
added a commit
that referenced
this pull request
May 5, 2026
* feat(storage): add ObjectStore interface and local filesystem implementation Introduces the ObjectStore interface abstracting Put/Delete/DeletePrefix over GCS and local filesystem, with a LocalStore implementation backed by the local filesystem and full TDD test coverage. * feat(storage): add GCS implementation of ObjectStore Implements GCSStore backed by cloud.google.com/go/storage v1.61.3, satisfying the ObjectStore interface with Put, Delete, DeletePrefix, and Close. Uses Application Default Credentials. * feat(config): add Storage config (STORAGE_BACKEND, STORAGE_BUCKET, STORAGE_LOCAL_DIR) * feat(backend): wire ObjectStore into Backend with StoreAudio and SetAudioURL * feat(conductor): upload audio to object storage concurrently with transcription Fires a goroutine in the voice branch of endTurn that uploads raw audio to object storage and records the URL on the message row, using context.Background so the upload is not cancelled when the request context ends. Adds OTel tracing via a package-level tracer. Introduces persistMessageWithID to share the pre-generated UUID between the upload goroutine and the DB insert. * feat(storage): add MockStore for testing * deploy: add STORAGE_BACKEND and STORAGE_BUCKET env vars to Cloud Run * refactor(backend): move storage init before pool creation No need to close pool on storage failure if pool hasn't been created yet. * fix(conductor): propagate trace context to upload goroutine Use context.WithoutCancel(ctx) instead of context.Background() so the upload span is a child of the request trace, not an orphan. Still detached from cancellation so upload completes after request ends. * refactor(storage): add Close to ObjectStore interface Explicit lifecycle — no type assertions. LocalStore and MockStore get no-op Close(), GCSStore already had it. * feat(conductor): derive audio format from message, simplify persist - Add AudioFormat field to WSMessage (defaults to "webm" for backward compat) - Frontend sends audio_format explicitly - Conductor uses msg.AudioFormat for storage key, content type, and transcription - Remove persistMessageWithID indirection: generate messageID at top of endTurn, single persistMessage with explicit ID param * refactor(conductor): use MIME type for audio format, revert v0 changes - Rename AudioFormat to AudioMIME on WSMessage (default: "audio/webm") - Add AudioExt() method to derive extension from MIME type - Conductor uses msg.AudioMIME for GCS content-type, msg.AudioExt() for storage key and Whisper format param - Revert v0 frontend changes — v0 is a separate codebase * fix(conductor): use strings.Cut for AudioExt, stdlib mime lacks audio/webm mime.ExtensionsByType returns empty for audio/webm and audio/wav. Simple strings.Cut on the MIME subtype is reliable for our use case. ---------
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ObjectStoreinterface (internal/storage/) with GCS and local filesystem implementations{session_id}/{message_id}.webm) enable GDPR right-to-erasure via prefix deleteaudio_urlcolumn (already existed, always NULL) is now populated after successful uploadsDetails
New package
internal/storage/:ObjectStoreinterface:Put,Delete,DeletePrefix,Close(explicit lifecycle, no type assertions)LocalStore— filesystem-backed, path traversal guard, empty dir cleanupGCSStore— thin wrapper overcloud.google.com/go/storageSDK with ADCMockStore— recording mock for test injectionConfig:
STORAGE_BACKEND(local/gcs),STORAGE_BUCKET,STORAGE_LOCAL_DIRBackend wiring: Storage initialized before pool creation — no unnecessary cleanup on storage init failure. Same DI pattern as Transcriber/Synthesizer/Sender.
Conductor change:
WSMessage.AudioMIMEcarries the MIME type on the wire (defaultaudio/webmfor backward compat)AudioExt()derives file extension from MIME subtype viastrings.Cut(stdlibmime.ExtensionsByTypedoesn't knowaudio/webm)SetAudioURLusingcontext.WithoutCancel(ctx)— propagates trace context but detaches from cancellationmessageIDgenerated at top ofendTurn, singlepersistMessagecall — no branchingInfra: Terraform Cloud Run env vars added, GCS bucket + IAM already provisioned
Test plan
-raceSTORAGE_BACKEND=local, submit voice input, verifydata/audio/{session_id}/{message_id}.webmcreatedaudio_urlpopulated in messages table after voice turn🤖 Generated with Claude Code