Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -9,6 +9,8 @@ import requests
|
|
| 9 |
import urllib.parse
|
| 10 |
from flask import Flask, request, jsonify, Response
|
| 11 |
from flask_cors import CORS
|
|
|
|
|
|
|
| 12 |
from datetime import datetime
|
| 13 |
from dotenv import load_dotenv
|
| 14 |
from llama_cpp import Llama
|
|
@@ -35,6 +37,14 @@ load_dotenv()
|
|
| 35 |
app = Flask(__name__)
|
| 36 |
CORS(app)
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
# --- Configuration & Model Selection ---
|
| 39 |
OLLAMA_URL = "http://localhost:11434"
|
| 40 |
HF_FALLBACK_REPO = "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF"
|
|
@@ -200,25 +210,28 @@ search_manager = SearchManager()
|
|
| 200 |
def detect_tools(message):
|
| 201 |
message = message.lower().strip()
|
| 202 |
tools = []
|
|
|
|
|
|
|
| 203 |
search_keywords = [
|
| 204 |
-
"search", "
|
| 205 |
-
"research", "browse", "today", "current", "facts", "data", "tell me about",
|
| 206 |
-
"what happened", "weather", "stock", "price", "how many", "who won",
|
| 207 |
-
"generate video", "create video", "make a video", "video about",
|
| 208 |
-
"temperature", "forecast", "update", "recent", "now", "happening",
|
| 209 |
-
"look up", "check", "get info", "information about", "details on",
|
| 210 |
-
"what's", "whats", "how's", "hows", "where is", "when did", "why did",
|
| 211 |
-
"did ", "does ", "is ", "are ", "was ", "were ", "will ", "can ", "could ",
|
| 212 |
-
"2024", "2025", "2026", "yesterday", "tomorrow", "this week", "this month"
|
| 213 |
]
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
tools.append("search")
|
|
|
|
|
|
|
| 216 |
question_starters = ["who ", "what ", "when ", "where ", "why ", "how "]
|
| 217 |
if any(message.startswith(kw) for kw in question_starters):
|
| 218 |
if "search" not in tools:
|
|
|
|
| 219 |
personal_words = ["you", "your", "we", "our", "i ", "my ", "me "]
|
| 220 |
if not any(pw in message for pw in personal_words):
|
| 221 |
tools.append("search")
|
|
|
|
| 222 |
image_keywords = ["image", "draw", "visualize", "photo", "paint", "picture", "generate image"]
|
| 223 |
if any(kw in message for kw in image_keywords) or ("create" in message and "image" in message) or ("make" in message and "image" in message):
|
| 224 |
tools.append("image")
|
|
@@ -264,8 +277,14 @@ def clean_search_query(query):
|
|
| 264 |
|
| 265 |
def get_time_response(message):
|
| 266 |
message = message.lower()
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
now = datetime.now()
|
| 270 |
return f"The current time is **{now.strftime('%H:%M:%S')}**. Today is **{now.strftime('%A, %Y-%m-%d')}**."
|
| 271 |
return None
|
|
@@ -292,6 +311,7 @@ def login():
|
|
| 292 |
return jsonify({"user": {"username": data.get("username", "Guest"), "messages_used": 0}}), 200
|
| 293 |
|
| 294 |
@app.route('/api/search', methods=['POST'])
|
|
|
|
| 295 |
def search_api():
|
| 296 |
try:
|
| 297 |
data = request.json or {}
|
|
@@ -313,6 +333,7 @@ def search_api():
|
|
| 313 |
return jsonify({"error": "Search service temporarily unavailable", "success": False}), 500
|
| 314 |
|
| 315 |
@app.route('/api/chat', methods=['POST'])
|
|
|
|
| 316 |
def chat():
|
| 317 |
data = request.json or {}
|
| 318 |
message = data.get("message", "").strip()
|
|
@@ -352,8 +373,19 @@ def chat():
|
|
| 352 |
dynamic_system_prompt += f"\n[Active Mode]: {mode.upper()} - {selected_mode_instruction}"
|
| 353 |
if is_deep_research:
|
| 354 |
dynamic_system_prompt += "\n[DEEP RESEARCH ACTIVE]: Use a multi-step reasoning approach. Analyze thoroughly."
|
|
|
|
|
|
|
| 355 |
if memory:
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
full_prompt = f"{message}"
|
| 359 |
if attachments:
|
|
@@ -365,6 +397,9 @@ def chat():
|
|
| 365 |
# Step 1: Attempt Colab (Ollama) - Skip if on HF Space to save time
|
| 366 |
if not IS_HF_SPACE:
|
| 367 |
try:
|
|
|
|
|
|
|
|
|
|
| 368 |
logger.info("Attempting Colab (Ollama) streaming...")
|
| 369 |
messages = [{"role": "system", "content": dynamic_system_prompt}]
|
| 370 |
for h in history:
|
|
@@ -372,10 +407,12 @@ def chat():
|
|
| 372 |
ollama_role = "assistant" if role == "ai" else "user"
|
| 373 |
messages.append({"role": ollama_role, "content": h.get('content', '')})
|
| 374 |
messages.append({"role": "user", "content": full_prompt})
|
|
|
|
|
|
|
| 375 |
response = requests.post(
|
| 376 |
f"{OLLAMA_URL}/api/chat",
|
| 377 |
json={"model": "phi3:mini", "messages": messages, "stream": True, "options": {"num_ctx": 1024, "num_predict": 512}},
|
| 378 |
-
timeout=
|
| 379 |
stream=True
|
| 380 |
)
|
| 381 |
if response.status_code == 200:
|
|
@@ -432,40 +469,49 @@ def generate_video_ai():
|
|
| 432 |
if not prompt:
|
| 433 |
return jsonify({"error": "No prompt provided"}), 400
|
| 434 |
|
| 435 |
-
# List of stable HF Spaces to try in sequence
|
| 436 |
video_spaces = [
|
| 437 |
"ali-vilas/text-to-video-ms-1.7b",
|
| 438 |
"fffiloni/zeroscope-v2-xl",
|
| 439 |
"damo-vilab/modelscope-text-to-video"
|
| 440 |
]
|
| 441 |
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 448 |
|
| 449 |
-
if
|
| 450 |
-
|
| 451 |
-
|
|
|
|
|
|
|
|
|
|
| 452 |
shutil.copy(result, output_path)
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
}
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
|
|
|
|
|
|
| 463 |
|
| 464 |
-
#
|
| 465 |
try:
|
| 466 |
-
logger.info(f"🚀
|
| 467 |
seed = int(time.time())
|
| 468 |
-
# We use a high-quality pollinations image as a "Motion Card" if video is down
|
| 469 |
motion_url = f"https://image.pollinations.ai/prompt/{requests.utils.quote(prompt)}?seed={seed}&nologo=true&width=1024&height=576"
|
| 470 |
return jsonify({
|
| 471 |
"status": "success",
|
|
@@ -476,9 +522,11 @@ def generate_video_ai():
|
|
| 476 |
})
|
| 477 |
except Exception as e:
|
| 478 |
logger.error(f"❌ All video methods failed: {e}")
|
| 479 |
-
|
|
|
|
| 480 |
|
| 481 |
@app.route('/api/image', methods=['POST'])
|
|
|
|
| 482 |
def generate_image():
|
| 483 |
data = request.json or {}
|
| 484 |
prompt = data.get("prompt", "").strip()
|
|
@@ -501,18 +549,23 @@ def analyze_file():
|
|
| 501 |
file = request.files['file']
|
| 502 |
filename = file.filename
|
| 503 |
ext = filename.split('.')[-1].lower()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 504 |
try:
|
| 505 |
text_content = ""
|
| 506 |
if ext == 'pdf':
|
| 507 |
-
with pdfplumber.open(
|
| 508 |
text_content = "\n".join([page.extract_text() for page in pdf.pages if page.extract_text()])
|
| 509 |
elif ext == 'docx':
|
| 510 |
-
doc = docx.Document(
|
| 511 |
text_content = "\n".join([para.text for para in doc.paragraphs])
|
| 512 |
elif ext == 'txt':
|
| 513 |
-
|
|
|
|
| 514 |
elif ext in ['png', 'jpg', 'jpeg', 'webp']:
|
| 515 |
-
img = Image.open(
|
| 516 |
text_content = pytesseract.image_to_string(img)
|
| 517 |
else:
|
| 518 |
return jsonify({"error": f"Unsupported file type: {ext}"}), 400
|
|
|
|
| 9 |
import urllib.parse
|
| 10 |
from flask import Flask, request, jsonify, Response
|
| 11 |
from flask_cors import CORS
|
| 12 |
+
from flask_limiter import Limiter
|
| 13 |
+
from flask_limiter.util import get_remote_address
|
| 14 |
from datetime import datetime
|
| 15 |
from dotenv import load_dotenv
|
| 16 |
from llama_cpp import Llama
|
|
|
|
| 37 |
app = Flask(__name__)
|
| 38 |
CORS(app)
|
| 39 |
|
| 40 |
+
# --- Rate Limiting ---
|
| 41 |
+
limiter = Limiter(
|
| 42 |
+
get_remote_address,
|
| 43 |
+
app=app,
|
| 44 |
+
default_limits=["200 per day", "50 per hour"],
|
| 45 |
+
storage_uri="memory://",
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
# --- Configuration & Model Selection ---
|
| 49 |
OLLAMA_URL = "http://localhost:11434"
|
| 50 |
HF_FALLBACK_REPO = "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF"
|
|
|
|
| 210 |
def detect_tools(message):
|
| 211 |
message = message.lower().strip()
|
| 212 |
tools = []
|
| 213 |
+
|
| 214 |
+
# Explicit search intent keywords
|
| 215 |
search_keywords = [
|
| 216 |
+
"search", "look up", "find information about", "latest news", "current weather"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
]
|
| 218 |
+
|
| 219 |
+
# Check for years only if other indicators are present
|
| 220 |
+
has_year = any(yr in message for yr in ["2024", "2025", "2026"])
|
| 221 |
+
search_indicators = ["who ", "what ", "when ", "where ", "how ", "news", "weather", "stock", "price"]
|
| 222 |
+
|
| 223 |
+
if any(kw in message for kw in search_keywords) or (has_year and any(si in message for si in search_indicators)):
|
| 224 |
tools.append("search")
|
| 225 |
+
|
| 226 |
+
# Refined search detection for questions
|
| 227 |
question_starters = ["who ", "what ", "when ", "where ", "why ", "how "]
|
| 228 |
if any(message.startswith(kw) for kw in question_starters):
|
| 229 |
if "search" not in tools:
|
| 230 |
+
# Avoid triggering search for personal questions
|
| 231 |
personal_words = ["you", "your", "we", "our", "i ", "my ", "me "]
|
| 232 |
if not any(pw in message for pw in personal_words):
|
| 233 |
tools.append("search")
|
| 234 |
+
|
| 235 |
image_keywords = ["image", "draw", "visualize", "photo", "paint", "picture", "generate image"]
|
| 236 |
if any(kw in message for kw in image_keywords) or ("create" in message and "image" in message) or ("make" in message and "image" in message):
|
| 237 |
tools.append("image")
|
|
|
|
| 277 |
|
| 278 |
def get_time_response(message):
|
| 279 |
message = message.lower()
|
| 280 |
+
# ONLY exact phrases or specific combinations
|
| 281 |
+
time_phrases = ["what time is it", "current time", "what's the date", "whats the date"]
|
| 282 |
+
|
| 283 |
+
# Require at least 2 time-related words using regex boundaries
|
| 284 |
+
time_words = ["time", "date", "clock", "hour", "minute"]
|
| 285 |
+
word_matches = [word for word in time_words if re.search(rf"\b{word}\b", message)]
|
| 286 |
+
|
| 287 |
+
if any(phrase in message for phrase in time_phrases) or len(word_matches) >= 2:
|
| 288 |
now = datetime.now()
|
| 289 |
return f"The current time is **{now.strftime('%H:%M:%S')}**. Today is **{now.strftime('%A, %Y-%m-%d')}**."
|
| 290 |
return None
|
|
|
|
| 311 |
return jsonify({"user": {"username": data.get("username", "Guest"), "messages_used": 0}}), 200
|
| 312 |
|
| 313 |
@app.route('/api/search', methods=['POST'])
|
| 314 |
+
@limiter.limit("5 per minute")
|
| 315 |
def search_api():
|
| 316 |
try:
|
| 317 |
data = request.json or {}
|
|
|
|
| 333 |
return jsonify({"error": "Search service temporarily unavailable", "success": False}), 500
|
| 334 |
|
| 335 |
@app.route('/api/chat', methods=['POST'])
|
| 336 |
+
@limiter.limit("10 per minute")
|
| 337 |
def chat():
|
| 338 |
data = request.json or {}
|
| 339 |
message = data.get("message", "").strip()
|
|
|
|
| 373 |
dynamic_system_prompt += f"\n[Active Mode]: {mode.upper()} - {selected_mode_instruction}"
|
| 374 |
if is_deep_research:
|
| 375 |
dynamic_system_prompt += "\n[DEEP RESEARCH ACTIVE]: Use a multi-step reasoning approach. Analyze thoroughly."
|
| 376 |
+
|
| 377 |
+
# Task 8: Memory injection security
|
| 378 |
if memory:
|
| 379 |
+
sanitized_memory = {}
|
| 380 |
+
for k, v in memory.items():
|
| 381 |
+
# Disallow sensitive keys
|
| 382 |
+
if k.lower() in ["role", "system", "instruction"]:
|
| 383 |
+
continue
|
| 384 |
+
# Sanitize values: strip [, ], <, > and limit length
|
| 385 |
+
clean_val = str(v).replace("[", "").replace("]", "").replace("<", "").replace(">", "")[:100]
|
| 386 |
+
sanitized_memory[k] = clean_val
|
| 387 |
+
if sanitized_memory:
|
| 388 |
+
dynamic_system_prompt += f"\n[User Memory]: {json.dumps(sanitized_memory)}"
|
| 389 |
|
| 390 |
full_prompt = f"{message}"
|
| 391 |
if attachments:
|
|
|
|
| 397 |
# Step 1: Attempt Colab (Ollama) - Skip if on HF Space to save time
|
| 398 |
if not IS_HF_SPACE:
|
| 399 |
try:
|
| 400 |
+
# Task 6: Connection check before request
|
| 401 |
+
requests.head(OLLAMA_URL, timeout=2)
|
| 402 |
+
|
| 403 |
logger.info("Attempting Colab (Ollama) streaming...")
|
| 404 |
messages = [{"role": "system", "content": dynamic_system_prompt}]
|
| 405 |
for h in history:
|
|
|
|
| 407 |
ollama_role = "assistant" if role == "ai" else "user"
|
| 408 |
messages.append({"role": ollama_role, "content": h.get('content', '')})
|
| 409 |
messages.append({"role": "user", "content": full_prompt})
|
| 410 |
+
|
| 411 |
+
# Task 6: Increase timeout to 30 seconds
|
| 412 |
response = requests.post(
|
| 413 |
f"{OLLAMA_URL}/api/chat",
|
| 414 |
json={"model": "phi3:mini", "messages": messages, "stream": True, "options": {"num_ctx": 1024, "num_predict": 512}},
|
| 415 |
+
timeout=30,
|
| 416 |
stream=True
|
| 417 |
)
|
| 418 |
if response.status_code == 200:
|
|
|
|
| 469 |
if not prompt:
|
| 470 |
return jsonify({"error": "No prompt provided"}), 400
|
| 471 |
|
| 472 |
+
# Task 7: List of stable HF Spaces to try in sequence
|
| 473 |
video_spaces = [
|
| 474 |
"ali-vilas/text-to-video-ms-1.7b",
|
| 475 |
"fffiloni/zeroscope-v2-xl",
|
| 476 |
"damo-vilab/modelscope-text-to-video"
|
| 477 |
]
|
| 478 |
|
| 479 |
+
# Try the first space, if it fails or blocks connections, fallback to Pollinations
|
| 480 |
+
space = video_spaces[0]
|
| 481 |
+
try:
|
| 482 |
+
logger.info(f"🎬 Attempting Video Gen via {space} for: {prompt}")
|
| 483 |
+
client = Client(space)
|
| 484 |
+
result = client.predict(prompt, api_name="/predict")
|
| 485 |
+
|
| 486 |
+
# result is a URL from Gradio Client, not necessarily a local path
|
| 487 |
+
if result:
|
| 488 |
+
output_filename = f"ai_video_{uuid.uuid4().hex}.mp4"
|
| 489 |
+
output_path = os.path.join(OUTPUT_DIR, output_filename)
|
| 490 |
|
| 491 |
+
# If it's a URL, download it; if it's a local path (some clients return paths), copy it
|
| 492 |
+
if result.startswith("http"):
|
| 493 |
+
resp = requests.get(result, stream=True)
|
| 494 |
+
with open(output_path, 'wb') as f:
|
| 495 |
+
shutil.copyfileobj(resp.raw, f)
|
| 496 |
+
elif os.path.exists(result):
|
| 497 |
shutil.copy(result, output_path)
|
| 498 |
+
else:
|
| 499 |
+
raise Exception("Invalid result from Gradio Client")
|
| 500 |
+
|
| 501 |
+
return jsonify({
|
| 502 |
+
"status": "success",
|
| 503 |
+
"video_url": f"/static/outputs/{output_filename}",
|
| 504 |
+
"provider": space,
|
| 505 |
+
"prompt": prompt
|
| 506 |
+
})
|
| 507 |
+
except Exception as e:
|
| 508 |
+
logger.warning(f"⚠️ Space {space} failed: {str(e)}")
|
| 509 |
+
# Task 7: Immediate fallback to Pollinations if Gradio fails
|
| 510 |
|
| 511 |
+
# Fallback: Pollinations "Motion Image"
|
| 512 |
try:
|
| 513 |
+
logger.info(f"🚀 Video space failed or blocked. Falling back to Pollinations Motion for: {prompt}")
|
| 514 |
seed = int(time.time())
|
|
|
|
| 515 |
motion_url = f"https://image.pollinations.ai/prompt/{requests.utils.quote(prompt)}?seed={seed}&nologo=true&width=1024&height=576"
|
| 516 |
return jsonify({
|
| 517 |
"status": "success",
|
|
|
|
| 522 |
})
|
| 523 |
except Exception as e:
|
| 524 |
logger.error(f"❌ All video methods failed: {e}")
|
| 525 |
+
# Task 11: Fix typo in error message
|
| 526 |
+
return jsonify({"error": "Our video generation servers are temporarily unavailable. Please try again later."}), 503
|
| 527 |
|
| 528 |
@app.route('/api/image', methods=['POST'])
|
| 529 |
+
@limiter.limit("5 per minute")
|
| 530 |
def generate_image():
|
| 531 |
data = request.json or {}
|
| 532 |
prompt = data.get("prompt", "").strip()
|
|
|
|
| 549 |
file = request.files['file']
|
| 550 |
filename = file.filename
|
| 551 |
ext = filename.split('.')[-1].lower()
|
| 552 |
+
|
| 553 |
+
# Task 10: Use independent copy of file stream
|
| 554 |
+
file_bytes = io.BytesIO(file.read())
|
| 555 |
+
|
| 556 |
try:
|
| 557 |
text_content = ""
|
| 558 |
if ext == 'pdf':
|
| 559 |
+
with pdfplumber.open(file_bytes) as pdf:
|
| 560 |
text_content = "\n".join([page.extract_text() for page in pdf.pages if page.extract_text()])
|
| 561 |
elif ext == 'docx':
|
| 562 |
+
doc = docx.Document(file_bytes)
|
| 563 |
text_content = "\n".join([para.text for para in doc.paragraphs])
|
| 564 |
elif ext == 'txt':
|
| 565 |
+
file_bytes.seek(0)
|
| 566 |
+
text_content = file_bytes.read().decode('utf-8', errors='ignore')
|
| 567 |
elif ext in ['png', 'jpg', 'jpeg', 'webp']:
|
| 568 |
+
img = Image.open(file_bytes)
|
| 569 |
text_content = pytesseract.image_to_string(img)
|
| 570 |
else:
|
| 571 |
return jsonify({"error": f"Unsupported file type: {ext}"}), 400
|