Spaces:
Runtime error
Runtime error
| """ | |
| AI Voice + Text Chatbot - Backend | |
| Flask + OpenRouter API with automatic multi-model fallback. | |
| RUN: | |
| pip install -r requirements.txt | |
| python app.py | |
| Then open http://127.0.0.1:5000 in Chrome or Edge (Web Speech API requirement). | |
| """ | |
| import time | |
| import logging | |
| import requests | |
| from flask import Flask, render_template, request, jsonify | |
| # ============================================================ | |
| # CONFIGURATION - put your OpenRouter API key here directly | |
| # ============================================================ | |
| API_KEY = "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" # <-- REPLACE THIS with your real key | |
| OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" | |
| # Models are tried in this exact order. If one fails (error, timeout, | |
| # rate limit, empty reply) the next one is tried automatically until | |
| # one succeeds or the list is exhausted. | |
| MODELS = [ | |
| "nvidia/nemotron-3-ultra-550b-a55b:free", | |
| "google/gemma-4-26b-a4b-it:free", | |
| "openai/gpt-oss-20b:free", | |
| "google/gemma-4-31b-it:free", | |
| "inclusionai/ling-3.0-flash:free", | |
| "cohere/north-mini-code:free", | |
| "poolside/laguna-s-2.1:free", | |
| "nvidia/nemotron-3-nano-30b-a3b:free", | |
| ] | |
| REQUEST_TIMEOUT = 30 # seconds allowed per model attempt | |
| # ============================================================ | |
| # ANSWER STYLE | |
| # ============================================================ | |
| # This system prompt makes every model answer like an experienced human | |
| # teacher: natural introduction -> detailed theory -> key points -> real | |
| # world examples -> and, if code is relevant, the FULL working code first, | |
| # followed by its explanation. | |
| SYSTEM_PROMPT = """You are an experienced, friendly human teacher explaining concepts to a student. | |
| Never answer with only bullet points or a dry list. Write naturally, in full sentences and | |
| paragraphs, the way a good teacher speaks, and use clear section headers. | |
| Follow this structure for every answer: | |
| 1. Short Introduction | |
| Explain the topic naturally in 1-3 paragraphs, in plain conversational language. | |
| 2. Detailed Theory | |
| Explain everything clearly using simple, professional language, step by step. Cover the | |
| important concepts and, where relevant, advantages and disadvantages. Full sentences, | |
| not just bullets. | |
| 3. Key Points | |
| A bullet list summarizing the most important takeaways. | |
| 4. Real World Example | |
| Give at least one, ideally two, practical real-world examples of the concept in use | |
| (e.g. banking, hospital systems, e-commerce, student records, interviews, etc). | |
| 5. Code (only if the question needs code) | |
| If the question requires code, put the COMPLETE, FULL WORKING CODE FIRST, before any | |
| explanation, inside a fenced code block. Never explain before showing the code. After | |
| the code, in this order: explanation of how it works, the expected output, time/space | |
| complexity if relevant, best practices, and common mistakes to avoid. | |
| If the question does not need code, simply skip section 5. | |
| """ | |
| # ============================================================ | |
| # LOGGING | |
| # ============================================================ | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s [%(levelname)s] %(message)s", | |
| ) | |
| logger = logging.getLogger("voice-chatbot") | |
| # ============================================================ | |
| # FLASK APP | |
| # ============================================================ | |
| app = Flask(__name__) | |
| def call_openrouter(model_name, messages): | |
| """ | |
| Send a single chat completion request to OpenRouter for one model. | |
| Returns the reply text on success, raises RuntimeError on any failure. | |
| """ | |
| headers = { | |
| "Authorization": f"Bearer {API_KEY}", | |
| "Content-Type": "application/json", | |
| # These two headers are recommended by OpenRouter (optional but nice to have) | |
| "HTTP-Referer": "http://localhost:5000", | |
| "X-Title": "AI Voice Chatbot", | |
| } | |
| payload = { | |
| "model": model_name, | |
| "messages": messages, | |
| } | |
| response = requests.post( | |
| OPENROUTER_URL, | |
| headers=headers, | |
| json=payload, | |
| timeout=REQUEST_TIMEOUT, | |
| ) | |
| if response.status_code != 200: | |
| raise RuntimeError(f"HTTP {response.status_code}: {response.text[:250]}") | |
| data = response.json() | |
| choices = data.get("choices") | |
| if not choices: | |
| raise RuntimeError("No choices returned from model") | |
| reply = choices[0].get("message", {}).get("content", "") | |
| if not reply or not reply.strip(): | |
| raise RuntimeError("Empty response from model") | |
| return reply.strip() | |
| def get_ai_response(user_text): | |
| """ | |
| Try every model in MODELS, in order, until one succeeds. | |
| Returns (reply_text, model_used, elapsed_seconds). | |
| Raises RuntimeError only if every single model failed. | |
| """ | |
| messages = [ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": user_text}, | |
| ] | |
| errors = [] | |
| for model_name in MODELS: | |
| start = time.time() | |
| try: | |
| logger.info(f"Trying model: {model_name}") | |
| reply = call_openrouter(model_name, messages) | |
| elapsed = round(time.time() - start, 2) | |
| logger.info(f"SUCCESS with {model_name} in {elapsed}s") | |
| return reply, model_name, elapsed | |
| except Exception as exc: | |
| elapsed = round(time.time() - start, 2) | |
| logger.warning(f"FAILED {model_name} after {elapsed}s: {exc}") | |
| errors.append(f"{model_name} -> {exc}") | |
| continue | |
| raise RuntimeError("All models failed:\n" + "\n".join(errors)) | |
| # ============================================================ | |
| # ROUTES | |
| # ============================================================ | |
| def home(): | |
| return render_template("index.html") | |
| def chat(): | |
| """ | |
| Receives the merged (voice transcript + manual text) prompt from the | |
| browser and returns an AI answer, trying each OpenRouter model in the | |
| fallback list until one succeeds. | |
| """ | |
| data = request.get_json(silent=True) or {} | |
| user_text = (data.get("text") or "").strip() | |
| if not user_text: | |
| return jsonify({"success": False, "error": "No text provided. Speak or type something first."}), 400 | |
| if API_KEY == "YOUR_OPENROUTER_API_KEY": | |
| return jsonify({ | |
| "success": False, | |
| "error": "Server misconfigured: set your real OpenRouter API key in app.py (API_KEY variable)." | |
| }), 400 | |
| try: | |
| reply, model_used, elapsed = get_ai_response(user_text) | |
| return jsonify({ | |
| "success": True, | |
| "reply": reply, | |
| "model": model_used, | |
| "response_time": elapsed, | |
| }) | |
| except Exception as exc: | |
| logger.error(f"Chat failed completely: {exc}") | |
| return jsonify({"success": False, "error": str(exc)}), 500 | |
| def status(): | |
| return jsonify({ | |
| "status": "ready", | |
| "models_configured": len(MODELS), | |
| "models": MODELS, | |
| "api_key_set": API_KEY != "YOUR_OPENROUTER_API_KEY", | |
| }) | |
| import os | |
| if __name__ == "__main__": | |
| port = int(os.environ.get("PORT", 7860)) | |
| logger.info(f"Starting AI Voice Chatbot on port {port}") | |
| app.run( | |
| host="0.0.0.0", | |
| port=port, | |
| debug=False | |
| ) |