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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +26 -31
app.py CHANGED
@@ -6,11 +6,11 @@ import gradio as gr
6
  from dotenv import load_dotenv
7
  from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
8
 
9
- # OpenAI SDK imports guarded for environments missing pieces
10
  try:
11
  from openai import OpenAI
12
  from openai import RateLimitError, APIConnectionError, APIStatusError
13
- except Exception: # pragma: no cover
14
  OpenAI = None
15
  RateLimitError = APIConnectionError = APIStatusError = Exception # fallback
16
 
@@ -27,7 +27,7 @@ class JobStatus:
27
  output_url: Optional[str] = None
28
  output_b64: Optional[str] = None
29
 
30
- # ---------- utilities ----------
31
  def _safe_video_path() -> str:
32
  return "/tmp/sora_output.mp4"
33
 
@@ -67,7 +67,7 @@ def _make_client(user_key: Optional[str]) -> OpenAI:
67
  raise ValueError("Missing API key. Paste a valid OpenAI API key.")
68
  return OpenAI(api_key=key)
69
 
70
- # ---------- networking ----------
71
  from requests import RequestException as ReqErr
72
  @retry(
73
  retry=retry_if_exception_type((ReqErr,)),
@@ -98,10 +98,8 @@ def _videos_generate(client: OpenAI, **kwargs) -> Any:
98
  Dual-path: try Videos API; fallback to Jobs API.
99
  """
100
  try:
101
- # Newer path
102
  return client.videos.generate(**kwargs)
103
  except Exception as e_a:
104
- # Fallback path
105
  try:
106
  return client.videos.jobs.create(**kwargs)
107
  except Exception as e_b:
@@ -139,7 +137,7 @@ def _extract_status(resp: Any) -> JobStatus:
139
 
140
  return JobStatus(status=status, error=err, output_url=out_url, output_b64=out_b64)
141
 
142
- # ---------- core (STREAMING) ----------
143
  def generate_video_stream(
144
  api_key: str,
145
  prompt: str,
@@ -150,16 +148,16 @@ def generate_video_stream(
150
  audio: str,
151
  guidance: float,
152
  init_image: Optional[str],
153
- ) -> Generator[Tuple[Optional[str], str], None, None]:
154
  """
155
- Generator that yields (video_or_url, status_text) messages as it works.
156
- This prevents UI "blink" and guarantees visible feedback even on errors.
157
  """
158
  # 0) Setup
159
  try:
160
  client = _make_client(api_key)
161
  except Exception as e_init:
162
- yield None, f"Setup error: {e_init}"
163
  return
164
 
165
  # 1) Validate inputs
@@ -174,7 +172,7 @@ def generate_video_stream(
174
  if init_image:
175
  mt, _ = mimetypes.guess_type(init_image)
176
  if not (mt and mt.startswith("image/")):
177
- yield None, "Provided conditioning file isn’t an image."
178
  return
179
 
180
  req: Dict[str, Any] = {
@@ -190,22 +188,22 @@ def generate_video_stream(
190
  if init_image:
191
  req["image"] = {"b64": _file_to_b64(init_image)}
192
  except Exception as e_val:
193
- yield None, f"Validation error: {e_val}"
194
  return
195
 
196
  # 2) Submit job
197
  try:
198
- yield None, "Submitting job…"
199
  job = _videos_generate(client, **req)
200
  job_id = getattr(job, "id", None) or getattr(job, "job_id", None)
201
  if not job_id:
202
- yield None, "Could not get a job id from the API response."
203
  return
204
  except _OAI_EXC as oe:
205
- yield None, f"OpenAI API issue on submit: {oe}"
206
  return
207
  except Exception as e_submit:
208
- yield None, f"Submit error: {e_submit}\n{traceback.format_exc(limit=2)}"
209
  return
210
 
211
  # 3) Poll job
@@ -216,17 +214,16 @@ def generate_video_stream(
216
  status_obj = _videos_retrieve(client, job_id)
217
  js = _extract_status(status_obj)
218
  except _OAI_EXC as oe:
219
- yield None, f"OpenAI API issue on poll: {oe}"
220
  return
221
  except Exception as e_poll:
222
- yield None, f"Polling error: {e_poll}\n{traceback.format_exc(limit=2)}"
223
  return
224
 
225
- # Emit a heartbeat log every ~5s so UI stays alive
226
  now = time.time()
227
  if now - last_emit > 5:
228
  last_emit = now
229
- yield None, f"Rendering… status={js.status}"
230
 
231
  if js.status in ("succeeded", "completed", "complete"):
232
  out_path = _safe_video_path()
@@ -236,33 +233,33 @@ def generate_video_stream(
236
  f.write(base64.b64decode(js.output_b64))
237
  yield out_path, f"Done with {model} ({size}, {duration}s)."
238
  except Exception as werr:
239
- yield None, f"Write error: {werr}"
240
  return
241
  if js.output_url:
242
- # Try download; if it fails, return URL anyway (Gradio Video can play URLs)
243
  try:
244
  _download_stream(js.output_url, out_path)
245
  yield out_path, f"Downloaded from URL. Done with {model}."
246
  except Exception as dl_err:
 
247
  yield js.output_url, f"Ready (URL) — local download failed: {dl_err}"
248
  return
249
- yield None, "Job succeeded but no video payload was returned."
250
  return
251
 
252
  if js.status in ("failed", "error", "canceled", "cancelled"):
253
  detail = f"Status: {js.status}."
254
  if js.error:
255
  detail += f" Error: {js.error}"
256
- yield None, detail
257
  return
258
 
259
  if now - start > 1800: # 30 min timeout
260
- yield None, "Timed out waiting for the video. Try shorter duration."
261
  return
262
 
263
  time.sleep(2)
264
 
265
- # ---------- UI ----------
266
  def build_ui():
267
  with gr.Blocks(title="ZEN — Sora / Sora-2 / Sora-2-Pro", theme=gr.themes.Soft()) as demo:
268
  gr.Markdown("## ZEN — Sora / Sora-2 / Sora-2-Pro (OpenAI Videos API)")
@@ -293,17 +290,15 @@ def build_ui():
293
  video = gr.Video(label="Result", autoplay=True)
294
  status = gr.Textbox(label="Status / Logs", interactive=False)
295
 
296
- # Use generator function so UI always gets visible updates
297
  go.click(
298
  fn=generate_video_stream,
299
  inputs=[api_key, prompt, model, duration, size, seed, audio, guidance, init_image],
300
- outputs=[video, status],
301
- queue=True
302
  )
303
  return demo
304
 
305
  demo = build_ui()
306
- demo.queue() # Compatible across 4.x/5.x
307
 
308
  if __name__ == "__main__":
309
  demo.launch()
 
6
  from dotenv import load_dotenv
7
  from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
8
 
9
+ # -------- OpenAI SDK (guarded import) --------
10
  try:
11
  from openai import OpenAI
12
  from openai import RateLimitError, APIConnectionError, APIStatusError
13
+ except Exception:
14
  OpenAI = None
15
  RateLimitError = APIConnectionError = APIStatusError = Exception # fallback
16
 
 
27
  output_url: Optional[str] = None
28
  output_b64: Optional[str] = None
29
 
30
+ # -------- Utilities --------
31
  def _safe_video_path() -> str:
32
  return "/tmp/sora_output.mp4"
33
 
 
67
  raise ValueError("Missing API key. Paste a valid OpenAI API key.")
68
  return OpenAI(api_key=key)
69
 
70
+ # -------- Networking --------
71
  from requests import RequestException as ReqErr
72
  @retry(
73
  retry=retry_if_exception_type((ReqErr,)),
 
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:
 
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
  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
 
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
  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
  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()
 
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)")
 
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],
297
+ outputs=[video, status]
 
298
  )
299
  return demo
300
 
301
  demo = build_ui()
 
302
 
303
  if __name__ == "__main__":
304
  demo.launch()