Download app.py from ST-x-Tony/X-RUDRA: direct link, hf CLI and curl.
- Browser
- Download file 24.4 kB
-
https://huggingface.co/spaces/ST-x-Tony/X-RUDRA/resolve/main/app.py
- Command line
-
hf download hf://spaces/ST-x-Tony/X-RUDRA/app.py
-
curl -L -o app.py https://huggingface.co/spaces/ST-x-Tony/X-RUDRA/resolve/main/app.py
24.4 kB
| # ============================================================ | |
| # X-RUDRA CHAT | |
| # Dual‑Model + Web Research · Gradio Space | |
| # ============================================================ | |
| # ────────────────────────────────────────────────────────────── | |
| # REASONING‑ENFORCED AGENT CONTRACT (AGENT.md) | |
| # See full policy in the multi‑line comment below. | |
| # ────────────────────────────────────────────────────────────── | |
| """ | |
| # REASONING-ENFORCED AGENT | |
| (full policy – keep as before) | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import json | |
| import re | |
| import time | |
| import asyncio | |
| import traceback | |
| import gradio as gr | |
| import spaces | |
| from gradio_client import Client | |
| # ============================================================ | |
| # CONFIG | |
| # ============================================================ | |
| APP_NAME = "X-RUDRA" | |
| VERSION = "3.8.3" # bumped | |
| M1_REPO = os.getenv("M1_REPO", "Shrijanagain/M1") | |
| M2_REPO = os.getenv("M2_REPO", "Shrijanagain/M2") | |
| PORT = int(os.getenv("PORT", "7860")) | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| if HF_TOKEN: | |
| os.environ["HF_TOKEN"] = HF_TOKEN | |
| # ============================================================ | |
| # GRADIO CLIENTS FOR M1 / M2 | |
| # ============================================================ | |
| _M1_CLIENT = None | |
| _M2_CLIENT = None | |
| def get_m1_client(): | |
| global _M1_CLIENT | |
| if _M1_CLIENT is None: | |
| try: | |
| url = f"https://{M1_REPO.replace('/', '-')}.hf.space" | |
| _M1_CLIENT = Client(url) | |
| except Exception as e: | |
| print(f"Could not connect to M1: {e}") | |
| _M1_CLIENT = None | |
| return _M1_CLIENT | |
| def get_m2_client(): | |
| global _M2_CLIENT | |
| if _M2_CLIENT is None: | |
| try: | |
| url = f"https://{M2_REPO.replace('/', '-')}.hf.space" | |
| _M2_CLIENT = Client(url) | |
| except Exception as e: | |
| print(f"Could not connect to M2: {e}") | |
| _M2_CLIENT = None | |
| return _M2_CLIENT | |
| # ============================================================ | |
| # MODEL-BASED INTENT CLASSIFIER | |
| # ============================================================ | |
| CLASSIFIER_SYSTEM_PROMPT = """ | |
| You are an intelligent assistant that classifies user messages into two categories: | |
| - ACTION: The user asks for information that requires research, fact‑checking, retrieval of current data, or external knowledge. This includes questions about news, comparisons, statistics, history, technology, science, politics, etc. | |
| - CASUAL: The user is just chatting, greeting, making small talk, joking, or asking a simple question that can be answered from general knowledge without a search. | |
| Respond with ONLY ONE WORD: ACTION or CASUAL. | |
| Do NOT add any extra text, punctuation, or explanation. | |
| """ | |
| def classify_intent(question: str) -> str: | |
| prompt = f"{CLASSIFIER_SYSTEM_PROMPT}\n\nUser message: \"{question}\"\n\nClassification:" | |
| # Try M1 | |
| m1_client = get_m1_client() | |
| if m1_client is not None: | |
| try: | |
| result = m1_client.predict( | |
| prompt=prompt, | |
| max_tokens=64, | |
| temperature=0.1, # FIXED: was 0.0 → now 0.1 | |
| api_name="/generate" | |
| ) | |
| if result: | |
| result = result.strip().upper() | |
| if "ACTION" in result: | |
| print(f"[Classifier] M1 → ACTION") | |
| return "ACTION" | |
| elif "CASUAL" in result: | |
| print(f"[Classifier] M1 → CASUAL") | |
| return "CASUAL" | |
| else: | |
| print(f"[Classifier] M1 ambiguous: {result}") | |
| except Exception as e: | |
| print(f"[Classifier] M1 call failed: {e}") | |
| # Try M2 | |
| m2_client = get_m2_client() | |
| if m2_client is not None: | |
| try: | |
| result = m2_client.predict( | |
| prompt=prompt, | |
| max_tokens=64, | |
| temperature=0.1, # FIXED | |
| api_name="/generate" | |
| ) | |
| if result: | |
| result = result.strip().upper() | |
| if "ACTION" in result: | |
| print(f"[Classifier] M2 → ACTION") | |
| return "ACTION" | |
| elif "CASUAL" in result: | |
| print(f"[Classifier] M2 → CASUAL") | |
| return "CASUAL" | |
| else: | |
| print(f"[Classifier] M2 ambiguous: {result}") | |
| except Exception as e: | |
| print(f"[Classifier] M2 call failed: {e}") | |
| # Fallback: if both fail, use simple heuristic (no keyword list) | |
| # For very short messages (<=3 words), assume CASUAL to avoid unnecessary search. | |
| word_count = len(question.split()) | |
| if word_count <= 3: | |
| print("[Classifier] Fallback → CASUAL (short message)") | |
| return "CASUAL" | |
| else: | |
| print("[Classifier] Fallback → ACTION (longer message)") | |
| return "ACTION" | |
| # ============================================================ | |
| # LAZY ENGINE (web search) | |
| # ============================================================ | |
| _ENGINE = None | |
| def get_engine(): | |
| global _ENGINE | |
| if _ENGINE is None: | |
| from web_search import XrudraWebSearch | |
| _ENGINE = XrudraWebSearch() | |
| return _ENGINE | |
| # ============================================================ | |
| # HELPERS – CALL MODELS | |
| # ============================================================ | |
| def call_model(client, prompt, max_tokens=512, temperature=0.7): | |
| if client is None: | |
| return None | |
| try: | |
| print(f"Calling model with max_tokens={max_tokens}, prompt length={len(prompt)}") | |
| result = client.predict( | |
| prompt=prompt, | |
| max_tokens=max_tokens, | |
| temperature=temperature, | |
| api_name="/generate" | |
| ) | |
| if result and isinstance(result, str) and result.strip(): | |
| print(f"Response length: {len(result)} chars") | |
| return result.strip() | |
| else: | |
| print("Empty response") | |
| return None | |
| except Exception as e: | |
| print(f"Model call failed: {e}") | |
| return None | |
| # ============================================================ | |
| # SYNTHESIS: M1 + M2 drafts → M2 merges (with higher token budget) | |
| # ============================================================ | |
| def get_combined_model_answer(question, sources, max_tokens=512, temperature=0.7): | |
| top_sources = sources[:5] if sources else [] | |
| sources_text = "" | |
| if top_sources: | |
| for i, src in enumerate(top_sources, 1): | |
| title = src.get("title", "Untitled") | |
| snippet = src.get("snippet", src.get("description", "")) | |
| sources_text += f"{i}. {title}: {snippet[:300]}\n" | |
| else: | |
| sources_text = "No specific information available." | |
| base_prompt = f"""Question: {question} | |
| Information: | |
| {sources_text} | |
| Based on the information above and your knowledge, provide a comprehensive, accurate, and well‑structured answer to the question. Be direct and natural – write as if you are an expert answering a user. | |
| Answer:""" | |
| m1_client = get_m1_client() | |
| m2_client = get_m2_client() | |
| # ----- 1. Get independent drafts from M1 and M2 ----- | |
| draft_m1 = call_model(m1_client, base_prompt, max_tokens, temperature) | |
| draft_m2 = call_model(m2_client, base_prompt, max_tokens, temperature) | |
| # Fallback if both fail | |
| if not draft_m1 and not draft_m2: | |
| if sources: | |
| parts = ["Based on available information:"] | |
| for i, src in enumerate(sources[:5], 1): | |
| title = src.get("title", "Untitled") | |
| snippet = src.get("snippet", src.get("description", "")) | |
| parts.append(f"{i}. {title}: {snippet[:200]}..." if snippet else f"{i}. {title}") | |
| return "\n\n".join(parts), "" | |
| else: | |
| return "I couldn't find specific information on that topic. Could you rephrase?", "" | |
| # If only one draft exists, use that as final | |
| if not draft_m1: | |
| thinking = "" | |
| clean = draft_m2 | |
| think_match = re.search(r"<think>(.*?)</think>", draft_m2, re.DOTALL) | |
| if think_match: | |
| thinking = think_match.group(1).strip() | |
| clean = re.sub(r"<think>.*?</think>", "", draft_m2, flags=re.DOTALL).strip() | |
| return clean, thinking | |
| if not draft_m2: | |
| thinking = "" | |
| clean = draft_m1 | |
| think_match = re.search(r"<think>(.*?)</think>", draft_m1, re.DOTALL) | |
| if think_match: | |
| thinking = think_match.group(1).strip() | |
| clean = re.sub(r"<think>.*?</think>", "", draft_m1, flags=re.DOTALL).strip() | |
| return clean, thinking | |
| # ----- 2. Merge both drafts using M2 with a larger token budget ----- | |
| merge_prompt = f"""Question: {question} | |
| Draft from Model A: | |
| {draft_m1} | |
| Draft from Model B: | |
| {draft_m2} | |
| Combine these two drafts into a single, comprehensive, accurate, and natural answer. Keep the best parts from each. Ensure the final answer directly addresses the question, is well‑structured, and reads as a single coherent response. Do NOT mention that you are combining drafts or that you used multiple models. Just provide the final answer. | |
| Final answer:""" | |
| merge_max_tokens = max(1024, max_tokens * 2) | |
| merged = call_model(m2_client, merge_prompt, merge_max_tokens, temperature) | |
| if merged and len(merged) < 100: | |
| print(f"Merged answer too short ({len(merged)} chars), retrying with 2048 tokens...") | |
| merged = call_model(m2_client, merge_prompt, 2048, temperature) | |
| if not merged: | |
| print("Merge failed, falling back to draft_m1") | |
| merged = draft_m1 | |
| # Extract thinking and clean tags | |
| thinking_content = "" | |
| clean_answer = merged | |
| think_match = re.search(r"<think>(.*?)</think>", merged, re.DOTALL) | |
| if think_match: | |
| thinking_content = think_match.group(1).strip() | |
| clean_answer = re.sub(r"<think>.*?</think>", "", merged, flags=re.DOTALL).strip() | |
| return clean_answer, thinking_content | |
| # ============================================================ | |
| # CASUAL REPLY (calls M1 or M2) | |
| # ============================================================ | |
| def get_casual_model_response(query: str) -> str: | |
| client = get_m1_client() | |
| if client is not None: | |
| try: | |
| result = client.predict( | |
| prompt=f"User: {query}\nAssistant:", | |
| max_tokens=128, | |
| temperature=0.7, | |
| api_name="/generate" | |
| ) | |
| if result and isinstance(result, str) and result.strip(): | |
| clean = re.sub(r"<think>.*?</think>", "", result, flags=re.DOTALL).strip() | |
| return clean | |
| except Exception as e: | |
| print(f"M1 casual failed: {e}") | |
| client = get_m2_client() | |
| if client is not None: | |
| try: | |
| result = client.predict( | |
| prompt=f"User: {query}\nAssistant:", | |
| max_tokens=128, | |
| temperature=0.7, | |
| api_name="/generate" | |
| ) | |
| if result and isinstance(result, str) and result.strip(): | |
| clean = re.sub(r"<think>.*?</think>", "", result, flags=re.DOTALL).strip() | |
| return clean | |
| except Exception as e: | |
| print(f"M2 casual failed: {e}") | |
| return ( | |
| f"👋 Hi there! I'm X‑RUDRA, your research assistant. " | |
| f"How can I help you today? (Your message `{query}` was casual, so I kept it light.)" | |
| ) | |
| # ============================================================ | |
| # FORMATTERS (Sources, Evidence, Verification) | |
| # ============================================================ | |
| def format_sources(sources): | |
| if not sources: | |
| return "## 📚 Sources\n\nNo sources were returned." | |
| output = ["## 📚 Sources", ""] | |
| for idx, src in enumerate(sources, 1): | |
| if not isinstance(src, dict): | |
| continue | |
| title = src.get("title", "Untitled") | |
| url = src.get("url", "") | |
| method = src.get("fetch_method", "web") | |
| score = src.get("source_score", src.get("score", "N/A")) | |
| snippet = src.get("snippet", src.get("description", "")) | |
| if url: | |
| output.append(f"### {idx}. [{title}]({url})") | |
| else: | |
| output.append(f"### {idx}. {title}") | |
| output.append(f"**Fetcher:** `{method}`") | |
| output.append(f"**Source score:** `{score}`") | |
| if snippet: | |
| output.append(f"\n> {snippet}") | |
| output.append("") | |
| return "\n".join(output) | |
| def format_evidence(claims): | |
| if not claims: | |
| return "## 🧠 Evidence\n\nNo structured evidence was returned." | |
| output = ["## 🧠 Evidence", ""] | |
| for idx, claim in enumerate(claims, 1): | |
| if not isinstance(claim, dict): | |
| continue | |
| text = claim.get("claim", claim.get("text", "")) | |
| score = claim.get("support_score", claim.get("score", "N/A")) | |
| source = claim.get("source_url", claim.get("url", "")) | |
| output.append(f"### Evidence {idx}") | |
| output.append(str(text)) | |
| output.append(f"**Support:** `{score}`") | |
| if source: | |
| output.append(f"**Source:** {source}") | |
| output.append("---") | |
| return "\n\n".join(output) | |
| def format_verification(contradictions): | |
| if not contradictions: | |
| return "## ⚖️ Verification\n\n✅ No major contradictions detected." | |
| output = ["## ⚖️ Verification", "", "⚠️ Potential contradictions detected:", ""] | |
| for idx, item in enumerate(contradictions, 1): | |
| if not isinstance(item, dict): | |
| continue | |
| claim_a = item.get("claim_a", "") | |
| claim_b = item.get("claim_b", "") | |
| source_a = item.get("source_a", "") | |
| source_b = item.get("source_b", "") | |
| output.append(f"### Contradiction {idx}") | |
| output.append(f"**A:** {claim_a}") | |
| if source_a: | |
| output.append(f"Source A: `{source_a}`") | |
| output.append("") | |
| output.append(f"**B:** {claim_b}") | |
| if source_b: | |
| output.append(f"Source B: `{source_b}`") | |
| output.append("---") | |
| return "\n\n".join(output) | |
| def build_activity(data, elapsed_ms): | |
| sources = data.get("sources", []) or data.get("results", []) | |
| claims = data.get("claims", []) | |
| contradictions = data.get("contradictions", []) | |
| rounds = data.get("rounds", data.get("research_rounds", "N/A")) | |
| return f""" | |
| ## ⚡ X-RUDRA Research | |
| | Stage | Status | | |
| |---|---| | |
| | Task analysis | ✅ Complete | | |
| | M1 research | ✅ Draft generated | | |
| | M2 research | ✅ Draft generated + merged | | |
| | Web discovery | ✅ Complete | | |
| | Evidence extraction | {"✅" if claims else "⚙️"} | | |
| | Source verification | ✅ Complete | | |
| | Contradiction check | {"⚠️ Found" if contradictions else "✅ Clear"} | | |
| | Final synthesis | ✅ Complete | | |
| **Sources:** `{len(sources)}` | |
| **Claims:** `{len(claims)}` | |
| **Rounds:** `{rounds}` | |
| **Time:** `{elapsed_ms} ms` | |
| ### Engine | |
| `M1` → `{M1_REPO}` | |
| `M2` → `{M2_REPO}` | |
| `Web` → `DuckDuckGo` | |
| `Fetcher` → `Scrapling` | |
| `Browser` → `Playwright` | |
| """ | |
| # ============================================================ | |
| # SAFE DICT HELPER | |
| # ============================================================ | |
| def safe_dict(value): | |
| if isinstance(value, dict): | |
| return value | |
| if hasattr(value, "model_dump"): | |
| try: | |
| return value.model_dump() | |
| except Exception: | |
| pass | |
| if hasattr(value, "dict"): | |
| try: | |
| return value.dict() | |
| except Exception: | |
| pass | |
| return {"result": str(value)} | |
| # ============================================================ | |
| # MAIN RESEARCH FUNCTION | |
| # ============================================================ | |
| async def do_research(question, max_results, max_rounds, use_models, freshness): | |
| empty_history = [] | |
| empty_activity = "⚪ Enter a question to start." | |
| empty_sources = "" | |
| empty_evidence = "" | |
| empty_verification = "" | |
| empty_thinking = "" | |
| if not question or not str(question).strip(): | |
| return empty_history, empty_activity, empty_sources, empty_evidence, empty_verification, empty_thinking | |
| question = str(question).strip() | |
| # ---- Step 1: Classify intent using M1 (or M2) ---- | |
| intent = classify_intent(question) | |
| print(f"[Intent] {intent} for: {question}") | |
| # ---- Step 2: If casual, reply directly ---- | |
| if intent == "CASUAL": | |
| answer = get_casual_model_response(question) | |
| history = [ | |
| {"role": "user", "content": question}, | |
| {"role": "assistant", "content": answer} | |
| ] | |
| return history, "⚡ Casual chat (model reply, no search).", "", "", "", "" | |
| # ---- Step 3: ACTION – run research pipeline ---- | |
| started = time.perf_counter() | |
| try: | |
| engine = get_engine() | |
| report = await engine.search( | |
| question=question, | |
| max_results=int(max_results), | |
| max_rounds=int(max_rounds), | |
| use_models=bool(use_models), | |
| freshness_mode=str(freshness), | |
| ) | |
| data = safe_dict(report) | |
| elapsed_ms = int((time.perf_counter() - started) * 1000) | |
| # Convert 'results' to 'sources' if needed | |
| sources = data.get("sources", []) | |
| if not sources: | |
| results = data.get("results", []) | |
| for res in results: | |
| if isinstance(res, dict): | |
| sources.append({ | |
| "title": res.get("title", ""), | |
| "url": res.get("url", ""), | |
| "snippet": res.get("snippet", ""), | |
| "fetch_method": "web", | |
| "source_score": res.get("rank", "N/A"), | |
| "description": res.get("snippet", ""), | |
| }) | |
| data["sources"] = sources | |
| # Generate final answer and thinking | |
| final_answer, thinking_content = get_combined_model_answer(question, sources) | |
| sources_md = format_sources(sources) | |
| evidence_md = format_evidence(data.get("claims", [])) | |
| verification_md = format_verification(data.get("contradictions", [])) | |
| activity_md = build_activity(data, elapsed_ms) | |
| thinking_md = f"### 🧠 Reasoning\n\n{thinking_content}" if thinking_content else "" | |
| history = [ | |
| {"role": "user", "content": question}, | |
| {"role": "assistant", "content": final_answer} | |
| ] | |
| return history, activity_md, sources_md, evidence_md, verification_md, thinking_md | |
| except Exception as exc: | |
| error = f"❌ **X-RUDRA Error**\n\n`{type(exc).__name__}: {exc}`" | |
| print("\n" + "="*70) | |
| print("X-RUDRA ERROR") | |
| print(traceback.format_exc()) | |
| print("="*70 + "\n") | |
| history = [ | |
| {"role": "user", "content": question}, | |
| {"role": "assistant", "content": error} | |
| ] | |
| return history, "❌ Research failed.", "", "", "", "" | |
| # ============================================================ | |
| # GRADIO SYNC WRAPPER WITH @spaces.GPU | |
| # ============================================================ | |
| def run_research(question, max_results, max_rounds, use_models, freshness): | |
| return asyncio.run(do_research(question, max_results, max_rounds, use_models, freshness)) | |
| # ============================================================ | |
| # HEALTH CHECK | |
| # ============================================================ | |
| def health_check(): | |
| return f""" | |
| ## 🟢 X-RUDRA Online | |
| **Version:** `{VERSION}` | |
| **M1:** `{M1_REPO}` | |
| **M2:** `{M2_REPO}` | |
| **Engine:** Lazy initialized | |
| """ | |
| # ============================================================ | |
| # CSS | |
| # ============================================================ | |
| CSS = """ | |
| body { background: #f7f7f8; } | |
| .gradio-container { max-width: 1500px !important; } | |
| #header { text-align: center; padding: 20px 0 10px 0; } | |
| #logo { font-size: 38px; font-weight: 800; } | |
| #tagline { opacity: 0.65; font-size: 15px; } | |
| #chat { border-radius: 18px; } | |
| #send { min-height: 52px; font-size: 18px; font-weight: 700; } | |
| footer { display: none !important; } | |
| @keyframes think-pulse { | |
| 0% { opacity: 0.3; transform: scale(0.95); } | |
| 50% { opacity: 1; transform: scale(1.05); } | |
| 100% { opacity: 0.3; transform: scale(0.95); } | |
| } | |
| .thinking-spinner { | |
| display: inline-block; | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| background: #6b7280; | |
| margin-right: 8px; | |
| animation: think-pulse 1.2s ease-in-out infinite; | |
| } | |
| .thinking-container { | |
| background: #f3f4f6; | |
| border-left: 4px solid #6366f1; | |
| padding: 12px 16px; | |
| border-radius: 8px; | |
| margin: 12px 0; | |
| font-family: monospace; | |
| white-space: pre-wrap; | |
| word-wrap: break-word; | |
| } | |
| """ | |
| # ============================================================ | |
| # GRADIO UI – 6 outputs | |
| # ============================================================ | |
| with gr.Blocks(title=APP_NAME) as demo: | |
| gr.HTML(""" | |
| <div id="header"> | |
| <div id="logo">⚡ X-RUDRA</div> | |
| <div id="tagline">Dual‑Model AI · Live Web Research · Evidence</div> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=7): | |
| chatbot = gr.Chatbot(label="X-RUDRA", height=600, elem_id="chat") | |
| with gr.Row(): | |
| question = gr.Textbox(placeholder="Ask X-RUDRA anything...", lines=2, show_label=False, scale=8) | |
| send = gr.Button("➤", variant="primary", elem_id="send", scale=1) | |
| with gr.Column(scale=4): | |
| gr.Markdown("## 🔬 Live Research") | |
| activity = gr.Markdown("⚪ Waiting for your question.") | |
| thinking = gr.Markdown("", visible=True) | |
| gr.Markdown("---") | |
| gr.Markdown(f""" | |
| ### Model Spaces | |
| **M1** `{M1_REPO}` | |
| **M2** `{M2_REPO}` | |
| ### Web Stack | |
| `DuckDuckGo` · `Scrapling` · `Playwright` | |
| """) | |
| with gr.Accordion("⚙️ Research Controls", open=False): | |
| with gr.Row(): | |
| max_results = gr.Slider(1, 30, value=10, step=1, label="Max Sources") | |
| max_rounds = gr.Slider(1, 5, value=3, step=1, label="Research Rounds") | |
| with gr.Row(): | |
| use_models = gr.Checkbox(value=True, label="Use M1 + M2") | |
| freshness = gr.Dropdown(["auto","latest","recent","current"], value="auto", label="Freshness") | |
| with gr.Tabs(): | |
| with gr.Tab("📚 Sources"): | |
| sources = gr.Markdown("Sources will appear here.") | |
| with gr.Tab("🧠 Evidence"): | |
| evidence = gr.Markdown("Evidence will appear here.") | |
| with gr.Tab("⚖️ Verification"): | |
| verification = gr.Markdown("Verification will appear here.") | |
| with gr.Accordion("🩺 System Health", open=False): | |
| health_button = gr.Button("Check X-RUDRA") | |
| health_output = gr.Markdown() | |
| gr.Markdown("### Try X-RUDRA") | |
| gr.Examples( | |
| examples=[ | |
| ["What are the latest UNESCO AI education initiatives?"], | |
| ["What are the latest developments in open source AI?"], | |
| ["Compare the latest major AI models."], | |
| ["Research India's current AI ecosystem."] | |
| ], | |
| inputs=question | |
| ) | |
| inputs = [question, max_results, max_rounds, use_models, freshness] | |
| outputs = [chatbot, activity, sources, evidence, verification, thinking] | |
| send.click(fn=run_research, inputs=inputs, outputs=outputs) | |
| question.submit(fn=run_research, inputs=inputs, outputs=outputs) | |
| health_button.click(fn=health_check, inputs=[], outputs=[health_output]) | |
| # ============================================================ | |
| # START | |
| # ============================================================ | |
| if __name__ == "__main__": | |
| print(f"Starting {APP_NAME} {VERSION}") | |
| print("M1:", M1_REPO) | |
| print("M2:", M2_REPO) | |
| print("Lazy engine initialization: ON") | |
| if HF_TOKEN: | |
| print("HF_TOKEN set – rate limits reduced.") | |
| else: | |
| print("HF_TOKEN not set – you may experience rate limits. Set it as a Secret in your Space.") | |
| demo.launch(server_name="0.0.0.0", server_port=PORT, css=CSS, show_error=True) |