Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -31,13 +31,11 @@ def is_remote(src: str) -> bool:
|
|
| 31 |
return bool(src) and src.startswith(("http://", "https://"))
|
| 32 |
|
| 33 |
def fetch_bytes(src: str, stream_threshold=20 * 1024 * 1024) -> bytes:
|
| 34 |
-
"""Fetch bytes. For remote large files stream to a temp file then read."""
|
| 35 |
if is_remote(src):
|
| 36 |
with requests.get(src, timeout=60, stream=True) as r:
|
| 37 |
r.raise_for_status()
|
| 38 |
content_length = r.headers.get("content-length")
|
| 39 |
if content_length and int(content_length) > stream_threshold:
|
| 40 |
-
# stream to temp file
|
| 41 |
fd, path = tempfile.mkstemp()
|
| 42 |
os.close(fd)
|
| 43 |
with open(path, "wb") as f:
|
|
@@ -59,7 +57,6 @@ def fetch_bytes(src: str, stream_threshold=20 * 1024 * 1024) -> bytes:
|
|
| 59 |
def convert_to_jpeg_bytes(media_bytes: bytes, base_h=480) -> bytes:
|
| 60 |
img = Image.open(BytesIO(media_bytes))
|
| 61 |
try:
|
| 62 |
-
# take first frame for animated formats
|
| 63 |
img.seek(0)
|
| 64 |
except Exception:
|
| 65 |
pass
|
|
@@ -95,7 +92,6 @@ def choose_model_for_src(src: str):
|
|
| 95 |
return DEFAULT_VIDEO_MODEL
|
| 96 |
if ext in IMAGE_EXTS:
|
| 97 |
return DEFAULT_IMAGE_MODEL
|
| 98 |
-
# fallback: remote URLs likely videos, local files inspect existence
|
| 99 |
if is_remote(src):
|
| 100 |
return DEFAULT_VIDEO_MODEL
|
| 101 |
return DEFAULT_IMAGE_MODEL
|
|
@@ -121,12 +117,14 @@ def extract_delta(chunk):
|
|
| 121 |
data = getattr(chunk, "data", None) or getattr(chunk, "response", None) or getattr(chunk, "delta", None)
|
| 122 |
if not data:
|
| 123 |
return None
|
|
|
|
| 124 |
try:
|
| 125 |
-
# try common shapes and coerce to string
|
| 126 |
c = data.choices[0].delta
|
| 127 |
if isinstance(c, dict):
|
| 128 |
txt = c.get("content") or c.get("text")
|
| 129 |
-
|
|
|
|
|
|
|
| 130 |
except Exception:
|
| 131 |
pass
|
| 132 |
try:
|
|
@@ -135,7 +133,9 @@ def extract_delta(chunk):
|
|
| 135 |
txt = msg.get("content")
|
| 136 |
else:
|
| 137 |
txt = getattr(msg, "content", None)
|
| 138 |
-
|
|
|
|
|
|
|
| 139 |
except Exception:
|
| 140 |
pass
|
| 141 |
try:
|
|
@@ -147,14 +147,12 @@ def extract_delta(chunk):
|
|
| 147 |
def generate_final_text(src: str, custom_prompt: str, api_key: str):
|
| 148 |
client = get_client(api_key)
|
| 149 |
prompt = (custom_prompt.strip() if custom_prompt and custom_prompt.strip() else "Please provide a detailed visual review.")
|
| 150 |
-
lower = (src or "").lower()
|
| 151 |
ext = ext_from_src(src)
|
| 152 |
is_image = ext in IMAGE_EXTS or (not is_remote(src) and os.path.isfile(src) and ext in IMAGE_EXTS)
|
| 153 |
parts = []
|
| 154 |
|
| 155 |
def stream_and_collect(model, messages):
|
| 156 |
try:
|
| 157 |
-
# prefer streaming if available; fall back to non-streaming
|
| 158 |
stream_gen = None
|
| 159 |
try:
|
| 160 |
stream_gen = client.chat.stream(model=model, messages=messages)
|
|
@@ -163,12 +161,14 @@ def generate_final_text(src: str, custom_prompt: str, api_key: str):
|
|
| 163 |
if stream_gen:
|
| 164 |
for chunk in stream_gen:
|
| 165 |
d = extract_delta(chunk)
|
| 166 |
-
if d:
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
return
|
| 169 |
-
# fallback complete
|
| 170 |
res = client.chat.complete(model=model, messages=messages, stream=False)
|
| 171 |
-
# try to extract text
|
| 172 |
try:
|
| 173 |
choices = getattr(res, "choices", None) or res.get("choices", [])
|
| 174 |
except Exception:
|
|
@@ -184,7 +184,6 @@ def generate_final_text(src: str, custom_prompt: str, api_key: str):
|
|
| 184 |
if isinstance(content, str):
|
| 185 |
parts.append(content)
|
| 186 |
else:
|
| 187 |
-
# handle list/dict shaped content
|
| 188 |
if isinstance(content, list):
|
| 189 |
for c in content:
|
| 190 |
if isinstance(c, dict) and c.get("type") == "text":
|
|
@@ -200,7 +199,7 @@ def generate_final_text(src: str, custom_prompt: str, api_key: str):
|
|
| 200 |
except Exception as e:
|
| 201 |
parts.append(f"[Model error: {e}]")
|
| 202 |
|
| 203 |
-
# Image
|
| 204 |
if is_image:
|
| 205 |
try:
|
| 206 |
raw = fetch_bytes(src)
|
|
@@ -212,13 +211,13 @@ def generate_final_text(src: str, custom_prompt: str, api_key: str):
|
|
| 212 |
stream_and_collect(choose_model_for_src(src), msgs)
|
| 213 |
return "".join(parts).strip()
|
| 214 |
|
| 215 |
-
# Remote video
|
| 216 |
if is_remote(src):
|
| 217 |
msgs = build_messages_for_text(prompt, f"Video URL: {src}")
|
| 218 |
stream_and_collect(choose_model_for_src(src), msgs)
|
| 219 |
return "".join(parts).strip()
|
| 220 |
|
| 221 |
-
# Local video
|
| 222 |
tmp_media = None
|
| 223 |
try:
|
| 224 |
media_bytes = fetch_bytes(src)
|
|
@@ -249,7 +248,6 @@ def generate_final_text(src: str, custom_prompt: str, api_key: str):
|
|
| 249 |
except Exception:
|
| 250 |
pass
|
| 251 |
else:
|
| 252 |
-
# ffmpeg failed or produced no frame
|
| 253 |
try:
|
| 254 |
if tmp_frame and os.path.exists(tmp_frame):
|
| 255 |
os.remove(tmp_frame)
|
|
@@ -270,7 +268,7 @@ def generate_final_text(src: str, custom_prompt: str, api_key: str):
|
|
| 270 |
except Exception:
|
| 271 |
pass
|
| 272 |
|
| 273 |
-
#
|
| 274 |
css = """
|
| 275 |
.preview_column { min-width: 380px; }
|
| 276 |
.preview_media img, .preview_media video { max-width: 100%; height: auto; }
|
|
|
|
| 31 |
return bool(src) and src.startswith(("http://", "https://"))
|
| 32 |
|
| 33 |
def fetch_bytes(src: str, stream_threshold=20 * 1024 * 1024) -> bytes:
|
|
|
|
| 34 |
if is_remote(src):
|
| 35 |
with requests.get(src, timeout=60, stream=True) as r:
|
| 36 |
r.raise_for_status()
|
| 37 |
content_length = r.headers.get("content-length")
|
| 38 |
if content_length and int(content_length) > stream_threshold:
|
|
|
|
| 39 |
fd, path = tempfile.mkstemp()
|
| 40 |
os.close(fd)
|
| 41 |
with open(path, "wb") as f:
|
|
|
|
| 57 |
def convert_to_jpeg_bytes(media_bytes: bytes, base_h=480) -> bytes:
|
| 58 |
img = Image.open(BytesIO(media_bytes))
|
| 59 |
try:
|
|
|
|
| 60 |
img.seek(0)
|
| 61 |
except Exception:
|
| 62 |
pass
|
|
|
|
| 92 |
return DEFAULT_VIDEO_MODEL
|
| 93 |
if ext in IMAGE_EXTS:
|
| 94 |
return DEFAULT_IMAGE_MODEL
|
|
|
|
| 95 |
if is_remote(src):
|
| 96 |
return DEFAULT_VIDEO_MODEL
|
| 97 |
return DEFAULT_IMAGE_MODEL
|
|
|
|
| 117 |
data = getattr(chunk, "data", None) or getattr(chunk, "response", None) or getattr(chunk, "delta", None)
|
| 118 |
if not data:
|
| 119 |
return None
|
| 120 |
+
# Try common streaming shapes and coerce to trimmed string
|
| 121 |
try:
|
|
|
|
| 122 |
c = data.choices[0].delta
|
| 123 |
if isinstance(c, dict):
|
| 124 |
txt = c.get("content") or c.get("text")
|
| 125 |
+
if txt is None:
|
| 126 |
+
return None
|
| 127 |
+
return str(txt)
|
| 128 |
except Exception:
|
| 129 |
pass
|
| 130 |
try:
|
|
|
|
| 133 |
txt = msg.get("content")
|
| 134 |
else:
|
| 135 |
txt = getattr(msg, "content", None)
|
| 136 |
+
if txt is None:
|
| 137 |
+
return None
|
| 138 |
+
return str(txt)
|
| 139 |
except Exception:
|
| 140 |
pass
|
| 141 |
try:
|
|
|
|
| 147 |
def generate_final_text(src: str, custom_prompt: str, api_key: str):
|
| 148 |
client = get_client(api_key)
|
| 149 |
prompt = (custom_prompt.strip() if custom_prompt and custom_prompt.strip() else "Please provide a detailed visual review.")
|
|
|
|
| 150 |
ext = ext_from_src(src)
|
| 151 |
is_image = ext in IMAGE_EXTS or (not is_remote(src) and os.path.isfile(src) and ext in IMAGE_EXTS)
|
| 152 |
parts = []
|
| 153 |
|
| 154 |
def stream_and_collect(model, messages):
|
| 155 |
try:
|
|
|
|
| 156 |
stream_gen = None
|
| 157 |
try:
|
| 158 |
stream_gen = client.chat.stream(model=model, messages=messages)
|
|
|
|
| 161 |
if stream_gen:
|
| 162 |
for chunk in stream_gen:
|
| 163 |
d = extract_delta(chunk)
|
| 164 |
+
if d is None:
|
| 165 |
+
continue
|
| 166 |
+
# ignore whitespace-only pieces unless parts is empty and meaningful
|
| 167 |
+
if d.strip() == "" and parts:
|
| 168 |
+
continue
|
| 169 |
+
parts.append(d)
|
| 170 |
return
|
|
|
|
| 171 |
res = client.chat.complete(model=model, messages=messages, stream=False)
|
|
|
|
| 172 |
try:
|
| 173 |
choices = getattr(res, "choices", None) or res.get("choices", [])
|
| 174 |
except Exception:
|
|
|
|
| 184 |
if isinstance(content, str):
|
| 185 |
parts.append(content)
|
| 186 |
else:
|
|
|
|
| 187 |
if isinstance(content, list):
|
| 188 |
for c in content:
|
| 189 |
if isinstance(c, dict) and c.get("type") == "text":
|
|
|
|
| 199 |
except Exception as e:
|
| 200 |
parts.append(f"[Model error: {e}]")
|
| 201 |
|
| 202 |
+
# Image
|
| 203 |
if is_image:
|
| 204 |
try:
|
| 205 |
raw = fetch_bytes(src)
|
|
|
|
| 211 |
stream_and_collect(choose_model_for_src(src), msgs)
|
| 212 |
return "".join(parts).strip()
|
| 213 |
|
| 214 |
+
# Remote video
|
| 215 |
if is_remote(src):
|
| 216 |
msgs = build_messages_for_text(prompt, f"Video URL: {src}")
|
| 217 |
stream_and_collect(choose_model_for_src(src), msgs)
|
| 218 |
return "".join(parts).strip()
|
| 219 |
|
| 220 |
+
# Local video: extract one frame with ffmpeg
|
| 221 |
tmp_media = None
|
| 222 |
try:
|
| 223 |
media_bytes = fetch_bytes(src)
|
|
|
|
| 248 |
except Exception:
|
| 249 |
pass
|
| 250 |
else:
|
|
|
|
| 251 |
try:
|
| 252 |
if tmp_frame and os.path.exists(tmp_frame):
|
| 253 |
os.remove(tmp_frame)
|
|
|
|
| 268 |
except Exception:
|
| 269 |
pass
|
| 270 |
|
| 271 |
+
# UI
|
| 272 |
css = """
|
| 273 |
.preview_column { min-width: 380px; }
|
| 274 |
.preview_media img, .preview_media video { max-width: 100%; height: auto; }
|