| """ |
| 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 |
|
|
| |
| |
| |
| 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") |
|
|
| |
| |
| |
| _HF_BASE = "https://router.huggingface.co/featherless-ai/v1/chat/completions" |
| _HF_MODELS = [ |
| ("polyglots/SinLlama_v01", _HF_BASE), |
| ("meta-llama/Llama-3.3-70B-Instruct", _HF_BASE), |
| ("google/gemma-3-27b-it", _HF_BASE), |
| ] |
|
|
| |
| _gradio_enabled: bool = True |
|
|
|
|
| |
| |
| |
| 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" |
|
|
|
|
| |
| |
| |
| 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 "" |
|
|
|
|
| |
| |
| |
| _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" |
|
|
|
|
| |
| |
| |
| _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 |
| |
| cleaned = _NOISE.sub(" ", query) |
| return re.sub(r"\s+", " ", cleaned).strip() or query |
|
|
|
|
| |
| |
| |
| 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 |
|
|
|
|
| |
| |
| |
| 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 {} |
|
|
|
|
| |
| |
| |
| 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." |
| ) |
|
|
|
|
| |
| |
| |
| def process(user_query: str) -> dict: |
| lang = detect_lang(user_query) |
| intent = classify_intent(user_query) |
|
|
| |
| 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} |
|
|
| |
| 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} |
|
|
| |
| if intent == "recommend": |
| msg = make_recommendation(user_query, lang) |
| return {"intent": "recommend", "response_language": lang, "message": msg} |
|
|
| |
| 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} |
|
|
| |
| custom = fetch_custom(imdb_id) |
| use_custom = bool(custom and custom.get("id", 0) > 0 and custom.get("title")) |
|
|
| |
| 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 |
| |
| 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 = [] |
|
|
| |
| 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, |
| } |
|
|
| |
| |
| 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} |
|
|
|
|
| |
| |
| |
| 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"<img src='{result['poster']}' style='max-width:220px;border-radius:10px;float:right;margin:0 0 12px 16px'>") |
|
|
| 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) |
|
|
|
|
| |
| |
| |
| 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 |
|
|
|
|
| |
| @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, |
| } |
|
|
|
|
| |
| @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 π΄'}", |
| } |
|
|
|
|
| |
| |
| |
| _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: |
|
|
| |
| |
| gr.HTML(f"<style>{_CSS}</style>") |
|
|
| gr.HTML(""" |
| <div id="saumya-header"> |
| <h1>SAUMYA</h1> |
| <div class="sub"> |
| π¬ AI Movie & TV Show Assistant |
| Β· |
| Created by <span class="creator">Samith Dilshan</span> |
| Β· |
| π±π° English & Sinhala |
| </div> |
| </div> |
| """) |
|
|
| gr.ChatInterface( |
| fn=_chat_fn, |
| |
| 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 ΰΆΰ·ΰΆ± ΰΆΰ·ΰΆΊΰΆ±ΰ·ΰΆ±", |
| ], |
| ) |
|
|
| gr.HTML(""" |
| <div id="saumya-footer"> |
| π |
| <b>REST API:</b> |
| <code>GET /api/chat?q=Inception</code> | |
| <code>POST /api/movie {"user_query":"..."}</code> | |
| <code>GET /api/status</code> | |
| <code>GET /api/docs</code> |
| </div> |
| """) |
|
|
|
|
| |
| |
| |
| app = gr.mount_gradio_app(api, gradio_ui, path="/") |
|
|
| if __name__ == "__main__": |
| uvicorn.run(app, host="0.0.0.0", port=7860) |