akhaliq HF Staff commited on
Commit
e34f60e
·
verified ·
1 Parent(s): 24c348f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -119
app.py CHANGED
@@ -1,140 +1,193 @@
1
- import gradio as gr
2
  import os
 
3
  import tempfile
4
  import shutil
 
5
  from typing import Optional, Tuple, Union
 
 
6
  from huggingface_hub import InferenceClient, whoami
7
- from pathlib import Path
8
 
9
- # Initialize Hugging Face Inference Client with fal-ai provider
 
 
10
  client = InferenceClient(
11
  provider="fal-ai",
12
  api_key=os.environ.get("HF_TOKEN"),
13
  bill_to="huggingface",
14
  )
15
 
 
 
 
16
  def verify_pro_status(token: Optional[Union[gr.OAuthToken, str]]) -> bool:
17
  """Verifies if the user is a Hugging Face PRO user or part of an enterprise org."""
18
  if not token:
19
  return False
20
-
21
  if isinstance(token, gr.OAuthToken):
22
  token_str = token.token
23
  elif isinstance(token, str):
24
  token_str = token
25
  else:
26
  return False
27
-
28
  try:
29
  user_info = whoami(token=token_str)
30
  return (
31
- user_info.get("isPro", False) or
32
- any(org.get("isEnterprise", False) for org in user_info.get("orgs", []))
33
  )
34
  except Exception as e:
35
  print(f"Could not verify user's PRO/Enterprise status: {e}")
36
  return False
37
 
 
 
 
38
  def cleanup_temp_files():
39
- """Clean up old temporary video files to prevent storage overflow."""
40
  try:
41
  temp_dir = tempfile.gettempdir()
42
- # Clean up old .mp4 files in temp directory
43
  for file_path in Path(temp_dir).glob("*.mp4"):
44
  try:
45
  # Remove files older than 5 minutes
46
- if file_path.stat().st_mtime < (os.time.time() - 300):
47
  file_path.unlink(missing_ok=True)
48
  except Exception:
49
  pass # Ignore errors for individual files
50
  except Exception as e:
51
  print(f"Cleanup error: {e}")
52
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  def generate_video(
54
  prompt: str,
55
  duration: int = 8,
56
  size: str = "1280x720",
57
- api_key: Optional[str] = None
58
  ) -> Tuple[Optional[str], str]:
59
  """
60
- Generate video using Sora-2 through Hugging Face Inference API with fal-ai provider.
61
- Returns tuple of (video_path, status_message).
62
  """
63
- # Clean up old files before generating new ones
64
  cleanup_temp_files()
65
-
66
  try:
67
  # Use provided API key or environment variable
68
- if api_key:
69
- temp_client = InferenceClient(
70
- provider="fal-ai",
71
- api_key=api_key,
72
- bill_to="huggingface",
73
- )
74
- else:
75
- temp_client = client
76
- if not os.environ.get("HF_TOKEN") and not api_key:
77
- return None, "❌ Please set HF_TOKEN environment variable."
78
-
79
- # Call Sora-2 through Hugging Face Inference API
80
  video_bytes = temp_client.text_to_video(
81
  prompt,
82
  model="akhaliq/sora-2",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  )
84
-
85
- # Save to temporary file with proper cleanup
86
- # Use NamedTemporaryFile with delete=True but keep reference
87
- temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
88
- try:
89
- temp_file.write(video_bytes)
90
- temp_file.flush()
91
- video_path = temp_file.name
92
- finally:
93
- temp_file.close()
94
-
95
- status_message = f"✅ Video generated successfully!"
96
- return video_path, status_message
97
-
98
  except Exception as e:
99
- error_msg = f"❌ Error generating video: {str(e)}"
100
- return None, error_msg
101
 
 
 
 
102
  def generate_with_pro_auth(
103
- prompt: str,
104
- oauth_token: Optional[gr.OAuthToken] = None
 
 
105
  ) -> Tuple[Optional[str], str]:
106
  """
107
- Wrapper function that checks if user is PRO before generating video.
 
108
  """
109
- # Check if user is PRO
110
  if not verify_pro_status(oauth_token):
111
- raise gr.Error("Access Denied. This app is exclusively for Hugging Face PRO users. Please subscribe to PRO to use this app.")
112
-
113
- if not prompt or prompt.strip() == "":
114
- return None, "❌ Please enter a prompt"
115
-
116
- # Use the environment token for API calls (with bill_to="huggingface")
117
- # Don't use the user's OAuth token for the API call
118
- video_path, status = generate_video(
119
- prompt,
120
- duration=8,
121
- size="1280x720",
122
- api_key=None # This will use the environment HF_TOKEN
123
- )
124
-
125
- return video_path, status
126
 
127
  def simple_generate(prompt: str) -> Optional[str]:
128
- """Simplified wrapper for examples that only returns video."""
129
- if not prompt or prompt.strip() == "":
130
  return None
131
-
132
  video_path, _ = generate_video(prompt, duration=8, size="1280x720", api_key=None)
133
  return video_path
134
 
 
 
 
135
  def create_ui():
136
- """Create the Gradio interface with PRO verification."""
137
-
138
  css = '''
139
  .logo-dark{display: none}
140
  .dark .logo-dark{display: block !important}
@@ -151,7 +204,7 @@ def create_ui():
151
  margin-left: 8px;
152
  }
153
  '''
154
-
155
  with gr.Blocks(title="Sora-2 Text-to-Video Generator", theme=gr.themes.Soft(), css=css) as demo:
156
  gr.HTML("""
157
  <div style="text-align: center; max-width: 800px; margin: 0 auto;">
@@ -169,52 +222,62 @@ def create_ui():
169
  </p>
170
  </div>
171
  """)
172
-
173
- # Login button for OAuth
174
  gr.LoginButton()
175
-
176
- # PRO message for non-PRO users
177
  pro_message = gr.Markdown(visible=False)
178
-
179
- # Main interface (hidden by default)
180
  main_interface = gr.Column(visible=False)
181
-
182
  with main_interface:
183
  gr.HTML("""
184
  <div style="text-align: center; margin: 20px 0;">
185
  <p style="color: #28a745; font-weight: bold;">✨ Welcome PRO User! You have full access to Sora-2.</p>
186
  </div>
187
  """)
188
-
189
  with gr.Row():
190
  with gr.Column(scale=1):
 
 
 
 
 
191
  prompt_input = gr.Textbox(
192
- label="Enter your prompt",
193
- placeholder="Describe the video you want to create...",
194
- lines=4
195
  )
196
-
 
 
 
 
 
 
 
 
 
197
  with gr.Accordion("Advanced Settings", open=False):
198
  gr.Markdown("*Coming soon: Duration and resolution controls*")
199
-
200
  generate_btn = gr.Button("🎥 Generate Video", variant="primary", size="lg")
201
-
202
  with gr.Column(scale=1):
203
  video_output = gr.Video(
204
  label="Generated Video",
205
  height=400,
206
  interactive=False,
207
- show_download_button=True
208
  )
209
  status_output = gr.Textbox(
210
  label="Status",
211
  interactive=False,
212
- visible=True
213
  )
214
-
215
- # Hidden manual token input removed - not needed anymore
216
-
217
- # Examples section with queue disabled
218
  gr.Examples(
219
  examples=[
220
  "A serene beach at sunset with waves gently rolling onto the shore",
@@ -222,60 +285,72 @@ def create_ui():
222
  "Northern lights dancing across a starry night sky",
223
  "A bustling city street transitioning from day to night in timelapse",
224
  "A close-up of coffee being poured into a cup with steam rising",
225
- "Cherry blossoms falling in slow motion in a Japanese garden"
226
  ],
227
  inputs=prompt_input,
228
  outputs=video_output,
229
- fn=simple_generate, # Examples use simplified function
230
  cache_examples=False,
231
  api_name=False,
232
  show_api=False,
233
  )
234
-
235
- # Event handler for generation with queue disabled
 
 
 
 
 
 
 
 
 
 
 
236
  generate_btn.click(
237
  fn=generate_with_pro_auth,
238
- inputs=[prompt_input],
239
  outputs=[video_output, status_output],
240
  queue=False,
241
  api_name=False,
242
  show_api=False,
243
  )
244
-
245
  # Footer
246
  gr.HTML("""
247
  <div style="text-align: center; margin-top: 40px; padding: 20px; border-top: 1px solid #e0e0e0;">
248
  <h3 style="color: #667eea;">Thank you for being a PRO user! 🤗</h3>
249
  </div>
250
  """)
251
-
252
- def control_access(profile: Optional[gr.OAuthProfile] = None, oauth_token: Optional[gr.OAuthToken] = None):
253
- """Control interface visibility based on PRO status."""
254
- if not profile:
255
- # User not logged in
 
 
 
256
  return gr.update(visible=False), gr.update(visible=False)
257
-
258
  if verify_pro_status(oauth_token):
259
- # User is PRO - show main interface
260
  return gr.update(visible=True), gr.update(visible=False)
261
  else:
262
- # User is not PRO - show upgrade message
263
  message = """
264
  ## ✨ Exclusive Access for PRO Users
265
-
266
  Thank you for your interest in the Sora-2 Text-to-Video Generator!
267
-
268
  This advanced AI video generation tool is available exclusively for Hugging Face **PRO** members.
269
-
270
  ### What you get with PRO:
271
  - ✅ Unlimited access to Sora-2 video generation
272
  - ✅ High-quality video outputs up to 1280x720
273
  - ✅ Fast generation times with priority queue
274
  - ✅ Access to other exclusive PRO Spaces
275
  - ✅ Support the development of cutting-edge AI tools
276
-
277
  ### Ready to create amazing videos?
278
-
279
  <div style="text-align: center; margin: 30px 0;">
280
  <a href="http://huggingface.co/subscribe/pro?source=sora2_video" target="_blank" style="
281
  display: inline-block;
@@ -292,39 +367,37 @@ def create_ui():
292
  🚀 Become a PRO Today!
293
  </a>
294
  </div>
295
-
296
  <p style="text-align: center; color: #666; margin-top: 20px;">
297
  Join thousands of creators who are already using PRO tools to bring their ideas to life.
298
  </p>
299
  """
300
  return gr.update(visible=False), gr.update(visible=True, value=message)
301
-
302
- # Check access on load
303
  demo.load(
304
  control_access,
305
  inputs=None,
306
- outputs=[main_interface, pro_message]
307
  )
308
-
309
  return demo
310
 
311
- # Launch the application
 
 
312
  if __name__ == "__main__":
313
  # Clean up any leftover files on startup
314
  try:
315
  cleanup_temp_files()
316
- # Also try to clear Gradio's cache
317
  if os.path.exists("gradio_cached_examples"):
318
  shutil.rmtree("gradio_cached_examples", ignore_errors=True)
319
  except Exception as e:
320
  print(f"Initial cleanup error: {e}")
321
-
322
  app = create_ui()
323
- # Launch without special auth parameters and no queue
324
- # OAuth is enabled via Space metadata (hf_oauth: true in README.md)
325
  app.launch(
326
  show_api=False,
327
  enable_monitoring=False,
328
  quiet=True,
329
- max_threads=10, # Limit threads to prevent resource exhaustion
330
- )
 
 
1
  import os
2
+ import time
3
  import tempfile
4
  import shutil
5
+ from pathlib import Path
6
  from typing import Optional, Tuple, Union
7
+
8
+ import gradio as gr
9
  from huggingface_hub import InferenceClient, whoami
 
10
 
11
+ # =========================
12
+ # Inference client (fal-ai)
13
+ # =========================
14
  client = InferenceClient(
15
  provider="fal-ai",
16
  api_key=os.environ.get("HF_TOKEN"),
17
  bill_to="huggingface",
18
  )
19
 
20
+ # =========================
21
+ # Auth / PRO helpers
22
+ # =========================
23
  def verify_pro_status(token: Optional[Union[gr.OAuthToken, str]]) -> bool:
24
  """Verifies if the user is a Hugging Face PRO user or part of an enterprise org."""
25
  if not token:
26
  return False
27
+
28
  if isinstance(token, gr.OAuthToken):
29
  token_str = token.token
30
  elif isinstance(token, str):
31
  token_str = token
32
  else:
33
  return False
34
+
35
  try:
36
  user_info = whoami(token=token_str)
37
  return (
38
+ user_info.get("isPro", False)
39
+ or any(org.get("isEnterprise", False) for org in user_info.get("orgs", []))
40
  )
41
  except Exception as e:
42
  print(f"Could not verify user's PRO/Enterprise status: {e}")
43
  return False
44
 
45
+ # =========================
46
+ # Storage hygiene
47
+ # =========================
48
  def cleanup_temp_files():
49
+ """Clean up old temporary .mp4 files to prevent storage overflow."""
50
  try:
51
  temp_dir = tempfile.gettempdir()
 
52
  for file_path in Path(temp_dir).glob("*.mp4"):
53
  try:
54
  # Remove files older than 5 minutes
55
+ if file_path.stat().st_mtime < (time.time() - 300):
56
  file_path.unlink(missing_ok=True)
57
  except Exception:
58
  pass # Ignore errors for individual files
59
  except Exception as e:
60
  print(f"Cleanup error: {e}")
61
 
62
+ def _write_video_bytes_to_tempfile(video_bytes: bytes) -> str:
63
+ temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
64
+ try:
65
+ temp_file.write(video_bytes)
66
+ temp_file.flush()
67
+ return temp_file.name
68
+ finally:
69
+ temp_file.close()
70
+
71
+ # =========================
72
+ # Generation (Text → Video)
73
+ # =========================
74
  def generate_video(
75
  prompt: str,
76
  duration: int = 8,
77
  size: str = "1280x720",
78
+ api_key: Optional[str] = None,
79
  ) -> Tuple[Optional[str], str]:
80
  """
81
+ Generate video using Sora-2 via Hugging Face Inference API (fal-ai provider).
82
+ Returns (video_path, status_message).
83
  """
 
84
  cleanup_temp_files()
85
+
86
  try:
87
  # Use provided API key or environment variable
88
+ temp_client = (
89
+ InferenceClient(provider="fal-ai", api_key=api_key, bill_to="huggingface")
90
+ if api_key
91
+ else client
92
+ )
93
+ if not (api_key or os.environ.get("HF_TOKEN")):
94
+ return None, "❌ Please set HF_TOKEN environment variable."
95
+
96
+ # Call text-to-video
 
 
 
97
  video_bytes = temp_client.text_to_video(
98
  prompt,
99
  model="akhaliq/sora-2",
100
+ # If your backend supports these, you can forward them as kwargs:
101
+ # duration=duration,
102
+ # size=size,
103
+ )
104
+
105
+ video_path = _write_video_bytes_to_tempfile(video_bytes)
106
+ return video_path, "✅ Video generated successfully!"
107
+ except Exception as e:
108
+ return None, f"❌ Error generating video: {str(e)}"
109
+
110
+ # =========================
111
+ # Generation (Image → Video)
112
+ # =========================
113
+ def generate_video_from_image(
114
+ prompt: str,
115
+ image_path: str,
116
+ api_key: Optional[str] = None,
117
+ ) -> Tuple[Optional[str], str]:
118
+ """
119
+ Generate video from a single input image using Sora-2 image-to-video.
120
+ Returns (video_path, status_message).
121
+ """
122
+ cleanup_temp_files()
123
+
124
+ if not image_path or not Path(image_path).exists():
125
+ return None, "❌ Please upload an image."
126
+
127
+ try:
128
+ temp_client = (
129
+ InferenceClient(provider="fal-ai", api_key=api_key, bill_to="huggingface")
130
+ if api_key
131
+ else client
132
+ )
133
+ if not (api_key or os.environ.get("HF_TOKEN")):
134
+ return None, "❌ Please set HF_TOKEN environment variable."
135
+
136
+ with open(image_path, "rb") as f:
137
+ input_image = f.read()
138
+
139
+ video_bytes = temp_client.image_to_video(
140
+ input_image,
141
+ prompt=prompt or "",
142
+ model="akhaliq/sora-2-image-to-video",
143
  )
144
+
145
+ video_path = _write_video_bytes_to_tempfile(video_bytes)
146
+ return video_path, "✅ Video generated successfully from image!"
 
 
 
 
 
 
 
 
 
 
 
147
  except Exception as e:
148
+ return None, f"❌ Error generating video from image: {str(e)}"
 
149
 
150
+ # =========================
151
+ # PRO wrapper (uses request)
152
+ # =========================
153
  def generate_with_pro_auth(
154
+ mode: str,
155
+ prompt: str,
156
+ image_path: Optional[str],
157
+ request: gr.Request,
158
  ) -> Tuple[Optional[str], str]:
159
  """
160
+ Check PRO status from the request's OAuth token, then route to the
161
+ appropriate generation function based on mode.
162
  """
163
+ oauth_token = getattr(request, "oauth_token", None)
164
  if not verify_pro_status(oauth_token):
165
+ raise gr.Error(
166
+ "Access Denied. This app is exclusively for Hugging Face PRO users. Please subscribe to PRO to use this app."
167
+ )
168
+
169
+ if mode == "Text → Video":
170
+ if not prompt or not prompt.strip():
171
+ return None, "❌ Please enter a prompt."
172
+ return generate_video(prompt, duration=8, size="1280x720", api_key=None)
173
+
174
+ # Image → Video
175
+ if not image_path:
176
+ return None, "❌ Please upload an image."
177
+ # Prompt is optional for image→video; pass empty string if not provided
178
+ return generate_video_from_image(prompt or "", image_path, api_key=None)
 
179
 
180
  def simple_generate(prompt: str) -> Optional[str]:
181
+ """Examples: only return the video path (text→video)."""
182
+ if not prompt or not prompt.strip():
183
  return None
 
184
  video_path, _ = generate_video(prompt, duration=8, size="1280x720", api_key=None)
185
  return video_path
186
 
187
+ # =========================
188
+ # UI
189
+ # =========================
190
  def create_ui():
 
 
191
  css = '''
192
  .logo-dark{display: none}
193
  .dark .logo-dark{display: block !important}
 
204
  margin-left: 8px;
205
  }
206
  '''
207
+
208
  with gr.Blocks(title="Sora-2 Text-to-Video Generator", theme=gr.themes.Soft(), css=css) as demo:
209
  gr.HTML("""
210
  <div style="text-align: center; max-width: 800px; margin: 0 auto;">
 
222
  </p>
223
  </div>
224
  """)
225
+
226
+ # HF OAuth (Spaces must have hf_oauth: true)
227
  gr.LoginButton()
228
+
229
+ # Hidden by default; we’ll toggle based on PRO status
230
  pro_message = gr.Markdown(visible=False)
 
 
231
  main_interface = gr.Column(visible=False)
232
+
233
  with main_interface:
234
  gr.HTML("""
235
  <div style="text-align: center; margin: 20px 0;">
236
  <p style="color: #28a745; font-weight: bold;">✨ Welcome PRO User! You have full access to Sora-2.</p>
237
  </div>
238
  """)
239
+
240
  with gr.Row():
241
  with gr.Column(scale=1):
242
+ mode_radio = gr.Radio(
243
+ choices=["Text → Video", "Image → Video"],
244
+ value="Text → Video",
245
+ label="Mode",
246
+ )
247
  prompt_input = gr.Textbox(
248
+ label="Prompt",
249
+ placeholder="Describe the video you want to create (optional for image→video)...",
250
+ lines=4,
251
  )
252
+
253
+ image_group = gr.Group(visible=False)
254
+ with image_group:
255
+ image_input = gr.Image(
256
+ label="Input Image (for Image → Video)",
257
+ type="filepath",
258
+ sources=["upload", "clipboard"],
259
+ image_mode="RGB",
260
+ )
261
+
262
  with gr.Accordion("Advanced Settings", open=False):
263
  gr.Markdown("*Coming soon: Duration and resolution controls*")
264
+
265
  generate_btn = gr.Button("🎥 Generate Video", variant="primary", size="lg")
266
+
267
  with gr.Column(scale=1):
268
  video_output = gr.Video(
269
  label="Generated Video",
270
  height=400,
271
  interactive=False,
272
+ show_download_button=True,
273
  )
274
  status_output = gr.Textbox(
275
  label="Status",
276
  interactive=False,
277
+ visible=True,
278
  )
279
+
280
+ # Examples (text→video only)
 
 
281
  gr.Examples(
282
  examples=[
283
  "A serene beach at sunset with waves gently rolling onto the shore",
 
285
  "Northern lights dancing across a starry night sky",
286
  "A bustling city street transitioning from day to night in timelapse",
287
  "A close-up of coffee being poured into a cup with steam rising",
288
+ "Cherry blossoms falling in slow motion in a Japanese garden",
289
  ],
290
  inputs=prompt_input,
291
  outputs=video_output,
292
+ fn=simple_generate,
293
  cache_examples=False,
294
  api_name=False,
295
  show_api=False,
296
  )
297
+
298
+ # Toggle image upload visibility with mode
299
+ def _toggle_image_group(mode: str):
300
+ return gr.update(visible=(mode == "Image → Video"))
301
+
302
+ mode_radio.change(
303
+ _toggle_image_group,
304
+ inputs=[mode_radio],
305
+ outputs=[image_group],
306
+ show_progress=False,
307
+ )
308
+
309
+ # Generation handler (uses request to read OAuth token)
310
  generate_btn.click(
311
  fn=generate_with_pro_auth,
312
+ inputs=[mode_radio, prompt_input, image_input],
313
  outputs=[video_output, status_output],
314
  queue=False,
315
  api_name=False,
316
  show_api=False,
317
  )
318
+
319
  # Footer
320
  gr.HTML("""
321
  <div style="text-align: center; margin-top: 40px; padding: 20px; border-top: 1px solid #e0e0e0;">
322
  <h3 style="color: #667eea;">Thank you for being a PRO user! 🤗</h3>
323
  </div>
324
  """)
325
+
326
+ # Use request to check access on load
327
+ def control_access(request: gr.Request):
328
+ oauth_profile = getattr(request, "oauth_profile", None)
329
+ oauth_token = getattr(request, "oauth_token", None)
330
+
331
+ if not oauth_profile:
332
+ # Not logged in
333
  return gr.update(visible=False), gr.update(visible=False)
334
+
335
  if verify_pro_status(oauth_token):
 
336
  return gr.update(visible=True), gr.update(visible=False)
337
  else:
 
338
  message = """
339
  ## ✨ Exclusive Access for PRO Users
340
+
341
  Thank you for your interest in the Sora-2 Text-to-Video Generator!
342
+
343
  This advanced AI video generation tool is available exclusively for Hugging Face **PRO** members.
344
+
345
  ### What you get with PRO:
346
  - ✅ Unlimited access to Sora-2 video generation
347
  - ✅ High-quality video outputs up to 1280x720
348
  - ✅ Fast generation times with priority queue
349
  - ✅ Access to other exclusive PRO Spaces
350
  - ✅ Support the development of cutting-edge AI tools
351
+
352
  ### Ready to create amazing videos?
353
+
354
  <div style="text-align: center; margin: 30px 0;">
355
  <a href="http://huggingface.co/subscribe/pro?source=sora2_video" target="_blank" style="
356
  display: inline-block;
 
367
  🚀 Become a PRO Today!
368
  </a>
369
  </div>
370
+
371
  <p style="text-align: center; color: #666; margin-top: 20px;">
372
  Join thousands of creators who are already using PRO tools to bring their ideas to life.
373
  </p>
374
  """
375
  return gr.update(visible=False), gr.update(visible=True, value=message)
376
+
 
377
  demo.load(
378
  control_access,
379
  inputs=None,
380
+ outputs=[main_interface, pro_message],
381
  )
382
+
383
  return demo
384
 
385
+ # =========================
386
+ # Entrypoint
387
+ # =========================
388
  if __name__ == "__main__":
389
  # Clean up any leftover files on startup
390
  try:
391
  cleanup_temp_files()
 
392
  if os.path.exists("gradio_cached_examples"):
393
  shutil.rmtree("gradio_cached_examples", ignore_errors=True)
394
  except Exception as e:
395
  print(f"Initial cleanup error: {e}")
396
+
397
  app = create_ui()
 
 
398
  app.launch(
399
  show_api=False,
400
  enable_monitoring=False,
401
  quiet=True,
402
+ max_threads=10,
403
+ )