v4.3: file-based media storage + manual VACUUM maintenance

This commit is contained in:
Ashim Kumar
2026-06-12 13:24:00 +06:00
parent 965470853e
commit cc57204aff
10 changed files with 789 additions and 164 deletions

View File

@@ -1,4 +1,4 @@
# routes/generation_routes.py - Combined Endpoint with Correct Task Polling
# routes/generation_routes.py - Combined Endpoint with Correct Task Polling (v4.3)
import json
import time
@@ -40,8 +40,6 @@ def poll_beam_task(task_id):
print(f" URL: {task_url}")
start_time = time.time()
# প্রথম কয়েকটা attempt এ 404 আসতে পারে — task register হতে delay
initial_delay = True
while True:
@@ -51,17 +49,14 @@ def poll_beam_task(task_id):
print(f"❌ Polling timeout after {POLL_MAX_WAIT}s")
return None, f'Task timed out after {int(POLL_MAX_WAIT)} seconds'
# প্রথম ২ সেকেন্ড wait করি task register হতে
if initial_delay and elapsed < 2:
time.sleep(2)
initial_delay = False
continue
try:
# ★ Bearer token দিয়ে try
resp = requests.get(task_url, headers=get_beam_auth_headers(), timeout=30)
# Bearer fail হলে Basic try করি
if resp.status_code in (401, 403):
print(f" Bearer auth failed, trying Basic...")
basic_headers = {
@@ -73,20 +68,14 @@ def poll_beam_task(task_id):
print(f" [{int(elapsed)}s] HTTP {resp.status_code} | Body: {len(resp.text)} chars")
if resp.status_code == 404:
# Task এখনও register হয়নি — wait
if elapsed < 30:
print(f" Task not found yet, waiting...")
time.sleep(POLL_INTERVAL)
continue
else:
# ৩০ সেকেন্ড পরেও 404 — সমস্যা
print(f"❌ Task not found after {int(elapsed)}s")
# ★ Debug: response body দেখি
print(f" 404 body: {resp.text[:300]}")
# ★ Alternative: Beam API base URL ভিন্ন হতে পারে
# কিছু Beam setup এ URL format ভিন্ন
alt_urls = [
f"https://api.beam.cloud/v2/task/{task_id}/status/",
f"https://api.beam.cloud/v2/task/{task_id}",
@@ -121,25 +110,14 @@ def poll_beam_task(task_id):
if status in ('COMPLETE', 'COMPLETED', 'SUCCESS'):
print(f"✅ Task complete!")
# ★ Result বের করা — Beam বিভিন্ন জায়গায় result রাখে
# 1. 'output' key
# 2. 'result' key
# 3. 'outputs' list (file-based)
# 4. response body তেই (endpoint mode)
actual_result = None
# Check 'output' (endpoint mode — function return value)
if data.get('output') and isinstance(data['output'], dict):
actual_result = data['output']
print(f" Result found in 'output' key")
# Check 'result'
elif data.get('result') and isinstance(data['result'], dict):
actual_result = data['result']
print(f" Result found in 'result' key")
# Check if top-level has audio_base64 (unlikely but possible)
elif data.get('audio_base64'):
actual_result = data
print(f" Result found in top-level data")
@@ -149,16 +127,12 @@ def poll_beam_task(task_id):
elif actual_result and actual_result.get('success'):
return actual_result, None
# ★ Outputs (file-based) — need to download
outputs = data.get('outputs', [])
if outputs:
print(f" Task has {len(outputs)} output files")
# For our use case, result should be in 'output' not files
# But log it for debug
for out in outputs:
print(f" Output: {out.get('name', '?')}{out.get('url', '?')}")
# No usable result found
print(f" ⚠️ Task complete but no audio in response")
print(f" Response keys: {list(data.keys())}")
print(f" Full response (first 500): {json.dumps(data, default=str)[:500]}")
@@ -177,7 +151,7 @@ def poll_beam_task(task_id):
return None, f'Task {status.lower()} on Beam. Container may not have started in time.'
elif status in ('PENDING', 'RUNNING', 'RETRY'):
pass # Keep polling
pass
else:
print(f" Unknown status: {status}")
@@ -221,16 +195,12 @@ def call_beam_and_get_result(text, voice='af_heart', speed=1.0):
task_id = response.headers.get('X-Task-Id', '')
# ========================================
# CASE 1: Task ID + empty/no body → Async → Poll
# ========================================
# CASE 1: Task ID + empty body → Async → Poll
if task_id and (not response.text or not response.text.strip() or response.headers.get('Content-Length') == '0'):
print(f"📋 Async mode — Task ID: {task_id}")
return poll_beam_task(task_id)
# ========================================
# CASE 2: Task ID + body
# ========================================
if task_id and response.text and response.text.strip():
print(f"📋 Task ID: {task_id} + body ({len(response.text)} chars)")
try:
@@ -238,20 +208,15 @@ def call_beam_and_get_result(text, voice='af_heart', speed=1.0):
if result.get('success') and result.get('audio_base64'):
print(f"✅ Direct sync result")
return _extract(result), None
# Body isn't the final result — poll
return poll_beam_task(task_id)
except Exception:
return poll_beam_task(task_id)
# ========================================
# CASE 3: No task_id + empty body → Error
# ========================================
if not response.text or not response.text.strip():
return None, 'Empty response from Beam with no task ID'
# ========================================
# CASE 4: Synchronous response
# ========================================
if response.status_code != 200:
try:
err = response.json().get('error', response.text[:200])
@@ -326,15 +291,26 @@ def generate_audio():
if source_format != 'mp3':
audio_base64 = convert_to_mp3(audio_base64, source_format)
# block_id থাকলে সরাসরি ফাইলে সেভ করি (v4.3)
if block_id:
from media_storage import save_audio
db = get_db()
cursor = db.cursor()
cursor.execute('''
UPDATE markdown_blocks
SET audio_data = ?, audio_format = 'mp3', transcription = ?
WHERE id = ?
''', (audio_base64, json.dumps(transcription), block_id))
db.commit()
SELECT c.project_id FROM markdown_blocks mb
JOIN chapters c ON mb.chapter_id = c.id
WHERE mb.id = ?
''', (block_id,))
row = cursor.fetchone()
if row:
project_id = row['project_id']
rel_path = save_audio(project_id, block_id, audio_base64, 'mp3')
cursor.execute('''
UPDATE markdown_blocks
SET audio_path = ?, audio_data = '', audio_format = 'mp3', transcription = ?
WHERE id = ?
''', (rel_path, json.dumps(transcription), block_id))
db.commit()
print(f"✅ DONE: audio={len(audio_base64)} bytes, words={len(transcription)}")
print(f"{'='*60}")
@@ -377,7 +353,7 @@ def generate_chapter_audio():
cursor = db.cursor()
cursor.execute('''
SELECT id, content, tts_text, block_type FROM markdown_blocks
SELECT id, content, tts_text, block_type, chapter_id FROM markdown_blocks
WHERE chapter_id = ? ORDER BY block_order
''', (chapter_id,))
blocks = cursor.fetchall()
@@ -385,6 +361,11 @@ def generate_chapter_audio():
if not blocks:
return jsonify({'error': 'No blocks found'}), 404
# project_id বের করি (ফাইল সেভের জন্য)
cursor.execute('SELECT project_id FROM chapters WHERE id = ?', (chapter_id,))
ch_row = cursor.fetchone()
project_id = ch_row['project_id'] if ch_row else None
results = []
success_count = 0
error_count = 0
@@ -394,6 +375,8 @@ def generate_chapter_audio():
print(f"📖 CHAPTER: {total} blocks, voice={voice}")
print(f"{'='*60}")
from media_storage import save_audio
for idx, block in enumerate(blocks):
block_id = block['id']
block_type = block['block_type'] if 'block_type' in block.keys() else 'paragraph'
@@ -437,11 +420,14 @@ def generate_chapter_audio():
if source_format != 'mp3':
audio_base64 = convert_to_mp3(audio_base64, source_format)
# v4.3: ফাইলে সেভ
rel_path = save_audio(project_id, block_id, audio_base64, 'mp3') if project_id else None
cursor.execute('''
UPDATE markdown_blocks
SET audio_data = ?, audio_format = 'mp3', transcription = ?
SET audio_path = ?, audio_data = '', audio_format = 'mp3', transcription = ?
WHERE id = ?
''', (audio_base64, json.dumps(transcription), block_id))
''', (rel_path, json.dumps(transcription), block_id))
results.append({
'block_id': block_id,