levalencia commited on
Commit
6d01f39
·
1 Parent(s): 854ba03

first commit

Browse files
README.md CHANGED
@@ -11,9 +11,57 @@ pinned: false
11
  short_description: Streamlit template space
12
  ---
13
 
14
- # Welcome to Streamlit!
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
- import streamlit as st
5
 
 
 
 
6
  """
7
- # Welcome to Streamlit!
 
 
8
 
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
 
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
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")