Serve call audio through c2-core instead of GCS signed URLs
upload_audio() could only sign a URL when GCP_CREDENTIALS_PATH pointed at a service-account key file. The deployed VM runs on Application Default Credentials with no key file, so every upload silently took the fallback branch and returned a bare gs:// URI. That broke two things at once: * Browsers cannot fetch a gs:// URI, so no recording was ever playable. * _public_url_to_gcs_uri() only matched https://storage.googleapis.com/ and returned None for it, so `if gcs_uri:` in the upload path was always false and transcription never ran. Nothing was logged, which is why this looked like an OpenAI credits problem rather than a storage one. The fallback also interpolated the client-supplied filename instead of the call_id-derived safe name, so the URI did not even name the object written. Calls now store only the canonical gs:// location. A short-lived playback link is minted per read as an HMAC over (call_id, expiry) keyed by SERVICE_KEY, and audio is served from the private bucket by the new /media route. An <audio src> cannot carry an Authorization header, so the link has to be the credential; that router is therefore public with the check done inline, as enrollment.py already does. Signing GCS URLs from the VM would have needed a serviceAccountTokenCreator grant on its own service account — this avoids the IAM change entirely and keeps the bucket private. gcs_uri_for_call() reconstructs the object name from call_id, so recordings made before this fix are reachable again without a data migration. Frontend rows come straight from Firestore via onSnapshot and never see a server-minted field, so CallRow fetches the link lazily on expand. Also removes the last long-lived (1 year) signed URL and the log line that printed it.
This commit is contained in:
@@ -4,6 +4,7 @@ from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
from app.internal import firestore as fstore
|
||||
from app.internal.auth import require_admin_token
|
||||
from app.internal.storage import gcs_uri_for_call, with_playback_url
|
||||
|
||||
|
||||
class TranscriptUpdate(BaseModel):
|
||||
@@ -25,7 +26,9 @@ async def list_calls(
|
||||
filters["status"] = status
|
||||
if system_id:
|
||||
filters["system_id"] = system_id
|
||||
return await fstore.collection_list("calls", **filters)
|
||||
calls = await fstore.collection_list("calls", **filters)
|
||||
# audio_url is not stored — it's a short-lived signed link minted per read.
|
||||
return [with_playback_url(c) for c in calls]
|
||||
|
||||
|
||||
@router.get("/{call_id}")
|
||||
@@ -33,7 +36,7 @@ async def get_call(call_id: str):
|
||||
call = await fstore.doc_get("calls", call_id)
|
||||
if not call:
|
||||
raise HTTPException(404, f"Call '{call_id}' not found.")
|
||||
return call
|
||||
return with_playback_url(call)
|
||||
|
||||
|
||||
@router.post("/{call_id}/reprocess")
|
||||
@@ -43,10 +46,9 @@ async def reprocess_call(call_id: str, background_tasks: BackgroundTasks):
|
||||
if not call:
|
||||
raise HTTPException(404, f"Call '{call_id}' not found.")
|
||||
|
||||
from app.routers.upload import _run_intelligence_pipeline, _public_url_to_gcs_uri
|
||||
from app.routers.upload import _run_intelligence_pipeline
|
||||
|
||||
audio_url = call.get("audio_url")
|
||||
gcs_uri = _public_url_to_gcs_uri(audio_url) if audio_url else None
|
||||
gcs_uri = gcs_uri_for_call(call)
|
||||
|
||||
background_tasks.add_task(
|
||||
_run_intelligence_pipeline,
|
||||
|
||||
Reference in New Issue
Block a user