ZENLLC commited on
Commit
cc681b6
·
verified ·
1 Parent(s): c2a50cd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +73 -37
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, Tuple, Dict, Any, Generator
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
- Dual-path: try Videos API; fallback to Jobs API.
99
  """
100
- try:
101
- return client.videos.generate(**kwargs)
102
- except Exception as e_a:
 
 
 
 
 
 
 
 
 
103
  try:
104
  return client.videos.jobs.create(**kwargs)
105
  except Exception as e_b:
106
- raise RuntimeError(f"videos.generate failed: {e_a}\njobs.create failed: {e_b}")
 
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
- try:
116
- return client.videos.retrieve(job_id)
117
- except Exception:
 
 
 
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
- # -------- Core (STREAMING with gr.update) --------
 
 
 
 
 
 
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[Tuple[Any, str], None, None]:
152
  """
153
- Generator that yields (video_component_value, status_text).
154
- Uses gr.update() to keep Gradio happy even when no video is ready yet.
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 = _validate_model(model)
167
  duration = _validate_duration(duration)
168
- size = _validate_size(size)
169
  guidance = _validate_guidance(guidance)
170
- audio = "on" if audio == "on" else "off"
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 from the API response."
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
- # Gradio Video can play URLs if local download fails
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", theme=gr.themes.Soft()) as demo:
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
- # Generator always yields something visible
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],