Spaces:
Sleeping
Sleeping
Commit
·
6d01f39
1
Parent(s):
854ba03
first commit
Browse files- README.md +52 -4
- src/__pycache__/sora_video_downloader.cpython-313.pyc +0 -0
- src/sora.py +0 -0
- src/sora_video_downloader.log +22 -0
- src/sora_video_downloader.py +202 -0
- src/streamlit_app.py +79 -35
README.md
CHANGED
@@ -11,9 +11,57 @@ pinned: false
|
|
11 |
short_description: Streamlit template space
|
12 |
---
|
13 |
|
14 |
-
#
|
15 |
|
16 |
-
|
17 |
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
short_description: Streamlit template space
|
12 |
---
|
13 |
|
14 |
+
# SoraWithAzure: Streamlit Video Generation App
|
15 |
|
16 |
+
This app lets you generate videos using the Azure Sora API via a simple Streamlit interface. Users can enter a text prompt, select video settings, and generate videos that are stored and viewable by anyone using the app.
|
17 |
|
18 |
+
## Key Components
|
19 |
+
|
20 |
+
- **SoraClient**: Handles all communication with the Azure Sora API (job creation, polling, download).
|
21 |
+
- **VideoJob**: Represents a video generation job, encapsulating prompt, parameters, and result.
|
22 |
+
- **VideoStorage**: Handles saving and listing generated videos in a persistent directory.
|
23 |
+
- **Streamlit App**: Provides the user interface for input, video generation, and video display.
|
24 |
+
|
25 |
+
## How it Works
|
26 |
+
|
27 |
+
1. User enters their Azure API key, endpoint, and a text prompt.
|
28 |
+
2. User selects video resolution, length, and number of variants.
|
29 |
+
3. The app creates a video generation job via the Sora API.
|
30 |
+
4. The app polls for job completion.
|
31 |
+
5. When ready, the app downloads the generated video(s) and stores them.
|
32 |
+
6. All generated videos are listed and playable in the app.
|
33 |
+
|
34 |
+
## Sequence Diagram (ASCII)
|
35 |
+
|
36 |
+
```
|
37 |
+
User StreamlitApp SoraClient AzureSoraAPI VideoStorage
|
38 |
+
| | | | |
|
39 |
+
|---input-------->| | | |
|
40 |
+
| |---start_job--->| | |
|
41 |
+
| | |---POST-------->| |
|
42 |
+
| | |<--job_id-------| |
|
43 |
+
| |<--job_id-------| | |
|
44 |
+
| |---wait-------->| | |
|
45 |
+
| | |---poll-------->| |
|
46 |
+
| | |<--status-------| |
|
47 |
+
| |<--result-------| | |
|
48 |
+
| |---download---->| | |
|
49 |
+
| | |---GET video--->| |
|
50 |
+
| | |<--video--------| |
|
51 |
+
| | |---save-------->| |
|
52 |
+
| | |<--file_path----| |
|
53 |
+
|<---display------| | | |
|
54 |
+
```
|
55 |
+
|
56 |
+
## Code Structure
|
57 |
+
|
58 |
+
- `src/sora_video_downloader.py`: Core logic for Sora API interaction and video job management.
|
59 |
+
- `src/streamlit_app.py`: Streamlit UI and app logic.
|
60 |
+
|
61 |
+
## Notes
|
62 |
+
- Only text-to-video is supported (no image+prompt).
|
63 |
+
- All generated videos are visible to all users.
|
64 |
+
|
65 |
+
---
|
66 |
+
|
67 |
+
For more, see the code comments and docstrings in each file.
|
src/__pycache__/sora_video_downloader.cpython-313.pyc
ADDED
Binary file (13.7 kB). View file
|
|
src/sora.py
ADDED
File without changes
|
src/sora_video_downloader.log
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
2025-08-12 09:22:53,468 - INFO - SoraClient initialized
|
2 |
+
2025-08-12 09:22:54,028 - ERROR - Failed to start job: 400 {"error":{"code":"BadRequest","message":"API version not supported"}}
|
3 |
+
2025-08-12 09:22:54,029 - ERROR - Failed to start video job
|
4 |
+
2025-08-12 09:24:41,358 - INFO - SoraClient initialized
|
5 |
+
2025-08-12 09:24:41,843 - ERROR - Failed to start job: 400 {"detail":"Resolution 2040x2040 is not supported. Supported resolutions are ((480, 480), (854, 480), (720, 720), (1280, 720), (1080, 1080), (1920, 1080))"}
|
6 |
+
2025-08-12 09:24:41,843 - ERROR - Failed to start video job
|
7 |
+
2025-08-12 09:26:02,313 - INFO - SoraClient initialized
|
8 |
+
2025-08-12 10:14:56,702 - INFO - SoraClient initialized
|
9 |
+
2025-08-12 10:14:57,191 - ERROR - Failed to start job: 401 {"error":{"code":"401","message":"Access denied due to invalid subscription key or wrong API endpoint. Make sure to provide a valid key for an active subscription and use a correct regional API endpoint for your resource."}}
|
10 |
+
2025-08-12 10:14:57,191 - ERROR - Failed to start video job
|
11 |
+
2025-08-12 10:15:24,555 - INFO - SoraClient initialized
|
12 |
+
2025-08-12 10:15:25,014 - ERROR - Failed to start job: 400 {"detail":"Duration 30 is not supported. Duration should be between 1 and 20 seconds"}
|
13 |
+
2025-08-12 10:15:25,015 - ERROR - Failed to start video job
|
14 |
+
2025-08-12 10:15:34,058 - INFO - SoraClient initialized
|
15 |
+
2025-08-12 10:15:34,496 - ERROR - Failed to start job: 400 {"detail":"Duration 20 currently is not supported for (1920, 1080)."}
|
16 |
+
2025-08-12 10:15:34,496 - ERROR - Failed to start video job
|
17 |
+
2025-08-12 10:22:18,027 - INFO - SoraClient initialized
|
18 |
+
2025-08-12 10:22:18,468 - ERROR - Failed to start job: 401 {"error":{"code":"401","message":"Access denied due to invalid subscription key or wrong API endpoint. Make sure to provide a valid key for an active subscription and use a correct regional API endpoint for your resource."}}
|
19 |
+
2025-08-12 10:22:18,469 - ERROR - Failed to start video job
|
20 |
+
2025-08-12 10:22:35,442 - INFO - SoraClient initialized
|
21 |
+
2025-08-12 10:25:58,756 - ERROR - Job failed or timed out
|
22 |
+
2025-08-12 10:26:56,665 - INFO - SoraClient initialized
|
src/sora_video_downloader.py
ADDED
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import time
|
3 |
+
import json
|
4 |
+
import logging
|
5 |
+
import requests
|
6 |
+
from typing import Optional, Dict, Any, List
|
7 |
+
from urllib.parse import urlparse
|
8 |
+
|
9 |
+
# --- Logging Setup ---
|
10 |
+
def get_logger(name: str = __name__):
|
11 |
+
logger = logging.getLogger(name)
|
12 |
+
if not logger.handlers:
|
13 |
+
logging.basicConfig(
|
14 |
+
level=logging.INFO,
|
15 |
+
format='%(asctime)s - %(levelname)s - %(message)s',
|
16 |
+
handlers=[
|
17 |
+
logging.FileHandler('sora_video_downloader.log'),
|
18 |
+
logging.StreamHandler()
|
19 |
+
]
|
20 |
+
)
|
21 |
+
return logger
|
22 |
+
|
23 |
+
logger = get_logger()
|
24 |
+
|
25 |
+
# --- Sora API Client ---
|
26 |
+
class SoraClient:
|
27 |
+
def __init__(self, api_key: str, base_url: str, api_version: str = "preview"):
|
28 |
+
self.api_key = api_key
|
29 |
+
self.base_url = base_url.rstrip('/')
|
30 |
+
self.api_version = api_version
|
31 |
+
self.session = requests.Session()
|
32 |
+
self.session.headers.update({
|
33 |
+
'api-key': self.api_key,
|
34 |
+
'Content-Type': 'application/json'
|
35 |
+
})
|
36 |
+
logger.info("SoraClient initialized")
|
37 |
+
|
38 |
+
def start_video_job(self, prompt: str, height: int = 1080, width: int = 1080, n_seconds: int = 5, n_variants: int = 1) -> Optional[str]:
|
39 |
+
url = f"{self.base_url}/openai/v1/video/generations/jobs?api-version={self.api_version}"
|
40 |
+
payload = {
|
41 |
+
"model": "sora",
|
42 |
+
"prompt": prompt,
|
43 |
+
"height": str(height),
|
44 |
+
"width": str(width),
|
45 |
+
"n_seconds": str(n_seconds),
|
46 |
+
"n_variants": str(n_variants)
|
47 |
+
}
|
48 |
+
try:
|
49 |
+
response = self.session.post(url, json=payload)
|
50 |
+
if response.status_code in [200, 201, 202]:
|
51 |
+
result = response.json()
|
52 |
+
job_id = result.get('id') or result.get('job_id') or result.get('jobId')
|
53 |
+
return job_id
|
54 |
+
else:
|
55 |
+
logger.error(f"Failed to start job: {response.status_code} {response.text}")
|
56 |
+
return None
|
57 |
+
except Exception as e:
|
58 |
+
logger.error(f"Exception in start_video_job: {e}")
|
59 |
+
return None
|
60 |
+
|
61 |
+
def get_job_status(self, job_id: str) -> Dict[str, Any]:
|
62 |
+
url = f"{self.base_url}/openai/v1/video/generations/jobs/{job_id}?api-version={self.api_version}"
|
63 |
+
try:
|
64 |
+
response = self.session.get(url)
|
65 |
+
if response.status_code == 200:
|
66 |
+
return response.json()
|
67 |
+
else:
|
68 |
+
logger.error(f"Failed to get job status: {response.status_code} {response.text}")
|
69 |
+
return {"status": "error", "error": f"HTTP {response.status_code}"}
|
70 |
+
except Exception as e:
|
71 |
+
logger.error(f"Exception in get_job_status: {e}")
|
72 |
+
return {"status": "error", "error": str(e)}
|
73 |
+
|
74 |
+
def wait_for_job(self, job_id: str, max_wait_time: int = 300, poll_interval: int = 10) -> Optional[Dict[str, Any]]:
|
75 |
+
start_time = time.time()
|
76 |
+
while time.time() - start_time < max_wait_time:
|
77 |
+
status_result = self.get_job_status(job_id)
|
78 |
+
status = status_result.get('status', '').lower()
|
79 |
+
if status in ['completed', 'succeeded', 'success']:
|
80 |
+
return status_result
|
81 |
+
elif status in ['failed', 'error']:
|
82 |
+
return None
|
83 |
+
time.sleep(poll_interval)
|
84 |
+
logger.error(f"Job {job_id} timed out after {max_wait_time}s")
|
85 |
+
return None
|
86 |
+
|
87 |
+
def get_generation_details(self, generation_id: str) -> Dict[str, Any]:
|
88 |
+
url = f"{self.base_url}/openai/v1/video/generations/{generation_id}?api-version={self.api_version}"
|
89 |
+
try:
|
90 |
+
resp = self.session.get(url)
|
91 |
+
if resp.status_code == 200:
|
92 |
+
return resp.json()
|
93 |
+
else:
|
94 |
+
logger.error(f"Failed to fetch generation details: HTTP {resp.status_code}")
|
95 |
+
return {}
|
96 |
+
except Exception as e:
|
97 |
+
logger.error(f"Exception in get_generation_details: {e}")
|
98 |
+
return {}
|
99 |
+
|
100 |
+
def extract_video_urls(self, job_result: Dict[str, Any]) -> List[str]:
|
101 |
+
video_urls = []
|
102 |
+
generations = job_result.get('generations', [])
|
103 |
+
if isinstance(generations, list) and generations:
|
104 |
+
for g in generations:
|
105 |
+
gen_id = g.get('id') if isinstance(g, dict) else None
|
106 |
+
if not gen_id:
|
107 |
+
continue
|
108 |
+
content_url = f"{self.base_url}/openai/v1/video/generations/{gen_id}/content/video?api-version={self.api_version}"
|
109 |
+
video_urls.append(content_url)
|
110 |
+
return video_urls
|
111 |
+
|
112 |
+
def download_video(self, video_url: str, output_filename: str) -> bool:
|
113 |
+
try:
|
114 |
+
download_session = requests.Session()
|
115 |
+
download_session.headers.update({'api-key': self.api_key})
|
116 |
+
response = download_session.get(video_url, stream=True)
|
117 |
+
if response.status_code == 200:
|
118 |
+
with open(output_filename, 'wb') as f:
|
119 |
+
for chunk in response.iter_content(chunk_size=8192):
|
120 |
+
if chunk:
|
121 |
+
f.write(chunk)
|
122 |
+
return True
|
123 |
+
else:
|
124 |
+
logger.error(f"Failed to download video: {response.status_code} {response.text}")
|
125 |
+
return False
|
126 |
+
except Exception as e:
|
127 |
+
logger.error(f"Exception in download_video: {e}")
|
128 |
+
return False
|
129 |
+
|
130 |
+
# --- Video Job Abstraction ---
|
131 |
+
class VideoJob:
|
132 |
+
def __init__(self, sora_client: SoraClient, prompt: str, height: int = 1080, width: int = 1080, n_seconds: int = 5, n_variants: int = 1):
|
133 |
+
self.sora_client = sora_client
|
134 |
+
self.prompt = prompt
|
135 |
+
self.height = height
|
136 |
+
self.width = width
|
137 |
+
self.n_seconds = n_seconds
|
138 |
+
self.n_variants = n_variants
|
139 |
+
self.job_id: Optional[str] = None
|
140 |
+
self.result: Optional[Dict[str, Any]] = None
|
141 |
+
self.video_urls: List[str] = []
|
142 |
+
|
143 |
+
def run(self, wait: bool = True) -> bool:
|
144 |
+
self.job_id = self.sora_client.start_video_job(
|
145 |
+
self.prompt, self.height, self.width, self.n_seconds, self.n_variants
|
146 |
+
)
|
147 |
+
if not self.job_id:
|
148 |
+
logger.error("Failed to start video job")
|
149 |
+
return False
|
150 |
+
if wait:
|
151 |
+
self.result = self.sora_client.wait_for_job(self.job_id)
|
152 |
+
if not self.result:
|
153 |
+
logger.error("Job failed or timed out")
|
154 |
+
return False
|
155 |
+
self.video_urls = self.sora_client.extract_video_urls(self.result)
|
156 |
+
if not self.video_urls:
|
157 |
+
logger.error("No video URLs found")
|
158 |
+
return False
|
159 |
+
return True
|
160 |
+
|
161 |
+
def download_videos(self, output_dir: str) -> List[str]:
|
162 |
+
saved_files = []
|
163 |
+
for i, url in enumerate(self.video_urls):
|
164 |
+
filename = f"sora_video_{self.job_id}_{i+1}.mp4"
|
165 |
+
filepath = os.path.join(output_dir, filename)
|
166 |
+
if self.sora_client.download_video(url, filepath):
|
167 |
+
saved_files.append(filepath)
|
168 |
+
return saved_files
|
169 |
+
|
170 |
+
# --- Video Storage Handler ---
|
171 |
+
class VideoStorage:
|
172 |
+
def __init__(self, storage_dir: str = "videos"):
|
173 |
+
self.storage_dir = storage_dir
|
174 |
+
os.makedirs(self.storage_dir, exist_ok=True)
|
175 |
+
|
176 |
+
def save_video(self, src_path: str, filename: str) -> str:
|
177 |
+
dst_path = os.path.join(self.storage_dir, filename)
|
178 |
+
os.rename(src_path, dst_path)
|
179 |
+
return dst_path
|
180 |
+
|
181 |
+
def list_videos(self) -> List[str]:
|
182 |
+
return [os.path.join(self.storage_dir, f) for f in os.listdir(self.storage_dir) if f.endswith('.mp4')]
|
183 |
+
|
184 |
+
# --- Main for CLI usage (optional) ---
|
185 |
+
def main():
|
186 |
+
api_key = os.getenv('AZURE_OPENAI_API_KEY') or os.getenv('AZURE_API_KEY')
|
187 |
+
base_url = os.getenv('AZURE_OPENAI_ENDPOINT') or "https://levm3-me7f7pgq-eastus2.cognitiveservices.azure.com"
|
188 |
+
if not api_key or not base_url:
|
189 |
+
print("Please set your Azure API key and endpoint.")
|
190 |
+
return
|
191 |
+
sora = SoraClient(api_key, base_url)
|
192 |
+
prompt = "A video of a cat playing with a ball of yarn in a sunny room"
|
193 |
+
job = VideoJob(sora, prompt)
|
194 |
+
if job.run():
|
195 |
+
storage = VideoStorage()
|
196 |
+
files = job.download_videos(storage.storage_dir)
|
197 |
+
print(f"Downloaded: {files}")
|
198 |
+
else:
|
199 |
+
print("Video generation failed.")
|
200 |
+
|
201 |
+
if __name__ == "__main__":
|
202 |
+
main()
|
src/streamlit_app.py
CHANGED
@@ -1,40 +1,84 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
import pandas as pd
|
4 |
-
import streamlit as st
|
5 |
|
|
|
|
|
|
|
6 |
"""
|
7 |
-
|
|
|
|
|
8 |
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
|
13 |
-
|
14 |
-
""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
df = pd.DataFrame({
|
27 |
-
"x": x,
|
28 |
-
"y": y,
|
29 |
-
"idx": indices,
|
30 |
-
"rand": np.random.randn(num_points),
|
31 |
-
})
|
32 |
-
|
33 |
-
st.altair_chart(alt.Chart(df, height=700, width=700)
|
34 |
-
.mark_point(filled=True)
|
35 |
-
.encode(
|
36 |
-
x=alt.X("x", axis=None),
|
37 |
-
y=alt.Y("y", axis=None),
|
38 |
-
color=alt.Color("idx", legend=None, scale=alt.Scale()),
|
39 |
-
size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
|
40 |
-
))
|
|
|
1 |
+
"""
|
2 |
+
Streamlit app for generating videos using Azure Sora API.
|
|
|
|
|
3 |
|
4 |
+
- Users provide an API key, endpoint, and a text prompt.
|
5 |
+
- Advanced settings allow selection of video resolution, length, and number of variants.
|
6 |
+
- Generated videos are stored and displayed for all users.
|
7 |
"""
|
8 |
+
import streamlit as st
|
9 |
+
import os
|
10 |
+
from sora_video_downloader import SoraClient, VideoJob, VideoStorage
|
11 |
|
12 |
+
# --- Hardcoded for testing, but can be made user-editable ---
|
13 |
+
DEFAULT_API_KEY = os.getenv('AZURE_OPENAI_API_KEY', 'YOUR_AZURE_API_KEY')
|
14 |
+
DEFAULT_ENDPOINT = os.getenv('AZURE_OPENAI_ENDPOINT', 'https://levm3-me7f7pgq-eastus2.cognitiveservices.azure.com')
|
15 |
|
16 |
+
# --- UI: Title and Sidebar ---
|
17 |
+
st.title("Sora Video Generator (Azure)")
|
18 |
+
|
19 |
+
st.sidebar.header("Azure Sora Settings")
|
20 |
+
api_key = st.sidebar.text_input("API Key", value=DEFAULT_API_KEY, type="password")
|
21 |
+
endpoint = st.sidebar.text_input("Azure AI Foundry Endpoint", value=DEFAULT_ENDPOINT)
|
22 |
+
|
23 |
+
# --- UI: Main Input ---
|
24 |
+
st.header("Generate a Video with Sora")
|
25 |
+
prompt = st.text_area("Video Prompt", "A video of a cat playing with a ball of yarn in a sunny room")
|
26 |
+
|
27 |
+
# --- UI: Advanced Settings ---
|
28 |
+
st.subheader("Advanced Settings")
|
29 |
+
col1, col2 = st.columns(2)
|
30 |
+
|
31 |
+
DURATION_RES_MAP = {
|
32 |
+
5: [
|
33 |
+
(480, 480), (854, 480), (720, 720), (1280, 720), (1080, 1080), (1920, 1080)
|
34 |
+
],
|
35 |
+
10: [
|
36 |
+
(480, 480), (854, 480), (720, 720), (1280, 720), (1080, 1080)
|
37 |
+
],
|
38 |
+
20: [
|
39 |
+
(480, 480), (854, 480), (720, 720), (1280, 720)
|
40 |
+
]
|
41 |
+
}
|
42 |
+
|
43 |
+
with col1:
|
44 |
+
n_seconds = st.selectbox("Video Length (seconds)", options=[5, 10, 20], index=0)
|
45 |
+
valid_resolutions = DURATION_RES_MAP[n_seconds]
|
46 |
+
res_labels = [f"{w}x{h}" for (w, h) in valid_resolutions]
|
47 |
+
res_idx = st.selectbox("Resolution", options=list(range(len(res_labels))), format_func=lambda i: res_labels[i], index=0)
|
48 |
+
width, height = valid_resolutions[res_idx]
|
49 |
+
with col2:
|
50 |
+
n_variants = st.slider("Number of Variants", min_value=1, max_value=4, value=1)
|
51 |
+
|
52 |
+
# --- Video Generation Logic ---
|
53 |
+
generate = st.button("Generate Video")
|
54 |
+
status_placeholder = st.empty()
|
55 |
+
|
56 |
+
video_storage = VideoStorage()
|
57 |
+
|
58 |
+
if generate:
|
59 |
+
# Validate required fields
|
60 |
+
if not api_key or not endpoint or not prompt.strip():
|
61 |
+
st.error("Please provide all required fields.")
|
62 |
+
else:
|
63 |
+
status_placeholder.info("Starting video generation...")
|
64 |
+
sora = SoraClient(api_key, endpoint)
|
65 |
+
job = VideoJob(sora, prompt, height=height, width=width, n_seconds=n_seconds, n_variants=n_variants)
|
66 |
+
if job.run():
|
67 |
+
saved_files = job.download_videos(video_storage.storage_dir)
|
68 |
+
if saved_files:
|
69 |
+
status_placeholder.success(f"Video(s) generated and saved: {', '.join([os.path.basename(f) for f in saved_files])}")
|
70 |
+
else:
|
71 |
+
status_placeholder.error("Video generation succeeded but download failed.")
|
72 |
+
else:
|
73 |
+
status_placeholder.error("Video generation failed.")
|
74 |
|
75 |
+
# --- Display All Generated Videos ---
|
76 |
+
st.header("All Generated Videos")
|
77 |
+
video_files = video_storage.list_videos()
|
78 |
+
if not video_files:
|
79 |
+
st.info("No videos generated yet.")
|
80 |
+
else:
|
81 |
+
for video_path in sorted(video_files, reverse=True):
|
82 |
+
st.video(video_path)
|
83 |
+
st.caption(os.path.basename(video_path))
|
84 |
+
st.download_button("Download", data=open(video_path, "rb").read(), file_name=os.path.basename(video_path), mime="video/mp4")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|