Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
import os, time, base64, mimetypes, requests, traceback
|
| 2 |
from dataclasses import dataclass
|
| 3 |
-
from typing import Optional,
|
| 4 |
|
| 5 |
import gradio as gr
|
| 6 |
from dotenv import load_dotenv
|
|
@@ -87,6 +87,7 @@ def _download_stream(url: str, out_path: str) -> str:
|
|
| 87 |
# Normalize OpenAI exceptions across versions
|
| 88 |
_OAI_EXC = tuple(e for e in [RateLimitError, APIConnectionError, APIStatusError] if isinstance(e, type)) or (Exception,)
|
| 89 |
|
|
|
|
| 90 |
@retry(
|
| 91 |
retry=retry_if_exception_type(_OAI_EXC),
|
| 92 |
wait=wait_exponential(multiplier=1, min=1, max=8),
|
|
@@ -95,15 +96,25 @@ _OAI_EXC = tuple(e for e in [RateLimitError, APIConnectionError, APIStatusError]
|
|
| 95 |
)
|
| 96 |
def _videos_generate(client: OpenAI, **kwargs) -> Any:
|
| 97 |
"""
|
| 98 |
-
|
| 99 |
"""
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
try:
|
| 104 |
return client.videos.jobs.create(**kwargs)
|
| 105 |
except Exception as e_b:
|
| 106 |
-
raise RuntimeError(f"videos.generate failed: {
|
|
|
|
| 107 |
|
| 108 |
@retry(
|
| 109 |
retry=retry_if_exception_type(_OAI_EXC),
|
|
@@ -112,10 +123,14 @@ def _videos_generate(client: OpenAI, **kwargs) -> Any:
|
|
| 112 |
reraise=True,
|
| 113 |
)
|
| 114 |
def _videos_retrieve(client: OpenAI, job_id: str) -> Any:
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
| 118 |
return client.videos.jobs.retrieve(job_id)
|
|
|
|
| 119 |
|
| 120 |
def _extract_status(resp: Any) -> JobStatus:
|
| 121 |
status = getattr(resp, "status", None) or getattr(resp, "state", None) or "unknown"
|
|
@@ -137,7 +152,13 @@ def _extract_status(resp: Any) -> JobStatus:
|
|
| 137 |
|
| 138 |
return JobStatus(status=status, error=err, output_url=out_url, output_b64=out_b64)
|
| 139 |
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
def generate_video_stream(
|
| 142 |
api_key: str,
|
| 143 |
prompt: str,
|
|
@@ -148,31 +169,46 @@ def generate_video_stream(
|
|
| 148 |
audio: str,
|
| 149 |
guidance: float,
|
| 150 |
init_image: Optional[str],
|
| 151 |
-
) -> Generator[
|
| 152 |
"""
|
| 153 |
-
Generator that yields
|
| 154 |
-
|
| 155 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
# 0) Setup
|
| 157 |
try:
|
| 158 |
client = _make_client(api_key)
|
| 159 |
except Exception as e_init:
|
| 160 |
-
yield gr.update(), f"Setup error: {e_init}"
|
| 161 |
return
|
| 162 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
# 1) Validate inputs
|
| 164 |
try:
|
| 165 |
prompt = _sanitize_prompt(prompt)
|
| 166 |
-
model
|
| 167 |
duration = _validate_duration(duration)
|
| 168 |
-
size
|
| 169 |
guidance = _validate_guidance(guidance)
|
| 170 |
-
audio
|
| 171 |
|
| 172 |
if init_image:
|
| 173 |
mt, _ = mimetypes.guess_type(init_image)
|
| 174 |
if not (mt and mt.startswith("image/")):
|
| 175 |
-
yield gr.update(), "Provided conditioning file isn’t an image."
|
| 176 |
return
|
| 177 |
|
| 178 |
req: Dict[str, Any] = {
|
|
@@ -188,22 +224,23 @@ def generate_video_stream(
|
|
| 188 |
if init_image:
|
| 189 |
req["image"] = {"b64": _file_to_b64(init_image)}
|
| 190 |
except Exception as e_val:
|
| 191 |
-
yield gr.update(), f"Validation error: {e_val}"
|
| 192 |
return
|
| 193 |
|
| 194 |
# 2) Submit job
|
| 195 |
try:
|
| 196 |
-
yield gr.update(), "Submitting job…"
|
| 197 |
job = _videos_generate(client, **req)
|
| 198 |
job_id = getattr(job, "id", None) or getattr(job, "job_id", None)
|
| 199 |
if not job_id:
|
| 200 |
-
yield gr.update(), "Could not get a job id
|
| 201 |
return
|
|
|
|
| 202 |
except _OAI_EXC as oe:
|
| 203 |
-
yield gr.update(), f"OpenAI API issue on submit: {oe}"
|
| 204 |
return
|
| 205 |
except Exception as e_submit:
|
| 206 |
-
yield gr.update(), f"Submit error: {e_submit}\n{traceback.format_exc(limit=2)}"
|
| 207 |
return
|
| 208 |
|
| 209 |
# 3) Poll job
|
|
@@ -214,16 +251,16 @@ def generate_video_stream(
|
|
| 214 |
status_obj = _videos_retrieve(client, job_id)
|
| 215 |
js = _extract_status(status_obj)
|
| 216 |
except _OAI_EXC as oe:
|
| 217 |
-
yield gr.update(), f"OpenAI API issue on poll: {oe}"
|
| 218 |
return
|
| 219 |
except Exception as e_poll:
|
| 220 |
-
yield gr.update(), f"Polling error: {e_poll}\n{traceback.format_exc(limit=2)}"
|
| 221 |
return
|
| 222 |
|
| 223 |
now = time.time()
|
| 224 |
if now - last_emit > 5:
|
| 225 |
last_emit = now
|
| 226 |
-
yield gr.update(), f"Rendering… status={js.status}"
|
| 227 |
|
| 228 |
if js.status in ("succeeded", "completed", "complete"):
|
| 229 |
out_path = _safe_video_path()
|
|
@@ -231,37 +268,36 @@ def generate_video_stream(
|
|
| 231 |
try:
|
| 232 |
with open(out_path, "wb") as f:
|
| 233 |
f.write(base64.b64decode(js.output_b64))
|
| 234 |
-
yield out_path, f"Done with {model} ({size}, {duration}s)."
|
| 235 |
except Exception as werr:
|
| 236 |
-
yield gr.update(), f"Write error: {werr}"
|
| 237 |
return
|
| 238 |
if js.output_url:
|
| 239 |
try:
|
| 240 |
_download_stream(js.output_url, out_path)
|
| 241 |
-
yield out_path, f"Downloaded from URL. Done with {model}."
|
| 242 |
except Exception as dl_err:
|
| 243 |
-
|
| 244 |
-
yield js.output_url, f"Ready (URL) — local download failed: {dl_err}"
|
| 245 |
return
|
| 246 |
-
yield gr.update(), "Job succeeded but no video payload was returned."
|
| 247 |
return
|
| 248 |
|
| 249 |
if js.status in ("failed", "error", "canceled", "cancelled"):
|
| 250 |
detail = f"Status: {js.status}."
|
| 251 |
if js.error:
|
| 252 |
detail += f" Error: {js.error}"
|
| 253 |
-
yield gr.update(), detail
|
| 254 |
return
|
| 255 |
|
| 256 |
if now - start > 1800: # 30 min timeout
|
| 257 |
-
yield gr.update(), "Timed out waiting for the video. Try shorter duration."
|
| 258 |
return
|
| 259 |
|
| 260 |
time.sleep(2)
|
| 261 |
|
| 262 |
# -------- UI --------
|
| 263 |
def build_ui():
|
| 264 |
-
with gr.Blocks(title="ZEN — Sora / Sora-2 / Sora-2-Pro"
|
| 265 |
gr.Markdown("## ZEN — Sora / Sora-2 / Sora-2-Pro (OpenAI Videos API)")
|
| 266 |
gr.Markdown(
|
| 267 |
"Paste an OpenAI API key (not stored). Provide a detailed prompt. "
|
|
@@ -290,7 +326,7 @@ def build_ui():
|
|
| 290 |
video = gr.Video(label="Result", autoplay=True)
|
| 291 |
status = gr.Textbox(label="Status / Logs", interactive=False)
|
| 292 |
|
| 293 |
-
#
|
| 294 |
go.click(
|
| 295 |
fn=generate_video_stream,
|
| 296 |
inputs=[api_key, prompt, model, duration, size, seed, audio, guidance, init_image],
|
|
|
|
| 1 |
+
import os, time, base64, mimetypes, requests, traceback, json
|
| 2 |
from dataclasses import dataclass
|
| 3 |
+
from typing import Optional, Dict, Any, Generator, List
|
| 4 |
|
| 5 |
import gradio as gr
|
| 6 |
from dotenv import load_dotenv
|
|
|
|
| 87 |
# Normalize OpenAI exceptions across versions
|
| 88 |
_OAI_EXC = tuple(e for e in [RateLimitError, APIConnectionError, APIStatusError] if isinstance(e, type)) or (Exception,)
|
| 89 |
|
| 90 |
+
# -------- Videos API wrappers (dual path) --------
|
| 91 |
@retry(
|
| 92 |
retry=retry_if_exception_type(_OAI_EXC),
|
| 93 |
wait=wait_exponential(multiplier=1, min=1, max=8),
|
|
|
|
| 96 |
)
|
| 97 |
def _videos_generate(client: OpenAI, **kwargs) -> Any:
|
| 98 |
"""
|
| 99 |
+
Try the new Videos API; fallback to Jobs API. Surface both errors if they fail.
|
| 100 |
"""
|
| 101 |
+
# Path A
|
| 102 |
+
if hasattr(client, "videos") and hasattr(client.videos, "generate"):
|
| 103 |
+
try:
|
| 104 |
+
return client.videos.generate(**kwargs)
|
| 105 |
+
except Exception as e_a:
|
| 106 |
+
# fall through to jobs
|
| 107 |
+
last_a = e_a
|
| 108 |
+
else:
|
| 109 |
+
last_a = "client.videos.generate not found"
|
| 110 |
+
|
| 111 |
+
# Path B
|
| 112 |
+
if hasattr(client, "videos") and hasattr(client.videos, "jobs") and hasattr(client.videos.jobs, "create"):
|
| 113 |
try:
|
| 114 |
return client.videos.jobs.create(**kwargs)
|
| 115 |
except Exception as e_b:
|
| 116 |
+
raise RuntimeError(f"videos.generate failed/absent: {last_a}\njobs.create failed: {e_b}")
|
| 117 |
+
raise RuntimeError(f"Your OpenAI SDK doesn't expose videos endpoints on this key/org. Seen attributes: {dir_safe(client)}")
|
| 118 |
|
| 119 |
@retry(
|
| 120 |
retry=retry_if_exception_type(_OAI_EXC),
|
|
|
|
| 123 |
reraise=True,
|
| 124 |
)
|
| 125 |
def _videos_retrieve(client: OpenAI, job_id: str) -> Any:
|
| 126 |
+
if hasattr(client, "videos") and hasattr(client.videos, "retrieve"):
|
| 127 |
+
try:
|
| 128 |
+
return client.videos.retrieve(job_id)
|
| 129 |
+
except Exception:
|
| 130 |
+
pass # try jobs next
|
| 131 |
+
if hasattr(client, "videos") and hasattr(client.videos, "jobs") and hasattr(client.videos.jobs, "retrieve"):
|
| 132 |
return client.videos.jobs.retrieve(job_id)
|
| 133 |
+
raise RuntimeError("No videos.retrieve or videos.jobs.retrieve on this SDK. Upgrade openai python.")
|
| 134 |
|
| 135 |
def _extract_status(resp: Any) -> JobStatus:
|
| 136 |
status = getattr(resp, "status", None) or getattr(resp, "state", None) or "unknown"
|
|
|
|
| 152 |
|
| 153 |
return JobStatus(status=status, error=err, output_url=out_url, output_b64=out_b64)
|
| 154 |
|
| 155 |
+
def dir_safe(obj) -> Dict[str, Any]:
|
| 156 |
+
try:
|
| 157 |
+
return sorted([a for a in dir(obj) if not a.startswith("_")])
|
| 158 |
+
except Exception:
|
| 159 |
+
return {"inspect_error": "dir() failed"}
|
| 160 |
+
|
| 161 |
+
# -------- Core (STREAMING; yields LISTS matching outputs) --------
|
| 162 |
def generate_video_stream(
|
| 163 |
api_key: str,
|
| 164 |
prompt: str,
|
|
|
|
| 169 |
audio: str,
|
| 170 |
guidance: float,
|
| 171 |
init_image: Optional[str],
|
| 172 |
+
) -> Generator[List[Any], None, None]:
|
| 173 |
"""
|
| 174 |
+
Generator that ALWAYS yields a list [video_value_or_update, status_text].
|
| 175 |
+
Many Gradio builds ignore tuple yields; lists are safest.
|
| 176 |
"""
|
| 177 |
+
|
| 178 |
+
# First visible tick so UI never blanks:
|
| 179 |
+
yield [gr.update(), "Starting…"]
|
| 180 |
+
|
| 181 |
# 0) Setup
|
| 182 |
try:
|
| 183 |
client = _make_client(api_key)
|
| 184 |
except Exception as e_init:
|
| 185 |
+
yield [gr.update(), f"Setup error: {e_init}"]
|
| 186 |
return
|
| 187 |
|
| 188 |
+
# 0.1) Preflight SDK visibility (prints once so we know what exists)
|
| 189 |
+
try:
|
| 190 |
+
vids = hasattr(client, "videos")
|
| 191 |
+
methods = []
|
| 192 |
+
if vids:
|
| 193 |
+
methods = [m for m in dir(client.videos) if not m.startswith("_")]
|
| 194 |
+
msg = f"SDK preflight → videos: {vids}, methods: {methods}"
|
| 195 |
+
yield [gr.update(), msg]
|
| 196 |
+
except Exception as e_pref:
|
| 197 |
+
yield [gr.update(), f"SDK preflight error: {e_pref}"]
|
| 198 |
+
|
| 199 |
# 1) Validate inputs
|
| 200 |
try:
|
| 201 |
prompt = _sanitize_prompt(prompt)
|
| 202 |
+
model = _validate_model(model)
|
| 203 |
duration = _validate_duration(duration)
|
| 204 |
+
size = _validate_size(size)
|
| 205 |
guidance = _validate_guidance(guidance)
|
| 206 |
+
audio = "on" if audio == "on" else "off"
|
| 207 |
|
| 208 |
if init_image:
|
| 209 |
mt, _ = mimetypes.guess_type(init_image)
|
| 210 |
if not (mt and mt.startswith("image/")):
|
| 211 |
+
yield [gr.update(), "Provided conditioning file isn’t an image."]
|
| 212 |
return
|
| 213 |
|
| 214 |
req: Dict[str, Any] = {
|
|
|
|
| 224 |
if init_image:
|
| 225 |
req["image"] = {"b64": _file_to_b64(init_image)}
|
| 226 |
except Exception as e_val:
|
| 227 |
+
yield [gr.update(), f"Validation error: {e_val}"]
|
| 228 |
return
|
| 229 |
|
| 230 |
# 2) Submit job
|
| 231 |
try:
|
| 232 |
+
yield [gr.update(), "Submitting job…"]
|
| 233 |
job = _videos_generate(client, **req)
|
| 234 |
job_id = getattr(job, "id", None) or getattr(job, "job_id", None)
|
| 235 |
if not job_id:
|
| 236 |
+
yield [gr.update(), f"Could not get a job id. Raw job object: {repr(job)}"]
|
| 237 |
return
|
| 238 |
+
yield [gr.update(), f"Job accepted → id={job_id}"]
|
| 239 |
except _OAI_EXC as oe:
|
| 240 |
+
yield [gr.update(), f"OpenAI API issue on submit: {oe}"]
|
| 241 |
return
|
| 242 |
except Exception as e_submit:
|
| 243 |
+
yield [gr.update(), f"Submit error: {e_submit}\n{traceback.format_exc(limit=2)}"]
|
| 244 |
return
|
| 245 |
|
| 246 |
# 3) Poll job
|
|
|
|
| 251 |
status_obj = _videos_retrieve(client, job_id)
|
| 252 |
js = _extract_status(status_obj)
|
| 253 |
except _OAI_EXC as oe:
|
| 254 |
+
yield [gr.update(), f"OpenAI API issue on poll: {oe}"]
|
| 255 |
return
|
| 256 |
except Exception as e_poll:
|
| 257 |
+
yield [gr.update(), f"Polling error: {e_poll}\n{traceback.format_exc(limit=2)}"]
|
| 258 |
return
|
| 259 |
|
| 260 |
now = time.time()
|
| 261 |
if now - last_emit > 5:
|
| 262 |
last_emit = now
|
| 263 |
+
yield [gr.update(), f"Rendering… status={js.status}"]
|
| 264 |
|
| 265 |
if js.status in ("succeeded", "completed", "complete"):
|
| 266 |
out_path = _safe_video_path()
|
|
|
|
| 268 |
try:
|
| 269 |
with open(out_path, "wb") as f:
|
| 270 |
f.write(base64.b64decode(js.output_b64))
|
| 271 |
+
yield [out_path, f"Done with {model} ({size}, {duration}s)."]
|
| 272 |
except Exception as werr:
|
| 273 |
+
yield [gr.update(), f"Write error: {werr}"]
|
| 274 |
return
|
| 275 |
if js.output_url:
|
| 276 |
try:
|
| 277 |
_download_stream(js.output_url, out_path)
|
| 278 |
+
yield [out_path, f"Downloaded from URL. Done with {model}."]
|
| 279 |
except Exception as dl_err:
|
| 280 |
+
yield [js.output_url, f"Ready (URL) — local download failed: {dl_err}"]
|
|
|
|
| 281 |
return
|
| 282 |
+
yield [gr.update(), "Job succeeded but no video payload was returned."]
|
| 283 |
return
|
| 284 |
|
| 285 |
if js.status in ("failed", "error", "canceled", "cancelled"):
|
| 286 |
detail = f"Status: {js.status}."
|
| 287 |
if js.error:
|
| 288 |
detail += f" Error: {js.error}"
|
| 289 |
+
yield [gr.update(), detail]
|
| 290 |
return
|
| 291 |
|
| 292 |
if now - start > 1800: # 30 min timeout
|
| 293 |
+
yield [gr.update(), "Timed out waiting for the video. Try shorter duration."]
|
| 294 |
return
|
| 295 |
|
| 296 |
time.sleep(2)
|
| 297 |
|
| 298 |
# -------- UI --------
|
| 299 |
def build_ui():
|
| 300 |
+
with gr.Blocks(title="ZEN — Sora / Sora-2 / Sora-2-Pro") as demo:
|
| 301 |
gr.Markdown("## ZEN — Sora / Sora-2 / Sora-2-Pro (OpenAI Videos API)")
|
| 302 |
gr.Markdown(
|
| 303 |
"Paste an OpenAI API key (not stored). Provide a detailed prompt. "
|
|
|
|
| 326 |
video = gr.Video(label="Result", autoplay=True)
|
| 327 |
status = gr.Textbox(label="Status / Logs", interactive=False)
|
| 328 |
|
| 329 |
+
# IMPORTANT: outputs must be exactly two and we yield LISTS [video, status]
|
| 330 |
go.click(
|
| 331 |
fn=generate_video_stream,
|
| 332 |
inputs=[api_key, prompt, model, duration, size, seed, audio, guidance, init_image],
|