File size: 11,660 Bytes
b87c1b6
d872fa5
bc2cb72
275a10f
b78c4e1
 
b87c1b6
d872fa5
b87c1b6
29ea35b
 
6b2dd9c
29ea35b
d872fa5
 
b78c4e1
 
 
 
b87c1b6
b78c4e1
 
 
 
 
24c348f
b87c1b6
b78c4e1
 
 
b87c1b6
 
b78c4e1
 
 
 
 
275a10f
b87c1b6
275a10f
 
b87c1b6
275a10f
 
 
b87c1b6
e34f60e
275a10f
 
b87c1b6
275a10f
 
 
d872fa5
 
24c348f
 
b87c1b6
d872fa5
b87c1b6
275a10f
d872fa5
b87c1b6
 
 
 
 
 
 
 
 
 
 
29ea35b
 
24c348f
e34f60e
b87c1b6
 
 
 
 
 
 
 
 
e34f60e
 
 
 
b87c1b6
e34f60e
b87c1b6
e34f60e
b87c1b6
e34f60e
b87c1b6
e34f60e
b87c1b6
 
e34f60e
b87c1b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e34f60e
 
 
b87c1b6
e34f60e
d872fa5
e34f60e
b87c1b6
 
 
 
 
 
 
 
 
d872fa5
e34f60e
d872fa5
b78c4e1
b87c1b6
 
b78c4e1
b87c1b6
efee1d5
b87c1b6
 
 
 
 
 
 
 
 
 
 
e34f60e
b87c1b6
 
 
 
 
 
 
 
 
e34f60e
b87c1b6
 
d872fa5
b78c4e1
b87c1b6
 
b78c4e1
 
 
170fe75
6b2dd9c
b78c4e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b87c1b6
24c348f
6b2dd9c
b78c4e1
 
24c348f
b78c4e1
 
fea8f7a
 
b78c4e1
 
 
fea8f7a
 
 
6b2dd9c
 
b87c1b6
6b2dd9c
b78c4e1
 
b87c1b6
b78c4e1
b87c1b6
 
 
 
 
24c348f
 
 
b87c1b6
 
 
137d0c0
24c348f
 
b87c1b6
 
 
24c348f
 
b87c1b6
24c348f
b87c1b6
24c348f
e34f60e
b87c1b6
b78c4e1
b87c1b6
 
 
b78c4e1
 
b87c1b6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b78c4e1
 
 
 
b87c1b6
b78c4e1
b87c1b6
 
 
6b2dd9c
d872fa5
 
275a10f
 
 
 
 
 
b87c1b6
6b2dd9c
b87c1b6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
import gradio as gr
import os
import tempfile
import shutil
from typing import Optional, Tuple, Union
from huggingface_hub import InferenceClient, whoami
from pathlib import Path

# Initialize Hugging Face Inference Client with fal-ai provider
client = InferenceClient(
    provider="fal-ai",
    api_key=os.environ.get("HF_TOKEN"),
    bill_to="huggingface",
)

def verify_pro_status(token: Optional[Union[gr.OAuthToken, str]]) -> bool:
    """Verifies if the user is a Hugging Face PRO user or part of an enterprise org."""
    if not token:
        return False
    
    if isinstance(token, gr.OAuthToken):
        token_str = token.token
    elif isinstance(token, str):
        token_str = token
    else:
        return False
    
    try:
        user_info = whoami(token=token_str)
        return (
            user_info.get("isPro", False) or
            any(org.get("isEnterprise", False) for org in user_info.get("orgs", []))
        )
    except Exception as e:
        print(f"Could not verify user's PRO/Enterprise status: {e}")
        return False

def cleanup_temp_files():
    """Clean up old temporary video files to prevent storage overflow."""
    try:
        temp_dir = tempfile.gettempdir()
        # Clean up old .mp4 files in temp directory
        for file_path in Path(temp_dir).glob("*.mp4"):
            try:
                # Remove files older than 5 minutes
                import time
                if file_path.stat().st_mtime < (time.time() - 300):
                    file_path.unlink(missing_ok=True)
            except Exception:
                pass
    except Exception as e:
        print(f"Cleanup error: {e}")

def generate_video(
    prompt: str,
    duration: int = 8,
    size: str = "1280x720",
    api_key: Optional[str] = None
) -> Tuple[Optional[str], str]:
    """Generate video using Sora-2 through Hugging Face Inference API with fal-ai provider."""
    cleanup_temp_files()
    try:
        if api_key:
            temp_client = InferenceClient(
                provider="fal-ai",
                api_key=api_key,
                bill_to="huggingface",
            )
        else:
            temp_client = client
            if not os.environ.get("HF_TOKEN") and not api_key:
                return None, "❌ Please set HF_TOKEN environment variable."
        
        video_bytes = temp_client.text_to_video(
            prompt,
            model="akhaliq/sora-2",
        )
        
        temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
        try:
            temp_file.write(video_bytes)
            temp_file.flush()
            video_path = temp_file.name
        finally:
            temp_file.close()
        
        return video_path, "βœ… Video generated successfully!"
    except Exception as e:
        return None, f"❌ Error generating video: {str(e)}"

# --- NEW: image -> video support ---
def generate_video_from_image(
    image: Union[str, bytes],
    prompt: str,
    api_key: Optional[str] = None
) -> Tuple[Optional[str], str]:
    """Generate a video from a single input image + prompt using Sora-2 image-to-video."""
    cleanup_temp_files()
    if not prompt or prompt.strip() == "":
        return None, "❌ Please enter a prompt"
    try:
        if api_key:
            temp_client = InferenceClient(
                provider="fal-ai",
                api_key=api_key,
                bill_to="huggingface",
            )
        else:
            temp_client = client
            if not os.environ.get("HF_TOKEN") and not api_key:
                return None, "❌ Please set HF_TOKEN environment variable."

        if isinstance(image, str):
            with open(image, "rb") as f:
                input_image = f.read()
        elif isinstance(image, (bytes, bytearray)):
            input_image = image
        else:
            return None, "❌ Invalid image input. Please upload an image."

        video_bytes = temp_client.image_to_video(
            input_image,
            prompt=prompt,
            model="akhaliq/sora-2-image-to-video",
        )

        temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
        try:
            temp_file.write(video_bytes)
            temp_file.flush()
            video_path = temp_file.name
        finally:
            temp_file.close()

        return video_path, "βœ… Video generated from image successfully!"
    except Exception as e:
        return None, f"❌ Error generating video from image: {str(e)}"

def generate_with_pro_auth(
    prompt: str, 
    oauth_token: Optional[gr.OAuthToken] = None
) -> Tuple[Optional[str], str]:
    """Wrapper function that checks if user is PRO before generating video."""
    if not verify_pro_status(oauth_token):
        raise gr.Error("Access Denied. This app is exclusively for Hugging Face PRO users.")
    
    if not prompt or prompt.strip() == "":
        return None, "❌ Please enter a prompt"
    
    return generate_video(
        prompt, 
        duration=8, 
        size="1280x720", 
        api_key=None
    )

# --- NEW: PRO-gated wrapper for image -> video ---
def generate_with_pro_auth_image(
    prompt: str,
    image_path: Optional[str] = None,
    oauth_token: Optional[gr.OAuthToken] = None
) -> Tuple[Optional[str], str]:
    """Checks PRO status then calls image->video generator."""
    if not verify_pro_status(oauth_token):
        raise gr.Error("Access Denied. This app is exclusively for Hugging Face PRO users.")
    if not image_path:
        return None, "❌ Please upload an image"
    return generate_video_from_image(image=image_path, prompt=prompt, api_key=None)

def simple_generate(prompt: str) -> Optional[str]:
    """Simplified wrapper for examples that only returns video."""
    if not prompt or prompt.strip() == "":
        return None
    video_path, _ = generate_video(prompt, duration=8, size="1280x720", api_key=None)
    return video_path

def create_ui():
    css = '''
    .logo-dark{display: none}
    .dark .logo-dark{display: block !important}
    .dark .logo-light{display: none}
    #sub_title{margin-top: -20px !important}
    .pro-badge{
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        color: white;
        padding: 4px 12px;
        border-radius: 20px;
        font-size: 0.9em;
        font-weight: bold;
        display: inline-block;
        margin-left: 8px;
    }
    '''
    
    with gr.Blocks(title="Sora-2 Text-to-Video Generator", theme=gr.themes.Soft(), css=css) as demo:
        gr.HTML("""
            <div style="text-align: center; max-width: 800px; margin: 0 auto;">
                <h1 style="font-size: 2.5em; margin-bottom: 0.5em;">
                    🎬 Sora-2 Text-to-Video Generator
                    <span class="pro-badge">PRO</span>
                </h1>
                <p style="font-size: 1.1em; color: #666; margin-bottom: 20px;">Generate stunning videos using OpenAI's Sora-2 model</p>
                <p id="sub_title" style="font-size: 1em; margin-top: 20px; margin-bottom: 15px;">
                    <strong>Exclusive access for Hugging Face PRO users.</strong> 
                    <a href="http://huggingface.co/subscribe/pro?source=sora2_video" target="_blank" style="color: #667eea;">Subscribe to PRO β†’</a>
                </p>
                <p style="font-size: 0.9em; color: #999; margin-top: 15px;">
                    Built with <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: #667eea;">anycoder</a>
                </p>
            </div>
        """)
        
        gr.LoginButton()
        pro_message = gr.Markdown(visible=False)
        main_interface = gr.Column(visible=False)
        
        with main_interface:
            gr.HTML("""<div style="text-align: center; margin: 20px 0;">
                <p style="color: #28a745; font-weight: bold;">✨ Welcome PRO User! You have full access to Sora-2.</p>
            </div>""")
            
            # Text -> Video
            with gr.Row():
                with gr.Column(scale=1):
                    prompt_input = gr.Textbox(
                        label="Enter your prompt",
                        placeholder="Describe the video you want to create...",
                        lines=4
                    )
                    generate_btn = gr.Button("πŸŽ₯ Generate Video", variant="primary", size="lg")
                with gr.Column(scale=1):
                    video_output = gr.Video(label="Generated Video", height=400, interactive=False, show_download_button=True)
                    status_output = gr.Textbox(label="Status", interactive=False, visible=True)
            
            generate_btn.click(
                fn=generate_with_pro_auth,
                inputs=[prompt_input],
                outputs=[video_output, status_output],
                queue=False
            )

            # --- NEW: Image -> Video UI ---
            gr.HTML("""
                <div style="text-align: center; margin: 40px 0 10px;">
                    <h3 style="margin-bottom: 8px;">πŸ–ΌοΈ ➜ 🎬 Image β†’ Video (beta)</h3>
                    <p style="color:#666; margin:0;">Turn a single image into a short video with a guiding prompt.</p>
                </div>
            """)
            with gr.Row():
                with gr.Column(scale=1):
                    img_prompt_input = gr.Textbox(
                        label="Describe how the scene should evolve",
                        placeholder="e.g., The cat starts to dance and spins playfully",
                        lines=3,
                    )
                    image_input = gr.Image(label="Upload an image", type="filepath")
                    generate_img_btn = gr.Button("πŸŽ₯ Generate from Image", variant="primary")
                with gr.Column(scale=1):
                    video_output_img = gr.Video(label="Generated Video (from Image)", height=400, interactive=False, show_download_button=True)
                    status_output_img = gr.Textbox(label="Status", interactive=False, visible=True)

            generate_img_btn.click(
                fn=generate_with_pro_auth_image,
                inputs=[img_prompt_input, image_input],
                outputs=[video_output_img, status_output_img],
                queue=False
            )
            
            gr.HTML("""<div style="text-align: center; margin-top: 40px; padding: 20px; border-top: 1px solid #e0e0e0;">
                <h3 style="color: #667eea;">Thank you for being a PRO user! πŸ€—</h3>
            </div>""")
        
        def control_access(profile: Optional[gr.OAuthProfile] = None, oauth_token: Optional[gr.OAuthToken] = None):
            if not profile:
                return gr.update(visible=False), gr.update(visible=False)
            if verify_pro_status(oauth_token):
                return gr.update(visible=True), gr.update(visible=False)
            else:
                message = "## ✨ Exclusive Access for PRO Users\n\nThis tool is available exclusively for Hugging Face **PRO** members."
                return gr.update(visible=False), gr.update(visible=True, value=message)
        
        demo.load(control_access, inputs=None, outputs=[main_interface, pro_message])
    
    return demo

if __name__ == "__main__":
    try:
        cleanup_temp_files()
        if os.path.exists("gradio_cached_examples"):
            shutil.rmtree("gradio_cached_examples", ignore_errors=True)
    except Exception as e:
        print(f"Initial cleanup error: {e}")
    
    app = create_ui()
    app.launch(show_api=False, enable_monitoring=False, quiet=True, max_threads=10)