summaryrefslogtreecommitdiff
path: root/apps/transcription/transcription.py
diff options
context:
space:
mode:
Diffstat (limited to 'apps/transcription/transcription.py')
-rw-r--r--apps/transcription/transcription.py143
1 files changed, 143 insertions, 0 deletions
diff --git a/apps/transcription/transcription.py b/apps/transcription/transcription.py
new file mode 100644
index 00000000..ad55f6d4
--- /dev/null
+++ b/apps/transcription/transcription.py
@@ -0,0 +1,143 @@
+import modal
+from pydantic import BaseModel
+
+app = modal.App("opencut-transcription")
+
+class TranscribeRequest(BaseModel):
+ filename: str
+ language: str = "auto"
+ decryptionKey: str = None
+ iv: str = None
+
+@app.function(
+ image=modal.Image.debian_slim()
+ .apt_install(["ffmpeg"])
+ .pip_install(["openai-whisper", "boto3", "fastapi[standard]", "pydantic", "cryptography"]),
+ gpu="A10G",
+ timeout=300, # 5m
+ secrets=[modal.Secret.from_name("opencut-r2-secrets")]
+)
+@modal.fastapi_endpoint(method="POST")
+def transcribe_audio(request: TranscribeRequest):
+ import whisper
+ import boto3
+ import tempfile
+ import os
+ import json
+
+ try:
+ filename = request.filename
+ language = request.language
+ decryption_key = request.decryptionKey
+ iv = request.iv
+
+ if not filename:
+ return {
+ "error": "Missing filename parameter"
+ }
+
+ # Initialize R2 client
+ s3_client = boto3.client(
+ 's3',
+ endpoint_url=f'https://{os.environ["CLOUDFLARE_ACCOUNT_ID"]}.r2.cloudflarestorage.com',
+ aws_access_key_id=os.environ["R2_ACCESS_KEY_ID"],
+ aws_secret_access_key=os.environ["R2_SECRET_ACCESS_KEY"],
+ region_name='auto'
+ )
+
+ # Create temporary file for audio
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.wav') as temp_file:
+ temp_path = temp_file.name
+
+ try:
+ # Download audio from R2
+ s3_client.download_file(
+ os.environ["R2_BUCKET_NAME"],
+ filename,
+ temp_path
+ )
+
+ # If decryption key provided, decrypt the file directly (zero-knowledge)
+ if decryption_key and iv:
+ import base64
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+ from cryptography.hazmat.backends import default_backend
+
+ # Read the encrypted file
+ with open(temp_path, 'rb') as f:
+ encrypted_data = f.read()
+
+ # Decode the key and IV from base64
+ key_bytes = base64.b64decode(decryption_key)
+ iv_bytes = base64.b64decode(iv)
+
+ # Decrypt the data using AES-GCM
+ # Extract the tag (last 16 bytes) and ciphertext
+ tag = encrypted_data[-16:]
+ ciphertext = encrypted_data[:-16]
+
+ cipher = Cipher(
+ algorithms.AES(key_bytes),
+ modes.GCM(iv_bytes, tag),
+ backend=default_backend()
+ )
+ decryptor = cipher.decryptor()
+ decrypted_data = decryptor.update(ciphertext) + decryptor.finalize()
+
+ # Write decrypted audio back to temp file
+ with open(temp_path, 'wb') as f:
+ f.write(decrypted_data)
+
+ # Load Whisper model
+ model = whisper.load_model("base")
+
+ # Transcribe audio
+ if language == "auto":
+ result = model.transcribe(temp_path)
+ else:
+ result = model.transcribe(temp_path, language=language.lower())
+
+ # Delete audio file from R2 (cleanup)
+ s3_client.delete_object(
+ Bucket=os.environ["R2_BUCKET_NAME"],
+ Key=filename
+ )
+
+ # Adjust segment timing - Whisper is consistently late by ~500ms
+ adjusted_segments = []
+ for segment in result["segments"]:
+ adjusted_segment = segment.copy()
+ # Shift start/end times earlier by 500ms, don't go below 0
+ adjusted_segment["start"] = max(0, segment["start"] - 0.5)
+ adjusted_segment["end"] = max(0.5, segment["end"] - 0.5) # Ensure duration is at least 0.5s
+ adjusted_segments.append(adjusted_segment)
+
+ return {
+ "text": result["text"],
+ "segments": adjusted_segments,
+ "language": result["language"]
+ }
+
+ finally:
+ # Clean up temporary file
+ if os.path.exists(temp_path):
+ os.unlink(temp_path)
+
+ except Exception as e:
+ import traceback
+ print(f"Transcription error: {str(e)}")
+ print(f"Traceback: {traceback.format_exc()}")
+
+ # Return error response that matches expected format
+ return {
+ "error": str(e),
+ "text": "",
+ "segments": [],
+ "language": "unknown"
+ }
+
+@app.local_entrypoint()
+def main():
+ # Test function - you can call this with modal run transcription.py
+ print("Transcription service is ready to deploy!")
+ print("Deploy with: modal deploy transcription.py") \ No newline at end of file