#!/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 ──────────────────────────────────── @app.get("/api/stats") 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 ──────────────────────────────────────────────── @app.get("/api/dictionary/regionalisme") 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) @app.post("/api/dictionary/regionalisme") 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)) @app.put("/api/dictionary/regionalisme/{index}") 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)) @app.delete("/api/dictionary/regionalisme/{index}") 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 ────────────────────────────────────────── @app.get("/api/datasets/list") async def list_datasets(): from engine.dataset_manager import DatasetManager mgr = DatasetManager() return {"datasets": mgr.list_datasets()} @app.get("/api/datasets/{dataset_id}/records") 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") @app.post("/api/datasets/{dataset_id}/records") 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)) @app.delete("/api/datasets/{dataset_id}/records/{index}") 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 ──────────────────────────────── @app.get("/api/settings/models") @app.get("/api/settings/config") async def get_model_settings(): from engine.model_dispatcher import ModelDispatcher disp = ModelDispatcher() return disp.get_settings() @app.post("/api/settings/models") async def save_model_settings(body: Dict = Body(...)): from engine.model_dispatcher import ModelDispatcher disp = ModelDispatcher() return disp.save_settings(body) @app.post("/api/settings/test-provider") 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) @app.post("/api/models/dispatch") 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 ───────────────────── @app.post("/api/media/audio/synthesize") 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 ) @app.post("/api/media/audio/tts") 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) @app.get("/api/media/audio/list") 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)} @app.get("/api/media/audio/stream/{filename}") 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 ──────────────────────────────────────── @app.post("/api/media/image/flux") 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) @app.get("/api/media/image/view/{filename}") 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 ─────────────────────────────────────────────────── @app.get("/api/personas") 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 @app.get("/api/personas/{persona_id}") 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 @app.post("/api/personas/{persona_id}/chat") 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 ───────────────────────────────────────── @app.get("/api/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 ───────────────────────────────────────────── @app.get("/api/music/genres") 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)} @app.get("/api/music") 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)} @app.post("/api/generate/music") 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)} @app.post("/api/media/song/create") 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)}) @app.post("/api/media/song/update") 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 ───────────────────── @app.get("/api/videos") 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)} @app.post("/api/media/video/create") 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 ) @app.post("/api/media/video/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) @app.get("/api/media/video/stream/{filename}") 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) ───────────────────────────────── @app.get("/api/media/vault") 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 ─────────────────────────────────────────────────────── @app.get("/api/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)} @app.post("/api/generate/digest") 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 ───────────────────────────────────── @app.post("/api/linguistics/convert") 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 @app.post("/api/linguistics/normalize") 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) @app.get("/api/linguistics/thesaurus") 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) @app.get("/api/linguistics/analytics") async def get_linguistic_analytics(): from linguistics.corpus_analytics import CorpusAnalytics analytics = CorpusAnalytics() return analytics.generate_report() # ─── 14. Meme & Cloner Endpoints ───────────────────────────────────────────── @app.post("/api/media/persona/clone") 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) @app.post("/api/media/meme/create") 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) @app.get("/api/media/meme/templates") 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 ─────────────────────────────────────────────────── @app.post("/api/pipeline/run") 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)}") @app.get("/api/pipeline/logs") 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)