""" Saumya — AI Movie & TV Show Assistant Created by Samith Dilshan Runs on HuggingFace Spaces (Docker SDK) · FastAPI + Gradio """ from __future__ import annotations import os import re import requests import gradio as gr from fastapi import FastAPI, Query, HTTPException from fastapi.responses import JSONResponse from pydantic import BaseModel from typing import Optional import uvicorn # ══════════════════════════════════════════════════════════════════ # CONFIGURATION (set via HF Space Secrets) # ══════════════════════════════════════════════════════════════════ TMDB_API_KEY = os.getenv("TMDB_API_KEY", "YOUR_TMDB_API_KEY") HF_TOKEN = os.getenv("HF_TOKEN", "YOUR_HF_TOKEN") ADMIN_KEY = os.getenv("ADMIN_KEY", "change_this_secret") # router.huggingface.co — model goes in the request BODY, not the URL path # SinLlama — Llama-3-8B fine-tuned on 10.7M Sinhala sentences (polyglots/SinLlama_v01) # Llama-3.3-70B — strong multilingual fallback _HF_BASE = "https://router.huggingface.co/featherless-ai/v1/chat/completions" _HF_MODELS = [ ("polyglots/SinLlama_v01", _HF_BASE), # primary: dedicated Sinhala LLM ("meta-llama/Llama-3.3-70B-Instruct", _HF_BASE), # fallback: strong multilingual ("google/gemma-3-27b-it", _HF_BASE), # last resort ] # Mutable toggle — flip via POST /api/admin/gradio-toggle _gradio_enabled: bool = True # ══════════════════════════════════════════════════════════════════ # LANGUAGE DETECTION # ══════════════════════════════════════════════════════════════════ def detect_lang(text: str) -> str: """'si' if Sinhala Unicode chars present, else 'en'.""" return "si" if re.search(r"[\u0D80-\u0DFF]", text) else "en" # ══════════════════════════════════════════════════════════════════ # HF INFERENCE CALL (retry + model fallback) # ══════════════════════════════════════════════════════════════════ import time def _call_model(name: str, url: str, prompt: str, max_tokens: int) -> str: """OpenAI-compatible chat/completions via router.huggingface.co""" headers = { "Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json", } payload = { "model": name, "messages": [ {"role": "system", "content": "You are Saumya, a friendly movie and TV show AI assistant. You support both English and Sinhala. Reply naturally and helpfully."}, {"role": "user", "content": prompt}, ], "max_tokens": max_tokens, "stream": False, } for attempt in range(3): r = requests.post(url, headers=headers, json=payload, timeout=90) print(f"[HF] {name} attempt {attempt+1} → {r.status_code}") if r.status_code in (503, 429): wait = min(int(r.headers.get("Retry-After", 20)), 30) print(f"[HF] Rate-limited/loading — waiting {wait}s …") time.sleep(wait) continue if not r.ok: raise RuntimeError(f"HTTP {r.status_code}: {r.text[:250]}") data = r.json() if isinstance(data, dict) and "error" in data: raise RuntimeError(data["error"]) return data["choices"][0]["message"]["content"].strip() raise TimeoutError(f"{name} still busy after 3 retries") def _hf(prompt: str, max_tokens: int = 350) -> str: """Try each model in _HF_MODELS until one succeeds.""" if not HF_TOKEN or HF_TOKEN == "YOUR_HF_TOKEN": print("[HF] ⚠️ HF_TOKEN not set") return "" for name, url in _HF_MODELS: try: result = _call_model(name, url, prompt, max_tokens) if result: print(f"[HF] ✅ {name}") return result except Exception as e: print(f"[HF] ❌ {name}: {e}") continue print("[HF] ⚠️ All models failed") return "" # ══════════════════════════════════════════════════════════════════ # INTENT CLASSIFICATION # ══════════════════════════════════════════════════════════════════ _RX_ABOUT_AI = re.compile( r"(who\s+(are|made|created|built)\s+you" r"|what\s+are\s+you" r"|your\s+(name|creator|developer|maker)" r"|introduce\s+yourself" r"|about\s+you" r"|ඔයා\s*කවුද|ඔබ\s*කවුද|හැදුවේ|නිර්මාණය|AI\s*කවුද" r"|ඔයාව\s*හැදුවේ|ඔබව\s*හැදුවේ)", re.I, ) _RX_CODE = re.compile( r"\b(code|script|program|implement|algorithm|function|class" r"|sql|python|javascript|typescript|html|css|api\s+endpoint" r"|කෝඩ්|ප්‍රොග්‍රෑම්|ලියලා\s*දෙන්න)\b", re.I, ) _RX_TV = re.compile( r"\b(tv\s*show|web\s*series|series|episode|season|serial|anime" r"|netflix|hbo|amazon\s*prime|disney\+|peacock|hulu|apple\s*tv" r"|ටෙලි|සීරීස්|ෂෝ|episodes|ටීවී)\b", re.I, ) _RX_MOVIE = re.compile( r"\b(movie|film|cinema|watch|download|trailer|cast|plot" r"|ෆිල්ම්|චිත්‍රපට|බලන්න|ඕනේ\s*ෆිල්ම්|ම්ෆිල්ම්)\b", re.I, ) _RX_RECOMMEND = re.compile( r"\b(recommend|suggest|best|top|good|popular|latest|trending|new\s+movie" r"|රිකමෙන්ඩ්|හොඳ|ජනප්‍රිය|නව|හොදම|suggest)\b", re.I, ) def classify_intent(q: str) -> str: if _RX_ABOUT_AI.search(q): return "about_ai" if _RX_CODE.search(q): return "code_request" if _RX_TV.search(q): return "tv" if _RX_MOVIE.search(q): return "movie" if _RX_RECOMMEND.search(q): return "recommend" return "general" # ══════════════════════════════════════════════════════════════════ # TITLE EXTRACTION # ══════════════════════════════════════════════════════════════════ _NOISE = re.compile( r"\b(i\s+want\s+to|want\s+to|i'?d\s+like\s+to|can\s+i|please" r"|help\s+me|show\s+me|tell\s+me\s+about|looking\s+for|find\s+me" r"|watch|see|find|get|download|the\s+movie|a\s+movie|the\s+film" r"|the\s+tv\s+show|the\s+series|the\s+show|movie|film|show|series|tv" r"|ෆිල්ම්\s*එක|ෆිල්ම්|චිත්‍රපටය|චිත්‍රපට" r"|බලන්න\s*ඕනේ|බලන්න|ඕනේ|ගෙනල්ලා|දෙන්න|ගෙනල්ලා\s*දෙන්න" r"|මට|මමට|ඒ|මම)\b", re.I, ) def extract_title(query: str, kind: str) -> str: """Use LLM to extract media title; fall back to simple regex cleaning.""" prompt = ( f"Extract only the {kind} title from this query. " f"Return ONLY the bare title — no explanation, no quotes, no punctuation.\n" f"Query: {query}\n" f"Title:" ) result = _hf(prompt, max_tokens=20).split("\n")[0].strip().strip("\"'.,!?") if result and 2 <= len(result) <= 80: return result # Fallback: strip noise words cleaned = _NOISE.sub(" ", query) return re.sub(r"\s+", " ", cleaned).strip() or query # ══════════════════════════════════════════════════════════════════ # TMDb HELPERS # ══════════════════════════════════════════════════════════════════ def _tmdb(path: str, **params) -> dict: r = requests.get( f"https://api.themoviedb.org/3{path}", params={"api_key": TMDB_API_KEY, **params}, timeout=10, ) r.raise_for_status() return r.json() def find_movie(title: str) -> tuple: """Returns (imdb_id, tmdb_detail) or (None, None).""" try: results = _tmdb("/search/movie", query=title, language="en-US").get("results", []) if not results: return None, None tid = results[0]["id"] detail = _tmdb(f"/movie/{tid}") return detail.get("imdb_id"), detail except Exception: return None, None def find_tv(title: str) -> tuple: """Returns (imdb_id, tmdb_detail) or (None, None).""" try: results = _tmdb("/search/tv", query=title, language="en-US").get("results", []) if not results: return None, None tid = results[0]["id"] ext = _tmdb(f"/tv/{tid}/external_ids") detail = _tmdb(f"/tv/{tid}") return ext.get("imdb_id"), detail except Exception: return None, None # ══════════════════════════════════════════════════════════════════ # CUSTOM MOVIES API # ══════════════════════════════════════════════════════════════════ def fetch_custom(imdb_id: str) -> dict: try: r = requests.get( "https://movies-api.accel.li/api/v2/movie_details.json", params={"imdb_id": imdb_id}, timeout=10, ) r.raise_for_status() return r.json() except Exception: return {} # ══════════════════════════════════════════════════════════════════ # AI MESSAGE GENERATORS # ══════════════════════════════════════════════════════════════════ def _genres_str(data: dict) -> str: raw = data.get("genres", []) return ", ".join( g["name"] if isinstance(g, dict) else str(g) for g in raw ) or "N/A" def make_media_message(data: dict, lang: str, kind: str = "movie", tmdb_data: dict = None) -> str: merged = {**(tmdb_data or {}), **{k: v for k, v in data.items() if v}} title = merged.get("title") or merged.get("name", "?") rel = merged.get("release_date") or merged.get("first_air_date") or "" year = str(merged.get("year") or (rel[:4] if len(rel) >= 4 else "")) genres = _genres_str(merged) rating = merged.get("rating") or merged.get("vote_average", "N/A") rt_raw = merged.get("episode_run_time") runtime = rt_raw[0] if isinstance(rt_raw, list) and rt_raw else merged.get("runtime", "?") desc = (merged.get("description_full") or merged.get("overview") or merged.get("summary", ""))[:450] tagline = (tmdb_data or {}).get("tagline", "") vote_cnt = (tmdb_data or {}).get("vote_count", "") label = "TV show" if kind == "tv" else "movie" facts = ( f"Title: {title}\n" f"Year: {year}\n" f"Genres: {genres}\n" f"Rating: {rating}/10 ({vote_cnt} votes)\n" f"Runtime: {runtime} min\n" + (f"Tagline: {tagline}\n" if tagline else "") + f"Plot: {desc}" ) if lang == "si": prompt = ( f"STRICT RULE 1: සිංහල භාෂාවෙන් පමණක් reply කරන්න. English words නොයොදන්න.\n" f"STRICT RULE 2: පහත VERIFIED FACTS හැරෙන්නට year, cast, හෝ වෙනත් කිසිදු detail invent කිරීම දැඩිලෙස තහනම්.\n\n" f"=== VERIFIED FACTS ===\n{facts}\n=== END FACTS ===\n\n" f"ඉහත facts පමණක් use කරලා \'Saumya\' ලෙස මෙම {label} ගැන " f"සිංහලෙන් 3 passionate sentences ලියන්න. Fact list repeat නොකරන්න." ) else: prompt = ( f"STRICT RULE 1: Reply ONLY in English. No Sinhala.\n" f"STRICT RULE 2: NEVER invent any year, cast, or detail not in VERIFIED FACTS below.\n\n" f"=== VERIFIED FACTS ===\n{facts}\n=== END FACTS ===\n\n" f"Using ONLY the facts above, write 3 passionate sentences as \'Saumya\' " f"for this {label}. Do not repeat the fact list — bring the emotional hook." ) result = _hf(prompt, max_tokens=320) if result: return result if lang == "si": return f"**{title}** ({year}) — {genres} | ⭐ {rating}/10\n\n{desc}" return f"**{title}** ({year}) — {genres} | ⭐ {rating}/10\n\n{desc}" def make_general_reply(query: str, lang: str) -> str: if lang == "si": prompt = ( f"IMPORTANT: සිංහල භාෂාවෙන් පමණක් reply කරන්න. English words නොයොදන්න.\n\n" f"ඔබ \'Saumya\', හිතවත් සිනමා AI සහායකයෙකි. " f"පහත ප්‍රශ්නයට සිංහලෙන් පමණක් කෙටි හා හිතවත් ලෙස පිළිතුරු දෙන්න:\n{query}" ) else: prompt = ( f"IMPORTANT: Reply ONLY in English. Do not use any Sinhala.\n\n" f"You are \'Saumya\', a friendly movie AI assistant. " f"Answer the following concisely and helpfully in English only:\n{query}" ) result = _hf(prompt, max_tokens=300) if result: return result if lang == "si": return "🎬 AI model ටිකක් busy. ෆිල්ම් නමක් කෙලින්ම කියන්න: *\"Interstellar ගැන කියන්න\"* 🚀" return "🎬 AI model is warming up. Try a direct movie name: *\"Tell me about Interstellar\"* 🚀" def make_recommendation(query: str, lang: str) -> str: if lang == "si": prompt = ( f"IMPORTANT: සිංහල භාෂාවෙන් පමණක් reply කරන්න. English words නොයොදන්න.\n\n" f"ඔබ \'Saumya\' සිනමා AI සහායකයෙකි. " f"\'{query}\' ගැන ඇසූ ප්‍රශ්නයට ගැළපෙන හොඳ ෆිල්ම් හෝ TV show 3ක් " f"සිංහලෙන් පමණක් ලිස්ට් ආකාරයෙන් recommend කරන්න." ) else: prompt = ( f"IMPORTANT: Reply ONLY in English. Do not use any Sinhala.\n\n" f"You are \'Saumya\', an expert movie curator. " f"Based on \'{query}\', recommend 3 great movies or TV shows " f"with a brief reason for each. Friendly list format, English only." ) return _hf(prompt, max_tokens=380) or ( "රිකමෙන්ඩ් ලිස්ට් දෙන්න බැරි වුනා, ටිකක් try කරන්නකෝ." if lang == "si" else "Unable to generate recommendations right now. Please try again." ) # ══════════════════════════════════════════════════════════════════ # CORE PIPELINE # ══════════════════════════════════════════════════════════════════ def process(user_query: str) -> dict: lang = detect_lang(user_query) intent = classify_intent(user_query) # ── About AI ───────────────────────────────────────────────── if intent == "about_ai": if lang == "si": msg = ( "හෙලෝ! 👋 මම **Saumya** — ඔබේ AI ෆිල්ම් හා TV show සහායකයා! 🎬\n\n" "මාව නිර්මාණය කළේ **සමිත් ඩිල්ෂාන්** විසිනි.\n\n" "ෆිල්ම් හෝ TV show ගැන ඕනෑම දෙයක් Sinhala හෝ English වලින් ඇහුවොත් " "මමගේ හොඳම info ලබා දෙන්නම්! 😊" ) else: msg = ( "Hey there! 👋 I'm **Saumya** — your AI Movie & TV Show companion! 🎬\n\n" "I was crafted by **Samith Dilshan**.\n\n" "Ask me about any movie or TV show in **English or Sinhala 🇱🇰** " "and I'll hook you up with everything you need! 😊" ) return {"intent": "about_ai", "response_language": lang, "message": msg} # ── Code request ────────────────────────────────────────────── if intent == "code_request": if lang == "si": msg = ( "😅 ඒ ටිකට Saumya ගෙ expertise නෙවෙයි! " "Code, programming, tech help ගන්න **GitHub Copilot**, " "**ChatGPT**, හෝ **Claude** try කරන්න.\n\n" "ෆිල්ම් recommendation ඕනේ නම් මමේ ළඟ ඉන්නව! 🎬" ) else: msg = ( "😅 Coding is a little outside my lane — I live and breathe movies & TV!\n\n" "For coding help, check out **GitHub Copilot**, **ChatGPT**, or **Claude**.\n\n" "But if you need a movie pick for tonight? That's ALL me! 🎬" ) return {"intent": "code_request", "response_language": lang, "message": msg} # ── Recommendation ──────────────────────────────────────────── if intent == "recommend": msg = make_recommendation(user_query, lang) return {"intent": "recommend", "response_language": lang, "message": msg} # ── Movie / TV Show ─────────────────────────────────────────── if intent in ("movie", "tv"): title = extract_title(user_query, intent) if intent == "tv": imdb_id, tmdb_data = find_tv(title) else: imdb_id, tmdb_data = find_movie(title) if not imdb_id: err = ( "කණගාටුයි, ඒ ෆිල්ම්/show හොයාගන්න බැරි වුනා 😢 නම නිවරදිව ලිව්වාද?" if lang == "si" else "Sorry, couldn't find that movie/show 😢 Could you double-check the title?" ) return {"intent": intent, "response_language": lang, "error": err} # Always fetch both custom API AND TMDB — merge for best data custom = fetch_custom(imdb_id) use_custom = bool(custom and custom.get("id", 0) > 0 and custom.get("title")) # TMDB poster is always preferred (higher quality) tmdb_poster_path = (tmdb_data or {}).get("poster_path", "") tmdb_poster = f"https://image.tmdb.org/t/p/w500{tmdb_poster_path}" if tmdb_poster_path else "" if use_custom: media_data = custom # Prefer TMDB poster, fall back to custom API poster poster = tmdb_poster or custom.get("large_cover_image", "") title_display = custom.get("title", title) genres = custom.get("genres", []) torrents = custom.get("torrents") or [] dl_links = [ { "quality": t.get("quality", "?"), "url": t.get("url", ""), "size": t.get("size", "?"), } for t in torrents if isinstance(t, dict) ] else: media_data = tmdb_data or {} poster = tmdb_poster title_display = (tmdb_data or {}).get("title") or (tmdb_data or {}).get("name", title) genres_raw = (tmdb_data or {}).get("genres", []) genres = [g["name"] for g in genres_raw if isinstance(g, dict)] dl_links = [] # Always pass tmdb_data separately so AI gets rich plot/runtime facts ai_msg = make_media_message(media_data, lang, intent, tmdb_data=tmdb_data) return { "intent": intent, "poster": poster, "movie_name": title_display, "categories": genres, "download_links": dl_links, "response_language": lang, "message": ai_msg, } # ── General — try movie lookup first before free-text AI ──────── # Even "general" queries might mention a film (e.g. "when was Inception released?") title_guess = extract_title(user_query, "movie") imdb_id_g, tmdb_data_g = find_movie(title_guess) if title_guess else (None, None) if imdb_id_g: custom_g = fetch_custom(imdb_id_g) use_cg = bool(custom_g and custom_g.get("id", 0) > 0 and custom_g.get("title")) media_data_g = custom_g if use_cg else (tmdb_data_g or {}) tmdb_pp = (tmdb_data_g or {}).get("poster_path", "") poster_g = f"https://image.tmdb.org/t/p/w500{tmdb_pp}" if tmdb_pp else (custom_g or {}).get("large_cover_image", "") title_disp = (media_data_g.get("title") or media_data_g.get("name") or title_guess) genres_raw = (tmdb_data_g or {}).get("genres", []) genres_g = custom_g.get("genres", []) if use_cg else [g["name"] for g in genres_raw if isinstance(g, dict)] torrents_g = (custom_g or {}).get("torrents") or [] dl_g = [{"quality": t.get("quality","?"), "url": t.get("url",""), "size": t.get("size","?")} for t in torrents_g if isinstance(t, dict)] ai_msg_g = make_media_message(media_data_g, lang, "movie", tmdb_data=tmdb_data_g) return { "intent": "movie", "poster": poster_g, "movie_name": title_disp, "categories": genres_g, "download_links": dl_g, "response_language": lang, "message": ai_msg_g, } msg = make_general_reply(user_query, lang) return {"intent": "general", "response_language": lang, "message": msg} # ══════════════════════════════════════════════════════════════════ # GRADIO CHAT FORMATTER # ══════════════════════════════════════════════════════════════════ def format_for_gradio(result: dict) -> str: if "error" in result: return f"❌ {result['error']}" intent = result.get("intent", "general") msg = result.get("message", "") if intent not in ("movie", "tv"): return msg lines = [] if result.get("poster"): lines.append(f"") if result.get("movie_name"): label = "📺" if intent == "tv" else "🎬" lines.append(f"## {label} {result['movie_name']}") if result.get("categories"): lines.append(f"🎭 **{'  ·  '.join(result['categories'])}**\n") lines.append(msg) dl = result.get("download_links", []) if dl: lines.append("\n---\n### 📥 Download Links") for item in dl[:6]: q = item.get("quality", "?") s = item.get("size", "?") u = item.get("url", "#") lines.append(f"- **[{q}  ·  {s}]({u})**") elif intent == "tv": lines.append( "\n_ℹ️ Direct download links are not available for TV shows " "via our database — check streaming services instead._" ) return "\n".join(lines) # ══════════════════════════════════════════════════════════════════ # FastAPI APPLICATION # ══════════════════════════════════════════════════════════════════ api = FastAPI( title="Saumya API", description="Multilingual (EN/SI) Movie & TV Show AI Assistant by Samith Dilshan", version="2.0.0", docs_url="/api/docs", redoc_url="/api/redoc", ) class QueryBody(BaseModel): query: Optional[str] = None user_query: Optional[str] = None message: Optional[str] = None class ToggleBody(BaseModel): admin_key: str enabled: Optional[bool] = None def _resolve_query(body: QueryBody) -> str: q = body.query or body.user_query or body.message if not q: raise HTTPException(400, "Provide 'query', 'user_query', or 'message'.") return q # ── GET ────────────────────────────────────────────────────────── @api.get("/api/chat") async def chat_get(q: str = Query(..., description="Query in English or Sinhala")): return JSONResponse(process(q)) @api.get("/api/movie") async def movie_get( user_query: Optional[str] = Query(None), imdb_id: Optional[str] = Query(None), ): if imdb_id: custom = fetch_custom(imdb_id) if not custom or custom.get("id", 0) == 0: return JSONResponse({"error": "Movie not found."}, status_code=404) lang = "en" ts = custom.get("torrents") or [] return JSONResponse({ "poster": custom.get("large_cover_image", ""), "movie_name": custom.get("title"), "categories": custom.get("genres", []), "download_links": [ {"quality": t.get("quality"), "url": t.get("url"), "size": t.get("size")} for t in ts if isinstance(t, dict) ], "response_language": lang, "message": make_media_message(custom, lang), }) if not user_query: raise HTTPException(400, "Provide user_query or imdb_id.") return JSONResponse(process(user_query)) @api.get("/api/status") async def status_get(): return { "assistant": "Saumya", "created_by": "Samith Dilshan", "version": "2.0.0", "gradio": _gradio_enabled, "status": "online", "supported": ["movies", "tv_shows", "recommendations", "general_qa"], "languages": ["en", "si"], } @api.get("/health") async def health(): return {"status": "ok"} @api.get("/api/debug") async def debug(): """Quick diagnostic — checks token + pings first model.""" token_ok = bool(HF_TOKEN and HF_TOKEN != "YOUR_HF_TOKEN") tmdb_ok = bool(TMDB_API_KEY and TMDB_API_KEY != "YOUR_TMDB_API_KEY") model_status = {} if token_ok: for name, url in _HF_MODELS: try: r = requests.post( url, headers={"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json"}, json={ "model": name, "messages": [{"role": "user", "content": "Hi"}], "max_tokens": 5, "stream": False, }, timeout=20, ) if r.status_code in (503, 429): model_status[name] = "⏳ loading / rate-limited — retry in ~20s" elif r.status_code == 200: model_status[name] = "✅ online" else: model_status[name] = f"❌ HTTP {r.status_code}: {r.text[:150]}" except Exception as e: model_status[name] = f"❌ {e}" else: model_status = {"all": "skipped — HF_TOKEN not set"} return { "HF_TOKEN_set": token_ok, "TMDB_KEY_set": tmdb_ok, "gradio_enabled": _gradio_enabled, "models": model_status, } # ── POST ───────────────────────────────────────────────────────── @api.post("/api/chat") async def chat_post(body: QueryBody): return JSONResponse(process(_resolve_query(body))) @api.post("/api/movie") async def movie_post(body: QueryBody): return JSONResponse(process(_resolve_query(body))) @api.post("/api/admin/gradio-toggle") async def gradio_toggle(body: ToggleBody): global _gradio_enabled if body.admin_key != ADMIN_KEY: raise HTTPException(403, "Invalid admin key.") _gradio_enabled = body.enabled if body.enabled is not None else not _gradio_enabled return { "gradio_enabled": _gradio_enabled, "message": f"Gradio {'enabled ✅' if _gradio_enabled else 'disabled 🔴'}", } # ══════════════════════════════════════════════════════════════════ # GRADIO INTERFACE (cinema dark theme) # ══════════════════════════════════════════════════════════════════ _CSS = """ /* ── Base & fonts ── */ @import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Sans:wght@300;400;500;600&display=swap'); body, .gradio-container { background: #0d0d0f !important; font-family: 'DM Sans', sans-serif !important; } footer { display: none !important; } /* ── Header ── */ #saumya-header { text-align: center; padding: 28px 0 10px; background: linear-gradient(180deg, #1a0a2e 0%, #0d0d0f 100%); border-bottom: 1px solid #2a1a4a; margin-bottom: 12px; } #saumya-header h1 { font-family: 'Bebas Neue', sans-serif; font-size: 3.4em; letter-spacing: 6px; background: linear-gradient(135deg, #c084fc 20%, #f472b6 80%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin: 0 0 4px; } #saumya-header .sub { color: #9ca3af; font-size: 0.92em; font-weight: 300; letter-spacing: 1px; } #saumya-header .creator { color: #7c3aed; font-weight: 600; } /* ── Chat bubbles ── */ .message.user { background: linear-gradient(135deg, #3b1f6e 0%, #1e1040 100%) !important; border: 1px solid #5b21b6 !important; border-radius: 14px 14px 4px 14px !important; color: #e9d5ff !important; } .message.bot { background: linear-gradient(135deg, #111827 0%, #1c1030 100%) !important; border: 1px solid #374151 !important; border-radius: 14px 14px 14px 4px !important; color: #e5e7eb !important; } .message.bot a { color: #c084fc !important; } .message.bot img { border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,.5); } /* ── Input bar ── */ .input-row textarea, #input-row textarea { background: #1a1a2e !important; border: 1px solid #3b1f6e !important; color: #e5e7eb !important; border-radius: 12px !important; } .input-row textarea::placeholder { color: #6b7280 !important; } .input-row button { border-radius: 10px !important; } /* ── Example pills ── */ .examples-row .example { background: #1a1a2e !important; border: 1px solid #3b1f6e !important; color: #c084fc !important; border-radius: 20px !important; font-size: 0.82em !important; transition: all .2s; } .examples-row .example:hover { background: #2d1b69 !important; border-color: #7c3aed !important; } /* ── Footer bar ── */ #saumya-footer { text-align: center; padding: 14px; color: #4b5563; font-size: 0.78em; border-top: 1px solid #1f2937; margin-top: 10px; letter-spacing: .3px; } #saumya-footer code { background: #1a1a2e; color: #a78bfa; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; } """ def _chat_fn(message: str, history: list) -> str: if not _gradio_enabled: return ( "🔧 **Saumya is temporarily offline for maintenance.**\n\n" "You can still use the REST API endpoints — see `/api/docs`." ) return format_for_gradio(process(message)) with gr.Blocks(title="Saumya 🎬 · Movie AI") as gradio_ui: # Inject CSS via ") gr.HTML("""

SAUMYA

🎬 AI Movie & TV Show Assistant   ·   Created by Samith Dilshan   ·   🇱🇰 English & Sinhala
""") gr.ChatInterface( fn=_chat_fn, # type="messages" removed — Gradio 6 uses dict-based history by default examples=[ "I want to watch Interstellar", "Tell me about Breaking Bad", "What is Oppenheimer about?", "Recommend a good horror movie", "Who are you?", "මට Inception ෆිල්ම් එක ගැන කියන්න", "හොඳ action ෆිල්ම් 3ක් recommend කරන්න", "Stranger Things series ගැන කියන්න", ], ) # retry_btn/undo_btn/clear_btn/submit_btn removed in Gradio 6 gr.HTML(""" """) # ══════════════════════════════════════════════════════════════════ # MOUNT GRADIO ON FastAPI + LAUNCH # ══════════════════════════════════════════════════════════════════ app = gr.mount_gradio_app(api, gradio_ui, path="/") if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=7860)