Spaces:
Running on Zero
Running on Zero
| #!/usr/bin/env python3 | |
| """ | |
| studio_server.py โ Moldovan AI Media Studio & Agentic Backend Server | |
| FastAPI server exposing REST & SSE endpoints for: | |
| - Master Regionalisms Dictionary CRUD & Search | |
| - Dataset Control Center (All project datasets explorer & editor) | |
| - HuggingFace & Multi-Provider Settings & Dispatcher (FLUX.1, Kokoro-82M, ACE-Step, OpenRouter) | |
| - 6 Moldovan Personas & Interactive Chat Arena | |
| - AI Music Studio & Studio Audio Generator (ACE-Step / MusicGen / 808 Synth / FLUX Album Art) | |
| - Viral 9:16 Video Storyboards & Full 1080x1920 MP4 Video Generator | |
| - Daily Satirical News Digests (PRO TV Chiศinฤu & Koroce News) | |
| - Harvested YouTube & Media Corpus | |
| - Linguistic Converter & 4-Level Dialect Slider | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import time | |
| import glob | |
| import subprocess | |
| from typing import List, Dict, Optional, Any | |
| from fastapi import FastAPI, Request, HTTPException, Body, Query | |
| from fastapi.responses import JSONResponse, FileResponse, HTMLResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.staticfiles import StaticFiles | |
| from starlette.concurrency import run_in_threadpool | |
| import uvicorn | |
| BASE_DIR = os.path.dirname(__file__) | |
| if BASE_DIR not in sys.path: | |
| sys.path.insert(0, BASE_DIR) | |
| DATA_DIR = os.path.join(BASE_DIR, "data") | |
| PERSONAS_PATH = os.path.join(BASE_DIR, "personas/personas_registry.json") | |
| CORPUS_PATH = os.path.join(DATA_DIR, "youtube_corpus/moldovan_youtube_corpus.jsonl") | |
| MUSIC_DIR = os.path.join(DATA_DIR, "generated_music") | |
| VIDEO_DIR = os.path.join(DATA_DIR, "generated_video_scripts") | |
| DIGEST_DIR = os.path.join(DATA_DIR, "generated_digests") | |
| AUDIO_DIR = os.path.join(DATA_DIR, "generated_audio") | |
| IMAGE_DIR = os.path.join(DATA_DIR, "generated_images") | |
| VIDEOS_OUT_DIR = os.path.join(DATA_DIR, "generated_videos") | |
| TRANSCRIPTS_DIR = os.path.join(DATA_DIR, "transcripts") | |
| PROJECTS_DIR = os.path.join(DATA_DIR, "media_projects") | |
| LOG_PATH = os.path.join(DATA_DIR, "pipeline.log") | |
| STATIC_DIR = os.path.join(BASE_DIR, "static") | |
| os.makedirs(DATA_DIR, exist_ok=True) | |
| os.makedirs(MUSIC_DIR, exist_ok=True) | |
| os.makedirs(VIDEO_DIR, exist_ok=True) | |
| os.makedirs(DIGEST_DIR, exist_ok=True) | |
| os.makedirs(AUDIO_DIR, exist_ok=True) | |
| os.makedirs(IMAGE_DIR, exist_ok=True) | |
| os.makedirs(VIDEOS_OUT_DIR, exist_ok=True) | |
| os.makedirs(TRANSCRIPTS_DIR, exist_ok=True) | |
| os.makedirs(PROJECTS_DIR, exist_ok=True) | |
| app = FastAPI( | |
| title="Moldovan AI Media Studio & Command Center API", | |
| description="Backend for Moldovan Cultural AI & Dense Terminal Media Manager", | |
| version="4.2.0" | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # โโโ 1. System Stats & Telemetry Endpoint โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def get_stats(): | |
| """Return dashboard analytics, inventory counts, and system telemetry.""" | |
| corpus_count = 0 | |
| categories = {} | |
| if os.path.exists(CORPUS_PATH): | |
| with open(CORPUS_PATH, "r", encoding="utf-8") as f: | |
| for line in f: | |
| try: | |
| v = json.loads(line) | |
| corpus_count += 1 | |
| cat = v.get("category", "general") | |
| categories[cat] = categories.get(cat, 0) + 1 | |
| except Exception: | |
| pass | |
| music_count = len(glob.glob(os.path.join(MUSIC_DIR, "*.json"))) | |
| if os.path.exists(os.path.join(MUSIC_DIR, "music_catalog_index.json")): | |
| music_count = max(0, music_count - 1) | |
| video_count = len(glob.glob(os.path.join(VIDEO_DIR, "*.json"))) | |
| digest_count = len(glob.glob(os.path.join(DIGEST_DIR, "*.json"))) | |
| audio_wav_count = len(glob.glob(os.path.join(AUDIO_DIR, "*.wav"))) | |
| audio_mp3_count = len(glob.glob(os.path.join(AUDIO_DIR, "*.mp3"))) | |
| total_audio_tracks = audio_wav_count + audio_mp3_count | |
| image_count = len(glob.glob(os.path.join(IMAGE_DIR, "*.png"))) + len(glob.glob(os.path.join(IMAGE_DIR, "*.jpg"))) | |
| video_mp4_count = len(glob.glob(os.path.join(VIDEOS_OUT_DIR, "*.mp4"))) | |
| transcript_count = len(glob.glob(os.path.join(TRANSCRIPTS_DIR, "*.json"))) | |
| personas_count = 6 | |
| if os.path.exists(PERSONAS_PATH): | |
| with open(PERSONAS_PATH, "r", encoding="utf-8") as f: | |
| personas_count = len(json.load(f).get("personas", [])) | |
| # Regionalisms count | |
| from engine.dictionary_manager import DictionaryManager | |
| dm = DictionaryManager() | |
| dict_entries = dm.load_all_entries() | |
| dict_count = len(dict_entries) | |
| # SFT tokens / lines count | |
| sft_lines = 0 | |
| sft_path = os.path.join(DATA_DIR, "moldovan_sft_sample.jsonl") | |
| if os.path.exists(sft_path): | |
| with open(sft_path, "r", encoding="utf-8") as f: | |
| sft_lines = sum(1 for _ in f) | |
| return { | |
| "status": "online", | |
| "timestamp": time.time(), | |
| "total_corpus_videos": corpus_count, | |
| "corpus_by_category": categories, | |
| "total_music_tracks": music_count, | |
| "total_audio_wavs": total_audio_tracks, | |
| "total_audio_tracks": total_audio_tracks, | |
| "total_generated_images": image_count, | |
| "total_generated_videos": video_mp4_count, | |
| "total_video_scripts": video_count, | |
| "total_digests": digest_count, | |
| "total_transcripts": transcript_count, | |
| "total_personas": personas_count, | |
| "total_regionalisms": dict_count, | |
| "sft_training_pairs": sft_lines, | |
| "active_models": { | |
| "image": "black-forest-labs/FLUX-1-dev", | |
| "music": "ACE-Step/acestep-v15-xl-sft", | |
| "tts": "hexgrad/Kokoro-82M", | |
| "video_render": "FFmpeg 1080x1920 9:16 Ken Burns", | |
| "chat": "Qwen/Qwen2.5-72B-Instruct" | |
| } | |
| } | |
| # โโโ 2. Dictionary Endpoints โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def get_regionalisms(search: str = Query(""), category: str = Query(""), page: int = Query(1), limit: int = Query(50)): | |
| from engine.dictionary_manager import DictionaryManager | |
| dm = DictionaryManager() | |
| return dm.get_entries(search=search, category=category, page=page, limit=limit) | |
| async def add_regionalism(body: Dict = Body(...)): | |
| term = body.get("term", "").strip() | |
| definition = body.get("definition", "").strip() | |
| category = body.get("category", "") | |
| example = body.get("example", "") | |
| synonyms = body.get("synonyms", []) | |
| source = body.get("source", "studio_ui") | |
| if not term or not definition: | |
| raise HTTPException(status_code=400, detail="Cรขmpurile 'term' ศi 'definition' sunt obligatorii.") | |
| from engine.dictionary_manager import DictionaryManager | |
| dm = DictionaryManager() | |
| try: | |
| res = dm.add_entry(term=term, definition=definition, category=category, example=example, synonyms=synonyms, source=source) | |
| return res | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def update_regionalism(index: int, body: Dict = Body(...)): | |
| term = body.get("term", "").strip() | |
| definition = body.get("definition", "").strip() | |
| category = body.get("category", "") | |
| example = body.get("example", "") | |
| synonyms = body.get("synonyms", []) | |
| source = body.get("source", "studio_ui") | |
| if not term or not definition: | |
| raise HTTPException(status_code=400, detail="Cรขmpurile 'term' ศi 'definition' sunt obligatorii.") | |
| from engine.dictionary_manager import DictionaryManager | |
| dm = DictionaryManager() | |
| try: | |
| res = dm.update_entry(index=index, term=term, definition=definition, category=category, example=example, synonyms=synonyms, source=source) | |
| return res | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_regionalism(index: int): | |
| from engine.dictionary_manager import DictionaryManager | |
| dm = DictionaryManager() | |
| try: | |
| res = dm.delete_entry(index=index) | |
| return res | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # โโโ 3. Datasets Explorer Endpoints โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def list_datasets(): | |
| from engine.dataset_manager import DatasetManager | |
| mgr = DatasetManager() | |
| return {"datasets": mgr.list_datasets()} | |
| async def get_dataset_records(dataset_id: str, search: str = Query(""), page: int = Query(1), limit: int = Query(25)): | |
| from engine.dataset_manager import DatasetManager | |
| mgr = DatasetManager() | |
| try: | |
| res = mgr.get_dataset_records(dataset_id=dataset_id, search=search, page=page, limit=limit) | |
| return res | |
| except FileNotFoundError: | |
| raise HTTPException(status_code=404, detail=f"Dataset '{dataset_id}' not found") | |
| async def append_dataset_record(dataset_id: str, body: Dict = Body(...)): | |
| from engine.dataset_manager import DatasetManager | |
| mgr = DatasetManager() | |
| try: | |
| res = mgr.append_record(dataset_id=dataset_id, record=body) | |
| return res | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| async def delete_dataset_record(dataset_id: str, index: int): | |
| from engine.dataset_manager import DatasetManager | |
| mgr = DatasetManager() | |
| try: | |
| res = mgr.delete_record(dataset_id=dataset_id, index=index) | |
| return res | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| # โโโ 4. HuggingFace & Multi-Provider Settings โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def get_model_settings(): | |
| from engine.model_dispatcher import ModelDispatcher | |
| disp = ModelDispatcher() | |
| return disp.get_settings() | |
| async def save_model_settings(body: Dict = Body(...)): | |
| from engine.model_dispatcher import ModelDispatcher | |
| disp = ModelDispatcher() | |
| return disp.save_settings(body) | |
| async def test_provider_connection(body: Dict = Body(...)): | |
| provider = body.get("provider", "huggingface") | |
| token = body.get("token") | |
| from engine.model_dispatcher import ModelDispatcher | |
| disp = ModelDispatcher() | |
| return await run_in_threadpool(disp.test_provider, provider=provider, token=token) | |
| async def dispatch_model_inference(body: Dict = Body(...)): | |
| """Dispatch inference off the asyncio event loop for long-running providers.""" | |
| task = body.get("task", "chat") | |
| prompt = body.get("prompt", "") | |
| params = body.get("params", {}) | |
| from engine.model_dispatcher import ModelDispatcher | |
| disp = ModelDispatcher() | |
| return await run_in_threadpool(disp.dispatch_inference, task=task, prompt=prompt, params=params) | |
| # โโโ 5. Audio Synthesizer, TTS & Media Stream Endpoints โโโโโโโโโโโโโโโโโโโโโ | |
| async def synthesize_track_audio(body: Dict = Body(...)): | |
| """Synthesizes complete multi-instrument audio track off the event loop.""" | |
| title = body.get("title", "Chiศinฤu Night Beat") | |
| genre = body.get("genre", "Chiศinฤu 808 Trap") | |
| bpm = int(body.get("bpm", 140)) | |
| lyrics = body.get("lyrics", "") | |
| engine = body.get("engine", "ai_studio") | |
| duration = int(body.get("duration", body.get("duration_seconds", 60))) | |
| from engine.model_dispatcher import ModelDispatcher | |
| disp = ModelDispatcher() | |
| if engine in ["acestep", "ai_studio", "musicgen"]: | |
| return await run_in_threadpool( | |
| disp.generate_music, | |
| prompt=title, | |
| lyrics=lyrics, | |
| genre=genre, | |
| duration_seconds=duration, | |
| bpm=bpm, | |
| engine="acestep" if engine in ["acestep", "ai_studio"] else "musicgen" | |
| ) | |
| from engine.audio_synthesizer import MoldovanAudioSynthesizer | |
| synth = MoldovanAudioSynthesizer(output_dir=AUDIO_DIR) | |
| return await run_in_threadpool( | |
| synth.generate_track_audio, | |
| title=title, | |
| genre=genre, | |
| bpm=bpm, | |
| bars=max(8, int((duration / 60) * 32)), | |
| custom_lyrics=lyrics | |
| ) | |
| async def synthesize_speech_tts(body: Dict = Body(...)): | |
| """Synthesizes realistic speech audio with Kokoro-82M TTS.""" | |
| text = body.get("text", "").strip() | |
| persona_id = body.get("persona_id", "dj_botanica") | |
| voice = body.get("voice") | |
| if not text: | |
| raise HTTPException(status_code=400, detail="Field 'text' is required") | |
| from engine.model_dispatcher import ModelDispatcher | |
| disp = ModelDispatcher() | |
| return await run_in_threadpool(disp.generate_tts, text=text, persona_id=persona_id, voice=voice) | |
| async def list_audio_files(): | |
| """List all synthesized audio files on disk.""" | |
| files = [] | |
| for ext in ["*.wav", "*.mp3"]: | |
| for fpath in glob.glob(os.path.join(AUDIO_DIR, ext)): | |
| fname = os.path.basename(fpath) | |
| files.append({ | |
| "filename": fname, | |
| "path": fpath, | |
| "url": f"/api/media/audio/stream/{fname}", | |
| "size_bytes": os.path.getsize(fpath), | |
| "format": fname.split(".")[-1].upper(), | |
| "created_at": os.path.getmtime(fpath) | |
| }) | |
| return {"files": sorted(files, key=lambda x: x["created_at"], reverse=True)} | |
| async def stream_audio_file(filename: str): | |
| """Stream a WAV or MP3 audio file from disk.""" | |
| clean_name = os.path.basename(filename) | |
| file_path = os.path.join(AUDIO_DIR, clean_name) | |
| if not os.path.exists(file_path): | |
| raise HTTPException(status_code=404, detail="Audio file not found") | |
| media_type = "audio/mpeg" if clean_name.endswith(".mp3") else "audio/wav" | |
| return FileResponse(file_path, media_type=media_type, filename=clean_name) | |
| # โโโ 6. FLUX.1 Images & Visual Assets โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def generate_flux_image(body: Dict = Body(...)): | |
| """Generate high-resolution image with FLUX.1.""" | |
| prompt = body.get("prompt", "").strip() | |
| width = int(body.get("width", 768)) | |
| height = int(body.get("height", 1344)) | |
| if not prompt: | |
| raise HTTPException(status_code=400, detail="Field 'prompt' is required") | |
| from engine.model_dispatcher import ModelDispatcher | |
| disp = ModelDispatcher() | |
| return await run_in_threadpool(disp.generate_image, prompt=prompt, width=width, height=height) | |
| async def view_image_file(filename: str): | |
| """View/download a generated image.""" | |
| clean_name = os.path.basename(filename) | |
| file_path = os.path.join(IMAGE_DIR, clean_name) | |
| if not os.path.exists(file_path): | |
| raise HTTPException(status_code=404, detail="Image file not found") | |
| media_type = "image/png" if clean_name.endswith(".png") else "image/jpeg" | |
| return FileResponse(file_path, media_type=media_type, filename=clean_name) | |
| # โโโ 7. Personas Endpoints โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def list_personas(): | |
| if not os.path.exists(PERSONAS_PATH): | |
| raise HTTPException(status_code=404, detail="Personas registry not found") | |
| with open(PERSONAS_PATH, "r", encoding="utf-8") as f: | |
| data = json.load(f) | |
| return data | |
| async def get_persona_detail(persona_id: str): | |
| from personas.persona_engine import PersonaEngine | |
| pe = PersonaEngine() | |
| p = pe.get_persona(persona_id) | |
| if not p: | |
| raise HTTPException(status_code=404, detail=f"Persona '{persona_id}' not found") | |
| return p | |
| async def chat_with_persona(persona_id: str, body: Dict = Body(...)): | |
| message = body.get("message", "").strip() | |
| history = body.get("history", []) | |
| if not message: | |
| raise HTTPException(status_code=400, detail="Message is required") | |
| from personas.persona_engine import PersonaEngine | |
| pe = PersonaEngine() | |
| return await run_in_threadpool(pe.chat_with_persona, persona_id=persona_id, user_message=message, chat_history=history) | |
| # โโโ 8. YouTube & Transcripts Corpus โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def list_corpus(page: int = Query(1), limit: int = Query(20), category: str = Query(""), search: str = Query("")): | |
| items = [] | |
| if os.path.exists(CORPUS_PATH): | |
| with open(CORPUS_PATH, "r", encoding="utf-8") as f: | |
| for line in f: | |
| try: | |
| items.append(json.loads(line)) | |
| except Exception: | |
| pass | |
| if category: | |
| items = [i for i in items if i.get("category") == category] | |
| if search: | |
| s = search.lower() | |
| items = [i for i in items if s in i.get("title", "").lower() or s in i.get("description", "").lower()] | |
| total = len(items) | |
| start = (page - 1) * limit | |
| end = start + limit | |
| paginated = items[start:end] | |
| return { | |
| "items": paginated, | |
| "total": total, | |
| "page": page, | |
| "limit": limit, | |
| "pages": (total + limit - 1) // limit if total > 0 else 1 | |
| } | |
| # โโโ 9. Music Catalog & Creation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def list_music_genres(): | |
| """List all 20+ authentic Moldovan music genre blueprints.""" | |
| from engine.media_creator import GENRE_REGISTRY | |
| genres = [] | |
| for k, v in GENRE_REGISTRY.items(): | |
| genres.append({ | |
| "id": k, | |
| "name": k, | |
| "bpm": v["bpm"], | |
| "key": v["key"], | |
| "vocal_style": v["vocal_style"], | |
| "instrumentation": v["instrumentation"], | |
| "prompt_vibe": v["prompt_vibe"] | |
| }) | |
| return {"genres": genres, "count": len(genres)} | |
| async def list_music(): | |
| """List all AI generated Moldovan music tracks.""" | |
| tracks = [] | |
| for fpath in sorted(glob.glob(os.path.join(MUSIC_DIR, "*.json")), reverse=True): | |
| if "music_catalog_index.json" in fpath: | |
| continue | |
| try: | |
| with open(fpath, "r", encoding="utf-8") as f: | |
| tracks.append(json.load(f)) | |
| except Exception: | |
| pass | |
| return {"tracks": tracks, "count": len(tracks)} | |
| async def trigger_generate_music(): | |
| """Generate or regenerate music track packages.""" | |
| from scripts.generate_moldovan_music import generate_music_catalog | |
| try: | |
| generate_music_catalog(MUSIC_DIR) | |
| return {"status": "success", "message": "Music catalog generated successfully"} | |
| except Exception as e: | |
| return {"status": "error", "message": str(e)} | |
| async def create_custom_song(body: Dict = Body(...)): | |
| """Generate a custom Moldovan song with lyrics, regionalisms, and real AI audio + FLUX cover.""" | |
| topic = body.get("topic", "") | |
| genre = body.get("genre", "Chiศinฤu 808 Trap") | |
| duration = int(body.get("duration", body.get("duration_seconds", 60))) | |
| bpm = body.get("bpm") | |
| key = body.get("key") | |
| vocal_style = body.get("vocal_style") | |
| dialect_level = int(body.get("dialect_level", 2)) | |
| mood = body.get("mood", "Energetic & Sarcastic") | |
| custom_lyrics = body.get("lyrics", body.get("custom_lyrics")) | |
| audio_engine = body.get("engine", body.get("audio_engine", "acestep")) | |
| cover_style = body.get("cover_style", "Cyberpunk Vinyl") | |
| from engine.media_creator import MediaCreator | |
| mc = MediaCreator() | |
| try: | |
| return await run_in_threadpool( | |
| mc.generate_song, | |
| topic=topic, | |
| genre=genre, | |
| duration_seconds=duration, | |
| bpm=int(bpm) if bpm else None, | |
| key=key, | |
| vocal_style=vocal_style, | |
| dialect_level=dialect_level, | |
| mood=mood, | |
| custom_lyrics=custom_lyrics, | |
| audio_engine=audio_engine, | |
| cover_style=cover_style | |
| ) | |
| except RuntimeError as e: | |
| # A selected real model must never silently degrade to procedural audio. | |
| return JSONResponse(status_code=503, content={"status": "error", "error": str(e), "fallback_used": False}) | |
| except Exception as e: | |
| return JSONResponse(status_code=500, content={"status": "error", "error": str(e)}) | |
| async def update_existing_song(body: Dict = Body(...)): | |
| """Surgically edit lyrics, BPM, genre, or vocal delivery of an existing song.""" | |
| song_id = body.get("id") or body.get("song_id") | |
| if not song_id: | |
| raise HTTPException(status_code=400, detail="Field 'id' or 'song_id' is required") | |
| lyrics = body.get("lyrics") or body.get("custom_lyrics") | |
| bpm = body.get("bpm") | |
| vocal_persona = body.get("vocal_persona") or body.get("persona_id") | |
| genre = body.get("genre") | |
| from engine.media_creator import MediaCreator | |
| mc = MediaCreator() | |
| try: | |
| return await run_in_threadpool( | |
| mc.update_song, | |
| song_id=song_id, | |
| custom_lyrics=lyrics, | |
| bpm=int(bpm) if bpm else None, | |
| vocal_persona=vocal_persona, | |
| genre=genre | |
| ) | |
| except FileNotFoundError as e: | |
| raise HTTPException(status_code=404, detail=str(e)) | |
| except RuntimeError as e: | |
| # A selected real model must never silently degrade to procedural audio. | |
| return JSONResponse(status_code=503, content={"status": "error", "error": str(e), "fallback_used": False}) | |
| except Exception as e: | |
| return JSONResponse(status_code=500, content={"status": "error", "error": str(e)}) | |
| # โโโ 10. Video Studio & Multi-Aspect MP4 Video Generator โโโโโโโโโโโโโโโโโโโโโ | |
| async def list_videos(): | |
| """List all saved viral video storyboards.""" | |
| videos = [] | |
| for fpath in sorted(glob.glob(os.path.join(VIDEO_DIR, "*.json")), reverse=True): | |
| try: | |
| with open(fpath, "r", encoding="utf-8") as f: | |
| videos.append(json.load(f)) | |
| except Exception: | |
| pass | |
| return {"videos": videos, "count": len(videos)} | |
| async def create_custom_video(body: Dict = Body(...)): | |
| """Generate a custom viral video with FLUX.1 scenes, Kokoro TTS, and rendered MP4.""" | |
| topic = body.get("topic", "") | |
| style = body.get("style", "Viral TikTok/Reel") | |
| duration = int(body.get("duration", body.get("duration_seconds", 60))) | |
| scenes_count = body.get("scenes_count") | |
| aspect_ratio = body.get("aspect_ratio", "9:16") | |
| visual_aesthetic = body.get("visual_aesthetic", "Photorealistic 8K") | |
| camera_motion = body.get("camera_motion", "dynamic_mix") | |
| voiceover_persona = body.get("voiceover_persona", "dj_botanica") | |
| render_mp4 = bool(body.get("render_mp4", True)) | |
| from engine.media_creator import MediaCreator | |
| mc = MediaCreator() | |
| return await run_in_threadpool( | |
| mc.generate_video_storyboard, | |
| topic=topic, | |
| style=style, | |
| duration_seconds=duration, | |
| scenes_count=int(scenes_count) if scenes_count else None, | |
| aspect_ratio=aspect_ratio, | |
| visual_aesthetic=visual_aesthetic, | |
| camera_motion=camera_motion, | |
| voiceover_persona=voiceover_persona, | |
| render_mp4=render_mp4 | |
| ) | |
| async def render_video_mp4_endpoint(body: Dict = Body(...)): | |
| """Compile or re-render an existing video storyboard into an MP4 video file.""" | |
| storyboard_id = body.get("id") or body.get("storyboard_id", "") | |
| if not storyboard_id: | |
| raise HTTPException(status_code=400, detail="Field 'id' or 'storyboard_id' is required") | |
| from engine.media_creator import MediaCreator | |
| mc = MediaCreator() | |
| return await run_in_threadpool(mc.render_storyboard_to_mp4, storyboard_id=storyboard_id) | |
| async def stream_video_file(filename: str): | |
| """Stream or download a rendered MP4 video file.""" | |
| clean_name = os.path.basename(filename) | |
| file_path = os.path.join(VIDEOS_OUT_DIR, clean_name) | |
| if not os.path.exists(file_path): | |
| raise HTTPException(status_code=404, detail="Video file not found") | |
| return FileResponse(file_path, media_type="video/mp4", filename=clean_name) | |
| # โโโ 11. Media Vault (Unified Asset Browser) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def list_media_assets(asset_type: str = Query("ALL"), search: str = Query("")): | |
| """Unified Media Vault: List all media assets across audio, video, images, music, digests.""" | |
| assets = [] | |
| # 1. MP4 Videos | |
| for fpath in glob.glob(os.path.join(VIDEOS_OUT_DIR, "*.mp4")): | |
| fname = os.path.basename(fpath) | |
| assets.append({ | |
| "type": "video_mp4", | |
| "filename": fname, | |
| "title": fname.replace(".mp4", "").replace("_", " ").title(), | |
| "path": fpath, | |
| "url": f"/api/media/video/stream/{fname}", | |
| "size_kb": round(os.path.getsize(fpath) / 1024, 1), | |
| "updated_at": os.path.getmtime(fpath) | |
| }) | |
| # 2. FLUX Images | |
| for fpath in glob.glob(os.path.join(IMAGE_DIR, "*.*")): | |
| fname = os.path.basename(fpath) | |
| if not (fname.endswith(".png") or fname.endswith(".jpg") or fname.endswith(".webp")): | |
| continue | |
| assets.append({ | |
| "type": "image", | |
| "filename": fname, | |
| "title": fname.replace(".png", "").replace(".jpg", "").replace("_", " ").title(), | |
| "path": fpath, | |
| "url": f"/api/media/image/view/{fname}", | |
| "size_kb": round(os.path.getsize(fpath) / 1024, 1), | |
| "updated_at": os.path.getmtime(fpath) | |
| }) | |
| # 3. Audio files (WAV & MP3) | |
| for ext in ["*.wav", "*.mp3"]: | |
| for fpath in glob.glob(os.path.join(AUDIO_DIR, ext)): | |
| fname = os.path.basename(fpath) | |
| assets.append({ | |
| "type": "audio", | |
| "filename": fname, | |
| "title": fname.replace(".wav", "").replace(".mp3", "").replace("_", " ").title(), | |
| "path": fpath, | |
| "url": f"/api/media/audio/stream/{fname}", | |
| "size_kb": round(os.path.getsize(fpath) / 1024, 1), | |
| "updated_at": os.path.getmtime(fpath) | |
| }) | |
| # 4. Music manifests | |
| for fpath in glob.glob(os.path.join(MUSIC_DIR, "*.json")): | |
| if "music_catalog_index.json" in fpath: | |
| continue | |
| try: | |
| with open(fpath, "r", encoding="utf-8") as f: | |
| d = json.load(f) | |
| assets.append({ | |
| "type": "music", | |
| "filename": os.path.basename(fpath), | |
| "title": d.get("title", os.path.basename(fpath)), | |
| "genre": d.get("genre", "Trap"), | |
| "bpm": d.get("bpm", 140), | |
| "audio_url": d.get("audio_url"), | |
| "cover_image_url": d.get("cover_image_url"), | |
| "size_kb": round(os.path.getsize(fpath) / 1024, 1), | |
| "updated_at": os.path.getmtime(fpath) | |
| }) | |
| except Exception: | |
| pass | |
| # 5. Video storyboards | |
| for fpath in glob.glob(os.path.join(VIDEO_DIR, "*.json")): | |
| try: | |
| with open(fpath, "r", encoding="utf-8") as f: | |
| d = json.load(f) | |
| assets.append({ | |
| "type": "video_script", | |
| "filename": os.path.basename(fpath), | |
| "title": d.get("title", os.path.basename(fpath)), | |
| "scenes_count": len(d.get("scenes", [])), | |
| "video_url": d.get("video_url"), | |
| "size_kb": round(os.path.getsize(fpath) / 1024, 1), | |
| "updated_at": os.path.getmtime(fpath) | |
| }) | |
| except Exception: | |
| pass | |
| # 6. News digests | |
| for fpath in glob.glob(os.path.join(DIGEST_DIR, "*.json")): | |
| try: | |
| with open(fpath, "r", encoding="utf-8") as f: | |
| d = json.load(f) | |
| assets.append({ | |
| "type": "digest", | |
| "filename": os.path.basename(fpath), | |
| "title": d.get("title", "Daily News Digest"), | |
| "date": d.get("date", ""), | |
| "size_kb": round(os.path.getsize(fpath) / 1024, 1), | |
| "updated_at": os.path.getmtime(fpath) | |
| }) | |
| except Exception: | |
| pass | |
| if asset_type and asset_type != "ALL": | |
| assets = [a for a in assets if a.get("type", "").lower() == asset_type.lower()] | |
| if search: | |
| s = search.lower() | |
| assets = [a for a in assets if s in a.get("title", "").lower() or s in a.get("filename", "").lower()] | |
| return {"assets": sorted(assets, key=lambda x: x.get("updated_at", 0), reverse=True), "count": len(assets)} | |
| # โโโ 12. Daily Digests โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def list_digests(): | |
| digests = [] | |
| for fpath in sorted(glob.glob(os.path.join(DIGEST_DIR, "*.json")), reverse=True): | |
| try: | |
| with open(fpath, "r", encoding="utf-8") as f: | |
| digests.append(json.load(f)) | |
| except Exception: | |
| pass | |
| return {"digests": digests, "count": len(digests)} | |
| async def trigger_digest_generation(): | |
| from engine.synthesize_daily_news import load_corpus, generate_daily_digest | |
| items = load_corpus(CORPUS_PATH) | |
| if not items: | |
| raise HTTPException(status_code=400, detail="Corpus is empty") | |
| res = await run_in_threadpool(generate_daily_digest, items, DIGEST_DIR) | |
| return {"status": "success", "digest": res} | |
| # โโโ 13. Linguistics & Dialect Endpoints โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def convert_dialect(body: Dict = Body(...)): | |
| text = body.get("text", "").strip() | |
| level = int(body.get("level", 2)) | |
| if not text: | |
| raise HTTPException(status_code=400, detail="Field 'text' is required") | |
| from linguistics.dialect_converter import DialectConverter | |
| conv = DialectConverter() | |
| res = conv.convert_to_moldovan(text, level=level) | |
| density_info = conv.analyze_dialect_density(res.get("converted", text)) | |
| res["density_pct"] = density_info.get("density_pct", 0) | |
| res["dialect_text"] = res.get("converted", text) | |
| return res | |
| async def normalize_dialect(body: Dict = Body(...)): | |
| text = body.get("text", "").strip() | |
| if not text: | |
| raise HTTPException(status_code=400, detail="Field 'text' is required") | |
| from linguistics.dialect_converter import DialectConverter | |
| conv = DialectConverter() | |
| return conv.normalize_to_standard(text) | |
| async def get_thesaurus(): | |
| thesaurus_path = os.path.join(BASE_DIR, "linguistics/moldovan_thesaurus.json") | |
| if not os.path.exists(thesaurus_path): | |
| raise HTTPException(status_code=404, detail="Thesaurus file not found") | |
| with open(thesaurus_path, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| async def get_linguistic_analytics(): | |
| from linguistics.corpus_analytics import CorpusAnalytics | |
| analytics = CorpusAnalytics() | |
| return analytics.generate_report() | |
| # โโโ 14. Meme & Cloner Endpoints โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def clone_custom_persona(body: Dict = Body(...)): | |
| name = body.get("name", "").strip() | |
| role = body.get("role", "").strip() | |
| avatar = body.get("avatar", "๐๏ธ") | |
| corpus = body.get("corpus", "") | |
| if not name or not role: | |
| raise HTTPException(status_code=400, detail="Fields 'name' and 'role' are required") | |
| from engine.media_creator import MediaCreator | |
| mc = MediaCreator() | |
| return mc.clone_persona_from_text(name=name, role=role, sample_transcript=corpus, avatar_emoji=avatar) | |
| async def create_custom_meme(body: Dict = Body(...)): | |
| situation = body.get("situation", "") | |
| template_id = body.get("template_id", "taximetrist") | |
| from engine.media_creator import MediaCreator | |
| mc = MediaCreator() | |
| return mc.generate_meme(situation=situation, template_id=template_id) | |
| async def get_meme_templates(): | |
| return { | |
| "templates": [ | |
| {"id": "taximetrist", "title": "๐ Taximetristul de la Garฤ", "punchline_style": "Dialog sarcastic pe preศuri"}, | |
| {"id": "babusca", "title": "๐ต Babuศca la Piaศa Centralฤ", "punchline_style": "Negociere agresivฤ la roศii"}, | |
| {"id": "vama", "title": "๐ Moldoveanul la Vama Leuศeni", "punchline_style": "Portbagajul plin cu kuleoace ศi vin"}, | |
| {"id": "rutiera", "title": "๐ Discuศie รฎn Rutiera 175", "punchline_style": "Opreศte la colศ la semafor"}, | |
| {"id": "cumetrie", "title": "๐ฅ Toast la Cumฤtrie", "punchline_style": "Urare lungฤ cu paharul sus"} | |
| ] | |
| } | |
| # โโโ 15. Pipeline Triggers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def trigger_pipeline(): | |
| script_path = os.path.join(BASE_DIR, "scripts/run_automated_pipeline.sh") | |
| if not os.path.exists(script_path): | |
| raise HTTPException(status_code=404, detail="Pipeline script not found") | |
| try: | |
| proc = subprocess.Popen(["bash", script_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| return {"status": "started", "pid": proc.pid, "message": "Pipeline execution started in background"} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Failed to start pipeline: {str(e)}") | |
| async def get_pipeline_logs(lines: int = 50): | |
| if not os.path.exists(LOG_PATH): | |
| return {"logs": ["No log file found at data/pipeline.log"], "lines_count": 0} | |
| try: | |
| with open(LOG_PATH, "r", encoding="utf-8") as f: | |
| all_lines = f.readlines() | |
| tail = [l.rstrip("\r\n") for l in all_lines[-lines:]] | |
| return {"logs": tail, "lines_count": len(all_lines)} | |
| except Exception as e: | |
| return {"logs": [f"Error reading logs: {str(e)}"], "lines_count": 0} | |
| # โโโ Static Mounts โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| app.mount("/data/generated_images", StaticFiles(directory=IMAGE_DIR), name="generated_images") | |
| app.mount("/data/generated_videos", StaticFiles(directory=VIDEOS_OUT_DIR), name="generated_videos") | |
| app.mount("/data/generated_audio", StaticFiles(directory=AUDIO_DIR), name="generated_audio") | |
| if os.path.exists(STATIC_DIR): | |
| app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static") | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 8090)) | |
| print(f"๐ Starting Moldovan AI Studio Server on http://127.0.0.1:{port}") | |
| uvicorn.run(app, host="0.0.0.0", port=port) | |