| from __future__ import annotations |
|
|
| import argparse |
| import json |
| import sys |
| from pathlib import Path |
|
|
|
|
| BACKEND_ROOT = Path(__file__).resolve().parents[1] |
| if str(BACKEND_ROOT) not in sys.path: |
| sys.path.insert(0, str(BACKEND_ROOT)) |
| if hasattr(sys.stdout, "reconfigure"): |
| sys.stdout.reconfigure(encoding="utf-8") |
|
|
| from app.schemas.video import GenerateAudioRequest |
| from app.services.tts_provider import generate_audio_for_scene_plan |
|
|
|
|
| SAMPLE_PLANS = { |
| "english_soft": { |
| "video_id": "sample-rest-api-en", |
| "scene_plan": { |
| "title": "REST API Explained", |
| "duration_minutes": 1, |
| "language": "English", |
| "style": "clean_explainer", |
| "scenes": [ |
| { |
| "scene_id": 1, |
| "type": "title", |
| "duration_seconds": 7, |
| "screen_text": "REST API Explained", |
| "voice_text": "REST API becomes simple when you remember request, server, and response.", |
| "keywords": ["request", "server", "response"], |
| "visual_hint": "clean title card", |
| }, |
| { |
| "scene_id": 2, |
| "type": "meaning", |
| "duration_seconds": 10, |
| "screen_text": "API = Messenger", |
| "voice_text": "An API is a messenger between frontend and backend. It carries a request and brings back a response.", |
| "keywords": ["frontend", "backend", "response"], |
| "visual_hint": "middleman diagram", |
| }, |
| { |
| "scene_id": 3, |
| "type": "example", |
| "duration_seconds": 10, |
| "screen_text": "Website asks server", |
| "voice_text": "Imagine a website asking the server for student marks. The API safely carries that request.", |
| "keywords": ["website", "server", "request"], |
| "visual_hint": "browser to server arrows", |
| }, |
| { |
| "scene_id": 4, |
| "type": "exam_answer", |
| "duration_seconds": 12, |
| "screen_text": "Write this in exam", |
| "voice_text": "REST API is a standard way for applications to communicate using HTTP methods like GET, POST, PUT, and DELETE.", |
| "keywords": ["HTTP", "GET", "POST", "DELETE"], |
| "visual_hint": "answer card", |
| }, |
| { |
| "scene_id": 5, |
| "type": "recap", |
| "duration_seconds": 8, |
| "screen_text": "Request to response", |
| "voice_text": "Remember the chain. Frontend sends request. API talks to backend. Backend sends response.", |
| "keywords": ["frontend", "API", "backend"], |
| "visual_hint": "three-step recap", |
| }, |
| ], |
| }, |
| }, |
| "ml_en_mix": { |
| "video_id": "sample-rest-api-mix", |
| "scene_plan": { |
| "title": "REST API Simple Explanation", |
| "duration_minutes": 1, |
| "language": "Malayalam+English", |
| "style": "clean_explainer", |
| "scenes": [ |
| { |
| "scene_id": 1, |
| "type": "hook", |
| "duration_seconds": 8, |
| "screen_text": "REST API simple ayi", |
| "voice_text": "ഇപ്പോൾ നമുക്ക് REST API simple ആയി മനസ്സിലാക്കാം.", |
| "segments": [ |
| {"lang": "ml", "text": "ഇപ്പോൾ നമുക്ക്"}, |
| {"lang": "en", "text": "REST API"}, |
| {"lang": "ml", "text": "simple ആയി മനസ്സിലാക്കാം."}, |
| ], |
| "keywords": ["REST API", "simple"], |
| "visual_hint": "warm hook card", |
| }, |
| { |
| "scene_id": 2, |
| "type": "meaning", |
| "duration_seconds": 10, |
| "screen_text": "Website to database?", |
| "voice_text": "Imagine ചെയ്യൂ, ഒരു website direct database-നോട് സംസാരിക്കുന്നില്ല.", |
| "keywords": ["website", "database"], |
| "visual_hint": "blocked direct arrow", |
| }, |
| { |
| "scene_id": 3, |
| "type": "example", |
| "duration_seconds": 10, |
| "screen_text": "Website calls API", |
| "voice_text": "അതിനു പകരം website ഒരു API-നെ call ചെയ്യുന്നു.", |
| "keywords": ["website", "API", "call"], |
| "visual_hint": "API middle card", |
| }, |
| { |
| "scene_id": 4, |
| "type": "exam_answer", |
| "duration_seconds": 11, |
| "screen_text": "API returns data", |
| "voice_text": "API server-ൽ നിന്ന് data എടുത്ത് website-ന് തിരിച്ച് കൊടുക്കും.", |
| "keywords": ["server", "data", "website"], |
| "visual_hint": "response arrow", |
| }, |
| { |
| "scene_id": 5, |
| "type": "recap", |
| "duration_seconds": 10, |
| "screen_text": "Middleman idea", |
| "voice_text": "That is why API is like a middleman between frontend and backend.", |
| "keywords": ["middleman", "frontend", "backend"], |
| "visual_hint": "three-node recap", |
| }, |
| ], |
| }, |
| }, |
| "lenses_teacher_mix": { |
| "video_id": "sample-lenses-ai4bharat-teacher", |
| "scene_plan": { |
| "title": "Lenses — AI4Bharat Teacher Voice", |
| "duration_minutes": 1, |
| "language": "English + Malayalam", |
| "style": "kerala_sslc_teacher", |
| "scenes": [ |
| { |
| "scene_id": 1, |
| "type": "concept", |
| "duration_seconds": 16, |
| "screen_text": "Convex lens: thicker at the centre", |
| "voice_text": "A convex lens is thicker at the centre. It bends parallel light rays so that they meet at the principal focus. This is why we call it a converging lens.", |
| "keywords": ["convex lens", "principal focus", "converging"], |
| "visual_hint": "simple labelled convex lens and two parallel rays", |
| }, |
| { |
| "scene_id": 2, |
| "type": "language_support", |
| "duration_seconds": 17, |
| "screen_text": "Remember this", |
| "voice_text": "കോൺവെക്സ് ലെൻസ് നടുവിൽ കട്ടിയുള്ളതാണ്. സമാന്തര രശ്മികൾ ഒരു ബിന്ദുവിലേക്ക് കൂടിച്ചേരും. പരീക്ഷാ ഉത്തരത്തിൽ നടുവിൽ കട്ടിയുള്ളത്, കൺവർജിങ് ലെൻസ്, പ്രിൻസിപ്പൽ ഫോക്കസ് എന്നീ പ്രധാന പദങ്ങൾ എഴുതണം.", |
| "keywords": ["thicker at the centre", "converging lens", "principal focus"], |
| "visual_hint": "three exam keywords beside the same lens", |
| }, |
| ], |
| }, |
| }, |
| } |
|
|
|
|
| def main() -> None: |
| parser = argparse.ArgumentParser(description="Generate sample Docdeo TTS audio.") |
| parser.add_argument( |
| "--provider", |
| default="mock", |
| help="mock, ai4bharat, kokoro, hybrid, edge, indic", |
| ) |
| parser.add_argument( |
| "--voice-mode", |
| default="english_soft", |
| choices=["english_soft", "malayalam_soft", "ml_en_mix", "lenses_teacher_mix"], |
| ) |
| args = parser.parse_args() |
|
|
| sample_key = args.voice_mode if args.voice_mode in SAMPLE_PLANS else "english_soft" |
| sample = SAMPLE_PLANS[sample_key] |
| effective_voice_mode = "ml_en_mix" if args.voice_mode == "lenses_teacher_mix" else args.voice_mode |
| request = GenerateAudioRequest( |
| scene_plan=sample["scene_plan"], |
| voice_mode=effective_voice_mode, |
| voice="teacher_mix" if args.voice_mode in {"ml_en_mix", "lenses_teacher_mix"} else "teacher_english", |
| language=sample["scene_plan"]["language"], |
| provider=args.provider, |
| ) |
| try: |
| response = generate_audio_for_scene_plan( |
| scene_plan=request.scene_plan, |
| voice_mode=request.voice_mode, |
| voice=request.voice, |
| language=request.language, |
| provider_name=request.provider, |
| video_id=sample["video_id"], |
| ) |
| except Exception as exc: |
| status_path = BACKEND_ROOT.parent / "outputs" / "video" / "physics" / "lenses" / "voice-tests" / "ai4bharat-error.json" |
| status_path.parent.mkdir(parents=True, exist_ok=True) |
| status_path.write_text( |
| json.dumps({"ok": False, "error_type": type(exc).__name__, "message": str(exc)}, indent=2, ensure_ascii=False), |
| encoding="utf-8", |
| ) |
| raise |
|
|
| props_path = ( |
| BACKEND_ROOT.parent |
| / "public" |
| / "generated" |
| / "audio" |
| / sample["video_id"] |
| / "scene-plan-with-audio.json" |
| ) |
| props_path.write_text( |
| json.dumps({"plan": response["updated_scene_plan"]}, indent=2, ensure_ascii=False), |
| encoding="utf-8", |
| ) |
| print(json.dumps({"ok": True, "props_path": str(props_path), **response}, indent=2, ensure_ascii=False)) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|