howe commited on
Commit
63ff2ac
·
1 Parent(s): 1fff2b7

init code

Browse files
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ *.pyc
app.py CHANGED
@@ -1,154 +1,81 @@
 
 
 
 
1
  import gradio as gr
2
- import numpy as np
3
- import random
4
-
5
- # import spaces #[uncomment to use ZeroGPU]
6
- from diffusers import DiffusionPipeline
7
- import torch
8
-
9
- device = "cuda" if torch.cuda.is_available() else "cpu"
10
- model_repo_id = "stabilityai/sdxl-turbo" # Replace to the model you would like to use
11
-
12
- if torch.cuda.is_available():
13
- torch_dtype = torch.float16
14
- else:
15
- torch_dtype = torch.float32
16
-
17
- pipe = DiffusionPipeline.from_pretrained(model_repo_id, torch_dtype=torch_dtype)
18
- pipe = pipe.to(device)
19
-
20
- MAX_SEED = np.iinfo(np.int32).max
21
- MAX_IMAGE_SIZE = 1024
22
-
23
-
24
- # @spaces.GPU #[uncomment to use ZeroGPU]
25
- def infer(
26
- prompt,
27
- negative_prompt,
28
- seed,
29
- randomize_seed,
30
- width,
31
- height,
32
- guidance_scale,
33
- num_inference_steps,
34
- progress=gr.Progress(track_tqdm=True),
35
- ):
36
- if randomize_seed:
37
- seed = random.randint(0, MAX_SEED)
38
-
39
- generator = torch.Generator().manual_seed(seed)
40
-
41
- image = pipe(
42
- prompt=prompt,
43
- negative_prompt=negative_prompt,
44
- guidance_scale=guidance_scale,
45
- num_inference_steps=num_inference_steps,
46
- width=width,
47
- height=height,
48
- generator=generator,
49
- ).images[0]
50
-
51
- return image, seed
52
-
53
-
54
- examples = [
55
- "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k",
56
- "An astronaut riding a green horse",
57
- "A delicious ceviche cheesecake slice",
58
- ]
59
-
60
- css = """
61
- #col-container {
62
- margin: 0 auto;
63
- max-width: 640px;
64
- }
65
- """
66
-
67
- with gr.Blocks(css=css) as demo:
68
- with gr.Column(elem_id="col-container"):
69
- gr.Markdown(" # Text-to-Image Gradio Template")
70
 
71
- with gr.Row():
72
- prompt = gr.Text(
73
- label="Prompt",
74
- show_label=False,
75
- max_lines=1,
76
- placeholder="Enter your prompt",
77
- container=False,
78
- )
79
-
80
- run_button = gr.Button("Run", scale=0, variant="primary")
81
-
82
- result = gr.Image(label="Result", show_label=False)
83
-
84
- with gr.Accordion("Advanced Settings", open=False):
85
- negative_prompt = gr.Text(
86
- label="Negative prompt",
87
- max_lines=1,
88
- placeholder="Enter a negative prompt",
89
- visible=False,
90
- )
91
-
92
- seed = gr.Slider(
93
- label="Seed",
94
- minimum=0,
95
- maximum=MAX_SEED,
96
- step=1,
97
- value=0,
98
- )
99
-
100
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
101
-
102
- with gr.Row():
103
- width = gr.Slider(
104
- label="Width",
105
- minimum=256,
106
- maximum=MAX_IMAGE_SIZE,
107
- step=32,
108
- value=1024, # Replace with defaults that work for your model
109
- )
110
-
111
- height = gr.Slider(
112
- label="Height",
113
- minimum=256,
114
- maximum=MAX_IMAGE_SIZE,
115
- step=32,
116
- value=1024, # Replace with defaults that work for your model
117
- )
118
-
119
- with gr.Row():
120
- guidance_scale = gr.Slider(
121
- label="Guidance scale",
122
- minimum=0.0,
123
- maximum=10.0,
124
- step=0.1,
125
- value=0.0, # Replace with defaults that work for your model
126
- )
127
-
128
- num_inference_steps = gr.Slider(
129
- label="Number of inference steps",
130
- minimum=1,
131
- maximum=50,
132
- step=1,
133
- value=2, # Replace with defaults that work for your model
134
- )
135
-
136
- gr.Examples(examples=examples, inputs=[prompt])
137
- gr.on(
138
- triggers=[run_button.click, prompt.submit],
139
- fn=infer,
140
- inputs=[
141
- prompt,
142
- negative_prompt,
143
- seed,
144
- randomize_seed,
145
- width,
146
- height,
147
- guidance_scale,
148
- num_inference_steps,
149
- ],
150
- outputs=[result, seed],
151
  )
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  if __name__ == "__main__":
154
- demo.launch()
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import sys
4
+
5
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
+ from gradio.processing_utils import PUBLIC_HOSTNAME_WHITELIST
8
+
9
+ PUBLIC_HOSTNAME_WHITELIST.append("aigame-skyreels.oss-accelerate.aliyuncs.com")
10
+ PUBLIC_HOSTNAME_WHITELIST.append("aigame-skyreels.oss-cn-shanghai.aliyuncs.com")
11
+ PUBLIC_HOSTNAME_WHITELIST.append("skyreels-infer-dev.oss-cn-shenzhen.aliyuncs.com")
12
+
13
+
14
+ def _setup_logging() -> None:
15
+ """Configure logging to output to console with detailed format."""
16
+ log_format = "%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s"
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format=log_format,
20
+ datefmt="%Y-%m-%d %H:%M:%S",
21
+ handlers=[logging.StreamHandler()],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  )
23
 
24
+
25
+ # Initialize logging at module load
26
+ _setup_logging()
27
+
28
+
29
+ def _ensure_import_paths() -> None:
30
+ # Make sure the current directory is on sys.path so `pages` can be imported
31
+ current_dir = os.path.dirname(os.path.abspath(__file__))
32
+ if current_dir not in sys.path:
33
+ sys.path.append(current_dir)
34
+
35
+
36
+ def create_app() -> gr.Blocks:
37
+ _ensure_import_paths()
38
+ # Delayed import to ensure sys.path is prepared
39
+ from pages import page_map
40
+
41
+ with gr.Blocks(title="SkyReels") as demo:
42
+ # State to store current logged in username
43
+ current_user = gr.State(value="default_user")
44
+
45
+ with gr.Row():
46
+ gr.Markdown("## SkyReels")
47
+ gr.Markdown("Use the tabs below to select a feature page.")
48
+ gr.Markdown("For API access, please refer to https://platform.skyreels.ai/")
49
+
50
+ with gr.Tabs():
51
+ video_extension_pages = ["video_shot_extend", "video_shot_switching"]
52
+ video_extension_pages_set = set(video_extension_pages)
53
+ categorized_pages = video_extension_pages_set
54
+ activate_pages = [
55
+ "video_shot_extend",
56
+ "video_shot_switching",
57
+ "audio2video_single",
58
+ "multimodel",
59
+ ]
60
+ other_pages = [p for p in activate_pages if p not in categorized_pages]
61
+ # Render other pages
62
+ for page_name in other_pages:
63
+ if page_name in page_map:
64
+ page_map[page_name]().render(current_user)
65
+ with gr.Tab(label="Video Extension"):
66
+ with gr.Tabs():
67
+ for page_name in video_extension_pages:
68
+ if page_name in activate_pages and page_name in page_map:
69
+ page_map[page_name]().render(current_user)
70
+
71
+ return demo
72
+
73
+
74
  if __name__ == "__main__":
75
+ app = create_app()
76
+ # ssr_mode=False disables Server-Side Rendering which can cause connection issues in Spaces
77
+ app.queue(max_size=64).launch(
78
+ server_name="0.0.0.0",
79
+ server_port=7860,
80
+ ssr_mode=False
81
+ )
pages/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .audio2video_single import Audio2VideoSinglePage
2
+ from .video_shot_extend import VideoShotExtendPage
3
+ from .video_shot_switching import VideoShotSwitchingPage
4
+ from .multimodel import MultiModelPage
5
+
6
+ page_map = {
7
+ "audio2video_single": Audio2VideoSinglePage,
8
+ "video_shot_extend": VideoShotExtendPage,
9
+ "video_shot_switching": VideoShotSwitchingPage,
10
+ "multimodel": MultiModelPage,
11
+ }
pages/audio2video_single.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import List, Optional
3
+
4
+ import gradio as gr
5
+
6
+ from pages.base_page import BasePage
7
+ from utils.ossutils import upload_to_oss
8
+
9
+
10
+ class Audio2VideoSinglePage(BasePage):
11
+ @property
12
+ def page_name(self) -> str:
13
+ return "Talking Avatar(Single Actor)"
14
+
15
+ @property
16
+ def page_description(self) -> str:
17
+ return """
18
+ ## Single-Actor Avatar
19
+ 1. Provide a video description prompt.
20
+ 2. Upload a starting image.
21
+ 3. Upload an audio file.
22
+ 4. Click "Start Generation."
23
+ """
24
+
25
+ @property
26
+ def submit_endpoint(self) -> str:
27
+ return "/api/v1/video/audio2video/single/submit"
28
+
29
+ @property
30
+ def query_endpoint(self) -> str:
31
+ return "/api/v1/video/audio2video/single/task"
32
+
33
+ def render_input_components(self) -> List[gr.Component]:
34
+ with gr.Row():
35
+ prompt = gr.Textbox(
36
+ label="Prompt",
37
+ placeholder="Enter video description prompt",
38
+ lines=3,
39
+ value="a person is talking",
40
+ )
41
+ with gr.Row():
42
+ image_input = gr.Image(
43
+ label="Upload First Frame Image",
44
+ sources=["upload"],
45
+ type="filepath",
46
+ )
47
+ with gr.Row():
48
+ audio_input = gr.Audio(
49
+ label="Upload Audio",
50
+ sources=["upload"],
51
+ type="filepath",
52
+ )
53
+
54
+ return [
55
+ prompt,
56
+ image_input,
57
+ audio_input,
58
+ ]
59
+
60
+ def _build_payload(
61
+ self,
62
+ prompt: str,
63
+ image_path: Optional[str],
64
+ audio_path: Optional[str],
65
+ ):
66
+ if not prompt:
67
+ raise ValueError("Prompt is required.")
68
+ if not image_path:
69
+ raise ValueError("Please upload an image file.")
70
+ if not audio_path:
71
+ raise ValueError("Please upload an audio file.")
72
+
73
+ # Upload image to OSS
74
+ if isinstance(image_path, str) and image_path.startswith("http"):
75
+ image_url = image_path
76
+ else:
77
+ if not (isinstance(image_path, str) and os.path.isfile(image_path)):
78
+ raise ValueError("Invalid image file.")
79
+ image_url = upload_to_oss(image_path)
80
+
81
+ # Upload audio to OSS
82
+ if isinstance(audio_path, str) and audio_path.startswith("http"):
83
+ audio_url = audio_path
84
+ else:
85
+ if not (isinstance(audio_path, str) and os.path.isfile(audio_path)):
86
+ raise ValueError("Invalid audio file.")
87
+ audio_url = upload_to_oss(audio_path)
88
+
89
+ payload = {
90
+ "prompt": prompt,
91
+ "mode": "std",
92
+ "first_frame_image": image_url,
93
+ "audios": [audio_url],
94
+ }
95
+
96
+ return payload
97
+
98
+ def render_example(self, inputs: List[gr.Component]):
99
+ gr.Examples(
100
+ label="Examples (click to autofill)",
101
+ examples=[],
102
+ inputs=inputs,
103
+ examples_per_page=6,
104
+ cache_examples=False,
105
+ )
pages/base_page.py ADDED
@@ -0,0 +1,242 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import logging
4
+ import time
5
+ from abc import ABC, abstractmethod
6
+ from typing import Dict, List, Optional, Tuple
7
+
8
+ import gradio as gr
9
+ import requests
10
+ from retry import retry
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class AbstractPage(ABC):
16
+ @abstractmethod
17
+ def render(self, current_user=None):
18
+ pass
19
+
20
+
21
+ class BasePage(AbstractPage):
22
+ base_url = "https://apis.skyreels.ai/"
23
+ token = ""
24
+ poll_interval = 5
25
+ loop_limit = 2000
26
+
27
+ @property
28
+ @abstractmethod
29
+ def page_name(self) -> str:
30
+ pass
31
+
32
+ @property
33
+ @abstractmethod
34
+ def page_description(self) -> str:
35
+ pass
36
+
37
+ @property
38
+ @abstractmethod
39
+ def submit_endpoint(self) -> str:
40
+ pass
41
+
42
+ @property
43
+ @abstractmethod
44
+ def query_endpoint(self) -> str:
45
+ pass
46
+
47
+ @abstractmethod
48
+ def render_input_components(self) -> List[gr.Component]:
49
+ pass
50
+
51
+ @abstractmethod
52
+ def _build_payload(self, *args, **kwargs):
53
+ pass
54
+
55
+ @abstractmethod
56
+ def render_example(self, inputs: List[gr.Component]):
57
+ pass
58
+
59
+ @retry(tries=3, delay=10, backoff=2)
60
+ def _submit_task(self, payload: dict) -> Tuple[str, dict]:
61
+ submit_url = f"{self.base_url}{self.submit_endpoint}"
62
+ headers = {
63
+ "accept": "application/json",
64
+ "Content-Type": "application/json",
65
+ }
66
+ if self.token:
67
+ headers["Authorization"] = self.token
68
+
69
+ #payload["api_key"] = os.environ.get("API_KEY")
70
+ real_payload = payload.copy()
71
+ real_payload["api_key"] = os.environ.get("API_KEY")
72
+
73
+ response = requests.post(
74
+ submit_url, headers=headers, data=json.dumps(real_payload), timeout=30
75
+ )
76
+ if response.status_code != 200:
77
+ raise ValueError(
78
+ f"Failed to submit task: {response.status_code} {response.text}"
79
+ )
80
+ body = response.json()
81
+ if "task_id" not in body:
82
+ raise ValueError(f"Response missing task_id: {body}")
83
+ return body["task_id"], body
84
+
85
+ @retry(tries=100, delay=5)
86
+ def _query_task(self, task_id: str) -> Dict:
87
+ query_url = f"{self.base_url}{self.query_endpoint}/{task_id}"
88
+ headers = {
89
+ "accept": "application/json",
90
+ }
91
+ if self.token:
92
+ headers["Authorization"] = self.token
93
+
94
+ response = requests.get(query_url, headers=headers, timeout=30)
95
+ response.raise_for_status()
96
+ return response.json()
97
+
98
+ def _extract_video_url_from_data(self, data: Dict) -> Optional[str]:
99
+ if not isinstance(data, dict):
100
+ return None
101
+ value = data.get("video_url")
102
+ return value if isinstance(value, str) and value != "" else None
103
+
104
+ def _run_generation(self, *args, current_user=None, **kwargs):
105
+ try:
106
+ log_text = ""
107
+ payload_obj = None
108
+ yield (
109
+ "Submitting task…",
110
+ log_text,
111
+ None, # payload_json (none yet)
112
+ None, # video
113
+ gr.update(value="Generating", interactive=False),
114
+ )
115
+
116
+ payload = self._build_payload(*args, **kwargs)
117
+ log_text += f"[{time.strftime('%H:%M:%S')}] payload: {json.dumps(payload, ensure_ascii=False)}\n"
118
+ payload_obj = payload
119
+
120
+ yield ("Payload ready", log_text, payload_obj, None, gr.update())
121
+
122
+ task_id, submit_body = self._submit_task(payload)
123
+ log_text += f"[{time.strftime('%H:%M:%S')}] submit resp: {json.dumps(submit_body, ensure_ascii=False)}\n"
124
+
125
+ # Log user submission info
126
+ logger.info(
127
+ f"Task submitted - user: {current_user}, task_id: {task_id}, payload: {json.dumps(payload, ensure_ascii=False)}"
128
+ )
129
+
130
+ yield (
131
+ f"Task submitted. task_id: `{task_id}`",
132
+ log_text,
133
+ payload_obj,
134
+ None,
135
+ gr.update(),
136
+ )
137
+
138
+ for _ in range(self.loop_limit):
139
+ rsp = self._query_task(task_id)
140
+ status = rsp.get("status")
141
+ data = rsp.get("data")
142
+ msg = rsp.get("msg")
143
+ log_text += f"[{time.strftime('%H:%M:%S')}] poll resp: {json.dumps(rsp, ensure_ascii=False)}\n"
144
+ yield (f"Status: `{status}`", log_text, payload_obj, None, gr.update())
145
+
146
+ if status == "success":
147
+ video_url = self._extract_video_url_from_data(
148
+ data if isinstance(data, dict) else {}
149
+ )
150
+ if video_url:
151
+ yield (
152
+ "Completed ✅",
153
+ log_text,
154
+ payload_obj,
155
+ video_url,
156
+ gr.update(value="Start Generating", interactive=True),
157
+ )
158
+ else:
159
+ yield (
160
+ "Completed (no video URL found)",
161
+ log_text,
162
+ payload_obj,
163
+ None,
164
+ gr.update(value="Start Generating", interactive=True),
165
+ )
166
+ return
167
+
168
+ elif status == "failed":
169
+ yield (
170
+ f"Failed ❌: {msg}",
171
+ log_text,
172
+ payload_obj,
173
+ None,
174
+ gr.update(value="Start Generating", interactive=True),
175
+ )
176
+ return
177
+
178
+ time.sleep(self.poll_interval)
179
+
180
+ yield (
181
+ f"Failed ❌: Task Timeout: {task_id}",
182
+ log_text,
183
+ payload_obj,
184
+ None,
185
+ gr.update(value="Start Generating", interactive=True),
186
+ )
187
+ return
188
+
189
+ except Exception as e:
190
+ yield (
191
+ f"Error: {e}",
192
+ "",
193
+ payload_obj,
194
+ None,
195
+ gr.update(value="Start Generating", interactive=True),
196
+ )
197
+
198
+ def render(self, current_user=None):
199
+ with gr.TabItem(self.page_name):
200
+ # Center content to ~80% width using 1-8-1 column scaling
201
+ with gr.Row():
202
+ gr.Column(scale=1)
203
+ with gr.Column(scale=8):
204
+ gr.Markdown(self.page_description)
205
+
206
+ input_components = self.render_input_components()
207
+
208
+ run_btn = gr.Button("Start Generating", variant="primary")
209
+
210
+ with gr.Row():
211
+ status_md = gr.Markdown()
212
+ with gr.Row():
213
+ payload_json = gr.JSON(label="Payload")
214
+ with gr.Row():
215
+ rsp_logs = gr.Textbox(label="Logs", lines=14, interactive=False)
216
+ with gr.Row():
217
+ video = gr.Video(label="Result Video")
218
+
219
+ self.render_example(input_components)
220
+
221
+ if current_user is not None:
222
+
223
+ def run_with_user(*args):
224
+ # Last arg is current_user from State
225
+ *input_args, user = args
226
+ yield from self._run_generation(
227
+ *input_args, current_user=user
228
+ )
229
+
230
+ # Use the passed current_user State directly in inputs
231
+ run_btn.click(
232
+ fn=run_with_user,
233
+ inputs=input_components + [current_user],
234
+ outputs=[status_md, rsp_logs, payload_json, video, run_btn],
235
+ )
236
+ else:
237
+ run_btn.click(
238
+ fn=self._run_generation,
239
+ inputs=input_components,
240
+ outputs=[status_md, rsp_logs, payload_json, video, run_btn],
241
+ )
242
+ gr.Column(scale=1)
pages/multimodel.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+
3
+ import gradio as gr
4
+
5
+ from pages.base_page import BasePage
6
+ from utils.ossutils import upload_to_oss
7
+
8
+
9
+ class MultiModelPage(BasePage):
10
+ @property
11
+ def page_name(self) -> str:
12
+ # return "多模型生成(视频延长/ 切镜/多主体)"
13
+ return "Reference to Video"
14
+
15
+ @property
16
+ def page_description(self) -> str:
17
+ return """
18
+ ### Reference to Video
19
+ - Supports uploading 1-4 reference images locally,
20
+ - After clicking "Start Generation", it will automatically poll task status and display video link when completed.
21
+ """
22
+
23
+ @property
24
+ def submit_endpoint(self) -> str:
25
+ return "/api/v1/video/multiobject/submit"
26
+
27
+ @property
28
+ def query_endpoint(self) -> str:
29
+ return "/api/v1/video/multiobject/task"
30
+
31
+ def render_input_components(self) -> List[gr.Component]:
32
+ with gr.Row():
33
+ prompt = gr.Textbox(
34
+ label="Prompt",
35
+ placeholder="Describe the scene…",
36
+ lines=4,
37
+ )
38
+
39
+ with gr.Row():
40
+ image_file_0 = gr.Image(
41
+ label="Reference image 0 (local)",
42
+ type="filepath",
43
+ sources=["upload", "clipboard"],
44
+ )
45
+ image_file_1 = gr.Image(
46
+ label="Reference image 1 (local)",
47
+ type="filepath",
48
+ sources=["upload", "clipboard"],
49
+ )
50
+ image_file_2 = gr.Image(
51
+ label="Reference image 2 (local)",
52
+ type="filepath",
53
+ sources=["upload", "clipboard"],
54
+ )
55
+ image_file_3 = gr.Image(
56
+ label="Reference image 3 (local)",
57
+ type="filepath",
58
+ sources=["upload", "clipboard"],
59
+ )
60
+
61
+ aspect_ratio = gr.Dropdown(
62
+ label="Aspect ratio",
63
+ choices=["16:9", "9:16"],
64
+ value="16:9",
65
+ )
66
+ return [
67
+ prompt,
68
+ image_file_0,
69
+ image_file_1,
70
+ image_file_2,
71
+ image_file_3,
72
+ aspect_ratio,
73
+ ]
74
+
75
+ def _build_payload(
76
+ self,
77
+ prompt: str,
78
+ image_file_0: Optional[str],
79
+ image_file_1: Optional[str],
80
+ image_file_2: Optional[str],
81
+ image_file_3: Optional[str],
82
+ aspect_ratio: str,
83
+ ):
84
+ if all(
85
+ image_file is None
86
+ for image_file in [image_file_0, image_file_1, image_file_2, image_file_3]
87
+ ):
88
+ raise ValueError("Please upload at least one reference image")
89
+ reference_list = []
90
+ if image_file_0 is not None:
91
+ if isinstance(image_file_0, str) and image_file_0.startswith("http"):
92
+ image_file_0_url = image_file_0
93
+ else:
94
+ image_file_0_url = upload_to_oss(image_file_0)
95
+ reference_list.append(image_file_0_url)
96
+
97
+ if image_file_1 is not None:
98
+ if isinstance(image_file_1, str) and image_file_1.startswith("http"):
99
+ image_file_1_url = image_file_1
100
+ else:
101
+ image_file_1_url = upload_to_oss(image_file_1)
102
+ reference_list.append(image_file_1_url)
103
+
104
+ if image_file_2 is not None:
105
+ if isinstance(image_file_2, str) and image_file_2.startswith("http"):
106
+ image_file_2_url = image_file_2
107
+ else:
108
+ image_file_2_url = upload_to_oss(image_file_2)
109
+ reference_list.append(image_file_2_url)
110
+
111
+ if image_file_3 is not None:
112
+ if isinstance(image_file_3, str) and image_file_3.startswith("http"):
113
+ image_file_3_url = image_file_3
114
+ else:
115
+ image_file_3_url = upload_to_oss(image_file_3)
116
+ reference_list.append(image_file_3_url)
117
+
118
+ assert (
119
+ len(reference_list) <= 4 and len(reference_list) >= 1
120
+ ), "Number of reference images must be between 1 and 4"
121
+
122
+ payload = {
123
+ # "task_type": "mutil_object",
124
+ "prompt": prompt or "",
125
+ "ref_images": reference_list,
126
+ "aspect_ratio": aspect_ratio,
127
+ }
128
+ return payload
129
+
130
+ def render_example(self, inputs: List[gr.Component]):
131
+ gr.Examples(
132
+ label="Examples (Click to auto-fill)",
133
+ examples=[
134
+ [
135
+ "The man pours the milk from the cup into a plate for the puppy to drink.",
136
+ "https://aigame-html.oss-cn-shanghai.aliyuncs.com/skyreels_evaluation_samples_control/multi_concept_benchmark/thing/bowl/1.png",
137
+ "https://aigame-html.oss-cn-shanghai.aliyuncs.com/skyreels_evaluation_samples_control/multi_concept_benchmark/thing/animal/dog/4.jpg",
138
+ "https://aigame-html.oss-cn-shanghai.aliyuncs.com/skyreels_evaluation_samples_control/multi_concept_benchmark/thing/cup/1.jpg",
139
+ "https://aigame-html.oss-cn-shanghai.aliyuncs.com/skyreels_evaluation_samples_control/multi_concept_benchmark/human/man/synthesis/2.png",
140
+ "16:9",
141
+ ],
142
+ ],
143
+ inputs=inputs,
144
+ examples_per_page=6,
145
+ cache_examples=False,
146
+ )
pages/video_shot_extend.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+
3
+ import gradio as gr
4
+
5
+ from pages.base_page import BasePage
6
+ from utils.ossutils import upload_to_oss
7
+
8
+
9
+ class VideoShotExtendPage(BasePage):
10
+ @property
11
+ def page_name(self) -> str:
12
+ return "Single-Shot Video Extension"
13
+
14
+ @property
15
+ def page_description(self) -> str:
16
+ return """
17
+ ## Single-Shot Video Extension
18
+ 1. Upload a local video file.
19
+ 2. Set the extension duration, choosing from 5 to 30 seconds.
20
+ 3. Click "Start Generation." A loading status will appear below to indicate that the process has started successfully.
21
+ """
22
+
23
+ @property
24
+ def submit_endpoint(self) -> str:
25
+ return "/api/v1/video/extension/submit"
26
+
27
+ @property
28
+ def query_endpoint(self) -> str:
29
+ return "/api/v1/video/extension/task"
30
+
31
+ def render_input_components(self) -> List[gr.Component]:
32
+ with gr.Row():
33
+ prompt = gr.Textbox(
34
+ label="Prompt",
35
+ placeholder="Describe the scene…",
36
+ lines=4,
37
+ )
38
+
39
+ with gr.Row():
40
+ reference_video = gr.Video(
41
+ label="Reference video (local)",
42
+ sources=["upload"],
43
+ )
44
+
45
+ with gr.Row():
46
+ duration = gr.Dropdown(
47
+ label="duration",
48
+ choices=["5", "10", "15", "20", "25", "30"],
49
+ value="5",
50
+ )
51
+ return [prompt, reference_video, duration]
52
+
53
+ def _build_payload(
54
+ self,
55
+ prompt: str,
56
+ reference_video: str,
57
+ duration: str,
58
+ ):
59
+ if reference_video is None:
60
+ raise ValueError("Please upload a reference video.")
61
+ elif isinstance(reference_video, str) and reference_video.startswith("http"):
62
+ reference_video_url = reference_video
63
+ else:
64
+ reference_video_url = upload_to_oss(reference_video)
65
+
66
+ if prompt is None:
67
+ raise ValueError("Please enter the prompt.")
68
+
69
+ if duration is None:
70
+ raise ValueError("Please select the duration.")
71
+
72
+ payload = {
73
+ "prompt": prompt or "",
74
+ "prefix_video": reference_video_url,
75
+ "duration": int(duration),
76
+ }
77
+
78
+ return payload
79
+
80
+ def render_example(self, inputs: List[gr.Component]):
81
+ gr.Examples(
82
+ label="Examples (click to autofill)",
83
+ examples=[
84
+ [
85
+ "A man is running along the beach, with the camera zooming out.",
86
+ "https://aigame-html.oss-cn-shanghai.aliyuncs.com/skyreels_evaluation_samples_control/video_extend_benchmark_v1_5/0.mp4",
87
+ "5",
88
+ ],
89
+ [
90
+ "The camera gradually zooms in and focuses on the white building in the center of the video",
91
+ "https://aigame-skyreels-pro-sv.oss-us-west-1.aliyuncs.com/skyreel_assets/20250623/2ac2f142-30dc-490d-97a4-5a68b3f3ff4f.mp4",
92
+ "5",
93
+ ],
94
+ ],
95
+ inputs=inputs,
96
+ examples_per_page=6,
97
+ cache_examples=False,
98
+ )
pages/video_shot_switching.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+
3
+ import gradio as gr
4
+
5
+ from pages.base_page import BasePage
6
+ from utils.ossutils import upload_to_oss
7
+
8
+
9
+ class VideoShotSwitchingPage(BasePage):
10
+ @property
11
+ def page_name(self) -> str:
12
+ return "Shot Switching Video Extension"
13
+
14
+ @property
15
+ def page_description(self) -> str:
16
+ return """
17
+ ## Shot Switching Video Extension
18
+ Extend an existing clip while **switching shots** with professional transition patterns (e.g., Cut-In / Cut-Out / Reverse Shot / Multi-Angle / Cut Away).
19
+
20
+ 1. Upload a reference video (or use an example URL).
21
+ 2. Choose a cut type (or Auto).
22
+ 3. Select duration (2–5 seconds, per API).
23
+ 4. Click "Start Generating."
24
+
25
+ ### Cut Type Parameter Details
26
+ - **Auto**: The model automatically determines the appropriate cut type based on the scene context (default).
27
+ - **Cut-In**: Transitions from a wide shot to a close-up within the current scene.
28
+ - **Cut-Out**: Transitions from a close-up to a wide shot within the current scene.
29
+ - **Shot/Reverse Shot**: In dialogue scenes, transitions from a shot facing one person to a shot facing the other person.
30
+ - **Multi-Angle**: Switches to a different angle to show the current scene.
31
+ - **Cut Away**: Transitions to a new area within the current scene.
32
+ """
33
+
34
+ @property
35
+ def submit_endpoint(self) -> str:
36
+ return "/api/v1/video/extension/cutshot/submit"
37
+
38
+ @property
39
+ def query_endpoint(self) -> str:
40
+ return "/api/v1/video/extension/cutshot/task"
41
+
42
+ def render_input_components(self) -> List[gr.Component]:
43
+ with gr.Row():
44
+ prompt = gr.Textbox(
45
+ label="prompt",
46
+ placeholder="Describe the scene…",
47
+ lines=4,
48
+ )
49
+
50
+ with gr.Row():
51
+ prefix_video = gr.Video(
52
+ label="prefix_video (local)",
53
+ sources=["upload"],
54
+ )
55
+
56
+ with gr.Row():
57
+ duration = gr.Dropdown(
58
+ label="duration",
59
+ choices=["2", "3", "4", "5"],
60
+ value="5",
61
+ )
62
+
63
+ cut_type = gr.Dropdown(
64
+ label="cut_type",
65
+ choices=[
66
+ "Auto",
67
+ "Cut-In",
68
+ "Cut-Out",
69
+ "Shot/Reverse Shot",
70
+ "Multi-Angle",
71
+ "Cut Away",
72
+ ],
73
+ value="Auto",
74
+ )
75
+
76
+ return [prompt, prefix_video, duration, cut_type]
77
+
78
+ def _build_payload(
79
+ self,
80
+ prompt: str,
81
+ prefix_video: str,
82
+ duration: str,
83
+ cut_type: str,
84
+ ):
85
+ if not prompt:
86
+ raise ValueError("Please enter the prompt.")
87
+
88
+ if prefix_video is None:
89
+ raise ValueError("Please upload a prefix_video.")
90
+ elif isinstance(prefix_video, str) and prefix_video.startswith("http"):
91
+ prefix_video_url = prefix_video
92
+ else:
93
+ prefix_video_url = upload_to_oss(prefix_video)
94
+
95
+ if duration is None:
96
+ raise ValueError("Please select the duration.")
97
+ duration_int = int(duration)
98
+ if duration_int < 2 or duration_int > 5:
99
+ raise ValueError("duration must be between 2 and 5 seconds (per API).")
100
+
101
+ if not cut_type:
102
+ raise ValueError("Please select the cut_type.")
103
+
104
+ payload = {
105
+ "prompt": prompt,
106
+ "prefix_video": prefix_video_url,
107
+ "duration": duration_int,
108
+ "cut_type": cut_type,
109
+ }
110
+ return payload
111
+
112
+ def render_example(self, inputs: List[gr.Component]):
113
+ gr.Examples(
114
+ label="Examples (click to autofill)",
115
+ examples=[
116
+ [
117
+ # self.API_KEY_PLACEHOLDER,
118
+ "The camera focus on the man.",
119
+ "https://aigame-html.oss-cn-shanghai.aliyuncs.com/skyreels_api/examples/video_extension/test.mp4",
120
+ "5",
121
+ "Auto",
122
+ ],
123
+ [
124
+ #self.API_KEY_PLACEHOLDER,
125
+ "Close-up on the girl's face as she aims, sweating",
126
+ "https://skyreels-infer-dev.oss-cn-shenzhen.aliyuncs.com/user/yuzhe.jin/data/shot_extention/2.mp4",
127
+ "5",
128
+ "Auto",
129
+ ],
130
+ [
131
+ # self.API_KEY_PLACEHOLDER,
132
+ "The camera pulls back to reveal the girl lying in a meadow of flowers.",
133
+ "https://skyreels-infer-dev.oss-cn-shenzhen.aliyuncs.com/user/yuzhe.jin/data/shot_extention/3.mp4",
134
+ "5",
135
+ "Auto",
136
+ ],
137
+ [
138
+ # self.API_KEY_PLACEHOLDER,
139
+ "Create a top side angle view of the robot playing the guitar",
140
+ "https://skyreels-infer-dev.oss-cn-shenzhen.aliyuncs.com/user/yuzhe.jin/data/shot_extention/6.mp4",
141
+ "5",
142
+ "Auto",
143
+ ],
144
+ ],
145
+ inputs=inputs,
146
+ examples_per_page=6,
147
+ cache_examples=False,
148
+ )
requirements.txt CHANGED
@@ -1,6 +1,3 @@
1
- accelerate
2
- diffusers
3
- invisible_watermark
4
- torch
5
- transformers
6
- xformers
 
1
+ oss2
2
+ retry
3
+ gradio==6.4.0
 
 
 
utils/ossutils.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime
3
+ import uuid
4
+
5
+ import oss2
6
+ from retry import retry
7
+
8
+ oss_ak = os.environ.get("ALIYUN_ACCESS_KEY_ID")
9
+ oss_sk = os.environ.get("ALIYUN_ACCESS_KEY_SECRET")
10
+ oss_endpoint = os.environ.get("ALIYUN_ENDPOINT")
11
+ oss_bucket_name = os.environ.get("ALIYUN_BUCKET_NAME")
12
+
13
+ # 配置 OSS 连接信息 - 请替换为你的实际信息
14
+
15
+ auth = oss2.Auth(oss_ak, oss_sk)
16
+ # Configure timeouts via config; use both connect and read timeout
17
+ # service = oss2.Service(auth, oss_endpoint, connect_timeout=60)
18
+ bucket = oss2.Bucket(auth, oss_endpoint, oss_bucket_name, connect_timeout=60)
19
+
20
+
21
+ @retry(tries=2)
22
+ def upload_to_oss(file_path):
23
+ if not os.path.isfile(file_path):
24
+ raise FileNotFoundError(f"文件不存在: {file_path}")
25
+ file_name = os.path.basename(file_path)
26
+ file_ext = file_name.split(".")[-1] if "." in file_name else ""
27
+ rand4 = uuid.uuid4().hex[:4]
28
+ unique_key = (
29
+ f"gradio_upload/{datetime.now().strftime('%Y%m%d%H%M%S')}_{rand4}.{file_ext}"
30
+ )
31
+ try:
32
+ with open(file_path, "rb") as f:
33
+ result = bucket.put_object(unique_key, f)
34
+ if result.status == 200:
35
+ oss_url = f"https://{bucket.bucket_name}.{bucket.endpoint.split('//')[1]}/{unique_key}"
36
+ return oss_url
37
+ else:
38
+ print(f"上传失败,错误码:{result.status}")
39
+ raise RuntimeError(f"上传失败,错误码:{result.status}")
40
+ except Exception as e:
41
+ print(f"上传出错:{e}")
42
+ raise RuntimeError(f"上传出错:{e}") from e