| |
| from fastapi import FastAPI, Request, Security, Depends, HTTPException, status |
| from fastapi.responses import JSONResponse, StreamingResponse |
| from fastapi.staticfiles import StaticFiles |
| from fastapi.security.http import HTTPBearer, HTTPAuthorizationCredentials |
| from pydantic import BaseModel |
| from typing import List |
| from engine import ( |
| SearchEngine, |
| get_batch_embeddings, |
| extract_content_with_playwright, |
| normalize_url, |
| ai_gatekeeper_clean, |
| ai_summarizer, |
| init_browser, |
| close_browser, |
| ai_research_planner, |
| ai_lore_extractor, |
| ai_json_healer, |
| ai_script_writer |
| ) |
| import constants |
| import uvicorn |
| import asyncio |
| import json |
| import requests |
| from contextlib import asynccontextmanager |
|
|
| from motor.motor_asyncio import AsyncIOMotorClient |
| from fastapi import BackgroundTasks |
| import uuid |
| from datetime import datetime, timezone |
| from dotenv import load_dotenv |
| import os |
| import traceback |
|
|
| load_dotenv() |
| MONGO_URI = os.getenv("MONGO_URI") |
|
|
| |
| mongo_client = AsyncIOMotorClient(MONGO_URI) |
| db = mongo_client.spark_studio |
| history_col = db.research_history |
|
|
| |
| |
| security = HTTPBearer(auto_error=False) |
| API_SECRET_TOKEN = os.getenv("API_SECRET_TOKEN", "sparkling_secret_123") |
|
|
| def verify_api_token(request: Request, credentials: HTTPAuthorizationCredentials = Security(security)): |
| |
| if request.url.path == "/": |
| return |
| |
| |
| if not credentials or credentials.credentials != API_SECRET_TOKEN: |
| raise HTTPException( |
| status_code=status.HTTP_403_FORBIDDEN, |
| detail="Not authenticated" |
| ) |
|
|
| |
| @asynccontextmanager |
| async def lifespan(app: FastAPI): |
| print("Application Startup: Warming up Global Browser...") |
| await init_browser() |
| |
| print("Application Startup: Initializing MongoDB Indexes...") |
| |
| await history_col.create_index("created_at", expireAfterSeconds=2505600) |
| |
| yield |
| |
| print("Application Shutdown: Cleaning up Browser...") |
| await close_browser() |
|
|
| |
| app = FastAPI(title="Sparkling Gyan Content Studio", lifespan=lifespan, dependencies=[Depends(verify_api_token)]) |
|
|
| search_tool = SearchEngine() |
|
|
| |
| browser_semaphore = asyncio.Semaphore(3) |
|
|
| class QueryModel(BaseModel): |
| query: str |
| threshold: float = constants.SEARCH_THRESHOLD |
|
|
| class ExtractModel(BaseModel): |
| url: str |
|
|
| class SynthesizeModel(BaseModel): |
| query: str |
| urls: List[str] |
|
|
| class DeepSearchModel(BaseModel): |
| query: str |
|
|
| class ScriptGenModel(BaseModel): |
| query: str |
| research_data: dict |
|
|
| class DispatchVideoModel(BaseModel): |
| script_text: str |
| topic: str |
| orientation: str = "horizontal" |
|
|
| @app.post("/api/fast-search") |
| def fast_search(data: QueryModel): |
| if not data.query: |
| return JSONResponse(status_code=400, content={"error": "No query provided"}) |
| |
| docs = search_tool.perform_search(data.query, max_results=constants.DEFAULT_MAX_RESULTS) |
| if not docs: |
| return {"results": []} |
|
|
| texts = [d["title"] + " " + d["body"] for d in docs] |
| emb = get_batch_embeddings([data.query] + texts) |
| scores = (emb[0] @ emb[1:].T).cpu().numpy().flatten() |
| |
| ranked_results = [] |
| for score, d in sorted(zip(scores, docs), key=lambda x: x[0], reverse=True): |
| |
| if score >= data.threshold: |
| ranked_results.append({ |
| "title": d['title'], |
| "snippet": d['body'], |
| "url": d['href'], |
| "score": f"{score:.4f}" |
| }) |
| return {"results": ranked_results} |
|
|
| @app.post("/api/deep-search") |
| def deep_search(data: QueryModel): |
| curr_k, seen = 50, set() |
| all_docs, all_scores = [], [] |
| best_score = 0 |
| |
| while curr_k <= constants.DEEP_SEARCH_LIMIT: |
| docs = search_tool.perform_search(data.query, max_results=curr_k) |
| new_docs = [] |
| for d in docs: |
| norm_url = normalize_url(d['href']) |
| if norm_url not in seen: |
| seen.add(norm_url) |
| new_docs.append(d) |
| |
| if new_docs: |
| texts = [d["title"] + " " + d["body"] for d in new_docs] |
| emb = get_batch_embeddings([data.query] + texts) |
| scores = (emb[0] @ emb[1:].T).cpu().numpy().flatten() |
| all_scores.extend(scores.tolist()) |
| all_docs.extend(new_docs) |
| |
| best_score = max(all_scores) if all_scores else 0 |
| if best_score >= data.threshold: |
| break |
| curr_k += 50 |
|
|
| ranked = sorted(zip(all_scores, all_docs), key=lambda x: x[0], reverse=True) |
| |
| |
| filtered_ranked = [(s, d) for s, d in ranked if s >= data.threshold][:10] |
| |
| results = [{"title": d['title'], "snippet": d['body'], "url": d['href'], "score": f"{s:.4f}"} for s, d in filtered_ranked] |
| |
| return { |
| "status": f"स्कैनिंग: {len(all_docs)} साइट्स | बेस्ट मैच: {best_score:.4f} | पास हुए: {len(filtered_ranked)}", |
| "results": results |
| } |
|
|
| @app.post("/api/extract") |
| async def extract(data: ExtractModel): |
| if not data.url: |
| return JSONResponse(status_code=400, content={"error": "No URL provided"}) |
| |
| async with browser_semaphore: |
| content = await extract_content_with_playwright(data.url) |
| return content |
|
|
| async def process_single_url_for_synthesis(url): |
| """एक URL का पूरा प्रोसेस Semaphore के साथ: Scrape -> Clean -> Structuring""" |
| async with browser_semaphore: |
| content = await extract_content_with_playwright(url) |
| |
| if "text" in content and content["text"] != "Content not found.": |
| clean_text = await ai_gatekeeper_clean(content["text"]) |
| return { |
| "url": url, |
| "content": clean_text |
| } |
| return None |
|
|
| @app.post("/api/synthesize") |
| async def synthesize_research(data: SynthesizeModel): |
| if not data.urls or not data.query: |
| return JSONResponse(status_code=400, content={"error": "URLs and Query required"}) |
| |
| tasks = [process_single_url_for_synthesis(url) for url in data.urls] |
| results = await asyncio.gather(*tasks) |
| |
| structured_data_list = [res for res in results if res] |
| |
| if not structured_data_list: |
| return JSONResponse(status_code=500, content={"error": "Failed to extract valid text from URLs."}) |
| |
| final_json_data = await ai_summarizer(structured_data_list, data.query) |
| |
| if "error" in final_json_data: |
| return JSONResponse(status_code=500, content={"error": "JSON Parsing Failed", "raw_data": final_json_data.get("raw_output")}) |
| |
| return {"summary_json": final_json_data} |
|
|
| |
| async def run_autonomous_agent_bg(task_id: str, query: str): |
| async def log_db(msg, color="var(--text-secondary)"): |
| await history_col.update_one({"task_id": task_id}, {"$push": {"logs": {"msg": msg, "color": color}}}) |
|
|
| try: |
| await log_db(f"Target locked: {query}", "var(--accent)") |
| await log_db("Phase 1: Engaging AI Planner for deep sub-queries...", "var(--text-secondary)") |
| |
| |
| sub_queries = await ai_research_planner(query) |
| if not isinstance(sub_queries, list): |
| sub_queries = [f"{query} scientific explanation", f"{query} history", f"{query} mysteries"] |
|
|
| await log_db(f"AI Strategy formulated. Parallel searching for: {sub_queries}", "var(--success)") |
| all_queries = [query] + sub_queries |
| |
| |
| await log_db("Phase 2: Harvesting raw nodes from Deep Web...", "var(--text-secondary)") |
| all_docs = [] |
| for q in all_queries: |
| docs = search_tool.perform_search(q, max_results=15) |
| all_docs.extend(docs) |
| await log_db(f"Scraped {len(docs)} initial links for '{q}'", "var(--text-muted)") |
| |
| seen = set() |
| unique_docs = [] |
| for d in all_docs: |
| norm_url = normalize_url(d['href']) |
| if norm_url not in seen: |
| seen.add(norm_url) |
| unique_docs.append(d) |
| |
| |
| await log_db(f"Phase 3: Found {len(unique_docs)} unique nodes. Running semantic reranking...", "var(--text-secondary)") |
| texts = [d["title"] + " " + d["body"] for d in unique_docs] |
| emb = get_batch_embeddings([query] + texts) |
| scores = (emb[0] @ emb[1:].T).cpu().numpy().flatten() |
| |
| |
| ranked = sorted(zip(scores, unique_docs), key=lambda x: x[0], reverse=True) |
| |
| MIN_THRESHOLD = getattr(constants, 'SEARCH_THRESHOLD', 0.5) |
| BANNED_WORDS = ['porn', 'xxx', 'sex', 'casino', 'betting', 'escort', 'viagra'] |
| |
| valid_nodes = [] |
| for s, d in ranked: |
| url_lower = d['href'].lower() |
| |
| if s >= MIN_THRESHOLD and not any(banned in url_lower for banned in BANNED_WORDS): |
| valid_nodes.append((s, d)) |
| |
| top_10_clean = valid_nodes[:10] |
| |
| if not top_10_clean: |
| await log_db("FATAL: All scraped nodes were irrelevant, spam, or scored below threshold. Aborting to save resources.", "var(--danger)") |
| await history_col.update_one({"task_id": task_id}, {"$set": {"status": "failed", "error": "No relevant data passed the AI embedding filter."}}) |
| return |
| |
| top_urls = [d['href'] for s, d in top_10_clean] |
| top_results_rich = [{"title": d['title'], "snippet": d['body'], "url": d['href']} for s, d in top_10_clean] |
| |
| await log_db(f"{len(top_urls)} High-Tension Nodes passed security. Commencing extraction...", "var(--success)") |
| for i, url in enumerate(top_urls, 1): |
| await log_db(f"Targeting Node [{i}/{len(top_urls)}]: {url}", "var(--accent-dim)") |
| |
| |
| await log_db("Phase 4: Bypassing firewalls & extracting context (Parallel)...", "var(--text-secondary)") |
| tasks = [process_single_url_for_synthesis(url) for url in top_urls] |
| results = await asyncio.gather(*tasks) |
| structured_data_list = [res for res in results if res] |
| |
| if not structured_data_list: |
| await log_db("FATAL: Extraction failed. All nodes blocked.", "var(--danger)") |
| await history_col.update_one({"task_id": task_id}, {"$set": {"status": "failed", "error": "All nodes blocked or no data extracted."}}) |
| return |
| |
| await log_db(f"Extraction successful for {len(structured_data_list)} nodes. Junk cleaned.", "var(--success)") |
| |
| |
| await log_db("Phase 5: Engaging Dual-Core AI...", "var(--text-secondary)") |
| task_sci = asyncio.create_task(ai_summarizer(structured_data_list, query)) |
| task_lore = asyncio.create_task(ai_lore_extractor(structured_data_list, query)) |
| |
| await asyncio.gather(task_sci, task_lore) |
| |
| try: |
| sci_data = task_sci.result() |
| except Exception as e: |
| sci_data = {"error": str(e), "raw_output": ""} |
| |
| try: |
| lore_data = task_lore.result() |
| except Exception as e: |
| lore_data = {"error": str(e), "raw_output": ""} |
|
|
| |
| if "error" in sci_data and "raw_output" in sci_data: |
| await log_db("⚠️ Scientist AI JSON broken. Engaging Gatekeeper Healer...", "var(--danger)") |
| healed_sci = await ai_json_healer(sci_data["raw_output"]) |
| if "error" not in healed_sci: |
| sci_data = healed_sci |
| await log_db("✅ Gatekeeper successfully healed Scientist Data!", "var(--success)") |
|
|
| if "error" in lore_data and "raw_output" in lore_data: |
| await log_db("⚠️ Storyteller AI JSON broken. Engaging Gatekeeper Healer...", "var(--danger)") |
| healed_lore = await ai_json_healer(lore_data["raw_output"]) |
| if "error" not in healed_lore: |
| lore_data = healed_lore |
| await log_db("✅ Gatekeeper successfully healed Storyteller Data!", "var(--success)") |
|
|
| final_data = {"conflict_engine": sci_data, "lore_engine": lore_data} |
| |
| |
| await history_col.update_one( |
| {"task_id": task_id}, |
| {"$set": {"status": "complete", "result": final_data, "urls": top_results_rich}} |
| ) |
| await log_db("Autonomous Research Complete & Saved to History.", "var(--success)") |
|
|
| except Exception as e: |
| await history_col.update_one( |
| {"task_id": task_id}, |
| {"$set": {"status": "failed", "error": str(e)}} |
| ) |
| await log_db(f"CRITICAL CRASH: {str(e)}", "var(--danger)") |
|
|
|
|
| |
| @app.post("/api/research/start") |
| async def start_research(data: DeepSearchModel, background_tasks: BackgroundTasks): |
| task_id = str(uuid.uuid4()) |
| |
| |
| await history_col.insert_one({ |
| "task_id": task_id, |
| "query": data.query, |
| "status": "processing", |
| "logs": [{"msg": "Initializing Autonomous Agent in Background...", "color": "var(--accent)"}], |
| "result": None, |
| "urls": [], |
| "created_at": datetime.now(timezone.utc) |
| }) |
| |
| |
| background_tasks.add_task(run_autonomous_agent_bg, task_id, data.query) |
| |
| |
| return {"task_id": task_id} |
|
|
| |
| SCRIPT_TASKS = {} |
|
|
| async def background_script_generator(task_id: str, research_data: dict, query: str): |
| try: |
| SCRIPT_TASKS[task_id] = {"status": "processing"} |
| final_script = await ai_script_writer(research_data, query) |
| SCRIPT_TASKS[task_id] = {"status": "completed", "script": final_script} |
| except Exception as e: |
| SCRIPT_TASKS[task_id] = {"status": "failed", "error": str(e)} |
|
|
| @app.post("/api/generate-script-async") |
| async def generate_script_async(request: Request, background_tasks: BackgroundTasks): |
| data = await request.json() |
| task_id = str(uuid.uuid4()) |
| SCRIPT_TASKS[task_id] = {"status": "pending"} |
| |
| |
| background_tasks.add_task(background_script_generator, task_id, data.get("research_data"), data.get("query")) |
| |
| |
| return {"task_id": task_id, "status": "processing"} |
|
|
| @app.get("/api/script-status/{task_id}") |
| async def get_script_status(task_id: str): |
| if task_id not in SCRIPT_TASKS: |
| raise HTTPException(status_code=404, detail="Task not found") |
| return SCRIPT_TASKS[task_id] |
|
|
| |
| @app.get("/api/research/status/{task_id}") |
| async def get_status(task_id: str): |
| task = await history_col.find_one({"task_id": task_id}, {"_id": 0}) |
| if not task: |
| return JSONResponse(status_code=404, content={"error": "Task not found"}) |
| return task |
|
|
| |
| @app.get("/api/history") |
| async def get_history(): |
| cursor = history_col.find({}, {"_id": 0, "task_id": 1, "query": 1, "status": 1, "created_at": 1}).sort("created_at", -1) |
| history = await cursor.to_list(length=100) |
| return {"history": history} |
|
|
| @app.post("/api/generate-script") |
| async def generate_script(data: ScriptGenModel): |
| try: |
| script_text = await ai_script_writer(data.research_data, data.query) |
| return {"script": script_text} |
| except Exception as e: |
| return JSONResponse(status_code=500, content={"error": str(e)}) |
|
|
| @app.post("/api/dispatch-to-creator") |
| async def dispatch_to_creator(data: DispatchVideoModel): |
| print(f"🚀 Dispatching Script to Vidspark API for topic: {data.topic}") |
| |
| headers = { |
| "X-Vidspark-Key": constants.VIDSPARK_API_KEY |
| } |
| |
| payload = { |
| "script_text": (None, data.script_text), |
| "orientation": (None, data.orientation) |
| } |
| |
| try: |
| response = requests.post(constants.VIDSPARK_URL, headers=headers, files=payload) |
| |
| if response.status_code == 200: |
| res_data = response.json() |
| return { |
| "status": "success", |
| "task_id": res_data.get("task_id"), |
| "message": res_data.get("message", "Video generation started!") |
| } |
| else: |
| return JSONResponse(status_code=response.status_code, content={"error": response.text}) |
| |
| except Exception as e: |
| return JSONResponse(status_code=500, content={"error": f"Failed to connect to Vidspark API: {str(e)}"}) |
|
|
| @app.get("/") |
| def index(): |
| return {"status": "success", "message": "API is up and running. 🚀"} |
| |
| if __name__ == "__main__": |
| uvicorn.run(app, host="0.0.0.0", port=7860) |
|
|