Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,7 +11,7 @@ from PIL import Image, ImageFile, UnidentifiedImageError
|
|
| 11 |
import gradio as gr
|
| 12 |
import time
|
| 13 |
import atexit
|
| 14 |
-
from requests.exceptions import RequestException
|
| 15 |
|
| 16 |
# --- Mistral Client Import & Placeholder for graceful degradation ---
|
| 17 |
_MISTRAL_CLIENT_INSTALLED = False
|
|
@@ -22,8 +22,9 @@ try:
|
|
| 22 |
except ImportError:
|
| 23 |
print(
|
| 24 |
"Warning: Mistral AI client library ('mistralai') not found. "
|
| 25 |
-
"Please install it with 'pip install mistralai' to enable AI analysis features. "
|
| 26 |
-
"The application will launch, but API calls will
|
|
|
|
| 27 |
)
|
| 28 |
# Define placeholder classes to prevent NameErrors and provide clear messages
|
| 29 |
class MistralAPIException(Exception):
|
|
@@ -38,6 +39,8 @@ except ImportError:
|
|
| 38 |
class _DummyMistralChatClient:
|
| 39 |
"""Placeholder for Mistral client's chat interface."""
|
| 40 |
def complete(self, *args, **kwargs):
|
|
|
|
|
|
|
| 41 |
raise MistralAPIException(
|
| 42 |
"Mistral AI chat client is unavailable. "
|
| 43 |
"Please install 'mistralai' with 'pip install mistralai'.",
|
|
@@ -46,6 +49,7 @@ except ImportError:
|
|
| 46 |
class _DummyMistralFilesClient:
|
| 47 |
"""Placeholder for Mistral client's files interface."""
|
| 48 |
def upload(self, *args, **kwargs):
|
|
|
|
| 49 |
raise MistralAPIException(
|
| 50 |
"Mistral AI files client is unavailable. "
|
| 51 |
"Please install 'mistralai' with 'pip install mistralai'.",
|
|
@@ -53,8 +57,8 @@ except ImportError:
|
|
| 53 |
)
|
| 54 |
class Mistral:
|
| 55 |
"""A placeholder for the Mistral client if the library is not installed."""
|
| 56 |
-
def __init__(self, *args, **kwargs):
|
| 57 |
-
|
| 58 |
@property
|
| 59 |
def chat(self):
|
| 60 |
return _DummyMistralChatClient()
|
|
@@ -105,15 +109,10 @@ atexit.register(_cleanup_all_temp_files)
|
|
| 105 |
# --- Mistral Client and API Helpers ---
|
| 106 |
def get_client(api_key: Optional[str] = None):
|
| 107 |
"""
|
| 108 |
-
Returns a Mistral client instance. If the API key is missing
|
| 109 |
-
is not installed, a
|
|
|
|
| 110 |
"""
|
| 111 |
-
if not _MISTRAL_CLIENT_INSTALLED:
|
| 112 |
-
raise MistralAPIException(
|
| 113 |
-
"Mistral AI client library is not installed. Please install it with 'pip install mistralai'.",
|
| 114 |
-
status_code=500 # Internal Server Error, as it's a server-side dependency issue
|
| 115 |
-
)
|
| 116 |
-
|
| 117 |
key_to_use = (api_key or "").strip() or DEFAULT_MISTRAL_KEY
|
| 118 |
if not key_to_use:
|
| 119 |
raise MistralAPIException(
|
|
@@ -121,8 +120,9 @@ def get_client(api_key: Optional[str] = None):
|
|
| 121 |
status_code=401 # Unauthorized
|
| 122 |
)
|
| 123 |
|
|
|
|
| 124 |
# If _MISTRAL_CLIENT_INSTALLED is True, this will be the real Mistral client.
|
| 125 |
-
# Otherwise, it's the placeholder that
|
| 126 |
return Mistral(api_key=key_to_use)
|
| 127 |
|
| 128 |
def is_remote(src: str) -> bool:
|
|
@@ -370,29 +370,48 @@ def chat_complete(client, model: str, messages, timeout: int = 120, progress=Non
|
|
| 370 |
if progress is not None:
|
| 371 |
progress(0.6 + 0.01 * attempt, desc=f"Sending request to model (attempt {attempt+1}/{max_retries})...")
|
| 372 |
|
| 373 |
-
res =
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
|
| 376 |
if progress is not None:
|
| 377 |
progress(0.8, desc="Model responded, parsing...")
|
| 378 |
|
|
|
|
|
|
|
|
|
|
| 379 |
if not choices:
|
| 380 |
return f"Empty response from model: {res}"
|
| 381 |
|
| 382 |
first = choices[0]
|
| 383 |
-
# Handle both object-style and dict-style responses
|
| 384 |
msg = getattr(first, "message", None) or (first.get("message") if isinstance(first, dict) else first)
|
| 385 |
content = getattr(msg, "content", None) or (msg.get("content") if isinstance(msg, dict) else None)
|
| 386 |
return content.strip() if isinstance(content, str) else str(content)
|
| 387 |
|
| 388 |
-
except MistralAPIException as e:
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
| 390 |
delay = initial_delay * (2 ** attempt)
|
| 391 |
-
print(f"
|
| 392 |
time.sleep(delay)
|
| 393 |
else:
|
| 394 |
-
return f"Error: Mistral API error occurred ({
|
| 395 |
-
except RequestException as e:
|
| 396 |
if attempt < max_retries - 1:
|
| 397 |
delay = initial_delay * (2 ** attempt)
|
| 398 |
print(f"Network/API request failed: {e}. Retrying in {delay:.2f}s...")
|
|
@@ -414,26 +433,45 @@ def upload_file_to_mistral(client, path: str, filename: str | None = None, purpo
|
|
| 414 |
if progress is not None:
|
| 415 |
progress(0.5 + 0.01 * attempt, desc=f"Uploading file to model service (attempt {attempt+1}/{max_retries})...")
|
| 416 |
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
-
fid = getattr(res, "id", None) or (res.get("id") if isinstance(res, dict) else None)
|
| 424 |
-
if not fid:
|
| 425 |
-
raise RuntimeError(f"Mistral API upload response missing file ID: {res}")
|
| 426 |
if progress is not None:
|
| 427 |
progress(0.6, desc="Upload complete")
|
| 428 |
return fid
|
| 429 |
|
| 430 |
-
except MistralAPIException as e:
|
| 431 |
-
|
|
|
|
|
|
|
| 432 |
delay = initial_delay * (2 ** attempt)
|
| 433 |
-
print(f"
|
| 434 |
time.sleep(delay)
|
| 435 |
else:
|
| 436 |
-
raise RuntimeError(f"Mistral API file upload failed with status {
|
| 437 |
except RequestException as e:
|
| 438 |
if attempt < max_retries - 1:
|
| 439 |
delay = initial_delay * (2 ** attempt)
|
|
@@ -650,7 +688,7 @@ def create_demo():
|
|
| 650 |
|
| 651 |
mistral_client_status_message = ""
|
| 652 |
if not _MISTRAL_CLIENT_INSTALLED:
|
| 653 |
-
mistral_client_status_message = "
|
| 654 |
else:
|
| 655 |
mistral_client_status_message = "🟢 Mistral AI client found."
|
| 656 |
|
|
@@ -883,7 +921,7 @@ def create_demo():
|
|
| 883 |
if ext_from_src(raw_media_path) in VIDEO_EXTENSIONS:
|
| 884 |
is_actually_video_for_analysis = True
|
| 885 |
|
| 886 |
-
client = get_client(key) # This will raise MistralAPIException if
|
| 887 |
|
| 888 |
if is_actually_video_for_analysis:
|
| 889 |
progress(0.25, desc="Running full-video analysis")
|
|
|
|
| 11 |
import gradio as gr
|
| 12 |
import time
|
| 13 |
import atexit
|
| 14 |
+
from requests.exceptions import RequestException, HTTPError # Import HTTPError for requests fallback
|
| 15 |
|
| 16 |
# --- Mistral Client Import & Placeholder for graceful degradation ---
|
| 17 |
_MISTRAL_CLIENT_INSTALLED = False
|
|
|
|
| 22 |
except ImportError:
|
| 23 |
print(
|
| 24 |
"Warning: Mistral AI client library ('mistralai') not found. "
|
| 25 |
+
"Please install it with 'pip install mistralai' to enable full AI analysis features. " # Updated message
|
| 26 |
+
"The application will launch, but API calls will fall back to direct HTTP requests "
|
| 27 |
+
"if an API key is provided." # Updated message to reflect fallback
|
| 28 |
)
|
| 29 |
# Define placeholder classes to prevent NameErrors and provide clear messages
|
| 30 |
class MistralAPIException(Exception):
|
|
|
|
| 39 |
class _DummyMistralChatClient:
|
| 40 |
"""Placeholder for Mistral client's chat interface."""
|
| 41 |
def complete(self, *args, **kwargs):
|
| 42 |
+
# This method will typically not be called if _MISTRAL_CLIENT_INSTALLED is False,
|
| 43 |
+
# as the `chat_complete` function will use the requests fallback instead.
|
| 44 |
raise MistralAPIException(
|
| 45 |
"Mistral AI chat client is unavailable. "
|
| 46 |
"Please install 'mistralai' with 'pip install mistralai'.",
|
|
|
|
| 49 |
class _DummyMistralFilesClient:
|
| 50 |
"""Placeholder for Mistral client's files interface."""
|
| 51 |
def upload(self, *args, **kwargs):
|
| 52 |
+
# This method will typically not be called if _MISTRAL_CLIENT_INSTALLED is False.
|
| 53 |
raise MistralAPIException(
|
| 54 |
"Mistral AI files client is unavailable. "
|
| 55 |
"Please install 'mistralai' with 'pip install mistralai'.",
|
|
|
|
| 57 |
)
|
| 58 |
class Mistral:
|
| 59 |
"""A placeholder for the Mistral client if the library is not installed."""
|
| 60 |
+
def __init__(self, api_key: str = "", *args, **kwargs): # Added api_key to store it for fallback
|
| 61 |
+
self.api_key = api_key # Store the API key
|
| 62 |
@property
|
| 63 |
def chat(self):
|
| 64 |
return _DummyMistralChatClient()
|
|
|
|
| 109 |
# --- Mistral Client and API Helpers ---
|
| 110 |
def get_client(api_key: Optional[str] = None):
|
| 111 |
"""
|
| 112 |
+
Returns a Mistral client instance. If the API key is missing, a MistralAPIException is raised.
|
| 113 |
+
If the client library is not installed, a placeholder client is returned, and API calls
|
| 114 |
+
will fall back to direct HTTP requests.
|
| 115 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
key_to_use = (api_key or "").strip() or DEFAULT_MISTRAL_KEY
|
| 117 |
if not key_to_use:
|
| 118 |
raise MistralAPIException(
|
|
|
|
| 120 |
status_code=401 # Unauthorized
|
| 121 |
)
|
| 122 |
|
| 123 |
+
# Always return a Mistral client instance.
|
| 124 |
# If _MISTRAL_CLIENT_INSTALLED is True, this will be the real Mistral client.
|
| 125 |
+
# Otherwise, it's the placeholder that has the api_key stored.
|
| 126 |
return Mistral(api_key=key_to_use)
|
| 127 |
|
| 128 |
def is_remote(src: str) -> bool:
|
|
|
|
| 370 |
if progress is not None:
|
| 371 |
progress(0.6 + 0.01 * attempt, desc=f"Sending request to model (attempt {attempt+1}/{max_retries})...")
|
| 372 |
|
| 373 |
+
res = None
|
| 374 |
+
if _MISTRAL_CLIENT_INSTALLED:
|
| 375 |
+
# Use the real Mistral client's chat.complete method
|
| 376 |
+
res = client.chat.complete(model=model, messages=messages, stream=False, timeout_ms=timeout * 1000)
|
| 377 |
+
else:
|
| 378 |
+
# Fallback to direct HTTP request if client library not installed
|
| 379 |
+
api_key = getattr(client, "api_key", "") # Get key from client, should always be present now
|
| 380 |
+
if not api_key: # Double check, though get_client already ensures this
|
| 381 |
+
return "Error: Mistral API key is not set for fallback."
|
| 382 |
+
|
| 383 |
+
url = "https://api.mistral.ai/v1/chat/completions"
|
| 384 |
+
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
|
| 385 |
+
payload = {"model": model, "messages": messages, "stream": False} # Removed timeout_ms for requests
|
| 386 |
+
r = requests.post(url, json=payload, headers=headers, timeout=timeout) # requests timeout in seconds
|
| 387 |
+
r.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx)
|
| 388 |
+
res = r.json()
|
| 389 |
|
| 390 |
if progress is not None:
|
| 391 |
progress(0.8, desc="Model responded, parsing...")
|
| 392 |
|
| 393 |
+
# Handle both object-style (from mistralai client) and dict-style (from requests.json()) responses
|
| 394 |
+
choices = getattr(res, "choices", None) or (res.get("choices") if isinstance(res, dict) else [])
|
| 395 |
+
|
| 396 |
if not choices:
|
| 397 |
return f"Empty response from model: {res}"
|
| 398 |
|
| 399 |
first = choices[0]
|
|
|
|
| 400 |
msg = getattr(first, "message", None) or (first.get("message") if isinstance(first, dict) else first)
|
| 401 |
content = getattr(msg, "content", None) or (msg.get("content") if isinstance(msg, dict) else None)
|
| 402 |
return content.strip() if isinstance(content, str) else str(content)
|
| 403 |
|
| 404 |
+
except (MistralAPIException, HTTPError) as e: # Catch both client lib and requests HTTP errors
|
| 405 |
+
status_code = getattr(e, "status_code", None) or (e.response.status_code if isinstance(e, HTTPError) else None)
|
| 406 |
+
message = getattr(e, "message", str(e))
|
| 407 |
+
|
| 408 |
+
if status_code == 429 and attempt < max_retries - 1:
|
| 409 |
delay = initial_delay * (2 ** attempt)
|
| 410 |
+
print(f"Mistral API: Rate limit exceeded (429). Retrying in {delay:.2f}s...")
|
| 411 |
time.sleep(delay)
|
| 412 |
else:
|
| 413 |
+
return f"Error: Mistral API error occurred ({status_code if status_code else 'unknown'}): {message}"
|
| 414 |
+
except RequestException as e: # Catch other requests errors (e.g., connection issues, timeout)
|
| 415 |
if attempt < max_retries - 1:
|
| 416 |
delay = initial_delay * (2 ** attempt)
|
| 417 |
print(f"Network/API request failed: {e}. Retrying in {delay:.2f}s...")
|
|
|
|
| 433 |
if progress is not None:
|
| 434 |
progress(0.5 + 0.01 * attempt, desc=f"Uploading file to model service (attempt {attempt+1}/{max_retries})...")
|
| 435 |
|
| 436 |
+
fid = None
|
| 437 |
+
if _MISTRAL_CLIENT_INSTALLED:
|
| 438 |
+
with open(path, "rb") as fh:
|
| 439 |
+
# Mistral client's file upload expects (filename, file_like_object) for 'file' param
|
| 440 |
+
res = client.files.upload(file=(fname, fh), purpose=purpose)
|
| 441 |
+
fid = getattr(res, "id", None) or (res.get("id") if isinstance(res, dict) else None)
|
| 442 |
+
if not fid:
|
| 443 |
+
raise RuntimeError(f"Mistral API upload response missing file ID from client: {res}")
|
| 444 |
+
else:
|
| 445 |
+
# Fallback to direct HTTP request
|
| 446 |
+
api_key = getattr(client, "api_key", "")
|
| 447 |
+
if not api_key:
|
| 448 |
+
raise RuntimeError("Mistral API key is not set for file upload fallback.")
|
| 449 |
+
|
| 450 |
+
url = "https://api.mistral.ai/v1/files"
|
| 451 |
+
headers = {"Authorization": f"Bearer {api_key}"}
|
| 452 |
+
with open(path, "rb") as fh:
|
| 453 |
+
files = {"file": (fname, fh)}
|
| 454 |
+
data = {"purpose": purpose}
|
| 455 |
+
r = requests.post(url, headers=headers, files=files, data=data, timeout=timeout)
|
| 456 |
+
r.raise_for_status()
|
| 457 |
+
jr = r.json()
|
| 458 |
+
fid = jr.get("id") or jr.get("data", [{}])[0].get("id") # Handle potential nested 'data' from older API
|
| 459 |
+
if not fid:
|
| 460 |
+
raise RuntimeError(f"Mistral API upload response missing file ID from direct request: {jr}")
|
| 461 |
|
|
|
|
|
|
|
|
|
|
| 462 |
if progress is not None:
|
| 463 |
progress(0.6, desc="Upload complete")
|
| 464 |
return fid
|
| 465 |
|
| 466 |
+
except (MistralAPIException, HTTPError) as e:
|
| 467 |
+
status_code = getattr(e, "status_code", None) or (e.response.status_code if isinstance(e, HTTPError) else None)
|
| 468 |
+
message = getattr(e, "message", str(e))
|
| 469 |
+
if status_code == 429 and attempt < max_retries - 1:
|
| 470 |
delay = initial_delay * (2 ** attempt)
|
| 471 |
+
print(f"Mistral API: Upload rate limit exceeded (429). Retrying in {delay:.2f}s...")
|
| 472 |
time.sleep(delay)
|
| 473 |
else:
|
| 474 |
+
raise RuntimeError(f"Mistral API file upload failed with status {status_code}: {message}") from e
|
| 475 |
except RequestException as e:
|
| 476 |
if attempt < max_retries - 1:
|
| 477 |
delay = initial_delay * (2 ** attempt)
|
|
|
|
| 688 |
|
| 689 |
mistral_client_status_message = ""
|
| 690 |
if not _MISTRAL_CLIENT_INSTALLED:
|
| 691 |
+
mistral_client_status_message = "🟡 Mistral AI client ('mistralai') not installed. AI analysis will fall back to direct HTTP requests. Run `pip install mistralai` for full features." # Updated message
|
| 692 |
else:
|
| 693 |
mistral_client_status_message = "🟢 Mistral AI client found."
|
| 694 |
|
|
|
|
| 921 |
if ext_from_src(raw_media_path) in VIDEO_EXTENSIONS:
|
| 922 |
is_actually_video_for_analysis = True
|
| 923 |
|
| 924 |
+
client = get_client(key) # This will raise MistralAPIException if key missing, but NOT if lib not installed
|
| 925 |
|
| 926 |
if is_actually_video_for_analysis:
|
| 927 |
progress(0.25, desc="Running full-video analysis")
|