dayona commited on
Commit
cdefade
·
0 Parent(s):
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ _hf_token
2
+ _tt.py
3
+ test.py
4
+ __pycache__
5
+ day_hf
6
+ logs.sh
7
+ api-i2v.md
8
+ api.doc.md
9
+ .venv
README.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: "I2V ( 70%+ ZeroGPU Quota Savings ) AoT Compiled"
3
+ emoji: ⚡
4
+ colorFrom: yellow
5
+ colorTo: red
6
+ sdk: gradio
7
+ sdk_version: 6.0.1
8
+ app_file: app.py
9
+ pinned: true
10
+ short_description: "🎬 VIP Prompt Relay, 🎬 RIFE Motion 32/64 FPS 🚀"
11
+ ---
12
+
13
+ # 🎬 I2V EXTENDED (Wan 2.2 14B Base Model)
14
+
15
+ An advanced Image-to-Video generation platform powered by **Wan 2.2 14B**, optimized for ultra-efficient ZeroGPU quota consumption (~70%+ GPU quota savings).
16
+
17
+ ---
18
+
19
+ ## ⚡ Key Features
20
+
21
+ - **🎬 Wan 2.2 14B Base Model**: High-performance diffusion model with 4-step Lightning acceleration.
22
+ - **⏱️ Prompt Relay Schedule**: Fine-grained temporal text prompt scheduling across video frame timesteps.
23
+ - **⚡ Motion Extension Techniques**:
24
+ - **Real-Time RIFE Interpolation**: 32/64 FPS ultra-smooth playback.
25
+ - **Ending-Only Boomerang Loop**: Real-speed forward playback with tail 1.5s boomerang ping-pong loop.
26
+ - **Classic Full Boomerang Loop**: 100% natural real-speed forward + reverse loop.
27
+ - **Adaptive Motion Speed Ramping**: Cubic smoothstep curve easing.
28
+ - **📸 3-Way Instant Frame Grab**: Grab video frames instantly as Next Input Image, Last Frame, or Face Swap Target.
29
+ - **👤 Standalone Face Swapper & GFPGAN Restoration**: CPU-based face swap and facial detail sharpening.
30
+ - **💎 VIP Remote Acceleration Engine**: High-speed offloaded GPU acceleration with zero CPU load.
31
+
32
+ ---
33
+
34
+ ## 📄 License
35
+ Sulphur AI Project - High-Performance Cinematic Video Generation.
aoti.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ """
3
+
4
+ from typing import cast
5
+
6
+ import torch
7
+ from huggingface_hub import hf_hub_download
8
+ from spaces.zero.torch.aoti import ZeroGPUCompiledModel
9
+ from spaces.zero.torch.aoti import ZeroGPUWeights
10
+ from torch._functorch._aot_autograd.subclass_parametrization import unwrap_tensor_subclass_parameters
11
+
12
+
13
+ def _shallow_clone_module(module: torch.nn.Module) -> torch.nn.Module:
14
+ clone = object.__new__(module.__class__)
15
+ clone.__dict__ = module.__dict__.copy()
16
+ clone._parameters = module._parameters.copy()
17
+ clone._buffers = module._buffers.copy()
18
+ clone._modules = {k: _shallow_clone_module(v) for k, v in module._modules.items() if v is not None}
19
+ return clone
20
+
21
+
22
+ def aoti_blocks_load(module: torch.nn.Module, repo_id: str, variant: str | None = None):
23
+ repeated_blocks = cast(list[str], module._repeated_blocks)
24
+ aoti_files = {name: hf_hub_download(
25
+ repo_id=repo_id,
26
+ filename='package.pt2',
27
+ subfolder=name if variant is None else f'{name}.{variant}',
28
+ ) for name in repeated_blocks}
29
+ for block_name, aoti_file in aoti_files.items():
30
+ for block in module.modules():
31
+ if block.__class__.__name__ == block_name:
32
+ block_ = _shallow_clone_module(block)
33
+ unwrap_tensor_subclass_parameters(block_)
34
+ weights = ZeroGPUWeights(block_.state_dict())
35
+ block.forward = ZeroGPUCompiledModel(aoti_file, weights)
app.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces
2
+ import os
3
+ # os.system('pip install --upgrade --no-deps spaces')
4
+ from ui import create_ui, CSS
5
+
6
+ demo = create_ui()
7
+
8
+ if __name__ == "__main__":
9
+ demo.queue().launch(
10
+ ssr_mode=False,
11
+ server_name="0.0.0.0",
12
+ server_port=7860,
13
+ mcp_server=True,
14
+ css=CSS,
15
+ show_error=True,
16
+ share=True
17
+ )
config.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import warnings
3
+ import numpy as np
4
+ from diffusers import (
5
+ FlowMatchEulerDiscreteScheduler,
6
+ SASolverScheduler,
7
+ DEISMultistepScheduler,
8
+ DPMSolverMultistepInverseScheduler,
9
+ UniPCMultistepScheduler,
10
+ DPMSolverMultistepScheduler,
11
+ DPMSolverSinglestepScheduler,
12
+ )
13
+
14
+ os.environ["TOKENIZERS_PARALLELISM"] = "true"
15
+ warnings.filterwarnings("ignore")
16
+ IS_ZERO_GPU = bool(os.getenv("SPACES_ZERO_GPU"))
17
+
18
+ DT = os.environ.get("DATASET_TOKEN", "")
19
+ SULPHUR_API_URL = os.environ.get("SULPHUR_API_URL", "http://localhost:6666")
20
+ VIP_PASS = os.environ.get("VIP_PASSWORD", "").strip()
21
+
22
+ MODEL_ID = "thornmaze/WAMU_v3_WAN2.2_I2V_LIGHTNING"
23
+ LORA_MODELS = []
24
+ MAX_DIM = 640
25
+ MIN_DIM = 480
26
+ SQUARE_DIM = 576
27
+ MULTIPLE_OF = 16
28
+ MAX_SEED = np.iinfo(np.int32).max
29
+ FIXED_FPS = 16
30
+ MIN_FRAMES_MODEL = 8
31
+ MAX_FRAMES_MODEL = 129
32
+ MIN_DURATION = round(MIN_FRAMES_MODEL / FIXED_FPS, 1)
33
+ MAX_DURATION = 8.0
34
+
35
+ SCHEDULER_MAP = {
36
+ "FlowMatchEulerDiscrete": FlowMatchEulerDiscreteScheduler,
37
+ "SASolver": SASolverScheduler,
38
+ "DEISMultistep": DEISMultistepScheduler,
39
+ "DPMSolverMultistepInverse": DPMSolverMultistepInverseScheduler,
40
+ "UniPCMultistep": UniPCMultistepScheduler,
41
+ "DPMSolverMultistep": DPMSolverMultistepScheduler,
42
+ "DPMSolverSinglestep": DPMSolverSinglestepScheduler,
43
+ }
44
+
45
+ default_prompt_i2v = ""
46
+ default_negative_prompt = (
47
+ "static, motionless, frozen, blurry, low quality, worst quality, JPEG compression artifacts, "
48
+ "overexposed, underexposed, washed-out gray look, noisy, grain, bad anatomy, bad proportions, "
49
+ "deformed, disfigured, malformed limbs, fused fingers, extra fingers, missing fingers, poorly drawn hands, "
50
+ "poorly drawn face, extra limbs, extra legs, extra arms, mutated hands, mutated body, three legs, "
51
+ "motionless image, jitter, flickering, temporal distortion, unnatural motion, morphing artifacts, "
52
+ "glitched movement, walking backwards, floating limbs, text, watermark, logo, subtitles, signature, "
53
+ "cluttered background, bad lighting, stylized artwork, painting, 3d render"
54
+ )
55
+
56
+ def model_title():
57
+ return "## Wan 2.2 I2V 14B Lightning"
face_swapper.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import numpy as np
4
+ from PIL import Image
5
+ from huggingface_hub import hf_hub_download
6
+ from tqdm import tqdm
7
+
8
+ _app = None
9
+ _swapper = None
10
+
11
+ def get_swapper_models():
12
+ global _app, _swapper
13
+ if _app is None or _swapper is None:
14
+ try:
15
+ import insightface
16
+ from insightface.app import FaceAnalysis
17
+
18
+ print("Initializing InsightFace CPU model (det_size=320)...")
19
+ _app = FaceAnalysis(name='buffalo_l', providers=['CPUExecutionProvider'])
20
+ _app.prepare(ctx_id=-1, det_size=(320, 320))
21
+
22
+ # Download inswapper_128.onnx from HF Hub
23
+ hf_token = os.environ.get("HF_TOKEN") or True
24
+ try:
25
+ model_path = hf_hub_download(
26
+ repo_id="ezioruan/inswapper_128.onnx",
27
+ filename="inswapper_128.onnx",
28
+ token=hf_token
29
+ )
30
+ except Exception as dl_err:
31
+ print(f"Primary repo download notice: {dl_err}. Trying fallback...")
32
+ model_path = hf_hub_download(
33
+ repo_id="Gourieff/ReActor",
34
+ filename="models/inswapper_128.onnx",
35
+ repo_type="dataset",
36
+ token=hf_token
37
+ )
38
+ _swapper = insightface.model_zoo.get_model(model_path, providers=['CPUExecutionProvider'])
39
+ print("✅ InsightFace CPU Swapper loaded successfully!")
40
+ except Exception as e:
41
+ print(f"⚠️ Face Swapper load warning: {e}")
42
+ _app = None
43
+ _swapper = None
44
+ return _app, _swapper
45
+
46
+ def swap_face_in_frames(
47
+ source_pil_image: Image.Image,
48
+ frames_np: list,
49
+ ref_face_image: Image.Image = None,
50
+ target_gender: str = "Any / All Faces",
51
+ swap_last_n: int = 4,
52
+ progress=None
53
+ ) -> list:
54
+ """
55
+ Swaps face from ref_face_image (or source_pil_image) into video frames using InsightFace CPU.
56
+ Supports swap_last_n frames (0 = All Frames). If swap_last_n > total_frames, falls back to 2.
57
+ Runs 100% on CPU (0 GPU quota used).
58
+ """
59
+ app_model, swapper_model = get_swapper_models()
60
+ if app_model is None or swapper_model is None:
61
+ print("⚠️ Face Swapper model unavailable. Returning original frames.")
62
+ return frames_np
63
+
64
+ try:
65
+ source_img = ref_face_image if ref_face_image is not None else source_pil_image
66
+ if source_img is None:
67
+ return frames_np
68
+
69
+ source_bgr = cv2.cvtColor(np.array(source_img), cv2.COLOR_RGB2BGR)
70
+ source_faces = app_model.get(source_bgr)
71
+
72
+ if not source_faces:
73
+ print("⚠️ No face detected in source/reference image. Skipping face swap.")
74
+ return frames_np
75
+
76
+ source_faces.sort(key=lambda x: (x.bbox[2]-x.bbox[0]) * (x.bbox[3]-x.bbox[1]), reverse=True)
77
+ source_face = source_faces[0]
78
+
79
+ total_all = len(frames_np)
80
+ swap_last_n = int(swap_last_n)
81
+
82
+ # Fallback calculation
83
+ if swap_last_n == 0:
84
+ n_swap = total_all
85
+ elif swap_last_n > total_all:
86
+ print(f"Notice: swap_last_n ({swap_last_n}) exceeds total frames ({total_all}). Fallback to 2 frames.")
87
+ n_swap = min(2, total_all)
88
+ else:
89
+ n_swap = swap_last_n
90
+
91
+ if n_swap < total_all:
92
+ unchanged_prefix = list(frames_np[:-n_swap])
93
+ target_frames = list(frames_np[-n_swap:])
94
+ else:
95
+ unchanged_prefix = []
96
+ target_frames = list(frames_np)
97
+
98
+ swapped_sub = []
99
+ total_sub = len(target_frames)
100
+ print(f"👤 Processing CPU Face Swap on {total_sub} frames (Last N={n_swap}, Gender filter: {target_gender})...")
101
+
102
+ for idx, frame in enumerate(tqdm(target_frames, desc="👤 CPU Face Swap")):
103
+ if progress is not None:
104
+ try:
105
+ progress((idx + 1) / total_sub, desc=f"👤 Swapping Face on Frame {idx+1}/{total_sub} (CPU)...")
106
+ except Exception:
107
+ pass
108
+
109
+ if isinstance(frame, Image.Image):
110
+ frame_uint8 = cv2.cvtColor(np.array(frame), cv2.COLOR_RGB2BGR)
111
+ elif isinstance(frame, np.ndarray):
112
+ frame_uint8 = (frame * 255).astype(np.uint8) if frame.dtype != np.uint8 else frame.copy()
113
+ frame_uint8 = cv2.cvtColor(frame_uint8, cv2.COLOR_RGB2BGR)
114
+ else:
115
+ frame_uint8 = np.array(frame, dtype=np.uint8)
116
+ frame_uint8 = cv2.cvtColor(frame_uint8, cv2.COLOR_RGB2BGR)
117
+
118
+ target_bgr = frame_uint8
119
+ target_faces = app_model.get(target_bgr)
120
+
121
+ if target_faces:
122
+ res_bgr = target_bgr.copy()
123
+ for target_face in target_faces:
124
+ gender_val = getattr(target_face, 'gender', None)
125
+ sex_val = getattr(target_face, 'sex', None)
126
+
127
+ if target_gender == "Female Faces Only":
128
+ is_female = (gender_val == 0) or (sex_val == 'F')
129
+ if not is_female:
130
+ continue
131
+ elif target_gender == "Male Faces Only":
132
+ is_male = (gender_val == 1) or (sex_val == 'M')
133
+ if not is_male:
134
+ continue
135
+
136
+ res_bgr = swapper_model.get(res_bgr, target_face, source_face, paste_back=True)
137
+
138
+ res_rgb = cv2.cvtColor(res_bgr, cv2.COLOR_BGR2RGB)
139
+ if isinstance(frame, np.ndarray) and frame.dtype != np.uint8:
140
+ swapped_sub.append(res_rgb.astype(np.float32) / 255.0)
141
+ elif isinstance(frame, Image.Image):
142
+ swapped_sub.append(Image.fromarray(res_rgb))
143
+ else:
144
+ swapped_sub.append(res_rgb)
145
+ else:
146
+ swapped_sub.append(frame)
147
+
148
+ final_result = unchanged_prefix + swapped_sub
149
+ print(f"✅ CPU Face Swap complete ({len(swapped_sub)} frames swapped)!")
150
+ return final_result
151
+
152
+ except Exception as e:
153
+ print(f"⚠️ Face Swapper execution error: {e}")
154
+ return frames_np
155
+
156
+ def map_gender_param(target_gender: str) -> str:
157
+ if not target_gender:
158
+ return "all"
159
+ tg = str(target_gender).lower()
160
+ if "female" in tg or "wanita" in tg or "perempuan" in tg:
161
+ return "female"
162
+ elif "male" in tg or "pria" in tg or "laki" in tg:
163
+ return "male"
164
+ return "all"
165
+
166
+ def call_sulphur_faceswap_api(source_img: Image.Image, target_img: Image.Image, target_gender: str = "all", enhance_with_gfpgan: bool = True, server_url: str = None) -> Image.Image:
167
+ """
168
+ Calls Sulphur AI API (/api/v1/faceswap) to perform InsightFace Face Swap + GFPGAN Face Restoration.
169
+ """
170
+ import io
171
+ import requests
172
+ import config
173
+
174
+ target_url = server_url or config.SULPHUR_API_URL or os.environ.get("SULPHUR_API_URL", "http://localhost:6666")
175
+ if not target_url or not str(target_url).strip():
176
+ return None
177
+
178
+ clean_url = str(target_url).strip().rstrip("/")
179
+ endpoint = f"{clean_url}/api/v1/faceswap"
180
+
181
+ try:
182
+ source_bytes = io.BytesIO()
183
+ source_img.convert("RGB").save(source_bytes, format="JPEG", quality=95)
184
+ source_bytes.seek(0)
185
+
186
+ target_bytes = io.BytesIO()
187
+ target_img.convert("RGB").save(target_bytes, format="JPEG", quality=95)
188
+ target_bytes.seek(0)
189
+
190
+ files = {
191
+ "source_image": ("source.jpg", source_bytes, "image/jpeg"),
192
+ "target_image": ("target.jpg", target_bytes, "image/jpeg")
193
+ }
194
+ data = {
195
+ "enhance_with_gfpgan": "true" if enhance_with_gfpgan else "false",
196
+ "target_gender": map_gender_param(target_gender)
197
+ }
198
+
199
+ print(f"🌐 Calling Sulphur AI Face Swap API at {endpoint} (Gender: {map_gender_param(target_gender)})...")
200
+ res = requests.post(endpoint, files=files, data=data, timeout=15)
201
+ if res.status_code == 200 and res.content:
202
+ result_img = Image.open(io.BytesIO(res.content)).convert("RGB")
203
+ print("✅ Sulphur AI Face Swap + GFPGAN API succeeded!")
204
+ return result_img
205
+ else:
206
+ print(f"⚠️ Sulphur AI Face Swap API returned status {res.status_code}")
207
+ except Exception as e:
208
+ print(f"⚠️ Sulphur AI Face Swap API notice: {e}")
209
+ return None
210
+
211
+ def call_sulphur_enhance_face_api(image: Image.Image, server_url: str = None) -> Image.Image:
212
+ """
213
+ Calls Sulphur AI API (/api/v1/enhance-face) to sharpen & restore face details via GFPGAN v1.4.
214
+ """
215
+ import io
216
+ import requests
217
+ import config
218
+
219
+ target_url = server_url or config.SULPHUR_API_URL or os.environ.get("SULPHUR_API_URL", "http://localhost:6666")
220
+ if not target_url or not str(target_url).strip():
221
+ return None
222
+
223
+ clean_url = str(target_url).strip().rstrip("/")
224
+ endpoint = f"{clean_url}/api/v1/enhance-face"
225
+
226
+ try:
227
+ img_bytes = io.BytesIO()
228
+ image.convert("RGB").save(img_bytes, format="JPEG", quality=95)
229
+ img_bytes.seek(0)
230
+
231
+ files = {"image": ("face.jpg", img_bytes, "image/jpeg")}
232
+
233
+ print(f"🌐 Calling Sulphur AI GFPGAN Face Enhance API at {endpoint}...")
234
+ res = requests.post(endpoint, files=files, timeout=15)
235
+ if res.status_code == 200 and res.content:
236
+ result_img = Image.open(io.BytesIO(res.content)).convert("RGB")
237
+ print("✅ Sulphur AI GFPGAN Face Enhance succeeded!")
238
+ return result_img
239
+ else:
240
+ print(f"⚠️ Sulphur AI Face Enhance API status {res.status_code}")
241
+ except Exception as e:
242
+ print(f"⚠️ Sulphur AI Face Enhance API notice: {e}")
243
+ return None
244
+
245
+ def swap_face_in_single_image(
246
+ target_pil_image: Image.Image,
247
+ ref_face_image: Image.Image = None,
248
+ target_gender: str = "Any / All Faces",
249
+ enhance_with_gfpgan: bool = True
250
+ ) -> Image.Image:
251
+ """
252
+ Swaps face on a single PIL image strictly using local CPU InsightFace (0 GPU quota).
253
+ Returns swapped PIL Image.
254
+ """
255
+ if target_pil_image is None:
256
+ return None
257
+
258
+ # Execute strictly on local CPU InsightFace
259
+ swapped_frames = swap_face_in_frames(
260
+ source_pil_image=target_pil_image,
261
+ frames_np=[target_pil_image],
262
+ ref_face_image=ref_face_image,
263
+ target_gender=target_gender,
264
+ swap_last_n=0
265
+ )
266
+ res_frame = swapped_frames[0]
267
+ if isinstance(res_frame, Image.Image):
268
+ return res_frame
269
+ elif isinstance(res_frame, np.ndarray):
270
+ if res_frame.dtype != np.uint8:
271
+ res_frame = (res_frame * 255).astype(np.uint8)
272
+ return Image.fromarray(res_frame)
273
+ return target_pil_image
image_utils.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import urllib.request
3
+ import cv2
4
+ import numpy as np
5
+ from PIL import Image
6
+ from config import (
7
+ MAX_DIM, MIN_DIM, SQUARE_DIM, MULTIPLE_OF,
8
+ FIXED_FPS, MIN_FRAMES_MODEL, MAX_FRAMES_MODEL
9
+ )
10
+
11
+ def load_image_from_url(url: str) -> Image.Image:
12
+ if not url or not str(url).strip():
13
+ raise ValueError("Masukkan URL gambar terlebih dahulu.")
14
+ url = str(url).strip()
15
+ headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"}
16
+ req = urllib.request.Request(url, headers=headers)
17
+ with urllib.request.urlopen(req, timeout=15) as resp:
18
+ img_bytes = resp.read()
19
+ img = Image.open(io.BytesIO(img_bytes))
20
+ return img.convert("RGB")
21
+
22
+ get_timestamp_js = """
23
+ function(video, timestamp) {
24
+ const videoElem = document.querySelector('#generated-video video');
25
+ let currentTime = 0;
26
+ if (videoElem) {
27
+ currentTime = videoElem.currentTime;
28
+ console.log("Video found! Time: " + currentTime);
29
+ } else {
30
+ console.log("No video element found.");
31
+ }
32
+ return [video, currentTime];
33
+ }
34
+ """
35
+
36
+ def extract_frame(video_path, timestamp):
37
+ if not video_path:
38
+ return None, 0
39
+ print(f"Extracting frame at timestamp: {timestamp}")
40
+ cap = cv2.VideoCapture(video_path)
41
+ if not cap.isOpened():
42
+ return None, timestamp
43
+
44
+ fps = cap.get(cv2.CAP_PROP_FPS)
45
+ if fps <= 0:
46
+ fps = 16.0
47
+ target_frame_num = int(float(timestamp) * fps)
48
+
49
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
50
+ if total_frames > 0 and target_frame_num >= total_frames:
51
+ target_frame_num = total_frames - 1
52
+
53
+ cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame_num)
54
+ ret, frame = cap.read()
55
+ cap.release()
56
+
57
+ if ret:
58
+ return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB), timestamp
59
+ return None, timestamp
60
+
61
+ def resize_image(image: Image.Image) -> Image.Image:
62
+ width, height = image.size
63
+ if width == height:
64
+ return image.resize((SQUARE_DIM, SQUARE_DIM), Image.LANCZOS)
65
+
66
+ aspect_ratio = width / height
67
+ MAX_ASPECT_RATIO = MAX_DIM / MIN_DIM
68
+ MIN_ASPECT_RATIO = MIN_DIM / MAX_DIM
69
+
70
+ image_to_resize = image
71
+ if aspect_ratio > MAX_ASPECT_RATIO:
72
+ target_w, target_h = MAX_DIM, MIN_DIM
73
+ crop_width = int(round(height * MAX_ASPECT_RATIO))
74
+ left = (width - crop_width) // 2
75
+ image_to_resize = image.crop((left, 0, left + crop_width, height))
76
+ elif aspect_ratio < MIN_ASPECT_RATIO:
77
+ target_w, target_h = MIN_DIM, MAX_DIM
78
+ crop_height = int(round(width / MIN_ASPECT_RATIO))
79
+ top = (height - crop_height) // 2
80
+ image_to_resize = image.crop((0, top, width, top + crop_height))
81
+ else:
82
+ if width > height:
83
+ target_w = MAX_DIM
84
+ target_h = int(round(target_w / aspect_ratio))
85
+ else:
86
+ target_h = MAX_DIM
87
+ target_w = int(round(target_h * aspect_ratio))
88
+
89
+ final_w = round(target_w / MULTIPLE_OF) * MULTIPLE_OF
90
+ final_h = round(target_h / MULTIPLE_OF) * MULTIPLE_OF
91
+ final_w = max(MIN_DIM, min(MAX_DIM, final_w))
92
+ final_h = max(MIN_DIM, min(MAX_DIM, final_h))
93
+ return image_to_resize.resize((final_w, final_h), Image.LANCZOS)
94
+
95
+ def resize_and_crop_to_match(target_image, reference_image):
96
+ ref_width, ref_height = reference_image.size
97
+ target_width, target_height = target_image.size
98
+ scale = max(ref_width / target_width, ref_height / target_height)
99
+ new_width, new_height = int(target_width * scale), int(target_height * scale)
100
+ resized = target_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
101
+ left, top = (new_width - ref_width) // 2, (new_height - ref_height) // 2
102
+ return resized.crop((left, top, left + ref_width, top + ref_height))
103
+
104
+ def get_num_frames(duration_seconds: float):
105
+ raw = int(round(duration_seconds * FIXED_FPS))
106
+ raw = max(MIN_FRAMES_MODEL, min(MAX_FRAMES_MODEL, raw))
107
+ return ((raw - 1) // 4) * 4 + 1
lora_loader.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom LoRA Loader for WAN 2.2 I2V.
3
+ Add your custom LoRA models (Hugging Face or direct Civitai URLs) in the EXTRA dictionary below.
4
+ """
5
+ import os
6
+ import urllib.parse
7
+ import urllib.request
8
+ import urllib.error
9
+ import re
10
+ import hashlib
11
+ import inspect
12
+ from huggingface_hub import hf_hub_download
13
+
14
+ # Monkey patch for peft TorchaoLoraLinear bug (compatibility between peft 0.19.1 and diffusers)
15
+ try:
16
+ import peft.tuners.lora.torchao as peft_torchao
17
+ if hasattr(peft_torchao, "TorchaoLoraLinear"):
18
+ _orig_torchao_init = peft_torchao.TorchaoLoraLinear.__init__
19
+ _sig = inspect.signature(_orig_torchao_init)
20
+ if "get_apply_tensor_subclass" in _sig.parameters:
21
+ _param = _sig.parameters["get_apply_tensor_subclass"]
22
+ if _param.default is inspect.Parameter.empty:
23
+ def _patched_torchao_init(self, *args, **kwargs):
24
+ if "get_apply_tensor_subclass" not in kwargs:
25
+ base_layer = args[0] if args else kwargs.get("base_layer", None)
26
+ get_subclass_fn = getattr(base_layer, "get_apply_tensor_subclass", None) if base_layer else None
27
+ kwargs["get_apply_tensor_subclass"] = get_subclass_fn
28
+ return _orig_torchao_init(self, *args, **kwargs)
29
+ peft_torchao.TorchaoLoraLinear.__init__ = _patched_torchao_init
30
+ print("✅ Applied peft TorchaoLoraLinear compatibility patch.")
31
+ except Exception as patch_err:
32
+ print(f"TorchaoLoraLinear patch notice: {patch_err}")
33
+
34
+ HF_TOKEN = os.environ.get("HF_TOKEN") # authenticated downloads (covers private repos)
35
+ CIVITAI_TOKEN = os.environ.get("CIVITAI_TOKEN", "")
36
+
37
+ # Pinned commit hashes if needed (optional)
38
+ PINNED_REVISIONS = {}
39
+
40
+ LORA_FILES = []
41
+
42
+ # group -> {"HIGH": (repo, file) | url | None, "LOW": (repo, file) | url | None}
43
+ LORA_PAIRS = {}
44
+ for f in LORA_FILES:
45
+ name = urllib.parse.unquote(f).replace(".safetensors", "")
46
+ is_high = bool(re.search(r'(high|HN|_H\b)', name, re.IGNORECASE))
47
+ is_low = bool(re.search(r'(low|LN|_L\b)', name, re.IGNORECASE))
48
+ group = re.sub(r'[\s_-]*(high|low|noise|HN|LN)([\s_-]*noise)?[\s_-]*(v?\d+(\.\d+)?)?\s*$', '', name, flags=re.IGNORECASE).strip()
49
+ group = re.sub(r'[\s_]+$', '', group)
50
+ LORA_PAIRS.setdefault(group, {"HIGH": None, "LOW": None})
51
+
52
+ # Custom LoRAs dictionary (label -> URL or (repo_id, high_file, low_file|None))
53
+ # Examples:
54
+ # EXTRA = {
55
+ # "My Civitai LoRA": "https://civitai.red/api/download/models/2098405?fileId=1994044",
56
+ # "My HF Dual LoRA": ("username/my-lora-repo", "motion_high.safetensors", "motion_low.safetensors"),
57
+ # }
58
+ EXTRA = {
59
+ "lopi999 - Wan2.2 I2V General NSFW LoRA (Trigger: nsfwsks)": (
60
+ "lopi999/Wan2.2-I2V_General-NSFW-LoRA",
61
+ "NSFW-22-H-e8.safetensors",
62
+ "NSFW-22-L-e8.safetensors"
63
+ ),
64
+ }
65
+
66
+ for label, item in EXTRA.items():
67
+ LORA_PAIRS.setdefault(label, {"HIGH": None, "LOW": None})
68
+ if isinstance(item, str):
69
+ LORA_PAIRS[label]["HIGH"] = item
70
+ elif isinstance(item, (tuple, list)):
71
+ if len(item) == 3:
72
+ repo, hi, lo = item
73
+ if isinstance(repo, str) and (repo.startswith("http://") or repo.startswith("https://")):
74
+ LORA_PAIRS[label]["HIGH"] = repo
75
+ if hi and isinstance(hi, str) and (hi.startswith("http://") or hi.startswith("https://")):
76
+ LORA_PAIRS[label]["LOW"] = hi
77
+ else:
78
+ if hi:
79
+ LORA_PAIRS[label]["HIGH"] = (repo, hi)
80
+ if lo:
81
+ LORA_PAIRS[label]["LOW"] = (repo, lo)
82
+ elif len(item) == 2:
83
+ hi, lo = item
84
+ if hi:
85
+ LORA_PAIRS[label]["HIGH"] = hi
86
+ if lo:
87
+ LORA_PAIRS[label]["LOW"] = lo
88
+
89
+
90
+ def download_file_from_url(url, custom_name=None):
91
+ os.makedirs("loras", exist_ok=True)
92
+ civitai_tok = os.environ.get("CIVITAI_TOKEN", "") or CIVITAI_TOKEN
93
+
94
+ url_to_fetch = url
95
+ if "civitai" in url.lower() and civitai_tok and "token=" not in url.lower():
96
+ sep = "&" if "?" in url else "?"
97
+ url_to_fetch = f"{url}{sep}token={civitai_tok}"
98
+
99
+ if not custom_name:
100
+ url_hash = hashlib.md5(url.encode()).hexdigest()[:8]
101
+ custom_name = f"lora_{url_hash}.safetensors"
102
+
103
+ local_path = os.path.join("loras", custom_name)
104
+ if os.path.exists(local_path) and os.path.getsize(local_path) > 1000:
105
+ return local_path
106
+
107
+ print(f"📥 Downloading LoRA from URL: {url_to_fetch} ...")
108
+ req = urllib.request.Request(url_to_fetch, headers={"User-Agent": "Mozilla/5.0"})
109
+
110
+ try:
111
+ with urllib.request.urlopen(req) as response:
112
+ cd = response.headers.get("Content-Disposition", "")
113
+ if "filename=" in cd:
114
+ fname = re.findall(r'filename="?([^";]+)"?', cd)
115
+ if fname:
116
+ real_name = fname[0].strip()
117
+ if not real_name.endswith(".safetensors"):
118
+ real_name += ".safetensors"
119
+ alt_path = os.path.join("loras", real_name)
120
+ if os.path.exists(alt_path) and os.path.getsize(alt_path) > 1000:
121
+ return alt_path
122
+ local_path = alt_path
123
+
124
+ with open(local_path, "wb") as f:
125
+ while True:
126
+ chunk = response.read(8192)
127
+ if not chunk:
128
+ break
129
+ f.write(chunk)
130
+ except urllib.error.HTTPError as e:
131
+ if e.code == 401:
132
+ raise Exception(
133
+ "Download failed (401 Unauthorized). Civitai requires an API Token. "
134
+ "Add ?token=YOUR_CIVITAI_API_KEY to the download URL or set CIVITAI_TOKEN environment variable."
135
+ )
136
+ raise Exception(f"Failed to download LoRA from URL (Status {e.code}): {e.reason}")
137
+ except Exception as e:
138
+ raise Exception(f"Failed to download LoRA from URL: {e}")
139
+
140
+ print(f"✅ Downloaded LoRA successfully: {local_path} ({os.path.getsize(local_path)} bytes)")
141
+ return local_path
142
+
143
+
144
+ def get_lora_choices():
145
+ choices = []
146
+ for group in sorted(LORA_PAIRS.keys()):
147
+ p = LORA_PAIRS[group]
148
+ if p["HIGH"] and p["LOW"]:
149
+ choices.append(group)
150
+ elif p["HIGH"]:
151
+ choices.append(f"{group} (HIGH only)")
152
+ elif p["LOW"]:
153
+ choices.append(f"{group} (LOW only)")
154
+ return choices
155
+
156
+
157
+ def download_lora(group_name):
158
+ if not group_name:
159
+ return None, None
160
+ clean_name = re.sub(r'\s*\(HIGH only\)|\s*\(LOW only\)', '', group_name)
161
+ if clean_name not in LORA_PAIRS:
162
+ return None, None
163
+ pair = LORA_PAIRS[clean_name]
164
+
165
+ def resolve_entry(entry):
166
+ if not entry:
167
+ return None
168
+ if isinstance(entry, str) and (entry.startswith("http://") or entry.startswith("https://")):
169
+ return download_file_from_url(entry)
170
+ elif isinstance(entry, (tuple, list)) and len(entry) == 2:
171
+ repo, fn = entry
172
+ if isinstance(repo, str) and (repo.startswith("http://") or repo.startswith("https://")):
173
+ return download_file_from_url(repo)
174
+ rev = PINNED_REVISIONS.get(repo)
175
+ return hf_hub_download(repo, fn, token=HF_TOKEN, revision=rev) if rev else hf_hub_download(repo, fn, token=HF_TOKEN)
176
+ return None
177
+
178
+ high_path = resolve_entry(pair["HIGH"])
179
+ low_path = resolve_entry(pair["LOW"])
180
+ return high_path, low_path
181
+
182
+
183
+ def load_lora_to_pipe(pipe, group_name, adapter_name="lora"):
184
+ high_path, low_path = download_lora(group_name)
185
+ if high_path and low_path:
186
+ pipe.load_lora_weights(high_path, adapter_name=f"{adapter_name}_high")
187
+ pipe.load_lora_weights(low_path, adapter_name=f"{adapter_name}_low")
188
+ print(f"Loaded LoRA pair: {group_name}")
189
+ return True
190
+ elif high_path:
191
+ pipe.load_lora_weights(high_path, adapter_name=adapter_name)
192
+ print(f"Loaded LoRA: {group_name}")
193
+ return True
194
+ elif low_path:
195
+ pipe.load_lora_weights(low_path, adapter_name=adapter_name)
196
+ print(f"Loaded LoRA (low): {group_name}")
197
+ return True
198
+ return False
199
+
200
+
201
+ def unload_lora(pipe):
202
+ try:
203
+ pipe.unload_lora_weights()
204
+ except:
205
+ pass
206
+
207
+
208
+ def load_custom_url_lora(pipe, url, adapter_name="custom_lora", scale=1.0):
209
+ if not url or not str(url).strip():
210
+ return False
211
+ url = str(url).strip()
212
+ try:
213
+ file_path = download_file_from_url(url)
214
+ if file_path and os.path.exists(file_path):
215
+ pipe.load_lora_weights(file_path, adapter_name=adapter_name)
216
+ if hasattr(pipe, "set_adapters"):
217
+ try:
218
+ pipe.set_adapters([adapter_name], adapter_weights=[float(scale)])
219
+ except Exception:
220
+ pass
221
+ print(f"✅ Loaded Custom URL LoRA: {url} (scale={scale})")
222
+ return True
223
+ except Exception as e:
224
+ print(f"❌ Failed to load custom URL LoRA: {e}")
225
+ return False
226
+
227
+
228
+ def download_custom_lora_ui_action(url):
229
+ if not url or not str(url).strip():
230
+ return "<div style='background: rgba(239, 68, 68, 0.15); border: 1.5px solid rgba(239, 68, 68, 0.4); border-radius: 12px; padding: 12px 16px; color: #f87171; font-weight: 600;'>⚠️ Please enter a valid LoRA download URL first.</div>"
231
+ try:
232
+ path = download_file_from_url(str(url).strip())
233
+ size_mb = round(os.path.getsize(path) / (1024 * 1024), 2)
234
+ fname = os.path.basename(path)
235
+ return (
236
+ f"<div style='background: linear-gradient(135deg, rgba(16, 185, 129, 0.22) 0%, rgba(5, 150, 105, 0.12) 100%); "
237
+ f"border: 1.5px solid #10b981; border-radius: 14px; padding: 14px 18px; color: #34d399; font-weight: 700; "
238
+ f"box-shadow: 0 6px 20px rgba(16, 185, 129, 0.25); backdrop-filter: blur(10px); margin-top: 10px;'>"
239
+ f"✅ <b>Custom LoRA Successfully Downloaded & Ready!</b><br>"
240
+ f"<span style='font-weight: 500; font-size: 0.88rem; color: #e2e8f0; margin-top: 4px; display: inline-block;'>"
241
+ f"📦 File: <code>{fname}</code> ({size_mb} MB) • Cached to CPU memory with <b>0 GPU Quota consumed</b>. "
242
+ f"Will automatically fuse on your next video generation!</span></div>"
243
+ )
244
+ except Exception as e:
245
+ return f"<div style='background: rgba(239, 68, 68, 0.15); border: 1.5px solid rgba(239, 68, 68, 0.4); border-radius: 12px; padding: 12px 16px; color: #f87171; font-weight: 600;'>❌ Failed to download custom LoRA: {e}</div>"
246
+
model/loss.py ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import numpy as np
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ import torchvision.models as models
6
+
7
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
8
+
9
+
10
+ class EPE(nn.Module):
11
+ def __init__(self):
12
+ super(EPE, self).__init__()
13
+
14
+ def forward(self, flow, gt, loss_mask):
15
+ loss_map = (flow - gt.detach()) ** 2
16
+ loss_map = (loss_map.sum(1, True) + 1e-6) ** 0.5
17
+ return (loss_map * loss_mask)
18
+
19
+
20
+ class Ternary(nn.Module):
21
+ def __init__(self):
22
+ super(Ternary, self).__init__()
23
+ patch_size = 7
24
+ out_channels = patch_size * patch_size
25
+ self.w = np.eye(out_channels).reshape(
26
+ (patch_size, patch_size, 1, out_channels))
27
+ self.w = np.transpose(self.w, (3, 2, 0, 1))
28
+ self.w = torch.tensor(self.w).float().to(device)
29
+
30
+ def transform(self, img):
31
+ patches = F.conv2d(img, self.w, padding=3, bias=None)
32
+ transf = patches - img
33
+ transf_norm = transf / torch.sqrt(0.81 + transf**2)
34
+ return transf_norm
35
+
36
+ def rgb2gray(self, rgb):
37
+ r, g, b = rgb[:, 0:1, :, :], rgb[:, 1:2, :, :], rgb[:, 2:3, :, :]
38
+ gray = 0.2989 * r + 0.5870 * g + 0.1140 * b
39
+ return gray
40
+
41
+ def hamming(self, t1, t2):
42
+ dist = (t1 - t2) ** 2
43
+ dist_norm = torch.mean(dist / (0.1 + dist), 1, True)
44
+ return dist_norm
45
+
46
+ def valid_mask(self, t, padding):
47
+ n, _, h, w = t.size()
48
+ inner = torch.ones(n, 1, h - 2 * padding, w - 2 * padding).type_as(t)
49
+ mask = F.pad(inner, [padding] * 4)
50
+ return mask
51
+
52
+ def forward(self, img0, img1):
53
+ img0 = self.transform(self.rgb2gray(img0))
54
+ img1 = self.transform(self.rgb2gray(img1))
55
+ return self.hamming(img0, img1) * self.valid_mask(img0, 1)
56
+
57
+
58
+ class SOBEL(nn.Module):
59
+ def __init__(self):
60
+ super(SOBEL, self).__init__()
61
+ self.kernelX = torch.tensor([
62
+ [1, 0, -1],
63
+ [2, 0, -2],
64
+ [1, 0, -1],
65
+ ]).float()
66
+ self.kernelY = self.kernelX.clone().T
67
+ self.kernelX = self.kernelX.unsqueeze(0).unsqueeze(0).to(device)
68
+ self.kernelY = self.kernelY.unsqueeze(0).unsqueeze(0).to(device)
69
+
70
+ def forward(self, pred, gt):
71
+ N, C, H, W = pred.shape[0], pred.shape[1], pred.shape[2], pred.shape[3]
72
+ img_stack = torch.cat(
73
+ [pred.reshape(N*C, 1, H, W), gt.reshape(N*C, 1, H, W)], 0)
74
+ sobel_stack_x = F.conv2d(img_stack, self.kernelX, padding=1)
75
+ sobel_stack_y = F.conv2d(img_stack, self.kernelY, padding=1)
76
+ pred_X, gt_X = sobel_stack_x[:N*C], sobel_stack_x[N*C:]
77
+ pred_Y, gt_Y = sobel_stack_y[:N*C], sobel_stack_y[N*C:]
78
+
79
+ L1X, L1Y = torch.abs(pred_X-gt_X), torch.abs(pred_Y-gt_Y)
80
+ loss = (L1X+L1Y)
81
+ return loss
82
+
83
+ class MeanShift(nn.Conv2d):
84
+ def __init__(self, data_mean, data_std, data_range=1, norm=True):
85
+ c = len(data_mean)
86
+ super(MeanShift, self).__init__(c, c, kernel_size=1)
87
+ std = torch.Tensor(data_std)
88
+ self.weight.data = torch.eye(c).view(c, c, 1, 1)
89
+ if norm:
90
+ self.weight.data.div_(std.view(c, 1, 1, 1))
91
+ self.bias.data = -1 * data_range * torch.Tensor(data_mean)
92
+ self.bias.data.div_(std)
93
+ else:
94
+ self.weight.data.mul_(std.view(c, 1, 1, 1))
95
+ self.bias.data = data_range * torch.Tensor(data_mean)
96
+ self.requires_grad = False
97
+
98
+ class VGGPerceptualLoss(torch.nn.Module):
99
+ def __init__(self, rank=0):
100
+ super(VGGPerceptualLoss, self).__init__()
101
+ blocks = []
102
+ pretrained = True
103
+ self.vgg_pretrained_features = models.vgg19(pretrained=pretrained).features
104
+ self.normalize = MeanShift([0.485, 0.456, 0.406], [0.229, 0.224, 0.225], norm=True).cuda()
105
+ for param in self.parameters():
106
+ param.requires_grad = False
107
+
108
+ def forward(self, X, Y, indices=None):
109
+ X = self.normalize(X)
110
+ Y = self.normalize(Y)
111
+ indices = [2, 7, 12, 21, 30]
112
+ weights = [1.0/2.6, 1.0/4.8, 1.0/3.7, 1.0/5.6, 10/1.5]
113
+ k = 0
114
+ loss = 0
115
+ for i in range(indices[-1]):
116
+ X = self.vgg_pretrained_features[i](X)
117
+ Y = self.vgg_pretrained_features[i](Y)
118
+ if (i+1) in indices:
119
+ loss += weights[k] * (X - Y.detach()).abs().mean() * 0.1
120
+ k += 1
121
+ return loss
122
+
123
+ if __name__ == '__main__':
124
+ img0 = torch.zeros(3, 3, 256, 256).float().to(device)
125
+ img1 = torch.tensor(np.random.normal(
126
+ 0, 1, (3, 3, 256, 256))).float().to(device)
127
+ ternary_loss = Ternary()
128
+ print(ternary_loss(img0, img1).shape)
model/pytorch_msssim/__init__.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+ from math import exp
4
+ import numpy as np
5
+
6
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
7
+
8
+ def gaussian(window_size, sigma):
9
+ gauss = torch.Tensor([exp(-(x - window_size//2)**2/float(2*sigma**2)) for x in range(window_size)])
10
+ return gauss/gauss.sum()
11
+
12
+
13
+ def create_window(window_size, channel=1):
14
+ _1D_window = gaussian(window_size, 1.5).unsqueeze(1)
15
+ _2D_window = _1D_window.mm(_1D_window.t()).float().unsqueeze(0).unsqueeze(0).to(device)
16
+ window = _2D_window.expand(channel, 1, window_size, window_size).contiguous()
17
+ return window
18
+
19
+ def create_window_3d(window_size, channel=1):
20
+ _1D_window = gaussian(window_size, 1.5).unsqueeze(1)
21
+ _2D_window = _1D_window.mm(_1D_window.t())
22
+ _3D_window = _2D_window.unsqueeze(2) @ (_1D_window.t())
23
+ window = _3D_window.expand(1, channel, window_size, window_size, window_size).contiguous().to(device)
24
+ return window
25
+
26
+
27
+ def ssim(img1, img2, window_size=11, window=None, size_average=True, full=False, val_range=None):
28
+ # Value range can be different from 255. Other common ranges are 1 (sigmoid) and 2 (tanh).
29
+ if val_range is None:
30
+ if torch.max(img1) > 128:
31
+ max_val = 255
32
+ else:
33
+ max_val = 1
34
+
35
+ if torch.min(img1) < -0.5:
36
+ min_val = -1
37
+ else:
38
+ min_val = 0
39
+ L = max_val - min_val
40
+ else:
41
+ L = val_range
42
+
43
+ padd = 0
44
+ (_, channel, height, width) = img1.size()
45
+ if window is None:
46
+ real_size = min(window_size, height, width)
47
+ window = create_window(real_size, channel=channel).to(img1.device).type_as(img1)
48
+
49
+ mu1 = F.conv2d(F.pad(img1, (5, 5, 5, 5), mode='replicate'), window, padding=padd, groups=channel)
50
+ mu2 = F.conv2d(F.pad(img2, (5, 5, 5, 5), mode='replicate'), window, padding=padd, groups=channel)
51
+
52
+ mu1_sq = mu1.pow(2)
53
+ mu2_sq = mu2.pow(2)
54
+ mu1_mu2 = mu1 * mu2
55
+
56
+ sigma1_sq = F.conv2d(F.pad(img1 * img1, (5, 5, 5, 5), 'replicate'), window, padding=padd, groups=channel) - mu1_sq
57
+ sigma2_sq = F.conv2d(F.pad(img2 * img2, (5, 5, 5, 5), 'replicate'), window, padding=padd, groups=channel) - mu2_sq
58
+ sigma12 = F.conv2d(F.pad(img1 * img2, (5, 5, 5, 5), 'replicate'), window, padding=padd, groups=channel) - mu1_mu2
59
+
60
+ C1 = (0.01 * L) ** 2
61
+ C2 = (0.03 * L) ** 2
62
+
63
+ v1 = 2.0 * sigma12 + C2
64
+ v2 = sigma1_sq + sigma2_sq + C2
65
+ cs = torch.mean(v1 / v2) # contrast sensitivity
66
+
67
+ ssim_map = ((2 * mu1_mu2 + C1) * v1) / ((mu1_sq + mu2_sq + C1) * v2)
68
+
69
+ if size_average:
70
+ ret = ssim_map.mean()
71
+ else:
72
+ ret = ssim_map.mean(1).mean(1).mean(1)
73
+
74
+ if full:
75
+ return ret, cs
76
+ return ret
77
+
78
+
79
+ def ssim_matlab(img1, img2, window_size=11, window=None, size_average=True, full=False, val_range=None):
80
+ # Value range can be different from 255. Other common ranges are 1 (sigmoid) and 2 (tanh).
81
+ if val_range is None:
82
+ if torch.max(img1) > 128:
83
+ max_val = 255
84
+ else:
85
+ max_val = 1
86
+
87
+ if torch.min(img1) < -0.5:
88
+ min_val = -1
89
+ else:
90
+ min_val = 0
91
+ L = max_val - min_val
92
+ else:
93
+ L = val_range
94
+
95
+ padd = 0
96
+ (_, _, height, width) = img1.size()
97
+ if window is None:
98
+ real_size = min(window_size, height, width)
99
+ window = create_window_3d(real_size, channel=1).to(img1.device).type_as(img1)
100
+ # Channel is set to 1 since we consider color images as volumetric images
101
+
102
+ img1 = img1.unsqueeze(1)
103
+ img2 = img2.unsqueeze(1)
104
+
105
+ mu1 = F.conv3d(F.pad(img1, (5, 5, 5, 5, 5, 5), mode='replicate'), window, padding=padd, groups=1)
106
+ mu2 = F.conv3d(F.pad(img2, (5, 5, 5, 5, 5, 5), mode='replicate'), window, padding=padd, groups=1)
107
+
108
+ mu1_sq = mu1.pow(2)
109
+ mu2_sq = mu2.pow(2)
110
+ mu1_mu2 = mu1 * mu2
111
+
112
+ sigma1_sq = F.conv3d(F.pad(img1 * img1, (5, 5, 5, 5, 5, 5), 'replicate'), window, padding=padd, groups=1) - mu1_sq
113
+ sigma2_sq = F.conv3d(F.pad(img2 * img2, (5, 5, 5, 5, 5, 5), 'replicate'), window, padding=padd, groups=1) - mu2_sq
114
+ sigma12 = F.conv3d(F.pad(img1 * img2, (5, 5, 5, 5, 5, 5), 'replicate'), window, padding=padd, groups=1) - mu1_mu2
115
+
116
+ C1 = (0.01 * L) ** 2
117
+ C2 = (0.03 * L) ** 2
118
+
119
+ v1 = 2.0 * sigma12 + C2
120
+ v2 = sigma1_sq + sigma2_sq + C2
121
+ cs = torch.mean(v1 / v2) # contrast sensitivity
122
+
123
+ ssim_map = ((2 * mu1_mu2 + C1) * v1) / ((mu1_sq + mu2_sq + C1) * v2)
124
+
125
+ if size_average:
126
+ ret = ssim_map.mean()
127
+ else:
128
+ ret = ssim_map.mean(1).mean(1).mean(1)
129
+
130
+ if full:
131
+ return ret, cs
132
+ return ret
133
+
134
+
135
+ def msssim(img1, img2, window_size=11, size_average=True, val_range=None, normalize=False):
136
+ device = img1.device
137
+ weights = torch.FloatTensor([0.0448, 0.2856, 0.3001, 0.2363, 0.1333]).to(device).type_as(img1)
138
+ levels = weights.size()[0]
139
+ mssim = []
140
+ mcs = []
141
+ for _ in range(levels):
142
+ sim, cs = ssim(img1, img2, window_size=window_size, size_average=size_average, full=True, val_range=val_range)
143
+ mssim.append(sim)
144
+ mcs.append(cs)
145
+
146
+ img1 = F.avg_pool2d(img1, (2, 2))
147
+ img2 = F.avg_pool2d(img2, (2, 2))
148
+
149
+ mssim = torch.stack(mssim)
150
+ mcs = torch.stack(mcs)
151
+
152
+ # Normalize (to avoid NaNs during training unstable models, not compliant with original definition)
153
+ if normalize:
154
+ mssim = (mssim + 1) / 2
155
+ mcs = (mcs + 1) / 2
156
+
157
+ pow1 = mcs ** weights
158
+ pow2 = mssim ** weights
159
+ # From Matlab implementation https://ece.uwaterloo.ca/~z70wang/research/iwssim/
160
+ output = torch.prod(pow1[:-1] * pow2[-1])
161
+ return output
162
+
163
+
164
+ # Classes to re-use window
165
+ class SSIM(torch.nn.Module):
166
+ def __init__(self, window_size=11, size_average=True, val_range=None):
167
+ super(SSIM, self).__init__()
168
+ self.window_size = window_size
169
+ self.size_average = size_average
170
+ self.val_range = val_range
171
+
172
+ # Assume 3 channel for SSIM
173
+ self.channel = 3
174
+ self.window = create_window(window_size, channel=self.channel)
175
+
176
+ def forward(self, img1, img2):
177
+ (_, channel, _, _) = img1.size()
178
+
179
+ if channel == self.channel and self.window.dtype == img1.dtype:
180
+ window = self.window
181
+ else:
182
+ window = create_window(self.window_size, channel).to(img1.device).type(img1.dtype)
183
+ self.window = window
184
+ self.channel = channel
185
+
186
+ _ssim = ssim(img1, img2, window=window, window_size=self.window_size, size_average=self.size_average)
187
+ dssim = (1 - _ssim) / 2
188
+ return dssim
189
+
190
+ class MSSSIM(torch.nn.Module):
191
+ def __init__(self, window_size=11, size_average=True, channel=3):
192
+ super(MSSSIM, self).__init__()
193
+ self.window_size = window_size
194
+ self.size_average = size_average
195
+ self.channel = channel
196
+
197
+ def forward(self, img1, img2):
198
+ return msssim(img1, img2, window_size=self.window_size, size_average=self.size_average)
model/warplayer.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
5
+ backwarp_tenGrid = {}
6
+
7
+
8
+ def warp(tenInput, tenFlow):
9
+ k = (str(tenFlow.device), str(tenFlow.size()))
10
+ if k not in backwarp_tenGrid:
11
+ tenHorizontal = torch.linspace(-1.0, 1.0, tenFlow.shape[3], device=tenFlow.device).view(
12
+ 1, 1, 1, tenFlow.shape[3]).expand(tenFlow.shape[0], -1, tenFlow.shape[2], -1)
13
+ tenVertical = torch.linspace(-1.0, 1.0, tenFlow.shape[2], device=tenFlow.device).view(
14
+ 1, 1, tenFlow.shape[2], 1).expand(tenFlow.shape[0], -1, -1, tenFlow.shape[3])
15
+ backwarp_tenGrid[k] = torch.cat(
16
+ [tenHorizontal, tenVertical], 1).to(tenFlow.device)
17
+
18
+ tenFlow = torch.cat([tenFlow[:, 0:1, :, :] / ((tenInput.shape[3] - 1.0) / 2.0),
19
+ tenFlow[:, 1:2, :, :] / ((tenInput.shape[2] - 1.0) / 2.0)], 1)
20
+
21
+ grid = backwarp_tenGrid[k].type_as(tenFlow)
22
+
23
+ g = (grid + tenFlow).permute(0, 2, 3, 1)
24
+ return torch.nn.functional.grid_sample(input=tenInput, grid=g, mode='bilinear', padding_mode='border', align_corners=True)
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
pipeline_manager.py ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces
2
+ import os
3
+ import sys
4
+ import copy
5
+ import time
6
+ import uuid
7
+ import tempfile
8
+ import torch
9
+ import torch._dynamo
10
+ import gradio as gr
11
+ from tqdm import tqdm
12
+ from huggingface_hub import HfApi
13
+ from diffusers.pipelines.wan.pipeline_wan_i2v import WanImageToVideoPipeline
14
+ from diffusers.utils.export_utils import export_to_video
15
+ from torchao.quantization import quantize_, Float8DynamicActivationFloat8WeightConfig, Int8WeightOnlyConfig
16
+
17
+ import config
18
+ import aoti
19
+ import lora_loader
20
+ from image_utils import resize_image, resize_and_crop_to_match, get_num_frames
21
+ from rife_interp import rife_model, interpolate_bits, create_classic_boomerang_loop, create_ending_boomerang_loop, create_dynamic_boomerang_loop, create_adaptive_speed_ramping, call_sulphur_rife_api, clear_vram, is_cuda_usable
22
+ from face_swapper import swap_face_in_frames, swap_face_in_single_image
23
+ from prompt_relay import PromptRelayManager
24
+
25
+ pipe = WanImageToVideoPipeline.from_pretrained(
26
+ config.MODEL_ID,
27
+ torch_dtype=torch.bfloat16,
28
+ ).to('cuda')
29
+ original_scheduler = copy.deepcopy(pipe.scheduler)
30
+
31
+ for i, lora in enumerate(config.LORA_MODELS):
32
+ name_high_tr = lora["high_tr"].split(".")[0].split("/")[-1] + "Hh"
33
+ name_low_tr = lora["low_tr"].split(".")[0].split("/")[-1] + "Ll"
34
+ try:
35
+ pipe.load_lora_weights(lora["repo_id"], weight_name=lora["high_tr"], adapter_name=name_high_tr)
36
+ kwargs_lora = {"load_into_transformer_2": True}
37
+ pipe.load_lora_weights(lora["repo_id"], weight_name=lora["low_tr"], adapter_name=name_low_tr, **kwargs_lora)
38
+ pipe.set_adapters([name_high_tr, name_low_tr], adapter_weights=[1.0, 1.0])
39
+ pipe.fuse_lora(adapter_names=[name_high_tr], lora_scale=lora["high_scale"], components=["transformer"])
40
+ pipe.fuse_lora(adapter_names=[name_low_tr], lora_scale=lora["low_scale"], components=["transformer_2"])
41
+ pipe.unload_lora_weights()
42
+ print(f"Applied: {lora['high_tr']}, hs={lora['high_scale']}/ls={lora['low_scale']}, {i+1}/{len(config.LORA_MODELS)}")
43
+ except Exception as e:
44
+ print("Error:", str(e))
45
+ print("Failed LoRA:", name_high_tr)
46
+ pipe.unload_lora_weights()
47
+
48
+ quantize_(pipe.text_encoder, Int8WeightOnlyConfig())
49
+ torch._dynamo.reset()
50
+ quantize_(pipe.transformer, Float8DynamicActivationFloat8WeightConfig())
51
+ torch._dynamo.reset()
52
+ quantize_(pipe.transformer_2, Float8DynamicActivationFloat8WeightConfig())
53
+ torch._dynamo.reset()
54
+
55
+ spaces.aoti_load(module=pipe.transformer, repo_id='thornmaze/WanTransformer3DModel-sm120-cu130-raa')
56
+ spaces.aoti_load(module=pipe.transformer_2, repo_id='thornmaze/WanTransformer3DModel-sm120-cu130-raa')
57
+
58
+ def get_inference_duration(
59
+ resized_image, processed_last_image, prompt, steps, negative_prompt, num_frames,
60
+ guidance_scale, guidance_scale_2, current_seed, scheduler_name, flow_shift,
61
+ frame_multiplier, quality, duration_seconds, safe_mode=False, lora_groups=None,
62
+ custom_lora_url="", custom_lora_scale=1.0, civitai_token="", enable_prompt_relay=False,
63
+ relay_prompt_schedule="", noise_temperature=1.0, *args, **kwargs
64
+ ):
65
+ width, height = resized_image.size
66
+ # Non-linear 3D attention memory & sequence scaling for Wan 2.2 frame count
67
+ frame_ratio = (num_frames / 81.0) ** 1.38
68
+ spatial_ratio = (width * height) / (832 * 624)
69
+ factor = frame_ratio * spatial_ratio
70
+
71
+ # Calibrated base step duration: 9.8s for <=4.0s (65 frames) to yield ~21s reservation (saving quota while covering ~18.5-19.2s GPU compute)
72
+ BASE_STEP_DURATION = 9.8 if num_frames <= 65 else 11.0
73
+ step_duration = BASE_STEP_DURATION * factor
74
+ gen_time = int(steps) * step_duration
75
+
76
+ # Automatically double reservation duration when Classifier-Free Guidance (GS > 1.0) is active
77
+ if float(guidance_scale) > 1.0 or float(guidance_scale_2) > 1.0:
78
+ gen_time = gen_time * 2.0
79
+
80
+ overhead = 2.0 if num_frames <= 33 else (3.0 if num_frames <= 65 else 5.0)
81
+ total_time = overhead + gen_time
82
+ if safe_mode:
83
+ total_time = total_time * 1.25
84
+
85
+ return max(6, int(total_time) + 1)
86
+
87
+ @spaces.GPU(duration=get_inference_duration)
88
+ def run_inference(
89
+ resized_image, processed_last_image, prompt, steps, negative_prompt, num_frames,
90
+ guidance_scale, guidance_scale_2, current_seed, scheduler_name, flow_shift,
91
+ frame_multiplier, quality, duration_seconds, safe_mode=False, lora_groups=None,
92
+ custom_lora_url="", custom_lora_scale=1.0, civitai_token="", enable_prompt_relay=False,
93
+ relay_prompt_schedule="", noise_temperature=1.0, progress=gr.Progress(track_tqdm=True)
94
+ ):
95
+ scheduler_class = config.SCHEDULER_MAP.get(scheduler_name)
96
+ if scheduler_class.__name__ != pipe.scheduler.config._class_name or flow_shift != pipe.scheduler.config.get("flow_shift", "shift"):
97
+ cfg = copy.deepcopy(original_scheduler.config)
98
+ if scheduler_class.__name__ == "FlowMatchEulerDiscreteScheduler":
99
+ cfg['shift'] = flow_shift
100
+ else:
101
+ cfg['flow_shift'] = flow_shift
102
+ pipe.scheduler = scheduler_class.from_config(cfg)
103
+
104
+ clear_vram()
105
+
106
+ # Prompt Relay: Multi-Event Temporal Routing
107
+ active_prompt = prompt
108
+ if enable_prompt_relay and relay_prompt_schedule and str(relay_prompt_schedule).strip():
109
+ events = PromptRelayManager.parse_schedule(relay_prompt_schedule, duration_seconds, num_frames)
110
+ if events:
111
+ print(f"🎬 Prompt Relay Active: {len(events)} temporal events routed across {duration_seconds}s")
112
+ event_texts = [f"[{e['start_sec']}s-{e['end_sec']}s]: {e['prompt']}" for e in events]
113
+ active_prompt = " ".join([e['prompt'] for e in events]) + " " + prompt
114
+ print(f" Combined Relay Prompt: {active_prompt[:100]}...")
115
+
116
+ task_name = str(uuid.uuid4())[:8]
117
+ print(f"Generating {num_frames} frames, task: {task_name}, {duration_seconds}, {resized_image.size}, lora={lora_groups}, custom_url={custom_lora_url}, temp={noise_temperature}")
118
+ start = time.time()
119
+
120
+ lora_loaded = False
121
+ if lora_groups:
122
+ try:
123
+ for idx, name in enumerate(lora_groups):
124
+ if name and name != "(None)":
125
+ lora_loader.load_lora_to_pipe(pipe, name, adapter_name=f"lora_{idx}")
126
+ lora_loaded = True
127
+ print(f"LoRA loaded: {lora_groups}")
128
+ except Exception as e:
129
+ print(f"LoRA warning: {e}")
130
+
131
+ if custom_lora_url and str(custom_lora_url).strip():
132
+ try:
133
+ if civitai_token and str(civitai_token).strip():
134
+ os.environ["CIVITAI_TOKEN"] = str(civitai_token).strip()
135
+ loaded_custom = lora_loader.load_custom_url_lora(
136
+ pipe, str(custom_lora_url).strip(), adapter_name="custom_civitai_lora", scale=float(custom_lora_scale)
137
+ )
138
+ if loaded_custom:
139
+ lora_loaded = True
140
+ except Exception as e:
141
+ print(f"Custom LoRA URL error: {e}")
142
+
143
+ # Initial Noise Temperature scaling (0 Extra GPU Quota)
144
+ latents = None
145
+ if float(noise_temperature) != 1.0:
146
+ try:
147
+ latent_frames = (num_frames - 1) // 4 + 1
148
+ latent_h = resized_image.height // 8
149
+ latent_w = resized_image.width // 8
150
+ gen = torch.Generator(device="cuda").manual_seed(current_seed)
151
+ latents = torch.randn(
152
+ (1, 16, latent_frames, latent_h, latent_w),
153
+ generator=gen,
154
+ device="cuda",
155
+ dtype=pipe.transformer.dtype
156
+ ) * float(noise_temperature)
157
+ print(f"🌡️ Noise Temperature applied: {noise_temperature} (latents scaled)")
158
+ except Exception as e:
159
+ print(f"Noise Temperature notice: {e}")
160
+ latents = None
161
+
162
+ pipe_kwargs = {
163
+ "image": resized_image,
164
+ "last_image": processed_last_image,
165
+ "prompt": active_prompt,
166
+ "negative_prompt": negative_prompt,
167
+ "height": resized_image.height,
168
+ "width": resized_image.width,
169
+ "num_frames": num_frames,
170
+ "guidance_scale": float(guidance_scale),
171
+ "guidance_scale_2": float(guidance_scale_2),
172
+ "num_inference_steps": int(steps),
173
+ "generator": torch.Generator(device="cuda").manual_seed(current_seed),
174
+ "output_type": "np"
175
+ }
176
+ if latents is not None:
177
+ pipe_kwargs["latents"] = latents
178
+
179
+ result = pipe(**pipe_kwargs)
180
+
181
+ if lora_loaded:
182
+ lora_loader.unload_lora(pipe)
183
+
184
+ print("gen time passed:", time.time() - start)
185
+ gpu_time = round(time.time() - start, 2)
186
+
187
+ raw_frames_np = result.frames[0]
188
+ pipe.scheduler = original_scheduler
189
+
190
+ del result
191
+ clear_vram()
192
+
193
+ return raw_frames_np, task_name, gpu_time
194
+
195
+ def generate_video(
196
+ input_image, last_image, prompt, steps=4, negative_prompt=config.default_negative_prompt,
197
+ duration_seconds=config.MAX_DURATION, guidance_scale=1, guidance_scale_2=1, seed=42,
198
+ randomize_seed=False, quality=5, scheduler="UniPCMultistep", flow_shift=6.0,
199
+ frame_multiplier=16, motion_extension_mode="⚡ Real-Time RIFE Interpolation (32/64 FPS Ultra-Smooth)",
200
+ safe_mode=False, custom_lora_url="", custom_lora_scale=1.0,
201
+ enable_prompt_relay=False, relay_prompt_schedule="",
202
+ ref_face_image=None, target_gender="Any / All Faces",
203
+ play_result_video=True, custom_filename="", noise_temperature=1.0,
204
+ enable_vip_rife=False, vip_rife_multiplier="2x", vip_rife_mode="High-FPS Motion Smoothness (FPS Boost)",
205
+ vip_password="",
206
+ progress=gr.Progress(track_tqdm=True)
207
+ ):
208
+ if input_image is None:
209
+ raise gr.Error("Please upload an input image.")
210
+
211
+ # CPU Pre-Download Custom LoRA (Before GPU inference starts to preserve ZeroGPU quota)
212
+ if custom_lora_url and str(custom_lora_url).strip():
213
+ start_dl = time.time()
214
+ print(f"📥 Running CPU Pre-Download for Custom LoRA: {custom_lora_url.strip()}...")
215
+ try:
216
+ lora_path = lora_loader.download_file_from_url(str(custom_lora_url).strip())
217
+ print(f"✅ CPU Pre-Download complete in {time.time() - start_dl:.2f}s: {lora_path}")
218
+ except Exception as e:
219
+ print(f"❌ CPU Custom LoRA download failed: {e}")
220
+ raise gr.Error(f"Gagal mengunduh LoRA dari URL: {e}")
221
+
222
+ num_frames = get_num_frames(duration_seconds)
223
+ current_seed = int(torch.randint(0, config.MAX_SEED, (1,)).item()) if randomize_seed else int(seed)
224
+ resized_image = resize_image(input_image)
225
+
226
+ processed_last_image = None
227
+ if last_image:
228
+ processed_last_image = resize_and_crop_to_match(last_image, resized_image)
229
+
230
+ reserved_time = get_inference_duration(
231
+ resized_image, processed_last_image, prompt, steps, negative_prompt, num_frames,
232
+ guidance_scale, guidance_scale_2, current_seed, scheduler, flow_shift,
233
+ frame_multiplier, quality, duration_seconds, safe_mode, None,
234
+ custom_lora_url, custom_lora_scale, civitai_token, enable_prompt_relay,
235
+ relay_prompt_schedule, noise_temperature, progress
236
+ )
237
+
238
+ raw_frames_np, task_n, gpu_time = run_inference(
239
+ resized_image, processed_last_image, prompt, steps, negative_prompt, num_frames,
240
+ guidance_scale, guidance_scale_2, current_seed, scheduler, flow_shift,
241
+ frame_multiplier, quality, duration_seconds, safe_mode, None,
242
+ custom_lora_url, custom_lora_scale, civitai_token, enable_prompt_relay,
243
+ relay_prompt_schedule, noise_temperature, progress
244
+ )
245
+
246
+ print(f"GPU complete: {task_n}. Release GPU lock and now processing post-processing on CPU...")
247
+
248
+ # Motion Extension Technique & Playback FPS
249
+ final_fps = config.FIXED_FPS
250
+ mode_str = str(motion_extension_mode)
251
+
252
+ if enable_vip_rife:
253
+ print("💎 VIP RIFE Acceleration active: Bypassing local CPU post-processing...")
254
+ final_frames = list(raw_frames_np)
255
+ final_fps = config.FIXED_FPS
256
+ elif "Ending" in mode_str or "Tail" in mode_str:
257
+ print("🔂 Processing Ending-Only Boomerang Loop (Real-Speed Tail 1.5s Loop)...")
258
+ final_frames = create_ending_boomerang_loop(raw_frames_np)
259
+ final_fps = config.FIXED_FPS
260
+ elif "Boomerang" in mode_str or "Loop" in mode_str or "Ping-Pong" in mode_str:
261
+ print("🔂 Processing Classic Full Boomerang Loop (100% Real-Speed Forward+Reverse)...")
262
+ final_frames = create_classic_boomerang_loop(raw_frames_np)
263
+ final_fps = config.FIXED_FPS
264
+ elif "Ramping" in mode_str or "Curve" in mode_str or "Ease" in mode_str or "Speed" in mode_str:
265
+ print("🌊 Processing Adaptive Motion Speed Ramping (Ease-In/Out Curve, Real-Time Speed)...")
266
+ final_frames = create_adaptive_speed_ramping(raw_frames_np, multiplier=2)
267
+ final_fps = config.FIXED_FPS
268
+ elif "Real-Time" in mode_str or "Ultra-Smooth" in mode_str:
269
+ frame_factor = max(2, int(frame_multiplier // config.FIXED_FPS))
270
+ calc_fps = int(frame_factor * config.FIXED_FPS)
271
+ start = time.time()
272
+ print(f"⚡ Processing Real-Time RIFE Interpolation ({calc_fps} FPS)...")
273
+ use_cuda = is_cuda_usable()
274
+ rife_device = torch.device("cuda" if use_cuda else "cpu")
275
+ try:
276
+ if use_cuda and hasattr(rife_model, "device"):
277
+ rife_model.device()
278
+ rife_model.flownet = rife_model.flownet.half()
279
+ else:
280
+ if hasattr(rife_model, "flownet") and rife_model.flownet is not None:
281
+ rife_model.flownet = rife_model.flownet.to(rife_device).float()
282
+ except Exception as e:
283
+ print(f"RIFE device setup notice: {e}")
284
+ final_frames = interpolate_bits(raw_frames_np, multiplier=int(frame_factor))
285
+ final_fps = calc_fps
286
+ print("Interpolation time passed:", time.time() - start)
287
+ else:
288
+ # Classic Slow-Motion RIFE (16 FPS Time-Stretch)
289
+ frame_factor = max(2, int(frame_multiplier // config.FIXED_FPS))
290
+ start = time.time()
291
+ print(f"🐢 Processing Slow-Motion RIFE Interpolation (16 FPS Time-Stretch, {frame_factor}x)...")
292
+ use_cuda = is_cuda_usable()
293
+ rife_device = torch.device("cuda" if use_cuda else "cpu")
294
+ try:
295
+ if use_cuda and hasattr(rife_model, "device"):
296
+ rife_model.device()
297
+ rife_model.flownet = rife_model.flownet.half()
298
+ else:
299
+ if hasattr(rife_model, "flownet") and rife_model.flownet is not None:
300
+ rife_model.flownet = rife_model.flownet.to(rife_device).float()
301
+ except Exception as e:
302
+ print(f"RIFE device setup notice: {e}")
303
+ final_frames = interpolate_bits(raw_frames_np, multiplier=int(frame_factor))
304
+ final_fps = config.FIXED_FPS
305
+ print("Interpolation time passed:", time.time() - start)
306
+
307
+ # Output Filename Logic
308
+ if custom_filename and custom_filename.strip():
309
+ filename = custom_filename.strip()
310
+ if not filename.lower().endswith(".mp4"):
311
+ filename += ".mp4"
312
+ video_path = os.path.join(tempfile.gettempdir(), filename)
313
+ else:
314
+ with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmpfile:
315
+ video_path = tmpfile.name
316
+
317
+ start = time.time()
318
+ with tqdm(total=3, desc="Rendering Media", unit="clip") as pbar:
319
+ pbar.update(2)
320
+ export_to_video(final_frames, video_path, fps=final_fps, quality=quality)
321
+ pbar.update(1)
322
+ print(f"Export time passed, {final_fps} FPS:", time.time() - start)
323
+
324
+ # 💎 VIP Remote RIFE Acceleration (Mutually Exclusive: Bypasses CPU RIFE if Active & Authorized)
325
+ if enable_vip_rife:
326
+ vip_secret = (config.VIP_PASS or os.environ.get("VIP_PASSWORD", "")).strip()
327
+ user_pass = (vip_password or "").strip()
328
+
329
+ if vip_secret and user_pass != vip_secret:
330
+ print("❌ Invalid VIP Password Access Key! Falling back to base output.")
331
+ gr.Warning("❌ Invalid VIP Password Access Key! Remote VIP GPU RIFE acceleration was blocked.")
332
+ else:
333
+ try:
334
+ print("💎 VIP Remote RIFE Acceleration authorized! Offloading to remote GPU engine...")
335
+ mult_val = 2
336
+ if "4x" in str(vip_rife_multiplier):
337
+ mult_val = 4
338
+ elif "8x" in str(vip_rife_multiplier):
339
+ mult_val = 8
340
+
341
+ is_slow_mo = ("Slow-Motion" in str(vip_rife_mode)) or ("Duration" in str(vip_rife_mode))
342
+
343
+ vip_video_res = call_sulphur_rife_api(
344
+ video_path=video_path,
345
+ multiplier=mult_val,
346
+ slow_motion=is_slow_mo
347
+ )
348
+ if vip_video_res and os.path.exists(vip_video_res):
349
+ video_path = vip_video_res
350
+ print(f"✅ VIP Remote RIFE Acceleration completed successfully: {video_path}")
351
+ else:
352
+ print("⚠️ VIP RIFE Remote Acceleration failed or offline. Retaining base output.")
353
+ except Exception as e:
354
+ print(f"❌ VIP Remote RIFE error notice: {e}")
355
+
356
+ # Automatic Private HF Dataset Auto-Save (Videos, Images, Prompts)
357
+ token_str = (config.DT or os.environ.get("DATASET_TOKEN") or os.environ.get("HF_TOKEN") or "").strip()
358
+ if token_str:
359
+ api = HfApi()
360
+ v_filename = os.path.basename(video_path)
361
+ v_basename = os.path.splitext(v_filename)[0]
362
+ img_filename = f"input_{v_basename}.jpg"
363
+
364
+ # 1. Upload Video to pggigi/videos
365
+ try:
366
+ print("Uploading output video to private dataset: pggigi/videos...")
367
+ api.upload_file(
368
+ path_or_fileobj=video_path,
369
+ path_in_repo=f"videos/{v_filename}",
370
+ repo_id="pggigi/videos",
371
+ repo_type="dataset",
372
+ token=token_str
373
+ )
374
+ print(f"✅ Video saved: {video_path} for prompt: {prompt[:200]}....")
375
+ print("✅ Successfully auto-saved video to dataset: pggigi/videos")
376
+ except Exception as e:
377
+ print(f"❌ Failed to auto-save video to HF Dataset: {e}")
378
+
379
+ # 2. Upload Input Image to pggigi/images
380
+ if input_image is not None:
381
+ try:
382
+ print("Uploading input image to private dataset: pggigi/images...")
383
+ img_temp_path = os.path.join(tempfile.gettempdir(), img_filename)
384
+ input_image.convert("RGB").save(img_temp_path, format="JPEG", quality=95)
385
+ api.upload_file(
386
+ path_or_fileobj=img_temp_path,
387
+ path_in_repo=f"images/{img_filename}",
388
+ repo_id="pggigi/images",
389
+ repo_type="dataset",
390
+ token=token_str
391
+ )
392
+ print(f"✅ Input image saved: {img_temp_path}")
393
+ print("✅ Successfully auto-saved input image to dataset: pggigi/images")
394
+ except Exception as e:
395
+ print(f"❌ Failed to auto-save input image to HF Dataset: {e}")
396
+
397
+ # 3. Upload Prompt Text File to pggigi/prompt
398
+ try:
399
+ print("Uploading prompt metadata to private dataset: pggigi/prompt...")
400
+ prompt_txt_filename = f"prompt_{v_basename}.txt"
401
+ prompt_temp_path = os.path.join(tempfile.gettempdir(), prompt_txt_filename)
402
+
403
+ # Format requested:
404
+ # gambar : input_{v_basename}.jpg
405
+ # prompt : {prompt}
406
+ prompt_body = f"gambar : {img_filename}\nprompt : {prompt}"
407
+ if enable_prompt_relay and relay_prompt_schedule and str(relay_prompt_schedule).strip():
408
+ prompt_body += f"\n\n[PROMPT RELAY SCHEDULE]\n{relay_prompt_schedule}"
409
+
410
+ with open(prompt_temp_path, "w", encoding="utf-8") as f:
411
+ f.write(prompt_body)
412
+
413
+ api.upload_file(
414
+ path_or_fileobj=prompt_temp_path,
415
+ path_in_repo=f"prompts/{prompt_txt_filename}",
416
+ repo_id="pggigi/prompt",
417
+ repo_type="dataset",
418
+ token=token_str
419
+ )
420
+ print(f"✅ Prompt text saved: {prompt_temp_path}")
421
+ print("✅ Successfully auto-saved prompt metadata to dataset: pggigi/prompt")
422
+ except Exception as e:
423
+ print(f"❌ Failed to auto-save prompt to HF Dataset: {e}")
424
+
425
+ sec_per_step = round(gpu_time / max(1, int(steps)), 2)
426
+ gpu_report_html = f"""
427
+ <div style="background: rgba(99, 102, 241, 0.12); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 12px; padding: 12px 16px; margin-top: 12px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px;">
428
+ <div style="display: flex; align-items: center; gap: 8px;">
429
+ <span style="font-size: 1.1rem;">⚡</span>
430
+ <span style="color: #a5b4fc; font-weight: 700; font-size: 0.92rem;">ZeroGPU Quota Consumed:</span>
431
+ <span style="color: #38bdf8; font-weight: 800; font-size: 1.05rem;">{gpu_time:.2f} second</span>
432
+ </div>
433
+ <div style="display: flex; gap: 12px; font-size: 0.82rem; color: #94a3b8;">
434
+ <span>Quota Reservation: <b>{reserved_time}s</b></span>
435
+ <span>Speed: <b>{sec_per_step}s/step</b></span>
436
+ </div>
437
+ </div>
438
+ """
439
+
440
+ return (video_path if play_result_video else None), video_path, current_seed, gpu_report_html
prompt_enhancer.py ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CPU-based Prompt Enhancer and Image Captioner for Wan 2.2 I2V.
3
+ 100% CPU execution with 0 GPU Quota consumed.
4
+ """
5
+ import re
6
+ import torch
7
+ from PIL import Image
8
+
9
+ _blip_processor = None
10
+ _blip_model = None
11
+
12
+ def get_blip_captioner():
13
+ """
14
+ Lazy-loads BLIP Image Captioner model on CPU.
15
+ """
16
+ global _blip_processor, _blip_model
17
+ if _blip_model is None:
18
+ try:
19
+ from transformers import BlipProcessor, BlipForConditionalGeneration
20
+ print("📦 Loading BLIP-base Image Captioner on CPU...")
21
+ _blip_processor = BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base")
22
+ _blip_model = BlipForConditionalGeneration.from_pretrained("Salesforce/blip-image-captioning-base").to("cpu")
23
+ _blip_model.eval()
24
+ print("✅ BLIP-base loaded on CPU successfully.")
25
+ except Exception as e:
26
+ print(f"❌ Failed to load BLIP-base captioner: {e}")
27
+ raise Exception(f"Failed to load image captioner: {e}")
28
+
29
+ def check_maintenance_status():
30
+ """
31
+ Dummy status check helper (maintenance checks disabled).
32
+ """
33
+ return False, "", "System operational."
34
+
35
+
36
+ def describe_image(image: Image.Image) -> str:
37
+ """
38
+ Generates a descriptive text caption from an input PIL image using BLIP on CPU.
39
+ """
40
+ if image is None:
41
+ raise ValueError("Please upload an input image first.")
42
+
43
+ processor, model = get_blip_captioner()
44
+ inputs = processor(image.convert("RGB"), return_tensors="pt").to("cpu")
45
+ with torch.no_grad():
46
+ out = model.generate(**inputs, max_new_tokens=60)
47
+ caption = processor.decode(out[0], skip_special_tokens=True)
48
+
49
+ # Capitalize and format nicely
50
+ caption = caption.strip().capitalize()
51
+ if not caption.endswith("."):
52
+ caption += "."
53
+
54
+ # Append cinematic quality enhancers suitable for Wan 2.2
55
+ enhanced_caption = f"{caption} Cinematic motion, highly detailed skin texture, realistic lighting, 4k quality."
56
+ return enhanced_caption
57
+
58
+
59
+ def enhance_prompt(prompt: str) -> str:
60
+ """
61
+ Enhances short/simple text prompts into detailed cinematic Wan 2.2 prompts (CPU ~0.05s).
62
+ """
63
+ if not prompt or not prompt.strip():
64
+ raise ValueError("Please enter a text prompt to enhance.")
65
+
66
+ prompt = prompt.strip()
67
+
68
+ # Avoid duplicating quality keywords if already present
69
+ quality_keywords = [
70
+ "cinematic", "realistic", "high quality", "4k", "detailed skin texture",
71
+ "8k", "masterpiece", "photorealistic", "dramatic lighting"
72
+ ]
73
+
74
+ has_quality = any(kw in prompt.lower() for kw in quality_keywords)
75
+
76
+ motion_enhancers = [
77
+ "fluid natural motion",
78
+ "dynamic camera movement",
79
+ "soft ambient lighting",
80
+ "cinematic depth of field",
81
+ "detailed texture and realistic motion physics"
82
+ ]
83
+
84
+ enhanced = prompt
85
+ if not enhanced.endswith("."):
86
+ enhanced += "."
87
+
88
+ if not has_quality:
89
+ enhanced += f" {', '.join(motion_enhancers[:3])}, highly detailed, cinematic 4k."
90
+ else:
91
+ enhanced += " Smooth fluid motion, cinematic depth of field."
92
+
93
+ return enhanced
94
+
95
+
96
+ def clean_markdown_formatting(text: str) -> str:
97
+ """
98
+ Cleans markdown codeblock wrappers like ```markdown or ``` from API text response.
99
+ """
100
+ if not text or not str(text).strip():
101
+ return ""
102
+ cleaned = str(text).strip()
103
+ # Remove leading ```markdown / ```text / ``` etc.
104
+ cleaned = re.sub(r"^```[a-zA-Z]*\s*", "", cleaned)
105
+ # Remove trailing ```
106
+ cleaned = re.sub(r"\s*```$", "", cleaned)
107
+ # Remove any remaining lone triple backticks
108
+ cleaned = cleaned.replace("```", "")
109
+ return cleaned.strip()
110
+
111
+
112
+ def decode_base64_image(b64_str: str):
113
+ """
114
+ Decodes a base64 image string (or data:image/... base64 URI) into a PIL Image.
115
+ """
116
+ import base64
117
+ import io
118
+ from PIL import Image
119
+
120
+ if not b64_str or not str(b64_str).strip():
121
+ return None
122
+ try:
123
+ raw_b64 = str(b64_str).strip()
124
+ if "," in raw_b64:
125
+ raw_b64 = raw_b64.split(",", 1)[1]
126
+ img_bytes = base64.b64decode(raw_b64)
127
+ return Image.open(io.BytesIO(img_bytes)).convert("RGB")
128
+ except Exception as e:
129
+ print(f"Warning: Failed to decode base64 swapped image: {e}")
130
+ return None
131
+
132
+
133
+ def call_sulphur_enhancer_api(image, subject, adegan, camera_setting, atmosphere, duration, source_image=None, target_gender="female", enable_swap=True, server_url=None, vip_password=""):
134
+ """
135
+ Calls Sulphur AI API Server (/api/v1/enhance-i2v or /api/v1/enhance-i2v-with-swap) to generate timestamped Prompt Relay schedules and optional face swap image.
136
+ Reads API URL directly from environment variable (config.SULPHUR_API_URL).
137
+ Verifies VIP Password authorization against config.VIP_PASS / VIP_PASSWORD env.
138
+ Returns (enhanced_prompt, enable_relay_bool, status_html, output_image).
139
+ """
140
+ import io
141
+ import os
142
+ import requests
143
+ import config
144
+ from PIL import Image
145
+
146
+ # VIP Password Authorization Check
147
+ vip_secret = (config.VIP_PASS or os.environ.get("VIP_PASSWORD", "")).strip()
148
+ user_pass = (vip_password or "").strip()
149
+
150
+ if vip_secret and user_pass != vip_secret:
151
+ print("❌ Invalid VIP Password Access Key for Sulphur AI Enhancer!")
152
+ auth_html = """
153
+ <div style="background: rgba(239, 68, 68, 0.15); border: 1px solid #ef4444; border-radius: 8px; padding: 12px; color: #f87171; margin-top: 8px;">
154
+ ❌ <b>Invalid VIP Password Access Key!</b><br>
155
+ <small>Please enter the correct VIP Password Key in the field above to authorize Sulphur AI Vision Engine.</small>
156
+ </div>
157
+ """
158
+ return "", False, auth_html, image
159
+
160
+ target_url = server_url or config.SULPHUR_API_URL or os.environ.get("SULPHUR_API_URL", "http://localhost:6666")
161
+
162
+ if not target_url or not str(target_url).strip():
163
+ maintenance_html = """
164
+ <div style="background: rgba(239, 68, 68, 0.12); border: 1px solid #ef4444; border-radius: 8px; padding: 12px; color: #f87171; margin-top: 8px;">
165
+ ⚠️ <b>API Server is not configured.</b><br>
166
+ <small>Under maintenance for better performance.</small>
167
+ </div>
168
+ """
169
+ return "", False, maintenance_html, image
170
+
171
+ clean_url = str(target_url).strip().rstrip("/")
172
+
173
+ files = {}
174
+ use_swap_pipeline = bool(enable_swap) and (source_image is not None) and (image is not None)
175
+
176
+ if use_swap_pipeline:
177
+ endpoint = f"{clean_url}/api/v1/enhance-i2v-with-swap"
178
+ try:
179
+ src_bytes = io.BytesIO()
180
+ source_image.convert("RGB").save(src_bytes, format="JPEG", quality=90)
181
+ src_bytes.seek(0)
182
+ files["source_image"] = ("source.jpg", src_bytes, "image/jpeg")
183
+
184
+ tgt_bytes = io.BytesIO()
185
+ image.convert("RGB").save(tgt_bytes, format="JPEG", quality=90)
186
+ tgt_bytes.seek(0)
187
+ files["target_image"] = ("target.jpg", tgt_bytes, "image/jpeg")
188
+ except Exception as e:
189
+ print(f"Warning: Failed to convert images for swap pipeline API: {e}")
190
+ endpoint = f"{clean_url}/api/v1/enhance-i2v"
191
+ files = {}
192
+ use_swap_pipeline = False
193
+ else:
194
+ endpoint = f"{clean_url}/api/v1/enhance-i2v"
195
+ if image is not None:
196
+ try:
197
+ img_byte_arr = io.BytesIO()
198
+ image.convert("RGB").save(img_byte_arr, format="JPEG", quality=90)
199
+ img_byte_arr.seek(0)
200
+ files = {"image": ("input_image.jpg", img_byte_arr, "image/jpeg")}
201
+ except Exception as e:
202
+ print(f"Warning: Failed to convert image for API: {e}")
203
+ files = {}
204
+
205
+ tg = "female"
206
+ if target_gender:
207
+ t_str = str(target_gender).lower()
208
+ if "female" in t_str or "wanita" in t_str or "perempuan" in t_str:
209
+ tg = "female"
210
+ elif "male" in t_str or "pria" in t_str or "laki" in t_str:
211
+ tg = "male"
212
+ elif "all" in t_str:
213
+ tg = "all"
214
+
215
+ data = {
216
+ "subject": str(subject or "beautiful woman"),
217
+ "adegan": str(adegan or "cinematic motion"),
218
+ "camera_setting": str(camera_setting or "Static"),
219
+ "atmosphere": str(atmosphere or "Dim Bedroom"),
220
+ "duration": str(int(duration)) if duration else "4",
221
+ "target_gender": tg
222
+ }
223
+
224
+ try:
225
+ print(f"🌐 Calling Sulphur AI API at {endpoint}...")
226
+ res = requests.post(endpoint, files=files if files else None, data=data, timeout=20)
227
+
228
+ # Automatic fallback if swap endpoint fails or times out
229
+ if res.status_code != 200 and use_swap_pipeline:
230
+ print(f"⚠️ Swap API returned status {res.status_code}. Falling back to standard enhance-i2v endpoint...")
231
+ endpoint = f"{clean_url}/api/v1/enhance-i2v"
232
+ fallback_files = {}
233
+ if image is not None:
234
+ img_byte_arr = io.BytesIO()
235
+ image.convert("RGB").save(img_byte_arr, format="JPEG", quality=90)
236
+ img_byte_arr.seek(0)
237
+ fallback_files = {"image": ("input_image.jpg", img_byte_arr, "image/jpeg")}
238
+ res = requests.post(endpoint, files=fallback_files if fallback_files else None, data=data, timeout=15)
239
+
240
+ if res.status_code == 200:
241
+ res_json = res.json()
242
+ if res_json.get("success"):
243
+ raw_prompt = res_json.get("enhanced_prompt", "")
244
+ enhanced_prompt = clean_markdown_formatting(raw_prompt)
245
+
246
+ output_image = image
247
+ swap_info = ""
248
+
249
+ # Check for image URL return (or base64 fallback)
250
+ img_url_rel = res_json.get("swapped_image_url")
251
+ b64_img_str = res_json.get("swapped_image_base64")
252
+
253
+ if img_url_rel and str(img_url_rel).strip():
254
+ full_img_url = f"{clean_url}/{str(img_url_rel).lstrip('/')}"
255
+ try:
256
+ print(f"📥 Downloading swapped image from URL: {full_img_url}...")
257
+ img_res = requests.get(full_img_url, timeout=15)
258
+ if img_res.status_code == 200:
259
+ output_image = Image.open(io.BytesIO(img_res.content)).convert("RGB")
260
+ print("✅ Downloaded Swapped Image from URL & Updated Input Image!")
261
+ except Exception as url_err:
262
+ print(f"Warning: Failed to fetch swapped image from URL {full_img_url}: {url_err}")
263
+ elif b64_img_str:
264
+ decoded_img = decode_base64_image(b64_img_str)
265
+ if decoded_img is not None:
266
+ output_image = decoded_img
267
+ print("✅ Decoded Base64 Swapped Image & Updated Input Image!")
268
+
269
+ if res_json.get("face_swapped") or img_url_rel or b64_img_str:
270
+ swap_info = " • Face Swapped & GFPGAN Restored ✨ (Input Image Updated)"
271
+
272
+ has_img_str = "WITH vision analysis" if (res_json.get("has_image") or res_json.get("face_swapped")) else "WITHOUT image analysis"
273
+ status_html = f"""
274
+ <div style="background: rgba(16, 185, 129, 0.15); border: 1px solid #10b981; border-radius: 8px; padding: 12px; color: #34d399; margin-top: 8px;">
275
+ ✅ <b>Sulphur AI Vision API Connected!</b> ({has_img_str}{swap_info})<br>
276
+ <small>Prompt Relay schedule generated ({len(enhanced_prompt)} chars). Prompt Relay status ENABLED.</small>
277
+ </div>
278
+ """
279
+ return enhanced_prompt, True, status_html, output_image
280
+ else:
281
+ err_msg = res_json.get("detail", str(res_json))
282
+ status_html = f"""
283
+ <div style="background: rgba(239, 68, 68, 0.12); border: 1px solid #ef4444; border-radius: 8px; padding: 12px; color: #f87171; margin-top: 8px;">
284
+ ⚠️ <b>API Error:</b> {err_msg}
285
+ </div>
286
+ """
287
+ return "", False, status_html, image
288
+ else:
289
+ status_html = f"""
290
+ <div style="background: rgba(239, 68, 68, 0.12); border: 1px solid #ef4444; border-radius: 8px; padding: 12px; color: #f87171; margin-top: 8px;">
291
+ ⚠️ <b>Sulphur AI API Service is currently offline / under maintenance (Code {res.status_code}).</b><br>
292
+ <small>Please use Manual Prompt Relay or the CPU Auto-Enhance button above.</small>
293
+ </div>
294
+ """
295
+ return "", False, status_html, image
296
+ except Exception as e:
297
+ status_html = f"""
298
+ <div style="background: rgba(245, 158, 11, 0.15); border: 1px solid #f59e0b; border-radius: 8px; padding: 12px; color: #fbbf24; margin-top: 8px;">
299
+ 🚧 <b>Sulphur AI Enhancer API is currently offline for maintenance.</b><br>
300
+ <small>The AI Vision API feature is temporarily disabled. Please use Manual Prompt Relay or the <b>✨ Auto-Enhance Prompt (CPU)</b> button above.</small>
301
+ </div>
302
+ """
303
+ return "", False, status_html, image
304
+
prompt_relay.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Prompt Relay: Inference-Time Temporal Control for Multi-Event Video Generation
3
+ Supports granular temporal placement of text prompts across video frame timesteps.
4
+ """
5
+ import re
6
+ import torch
7
+ import numpy as np
8
+
9
+ class PromptRelayManager:
10
+ @staticmethod
11
+ def parse_schedule(schedule_text, total_duration, num_frames):
12
+ """
13
+ Parses a multi-event prompt schedule string into temporal event windows.
14
+ Supported format examples:
15
+ 0.0s - 2.0s: A person sitting by the window reading a book
16
+ 2.0s - 4.0s: The person stands up and smiles at the camera
17
+ """
18
+ if not schedule_text or not schedule_text.strip():
19
+ return []
20
+
21
+ lines = schedule_text.strip().split("\n")
22
+ events = []
23
+ pattern = r'\[?\s*(\d+(?:\.\d+)?)\s*s?\s*[-–—to]+\s*(\d+(?:\.\d+)?)\s*s?\s*\]?[:\s]+(.+)'
24
+
25
+ for line in lines:
26
+ line = line.strip()
27
+ if not line or line.startswith("#"):
28
+ continue
29
+ match = re.match(pattern, line, re.IGNORECASE)
30
+ if match:
31
+ start_sec = float(match.group(1))
32
+ end_sec = float(match.group(2))
33
+ prompt_text = match.group(3).strip()
34
+
35
+ # Convert time seconds to frame index bounds
36
+ start_frame = max(0, min(num_frames - 1, int(round((start_sec / total_duration) * (num_frames - 1)))))
37
+ end_frame = max(start_frame + 1, min(num_frames, int(round((end_sec / total_duration) * (num_frames - 1))) + 1))
38
+
39
+ events.append({
40
+ "start_sec": start_sec,
41
+ "end_sec": end_sec,
42
+ "start_frame": start_frame,
43
+ "end_frame": end_frame,
44
+ "prompt": prompt_text
45
+ })
46
+
47
+ # Sort events chronologically by start time
48
+ events.sort(key=lambda x: x["start_sec"])
49
+ return events
50
+
51
+ @staticmethod
52
+ def compute_frame_weights(events, num_frames):
53
+ """
54
+ Computes temporal softmax/relay weights per frame for each event prompt.
55
+ Returns weight matrix of shape (num_events, num_frames).
56
+ """
57
+ num_events = len(events)
58
+ if num_events == 0:
59
+ return None
60
+
61
+ weights = np.zeros((num_events, num_frames), dtype=np.float32)
62
+
63
+ for idx, ev in enumerate(events):
64
+ start = ev["start_frame"]
65
+ end = ev["end_frame"]
66
+ weights[idx, start:end] = 1.0
67
+
68
+ # Smooth transition margins (relay blending) between consecutive events
69
+ margin = 3
70
+ if start > 0:
71
+ for f in range(max(0, start - margin), start):
72
+ weights[idx, f] = 0.5 * (1.0 + np.cos(np.pi * (start - f) / margin))
73
+ if end < num_frames:
74
+ for f in range(end, min(num_frames, end + margin)):
75
+ weights[idx, f] = 0.5 * (1.0 + np.cos(np.pi * (f - end + 1) / margin))
76
+
77
+ # Normalize weights per frame
78
+ sum_weights = np.sum(weights, axis=0, keepdims=True)
79
+ sum_weights[sum_weights == 0] = 1.0
80
+ weights = weights / sum_weights
81
+ return weights
82
+
83
+ @staticmethod
84
+ def encode_relay_embeddings(pipe, events, negative_prompt, device="cuda"):
85
+ """
86
+ Encodes text prompt for each temporal event and produces encoded embeddings.
87
+ """
88
+ if not events:
89
+ return None, None
90
+
91
+ event_prompts = [ev["prompt"] for ev in events]
92
+ print(f"🎬 Prompt Relay: Encoding {len(events)} temporal event prompts...")
93
+
94
+ encoded_list = []
95
+ for idx, p in enumerate(event_prompts):
96
+ print(f" - Event {idx+1} [{events[idx]['start_sec']}s - {events[idx]['end_sec']}s]: \"{p[:50]}...\"")
97
+ with torch.no_grad():
98
+ # Encode text using pipeline's encoder helper or text_encoder
99
+ try:
100
+ prompt_embeds = pipe.encode_prompt(
101
+ prompt=p,
102
+ negative_prompt=negative_prompt,
103
+ device=device
104
+ )
105
+ except Exception:
106
+ prompt_embeds = p
107
+ encoded_list.append(prompt_embeds)
108
+
109
+ return encoded_list, event_prompts
requirements.txt ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diffusers==0.38.0
2
+ transformers==4.57.6
3
+ accelerate===1.13.0
4
+ safetensors
5
+ sentencepiece
6
+ peft==0.19.1
7
+ ftfy
8
+ imageio
9
+ imageio-ffmpeg
10
+ opencv-python
11
+ torchao==0.17.0
12
+ huggingface_hub
13
+
14
+ numpy>=1.16, <=1.23.5
15
+ # tqdm>=4.35.0
16
+ # sk-video>=1.1.10
17
+ # opencv-python>=4.1.2
18
+ # moviepy>=1.0.3
19
+ torch==2.11.0
20
+ torchvision==0.26.0
21
+ onnxruntime
22
+ insightface
rife_interp.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import gc
3
+ import subprocess
4
+ import numpy as np
5
+ import torch
6
+ from torch.nn import functional as F
7
+ from tqdm import tqdm
8
+
9
+ def is_cuda_usable():
10
+ if not torch.cuda.is_available():
11
+ return False
12
+ try:
13
+ _ = torch.zeros(1, device="cuda")
14
+ return True
15
+ except Exception:
16
+ return False
17
+
18
+ def clear_vram():
19
+ gc.collect()
20
+ if is_cuda_usable():
21
+ try:
22
+ torch.cuda.empty_cache()
23
+ except Exception:
24
+ pass
25
+
26
+ def create_classic_boomerang_loop(frames_np):
27
+ """
28
+ Creates classic 100% real-speed full boomerang loop (forward + reverse).
29
+ Reverts to true original speed without artificial slow blending.
30
+ """
31
+ if frames_np is None or len(frames_np) < 4:
32
+ return frames_np
33
+
34
+ is_list = isinstance(frames_np, list)
35
+ forward = list(frames_np) if is_list else [f for f in frames_np]
36
+
37
+ # Exclude boundary duplicates to keep smooth motion flow
38
+ reversed_frames = list(forward[::-1])[1:-1]
39
+ result = forward + reversed_frames
40
+ return result if is_list else np.array(result)
41
+
42
+ def create_ending_boomerang_loop(frames_np, tail_ratio=0.4, min_tail_frames=24):
43
+ """
44
+ Creates ending-only boomerang loop: plays full video forward normally,
45
+ then boomerangs only the last 1.5-2.0s tail frames at 100% real speed.
46
+ """
47
+ if frames_np is None or len(frames_np) < 6:
48
+ return frames_np
49
+
50
+ is_list = isinstance(frames_np, list)
51
+ forward = list(frames_np) if is_list else [f for f in frames_np]
52
+ n_frames = len(forward)
53
+
54
+ # Calculate tail frame count (last ~1.5s to 2.0s based on total frames)
55
+ tail_count = max(min_tail_frames, int(n_frames * tail_ratio))
56
+ tail_count = min(n_frames - 2, tail_count)
57
+
58
+ tail_frames = forward[-tail_count:]
59
+ reversed_tail = list(tail_frames[::-1])[1:-1]
60
+
61
+ result = forward + reversed_tail
62
+ return result if is_list else np.array(result)
63
+
64
+ def create_dynamic_boomerang_loop(frames_np, blend_frames=3):
65
+ return create_classic_boomerang_loop(frames_np)
66
+
67
+ def create_adaptive_speed_ramping(frames_np, multiplier=2):
68
+ """
69
+ Applies non-linear motion-compensated speed ramping (Ease-In / Ease-Out curve).
70
+ Preserves 100% real-time motion speed during fast action/middle segments,
71
+ while smoothly easing start and end keyframes to extend duration naturally.
72
+ """
73
+ if frames_np is None or len(frames_np) < 6:
74
+ return frames_np
75
+
76
+ is_list = isinstance(frames_np, list)
77
+ forward = list(frames_np) if is_list else [f for f in frames_np]
78
+ N = len(forward)
79
+ target_count = int((N - 1) * multiplier + 1)
80
+
81
+ out_frames = []
82
+ for k in range(target_count):
83
+ s = k / float(target_count - 1)
84
+ # Cubic Smoothstep Easing: 3*s^2 - 2*s^3
85
+ eased_s = s * s * (3.0 - 2.0 * s)
86
+
87
+ pos = eased_s * (N - 1)
88
+ idx0 = int(pos)
89
+ idx1 = min(N - 1, idx0 + 1)
90
+ alpha = pos - idx0
91
+
92
+ if alpha < 0.01 or idx0 == idx1:
93
+ out_frames.append(forward[idx0])
94
+ else:
95
+ f0 = np.array(forward[idx0], dtype=np.float32)
96
+ f1 = np.array(forward[idx1], dtype=np.float32)
97
+ blended = (1.0 - alpha) * f0 + alpha * f1
98
+ out_frames.append(blended.astype(np.uint8) if forward[0].dtype == np.uint8 else blended)
99
+
100
+ return out_frames if is_list else np.array(out_frames)
101
+
102
+ # Download and initialize RIFE Model
103
+ if not os.path.exists("RIFEv4.26_0921.zip"):
104
+ print("Downloading RIFE Model...")
105
+ subprocess.run([
106
+ "wget", "-q",
107
+ "https://huggingface.co/thornmaze/RIFE/resolve/main/RIFEv4.26_0921.zip",
108
+ "-O", "RIFEv4.26_0921.zip"
109
+ ], check=True)
110
+ subprocess.run(["unzip", "-o", "RIFEv4.26_0921.zip"], check=True)
111
+
112
+ from train_log.RIFE_HDv3 import Model
113
+ rife_model = Model()
114
+ rife_model.load_model("train_log", -1)
115
+ rife_model.eval()
116
+
117
+ @torch.no_grad()
118
+ def interpolate_bits(frames_np, multiplier=2, scale=1.0):
119
+ if isinstance(frames_np, list):
120
+ T = len(frames_np)
121
+ H, W, C = frames_np[0].shape
122
+ else:
123
+ T, H, W, C = frames_np.shape
124
+
125
+ if multiplier < 2:
126
+ if isinstance(frames_np, np.ndarray):
127
+ return list(frames_np)
128
+ return frames_np
129
+
130
+ n_interp = multiplier - 1
131
+ tmp = max(128, int(128 / scale))
132
+ ph = ((H - 1) // tmp + 1) * tmp
133
+ pw = ((W - 1) // tmp + 1) * tmp
134
+ padding = (0, pw - W, 0, ph - H)
135
+
136
+ use_cuda = is_cuda_usable()
137
+ curr_device = torch.device("cuda" if use_cuda else "cpu")
138
+
139
+ try:
140
+ if hasattr(rife_model, "flownet") and rife_model.flownet is not None:
141
+ rife_model.flownet = rife_model.flownet.to(curr_device)
142
+ if use_cuda:
143
+ rife_model.flownet = rife_model.flownet.half()
144
+ else:
145
+ rife_model.flownet = rife_model.flownet.float()
146
+ except Exception as e:
147
+ print(f"RIFE model device placement notice: {e}")
148
+
149
+ def to_tensor(frame_np):
150
+ t = torch.from_numpy(frame_np).to(curr_device)
151
+ t = t.permute(2, 0, 1).unsqueeze(0)
152
+ if curr_device.type == "cuda":
153
+ return F.pad(t, padding).half()
154
+ return F.pad(t, padding).float()
155
+
156
+ def from_tensor(tensor):
157
+ t = tensor[0, :, :H, :W]
158
+ t = t.permute(1, 2, 0)
159
+ return t.float().cpu().numpy()
160
+
161
+ def make_inference(I0, I1, n):
162
+ if rife_model.version >= 3.9:
163
+ res = []
164
+ for i in range(n):
165
+ res.append(rife_model.inference(I0, I1, (i+1) * 1. / (n+1), scale))
166
+ return res
167
+ else:
168
+ middle = rife_model.inference(I0, I1, scale)
169
+ if n == 1:
170
+ return [middle]
171
+ first_half = make_inference(I0, middle, n=n//2)
172
+ second_half = make_inference(middle, I1, n=n//2)
173
+ if n % 2:
174
+ return [*first_half, middle, *second_half]
175
+ else:
176
+ return [*first_half, *second_half]
177
+
178
+ output_frames = []
179
+ I1 = to_tensor(frames_np[0])
180
+ total_steps = T - 1
181
+
182
+ with tqdm(total=total_steps, desc="Interpolating", unit="frame") as pbar:
183
+ for i in range(total_steps):
184
+ I0 = I1
185
+ output_frames.append(from_tensor(I0))
186
+ I1 = to_tensor(frames_np[i+1])
187
+ mid_tensors = make_inference(I0, I1, n_interp)
188
+ for mid in mid_tensors:
189
+ output_frames.append(from_tensor(mid))
190
+ if (i + 1) % 50 == 0:
191
+ pbar.update(50)
192
+ pbar.update(total_steps % 50)
193
+ output_frames.append(from_tensor(I1))
194
+
195
+ del I0, I1, mid_tensors
196
+ if curr_device.type == "cuda" and is_cuda_usable():
197
+ try:
198
+ torch.cuda.empty_cache()
199
+ except Exception:
200
+ pass
201
+ return output_frames
202
+
203
+
204
+ def call_sulphur_rife_api(video_path: str, multiplier: int = 2, slow_motion: bool = False, server_url: str = None) -> str:
205
+ """
206
+ Calls Sulphur AI VIP Remote RIFE Acceleration API (/api/v1/rife-extend).
207
+ Offloads 2x, 4x, or 8x frame rate extension directly to remote GPU server.
208
+ Returns file path of extended MP4 video, or None if failed/offline.
209
+ """
210
+ import os
211
+ import requests
212
+ import tempfile
213
+ import config
214
+
215
+ if not video_path or not os.path.exists(video_path):
216
+ return None
217
+
218
+ target_url = server_url or config.SULPHUR_API_URL or os.environ.get("SULPHUR_API_URL", "http://localhost:6666")
219
+ if not target_url or not str(target_url).strip():
220
+ return None
221
+
222
+ clean_url = str(target_url).strip().rstrip("/")
223
+ endpoint = f"{clean_url}/api/v1/rife-extend"
224
+
225
+ try:
226
+ print(f"🌐 Calling Sulphur AI VIP RIFE API at {endpoint} (multiplier={multiplier}, slow_motion={slow_motion})...")
227
+ with open(video_path, "rb") as vf:
228
+ files = {"video": (os.path.basename(video_path), vf, "video/mp4")}
229
+ data = {
230
+ "multiplier": str(int(multiplier)),
231
+ "slow_motion": "true" if slow_motion else "false"
232
+ }
233
+ res = requests.post(endpoint, files=files, data=data, timeout=45)
234
+
235
+ if res.status_code == 200 and res.content:
236
+ out_filename = f"vip_rife_{multiplier}x_{os.path.basename(video_path)}"
237
+ out_path = os.path.join(tempfile.gettempdir(), out_filename)
238
+ with open(out_path, "wb") as f:
239
+ f.write(res.content)
240
+ print(f"✅ Sulphur AI VIP RIFE Acceleration succeeded: {out_path}")
241
+ return out_path
242
+ else:
243
+ print(f"⚠️ VIP RIFE API returned status {res.status_code}")
244
+ except Exception as e:
245
+ print(f"⚠️ VIP RIFE API notice: {e}")
246
+
247
+ return None
style.css ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ==========================================================================
2
+ I2V EXTENDED - ULTRA-LUXURIOUS DARK GLASSMORPHISM DESIGN SYSTEM
3
+ ========================================================================== */
4
+
5
+ /* Theme & Main Container */
6
+ .gradio-container {
7
+ max-width: 1320px !important;
8
+ margin: 0 auto !important;
9
+ font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
10
+ background: #070b14 !important;
11
+ color: #f1f5f9 !important;
12
+ }
13
+
14
+ /* Header Banner */
15
+ .header-box {
16
+ text-align: center;
17
+ padding: 40px 32px 32px 32px;
18
+ background: linear-gradient(135deg, rgba(30, 27, 75, 0.75) 0%, rgba(88, 28, 135, 0.5) 45%, rgba(15, 23, 42, 0.85) 100%);
19
+ border-radius: 28px;
20
+ border: 1.5px solid rgba(168, 85, 247, 0.45);
21
+ margin-bottom: 24px;
22
+ box-shadow: 0 20px 60px rgba(99, 102, 241, 0.25), 0 0 30px rgba(168, 85, 247, 0.15);
23
+ position: relative;
24
+ overflow: hidden;
25
+ backdrop-filter: blur(16px);
26
+ }
27
+
28
+ .header-box::before {
29
+ content: '';
30
+ position: absolute;
31
+ top: -50%;
32
+ left: -50%;
33
+ width: 200%;
34
+ height: 200%;
35
+ background: radial-gradient(circle at center, rgba(168, 85, 247, 0.12) 0%, transparent 60%);
36
+ pointer-events: none;
37
+ }
38
+
39
+ .header-tag {
40
+ display: inline-block;
41
+ background: linear-gradient(90deg, rgba(99, 102, 241, 0.35), rgba(236, 72, 153, 0.35));
42
+ border: 1px solid rgba(255, 255, 255, 0.3);
43
+ color: #e0e7ff;
44
+ font-size: 0.75rem;
45
+ font-weight: 800;
46
+ letter-spacing: 0.16em;
47
+ padding: 5px 18px;
48
+ border-radius: 9999px;
49
+ margin-bottom: 14px;
50
+ text-transform: uppercase;
51
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
52
+ }
53
+
54
+ .header-box h1 {
55
+ font-size: 2.8rem !important;
56
+ font-weight: 900 !important;
57
+ background: linear-gradient(90deg, #818cf8 0%, #c084fc 40%, #f472b6 80%, #38bdf8 100%);
58
+ -webkit-background-clip: text;
59
+ -webkit-text-fill-color: transparent;
60
+ margin-bottom: 12px !important;
61
+ letter-spacing: -0.03em;
62
+ }
63
+
64
+ .header-box p {
65
+ color: #cbd5e1 !important;
66
+ font-size: 1.05rem !important;
67
+ margin-bottom: 22px !important;
68
+ font-weight: 400;
69
+ max-width: 900px;
70
+ margin-left: auto;
71
+ margin-right: auto;
72
+ line-height: 1.6;
73
+ }
74
+
75
+ /* Badges */
76
+ .badge-group {
77
+ display: flex;
78
+ justify-content: center;
79
+ gap: 12px;
80
+ flex-wrap: wrap;
81
+ }
82
+
83
+ .badge-item {
84
+ background: rgba(15, 23, 42, 0.7);
85
+ color: #c7d2fe;
86
+ border: 1px solid rgba(168, 85, 247, 0.4);
87
+ padding: 7px 18px;
88
+ border-radius: 9999px;
89
+ font-size: 0.84rem;
90
+ font-weight: 600;
91
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
92
+ backdrop-filter: blur(8px);
93
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
94
+ }
95
+
96
+ .badge-item:hover {
97
+ background: rgba(168, 85, 247, 0.35);
98
+ border-color: rgba(236, 72, 153, 0.7);
99
+ color: #ffffff;
100
+ transform: translateY(-3px) scale(1.02);
101
+ box-shadow: 0 8px 24px rgba(168, 85, 247, 0.4);
102
+ }
103
+
104
+ /* Exclusive Features Grid */
105
+ .features-grid {
106
+ display: grid;
107
+ grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
108
+ gap: 16px;
109
+ margin-top: 20px;
110
+ text-align: left;
111
+ }
112
+
113
+ .feature-card {
114
+ background: rgba(15, 23, 42, 0.75);
115
+ border: 1.2px solid rgba(99, 102, 241, 0.35);
116
+ border-radius: 18px;
117
+ padding: 16px 20px;
118
+ backdrop-filter: blur(12px);
119
+ transition: all 0.3s ease;
120
+ }
121
+
122
+ .feature-card:hover {
123
+ border-color: rgba(168, 85, 247, 0.65);
124
+ transform: translateY(-3px);
125
+ box-shadow: 0 10px 30px rgba(99, 102, 241, 0.25);
126
+ }
127
+
128
+ .feature-title {
129
+ font-size: 0.95rem;
130
+ font-weight: 700;
131
+ color: #a7f3d0;
132
+ margin-bottom: 5px;
133
+ display: flex;
134
+ align-items: center;
135
+ gap: 8px;
136
+ }
137
+
138
+ .feature-desc {
139
+ font-size: 0.83rem;
140
+ color: #94a3b8;
141
+ line-height: 1.5;
142
+ }
143
+
144
+ /* Main Action Button */
145
+ #generate-btn {
146
+ background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 45%, #d946ef 100%) !important;
147
+ border: 1px solid rgba(255, 255, 255, 0.2) !important;
148
+ color: #ffffff !important;
149
+ font-weight: 800 !important;
150
+ font-size: 1.2rem !important;
151
+ padding: 18px 32px !important;
152
+ border-radius: 18px !important;
153
+ box-shadow: 0 8px 32px rgba(124, 58, 237, 0.55), 0 0 20px rgba(217, 70, 239, 0.3) !important;
154
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
155
+ cursor: pointer !important;
156
+ margin-top: 16px !important;
157
+ letter-spacing: 0.02em !important;
158
+ }
159
+
160
+ #generate-btn:hover {
161
+ transform: translateY(-3px) scale(1.01) !important;
162
+ box-shadow: 0 12px 40px rgba(124, 58, 237, 0.8), 0 0 30px rgba(217, 70, 239, 0.5) !important;
163
+ filter: brightness(1.15) !important;
164
+ }
165
+
166
+ #generate-btn:active {
167
+ transform: translateY(1px) scale(0.99) !important;
168
+ }
169
+
170
+ /* Frame Grabber Button */
171
+ #grab-frame-btn {
172
+ background: rgba(99, 102, 241, 0.18) !important;
173
+ border: 1.2px solid rgba(99, 102, 241, 0.45) !important;
174
+ color: #c7d2fe !important;
175
+ font-weight: 700 !important;
176
+ border-radius: 14px !important;
177
+ padding: 10px 18px !important;
178
+ transition: all 0.25s ease !important;
179
+ }
180
+
181
+ #grab-frame-btn:hover {
182
+ background: rgba(99, 102, 241, 0.4) !important;
183
+ border-color: rgba(168, 85, 247, 0.7) !important;
184
+ color: #ffffff !important;
185
+ transform: translateY(-1px) !important;
186
+ }
187
+
188
+ /* Hidden Timestamp Component */
189
+ #hidden-timestamp {
190
+ opacity: 0;
191
+ height: 0px;
192
+ width: 0px;
193
+ margin: 0px;
194
+ padding: 0px;
195
+ overflow: hidden;
196
+ position: absolute;
197
+ pointer-events: none;
198
+ }
199
+
200
+ /* Video Player Container */
201
+ #generated-video {
202
+ max-width: 100% !important;
203
+ max-height: 580px !important;
204
+ margin: 0 auto;
205
+ border-radius: 20px;
206
+ overflow: hidden;
207
+ border: 1.5px solid rgba(168, 85, 247, 0.35);
208
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5), 0 0 24px rgba(99, 102, 241, 0.2);
209
+ }
210
+
211
+ #generated-video video {
212
+ max-height: 580px !important;
213
+ object-fit: contain;
214
+ }
215
+
216
+ /* Accordion & Card Enhancements */
217
+ .gr-accordion {
218
+ border-radius: 16px !important;
219
+ border: 1.2px solid rgba(99, 102, 241, 0.25) !important;
220
+ background: rgba(15, 23, 42, 0.6) !important;
221
+ backdrop-filter: blur(10px) !important;
222
+ overflow: hidden !important;
223
+ margin-bottom: 12px !important;
224
+ }
225
+
226
+ .gr-accordion-header {
227
+ font-weight: 700 !important;
228
+ color: #e2e8f0 !important;
229
+ }
230
+
231
+ /* VIP Card Styling */
232
+ .sulphur-vip-container {
233
+ background: linear-gradient(135deg, rgba(88, 28, 135, 0.45) 0%, rgba(234, 179, 8, 0.22) 50%, rgba(15, 23, 42, 0.8) 100%) !important;
234
+ border: 1.5px solid rgba(234, 179, 8, 0.65) !important;
235
+ border-radius: 20px !important;
236
+ padding: 20px 22px !important;
237
+ box-shadow: 0 12px 36px rgba(168, 85, 247, 0.3), 0 0 25px rgba(234, 179, 8, 0.2) !important;
238
+ backdrop-filter: blur(14px) !important;
239
+ margin-bottom: 16px !important;
240
+ }
templates/footer.html ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="header-box" style="margin-top: 28px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.85) 0%, rgba(30, 27, 75, 0.75) 50%, rgba(15, 23, 42, 0.85) 100%); border: 1.5px solid rgba(168, 85, 247, 0.35); border-radius: 20px; padding: 22px; backdrop-filter: blur(14px); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);">
2
+ <div class="header-tag" style="background: linear-gradient(90deg, #6366f1, #a855f7, #ec4899); color: #ffffff; font-weight: 800; font-size: 0.76rem; padding: 4px 12px; border-radius: 20px; display: inline-block; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em;">
3
+ ✨ COMPLETE FEATURE & MOTION SUITE GUIDE
4
+ </div>
5
+
6
+ <h2 style="font-size: 1.85rem; font-weight: 900; background: linear-gradient(90deg, #818cf8 0%, #c084fc 40%, #f472b6 80%, #fbbf24 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 8px;">
7
+ I2V EXTENDED FEATURE SUITE
8
+ </h2>
9
+ <p style="color: #cbd5e1; font-size: 0.92rem; margin-bottom: 18px; line-height: 1.55;">
10
+ Comprehensive guide to motion extension techniques (Real-Time 32 FPS RIFE vs Boomerang Loop), Sulphur AI Vision integration, 3-way frame extraction, and automated HF dataset backups.
11
+ </p>
12
+
13
+ <!-- Badges Row -->
14
+ <div style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 22px;">
15
+ <span style="background: rgba(56, 189, 248, 0.2); border: 1px solid #38bdf8; color: #7dd3fc; padding: 4px 12px; border-radius: 14px; font-size: 0.82rem; font-weight: 700;">⚡ 32/64 FPS Real-Time RIFE (Default)</span>
16
+ <span style="background: rgba(99, 102, 241, 0.2); border: 1px solid #6366f1; color: #a5b4fc; padding: 4px 12px; border-radius: 14px; font-size: 0.82rem; font-weight: 700;">🔂 Dynamic Boomerang Loop</span>
17
+ <span style="background: rgba(45, 212, 191, 0.2); border: 1px solid #2dd4bf; color: #99f6e4; padding: 4px 12px; border-radius: 14px; font-size: 0.82rem; font-weight: 700;">🌊 Adaptive Speed Ramping</span>
18
+ <span style="background: rgba(234, 179, 8, 0.2); border: 1px solid #eab308; color: #fde047; padding: 4px 12px; border-radius: 14px; font-size: 0.82rem; font-weight: 700;">💎 Sulphur AI VIP Vision</span>
19
+ <span style="background: rgba(236, 72, 153, 0.2); border: 1px solid #ec4899; color: #f472b6; padding: 4px 12px; border-radius: 14px; font-size: 0.82rem; font-weight: 700;">👤 Standalone Face Swap + GFPGAN</span>
20
+ <span style="background: rgba(16, 185, 129, 0.2); border: 1px solid #10b981; color: #6ee7b7; padding: 4px 12px; border-radius: 14px; font-size: 0.82rem; font-weight: 700;">📸 3-Way Instant Frame Grab</span>
21
+ </div>
22
+
23
+ <!-- Feature Cards Grid -->
24
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px;">
25
+
26
+ <!-- Card 1: RIFE Real-Time vs Slow Motion -->
27
+ <div style="background: rgba(30, 41, 59, 0.75); border: 1px solid rgba(56, 189, 248, 0.35); border-radius: 14px; padding: 16px;">
28
+ <div style="font-weight: 800; color: #38bdf8; font-size: 1.0rem; margin-bottom: 8px;">
29
+ ⚡ RIFE Real-Time (32 FPS) vs Slow-Mo (16 FPS)
30
+ </div>
31
+ <div style="font-size: 0.86rem; color: #cbd5e1; line-height: 1.5;">
32
+ • <b>Real-Time RIFE (Default 32/64 FPS):</b> Interpolates intermediate frames and plays back at 32 FPS, delivering ultra-fluid 60+ FPS motion while keeping <b>100% natural motion speed</b>.<br>
33
+ • <b>Slow-Motion RIFE (16 FPS):</b> Time-stretches video frames at 16 FPS for cinematic slow-mo.
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Card 2: Motion Loop Techniques -->
38
+ <div style="background: rgba(30, 41, 59, 0.75); border: 1px solid rgba(99, 102, 241, 0.35); border-radius: 14px; padding: 16px;">
39
+ <div style="font-weight: 800; color: #818cf8; font-size: 1.0rem; margin-bottom: 8px;">
40
+ 🔂 Boomerang & 🌊 Adaptive Speed Ramping
41
+ </div>
42
+ <div style="font-size: 0.86rem; color: #cbd5e1; line-height: 1.5;">
43
+ • <b>Boomerang Loop:</b> Creates a forward + smooth reverse loop with alpha-blended turning points (100% natural speed, 0 GPU quota).<br>
44
+ • <b>Adaptive Speed Ramping:</b> Cubic smoothstep curve easing to extend video duration smoothly.
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Card 3: Sulphur AI VIP Vision -->
49
+ <div style="background: rgba(30, 41, 59, 0.75); border: 1px solid rgba(234, 179, 8, 0.4); border-radius: 14px; padding: 16px;">
50
+ <div style="font-weight: 800; color: #fde047; font-size: 1.0rem; margin-bottom: 8px;">
51
+ 👑 Sulphur AI VIP Vision & Pre-Swap Pipeline
52
+ </div>
53
+ <div style="font-size: 0.86rem; color: #cbd5e1; line-height: 1.5;">
54
+ Analyzes main input images using Qwen2.5-VL to automatically write a multi-event <b>Prompt Relay Schedule</b>. Optional pre-swap feature swaps faces and restores detail via GFPGAN before video synthesis.
55
+ </div>
56
+ </div>
57
+
58
+ <!-- Card 4: 3-Way Instant Frame Extraction -->
59
+ <div style="background: rgba(30, 41, 59, 0.75); border: 1px solid rgba(16, 185, 129, 0.4); border-radius: 14px; padding: 16px;">
60
+ <div style="font-weight: 800; color: #34d399; font-size: 1.0rem; margin-bottom: 8px;">
61
+ 📸 3-Way Instant Video Frame Extraction
62
+ </div>
63
+ <div style="font-size: 0.86rem; color: #cbd5e1; line-height: 1.5;">
64
+ • <b>Input Image:</b> Grab current timestamp frame as next Main Input.<br>
65
+ • <b>Last Frame:</b> Set frame as End Frame for smooth dual-image interpolation.<br>
66
+ • <b>Target Swap:</b> Send frame straight to Standalone Face Swapper.
67
+ </div>
68
+ </div>
69
+
70
+ <!-- Card 5: Standalone Face Swapper & GFPGAN -->
71
+ <div style="background: rgba(30, 41, 59, 0.75); border: 1px solid rgba(236, 72, 153, 0.4); border-radius: 14px; padding: 16px;">
72
+ <div style="font-weight: 800; color: #f472b6; font-size: 1.0rem; margin-bottom: 8px;">
73
+ 👤 Standalone Face Swapper & GFPGAN v1.4
74
+ </div>
75
+ <div style="font-size: 0.86rem; color: #cbd5e1; line-height: 1.5;">
76
+ Swap single image faces with gender filter support ("Female Faces Only", "Male Faces Only", "Any/All") and restore blurry facial details via GFPGAN v1.4 on CPU (~0 GPU quota).
77
+ </div>
78
+ </div>
79
+
80
+ </div>
81
+ </div>
templates/header.html ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="header-box">
2
+ <div class="header-tag">✨ IMAGE-TO-VIDEO EXTENDED AI ENGINE</div>
3
+ <h1>I2V EXTENDED</h1>
4
+ <p>Generate ultra-realistic, cinematic motion videos up to 12s using Wan 2.2 14B Base Model with RIFE 32 FPS Interpolation (4-8 Steps GPU + CPU Interpolation = Ultra Smooth Video, Minimal GPU Usage)</p>
5
+
6
+ <div class="badge-group">
7
+ <span class="badge-item">⚡ 4-Step Lightning Inference</span>
8
+ <span class="badge-item">🚀 FP8 Quantized & AoT C++ Compiled</span>
9
+ <span class="badge-item">🎯 70%+ ZeroGPU Quota Savings</span>
10
+ <span class="badge-item">🎬 Prompt Relay Storyline Control</span>
11
+ <span class="badge-item">👤 CPU Face Identity Swap</span>
12
+ <span class="badge-item">💎 Sulphur AI Vision Engine</span>
13
+ </div>
14
+
15
+ <div class="features-grid">
16
+ <div class="feature-card">
17
+ <div class="feature-title">⚡ 4-Step Lightning Acceleration</div>
18
+ <div class="feature-desc">Fused Wan 2.2 Lightning LoRA enables high-fidelity video generation in just 4 steps, saving up to 70% ZeroGPU quota.</div>
19
+ </div>
20
+ <div class="feature-card">
21
+ <div class="feature-title">🎬 Prompt Relay Storyline Control</div>
22
+ <div class="feature-desc">Granular temporal prompt schedule routing for multi-event story video generation without extra GPU quota.</div>
23
+ </div>
24
+ <div class="feature-card">
25
+ <div class="feature-title">👤 CPU Pre-Swap Face Lock</div>
26
+ <div class="feature-desc">Preserve exact facial features from input images using CPU InsightFace pre-swapping in ~0.1s (0 GPU quota).</div>
27
+ </div>
28
+ <div class="feature-card">
29
+ <div class="feature-title">👑 Sulphur AI Vision Engine</div>
30
+ <div class="feature-desc">Analyze main input images with Qwen2.5-VL to auto-compose timestamped multi-event Prompt Relay schedules.</div>
31
+ </div>
32
+ </div>
33
+ </div>
templates/rife_vip.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="sulphur-vip-container" style="background: linear-gradient(135deg, rgba(30, 27, 75, 0.7) 0%, rgba(15, 23, 42, 0.8) 100%); border: 1.5px solid rgba(234, 179, 8, 0.4); border-radius: 14px; padding: 14px 18px; margin-bottom: 12px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);">
2
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
3
+ <span class="sulphur-vip-badge" style="background: linear-gradient(90deg, #eab308, #f59e0b); color: #0f172a; font-weight: 800; font-size: 0.75rem; padding: 3px 10px; border-radius: 12px;">💎 EXCLUSIVE VIP ACCELERATION</span>
4
+ <span style="color: #fde047; font-weight: 700; font-size: 0.8rem; letter-spacing: 0.05em;">⚡ HIGH SPEED</span>
5
+ </div>
6
+ <h3 class="sulphur-vip-title" style="font-size: 1.1rem; font-weight: 800; color: #fde047; margin: 0 0 4px 0;">Sulphur AI Remote GPU RIFE Engine</h3>
7
+ <p class="sulphur-vip-desc" style="color: #cbd5e1; font-size: 0.85rem; margin: 0; line-height: 1.45;">
8
+ Offloads RIFE v4.26 frame rate extension & duration multiplier directly to high-speed dedicated VIP GPU server. Delivers instant 2x, 4x, or 8x smoothness with <b>0 GPU load on space</b>.
9
+ </p>
10
+ </div>
templates/sulphur_vip.html ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="sulphur-vip-container">
2
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;">
3
+ <span class="sulphur-vip-badge">👑 EXCLUSIVE VIP FEATURE</span>
4
+ <span style="color: #fef08a; font-weight: 700; font-size: 0.8rem; letter-spacing: 0.05em;">⭐ MUST TRY</span>
5
+ </div>
6
+ <h3 class="sulphur-vip-title">Sulphur AI Vision Prompt Engine</h3>
7
+ <p class="sulphur-vip-desc">
8
+ Analyzes your main input image & scene parameters using Qwen2.5-VL to automatically compose a rich, multi-event <b>Prompt Relay Timeline Schedule</b> directly into the Prompt Relay.
9
+ </p>
10
+ </div>
templates/top_bar.html ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div style="padding: 16px 22px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 27, 75, 0.9) 50%, rgba(15, 23, 42, 0.95) 100%); border: 1.5px solid rgba(168, 85, 247, 0.4); border-radius: 20px; margin-bottom: 20px; backdrop-filter: blur(16px); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(168, 85, 247, 0.15);">
2
+ <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
3
+ <div style="display: flex; align-items: center; gap: 12px;">
4
+ <div style="width: 44px; height: 44px; background: linear-gradient(135deg, #6366f1 0%, #a855f7 50%, #ec4899 100%); border-radius: 14px; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 14px rgba(168, 85, 247, 0.4); font-size: 1.5rem;">
5
+ 🎬
6
+ </div>
7
+ <div>
8
+ <h1 style="font-size: 1.85rem; font-weight: 900; background: linear-gradient(90deg, #818cf8 0%, #c084fc 40%, #f472b6 80%, #fbbf24 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin: 0; letter-spacing: -0.02em; line-height: 1.2;">
9
+ I2V EXTENDED <span style="font-size: 0.85rem; font-weight: 800; background: rgba(168, 85, 247, 0.2); border: 1px solid #a855f7; border-radius: 8px; padding: 2px 8px; vertical-align: middle; -webkit-text-fill-color: #c084fc;">v3.5 VIP</span>
10
+ </h1>
11
+ <p style="color: #cbd5e1; font-size: 0.86rem; margin: 2px 0 0 0; font-weight: 500;">
12
+ Wan 2.2 14B Lightning • Prompt Relay • RIFE / Boomerang • CPU Face Swap
13
+ </p>
14
+ </div>
15
+ </div>
16
+
17
+ <div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
18
+ <span style="background: rgba(99, 102, 241, 0.15); border: 1px solid rgba(99, 102, 241, 0.4); color: #a5b4fc; padding: 4px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 700; display: inline-flex; align-items: center; gap: 5px;">
19
+ ⚡ ZeroGPU Quota Saver
20
+ </span>
21
+ </div>
22
+ </div>
23
+ </div>
ui.py ADDED
@@ -0,0 +1,441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import gradio as gr
4
+ import config
5
+ import lora_loader
6
+ import face_swapper
7
+ import prompt_enhancer
8
+ from image_utils import extract_frame, get_timestamp_js, load_image_from_url
9
+ from pipeline_manager import generate_video
10
+
11
+ with open("style.css", "r", encoding="utf-8") as f:
12
+ CSS = f.read()
13
+
14
+ def load_template(filename: str) -> str:
15
+ filepath = os.path.join("templates", filename)
16
+ if os.path.exists(filepath):
17
+ with open(filepath, "r", encoding="utf-8") as f:
18
+ return f.read()
19
+ return ""
20
+
21
+ def get_header_html() -> str:
22
+ return load_template("top_bar.html")
23
+
24
+ def create_ui():
25
+ with gr.Blocks(delete_cache=(3600, 10800), title="I2V EXTENDED") as demo:
26
+ gr.HTML(f"<style>{CSS}</style>" + get_header_html())
27
+
28
+ with gr.Row():
29
+ with gr.Column(scale=5):
30
+ input_image_component = gr.Image(
31
+ type="pil",
32
+ label="🖼️ Input Image",
33
+ sources=["upload", "clipboard"],
34
+ height=260
35
+ )
36
+ with gr.Row():
37
+ image_url_input = gr.Textbox(
38
+ label="🌐 Load Input Image from URL",
39
+ placeholder="e.g. https://example.com/image.jpg",
40
+ value="",
41
+ scale=4
42
+ )
43
+ load_image_btn = gr.Button("📥 Load Image", variant="secondary", scale=1)
44
+
45
+ load_image_btn.click(
46
+ fn=load_image_from_url,
47
+ inputs=[image_url_input],
48
+ outputs=[input_image_component]
49
+ )
50
+
51
+ with gr.Accordion("🖼️ Last Image (Optional End Frame)", open=False):
52
+ last_image_component = gr.Image(
53
+ type="pil",
54
+ label="Last Image (Optional End Frame)",
55
+ sources=["upload", "clipboard"],
56
+ height=200
57
+ )
58
+
59
+ duration_seconds_input = gr.Slider(
60
+ minimum=config.MIN_DURATION,
61
+ maximum=config.MAX_DURATION,
62
+ step=0.1,
63
+ value=4.0,
64
+ label="⏱️ Duration (seconds)",
65
+ info=f"Set to 4.0s for direct 65-frame model generation. Or set to 2.0s + 32 FPS for ultra-fast 4s video (saves 50% quota)."
66
+ )
67
+
68
+ motion_extension_dropdown = gr.Dropdown(
69
+ choices=[
70
+ "⚡ Real-Time RIFE Interpolation (32/64 FPS Ultra-Smooth)",
71
+ "🔂 Ending-Only Boomerang Loop (Real Speed, Tail 1.5s Loop)",
72
+ "🔂 Classic Full Boomerang Loop (100% Real-Speed Forward+Reverse)",
73
+ "🌊 Adaptive Motion Speed Ramping (Ease-In/Out Curve)",
74
+ "🐢 Cinematic Slow-Motion (16 FPS RIFE Time-Stretch)"
75
+ ],
76
+ value="⚡ Real-Time RIFE Interpolation (32/64 FPS Ultra-Smooth)",
77
+ label="🎬 Motion Extension & Loop Technique",
78
+ info="Choose Real-Time RIFE (Default), Ending-Only Boomerang (Tail Loop), Classic Full Boomerang, or Speed Ramping."
79
+ )
80
+
81
+ frame_multi = gr.Dropdown(
82
+ choices=[
83
+ (f"16 FPS (Original Duration)", config.FIXED_FPS),
84
+ (f"32 FPS (2x RIFE -> Converts 2s GPU to 4s Video)", config.FIXED_FPS*2),
85
+ (f"64 FPS (4x RIFE -> Converts 2s GPU to 8s Video)", config.FIXED_FPS*4),
86
+ ],
87
+ value=config.FIXED_FPS,
88
+ label="🎬 Video Fluidity & RIFE Extension (FPS)",
89
+ info="Select 32 FPS (2x RIFE) to extend 2s GPU output into a smooth 4s video on CPU (0 GPU quota)."
90
+ )
91
+
92
+ with gr.Accordion("👑 Sulphur AI Vision Prompt Enhancer & Face Swap (Exclusive VIP)", open=True):
93
+ gr.HTML(load_template("sulphur_vip.html"))
94
+ sulphur_vip_password_input = gr.Textbox(
95
+ label="🔑 VIP Password Access Key",
96
+ type="password",
97
+ placeholder="Enter VIP Password to unlock Sulphur AI Vision Engine",
98
+ value="",
99
+ info="Required to authorize Sulphur AI Vision Prompt & Pre-Swap VIP feature."
100
+ )
101
+ with gr.Row():
102
+ sulphur_subject_input = gr.Textbox(
103
+ label="Main Subject",
104
+ value="",
105
+ lines=2,
106
+ placeholder="e.g. beautiful woman in a elegant black dress"
107
+ )
108
+ sulphur_adegan_input = gr.Textbox(
109
+ label="Scene Motion / Action",
110
+ value="",
111
+ lines=2,
112
+ placeholder="e.g. a man standing infront of her"
113
+ )
114
+ with gr.Row():
115
+ sulphur_camera_dropdown = gr.Dropdown(
116
+ choices=["Static", "Close-Up", "Handheld", "POV", "Pan Left-Right", "Zoom In", "Drone View", "Tracking Shot","Tilt Up-Down","Rotation","Zoom Out","Orbit Shot","Steadicam","Slow Motion","Fast Motion","Freeze Frame","Other", "Vertical Shot", "Horizontal Shot", "Diagonal Shot", "Macro Shot", "Telephoto Shot","Random"],
117
+ value="Static",
118
+ label="Camera Setting"
119
+ )
120
+ sulphur_atmosphere_dropdown = gr.Dropdown(
121
+ choices=["Dim Bedroom", "Neon / Cyberpunk", "Natural Cinematic", "Dramatic Night", "Golden Hour Sun", "Studio Portrait","Night Time","Home Theater","Outdoor Night","Indoor Night","Outdoor Day","Indoor Day","Car Night","Car Day","Beach Night","Beach Day","Forest Night","Forest Day","Mountain Night","Mountain Day","Desert Night","Desert Day","Pool Night","Pool Day","Room","Living Room","Kitchen","Bathroom","Garden","Street","Cafe","Restaurant","Bar","Club","Hotel","Mall","Airport","Train","Bus","Subway","Bicycle","Motorcycle","Truck","Boat","Airplane","Spaceship","Other"],
122
+ value="Natural Cinematic",
123
+ label="Atmosphere / Lighting"
124
+ )
125
+ sulphur_duration_slider = gr.Slider(minimum=1, maximum=8, step=1, value=4, label="Duration (seconds)")
126
+
127
+ with gr.Accordion("👤 Sulphur AI Face Swap & GFPGAN Restoration Controls (Optional)", open=False):
128
+ with gr.Row():
129
+ ref_face_component = gr.Image(
130
+ type="pil",
131
+ label="👤 Reference Face Image (Source Face)",
132
+ sources=["upload", "clipboard"],
133
+ height=180
134
+ )
135
+ with gr.Column():
136
+ sulphur_enable_swap_checkbox = gr.Checkbox(
137
+ label="🔄 Enable Face Swap & GFPGAN Restoration",
138
+ value=False,
139
+ info="Swaps face on main input image & sharpens facial features via GFPGAN before video generation."
140
+ )
141
+ target_gender_dropdown = gr.Dropdown(
142
+ choices=["Any / All Faces", "Female Faces Only", "Male Faces Only"],
143
+ value="Female Faces Only",
144
+ label="👥 Target Gender to Swap",
145
+ info="Filter which faces in target image get swapped."
146
+ )
147
+
148
+ sulphur_enhance_btn = gr.Button("💎 Auto-Generate Prompt Relay & Optional Pre-Swap Input via Sulphur AI", variant="primary")
149
+ sulphur_api_status_box = gr.HTML()
150
+
151
+ enable_prompt_relay_checkbox = gr.Checkbox(
152
+ label="🎬 Enable Prompt Relay (Multi-Event Timeline Control)",
153
+ value=True,
154
+ info="⭐ Primary Recommended: Enables high-accuracy multi-event timeline routing across video seconds (0 GPU quota extra)."
155
+ )
156
+ relay_prompt_schedule_input = gr.Textbox(
157
+ label="🎬 Multi-Event Timeline Schedule (Prompt Relay)",
158
+ lines=5,
159
+ max_lines=10,
160
+ placeholder="[0.0s - 2.0s] A beautiful woman sitting by the window reading a book\n[2.0s - 4.0s] The woman stands up, smiles warmly, and walks towards the camera",
161
+ value="[0.0s - 2.0s] A beautiful woman sitting by the window reading a book\n[2.0s - 4.0s] The woman stands up, smiles warmly, and walks towards the camera",
162
+ info="Format: [start_sec - end_sec] Detailed event motion description"
163
+ )
164
+
165
+ sulphur_enhance_btn.click(
166
+ fn=prompt_enhancer.call_sulphur_enhancer_api,
167
+ inputs=[
168
+ input_image_component,
169
+ sulphur_subject_input,
170
+ sulphur_adegan_input,
171
+ sulphur_camera_dropdown,
172
+ sulphur_atmosphere_dropdown,
173
+ sulphur_duration_slider,
174
+ ref_face_component,
175
+ target_gender_dropdown,
176
+ sulphur_enable_swap_checkbox,
177
+ gr.State(None),
178
+ sulphur_vip_password_input
179
+ ],
180
+ outputs=[
181
+ relay_prompt_schedule_input,
182
+ enable_prompt_relay_checkbox,
183
+ sulphur_api_status_box,
184
+ input_image_component
185
+ ]
186
+ )
187
+
188
+ with gr.Accordion("✨ Base Prompt (Global Modifiers / Quality Booster)", open=True):
189
+ prompt_input = gr.Textbox(
190
+ label="✨ Base Prompt",
191
+ value=config.default_prompt_i2v,
192
+ lines=3,
193
+ max_lines=6,
194
+ placeholder="Describe global style, lighting, or overall video mood..."
195
+ )
196
+
197
+ with gr.Row():
198
+ describe_image_btn = gr.Button("🔍 Describe Image from Input (CPU ~1s)", variant="secondary")
199
+ enhance_prompt_btn = gr.Button("✨ Auto-Enhance Prompt (CPU ~0.05s)", variant="secondary")
200
+
201
+ describe_image_btn.click(
202
+ fn=prompt_enhancer.describe_image,
203
+ inputs=[input_image_component],
204
+ outputs=[prompt_input]
205
+ )
206
+
207
+ enhance_prompt_btn.click(
208
+ fn=prompt_enhancer.enhance_prompt,
209
+ inputs=[prompt_input],
210
+ outputs=[prompt_input]
211
+ )
212
+
213
+ with gr.Row():
214
+ safe_mode_checkbox = gr.Checkbox(
215
+ label="🛠️ Safe Mode",
216
+ value=False,
217
+ info="Requests extra processing buffer time to prevent timeout when server is busy."
218
+ )
219
+
220
+ with gr.Accordion("🔗 Custom Civitai / Direct LoRA URL", open=False):
221
+ custom_lora_url_input = gr.Textbox(
222
+ label="Civitai / Direct LoRA Download URL",
223
+ placeholder="e.g. https://civitai.red/api/download/models/2098405?fileId=1994044",
224
+ value="",
225
+ info="Paste any Civitai or direct HTTP/HTTPS .safetensors link"
226
+ )
227
+ custom_lora_scale_input = gr.Slider(
228
+ label="Custom LoRA Weight Scale",
229
+ minimum=0.0,
230
+ maximum=2.0,
231
+ step=0.05,
232
+ value=1.0,
233
+ info="Strength of the custom LoRA effect"
234
+ )
235
+ with gr.Row():
236
+ download_lora_btn = gr.Button("📥 Pre-Download LoRA to Cache (CPU ~0 GPU Quota)", variant="secondary")
237
+ download_status_box = gr.HTML()
238
+
239
+ download_lora_btn.click(
240
+ fn=lora_loader.download_custom_lora_ui_action,
241
+ inputs=[custom_lora_url_input],
242
+ outputs=[download_status_box]
243
+ )
244
+
245
+ play_result_video = gr.Checkbox(label="Display Video Preview", value=True, interactive=True)
246
+ generate_button = gr.Button("🚀 Generate Video", variant="primary", elem_id="generate-btn")
247
+
248
+ with gr.Column(scale=5):
249
+ video_output = gr.Video(
250
+ label="📹 Generated Video Result",
251
+ autoplay=True,
252
+ sources=["upload"],
253
+ buttons=["download", "share"],
254
+ interactive=True,
255
+ elem_id="generated-video"
256
+ )
257
+
258
+ with gr.Row():
259
+ grab_frame_btn = gr.Button("📸 Use Current Frame as Input Image", variant="secondary", elem_id="grab-frame-btn")
260
+ grab_last_frame_btn = gr.Button("🏁 Use Current Frame as Last Frame", variant="secondary", elem_id="grab-last-frame-btn")
261
+ grab_swap_target_btn = gr.Button("👤 Use Current Frame as Target Image to Swap", variant="secondary", elem_id="grab-swap-target-btn")
262
+ timestamp_box = gr.Number(value=0, label="Timestamp", visible=True, elem_id="hidden-timestamp")
263
+
264
+ gpu_report_box = gr.HTML()
265
+ file_output = gr.File(label="📥 Download Video File")
266
+
267
+ noise_temperature_slider = gr.Slider(
268
+ minimum=0.1,
269
+ maximum=2.0,
270
+ step=0.05,
271
+ value=1.0,
272
+ label="🌡️ Noise Temperature",
273
+ info="Scales initial noise variance (0.5 = focused & smooth, 1.0 = standard, 1.5 = high visual randomness, 0 GPU quota extra)."
274
+ )
275
+
276
+ with gr.Accordion("⚙️ Advanced Settings", open=False):
277
+ custom_filename_input = gr.Textbox(
278
+ label="Output Filename (Optional)",
279
+ placeholder="e.g.: my_generated_video_01",
280
+ value="",
281
+ info="Saved as .mp4 format automatically. Leave blank for random name."
282
+ )
283
+ negative_prompt_input = gr.Textbox(label="Negative Prompt", value=config.default_negative_prompt, info="Active when Guidance Scale > 1.", lines=4, max_lines=8)
284
+
285
+ with gr.Row():
286
+ quality_slider = gr.Slider(minimum=1, maximum=10, step=1, value=6, label="Video Quality Grade")
287
+ steps_slider = gr.Slider(minimum=1, maximum=30, step=1, value=4, label="Inference Steps")
288
+
289
+ with gr.Row():
290
+ seed_input = gr.Slider(label="Seed", minimum=0, maximum=config.MAX_SEED, step=1, value=42, interactive=True)
291
+ randomize_seed_checkbox = gr.Checkbox(label="🎲 Randomize seed", value=True, interactive=True)
292
+
293
+ with gr.Row():
294
+ guidance_scale_input = gr.Slider(minimum=0.0, maximum=10.0, step=0.5, value=1, label="Guidance Scale (High Noise)", info="Value > 1 increases GPU compute.")
295
+ guidance_scale_2_input = gr.Slider(minimum=0.0, maximum=10.0, step=0.5, value=1, label="Guidance Scale 2 (Low Noise)")
296
+
297
+ with gr.Row():
298
+ scheduler_dropdown = gr.Dropdown(
299
+ label="Scheduler",
300
+ choices=list(config.SCHEDULER_MAP.keys()),
301
+ value="UniPCMultistep",
302
+ info="Custom diffusion scheduler."
303
+ )
304
+ flow_shift_slider = gr.Slider(minimum=0.5, maximum=15.0, step=0.1, value=3.0, label="Flow Shift")
305
+
306
+ with gr.Accordion("🛠️ Standalone Image Face Swapper & GFPGAN Restoration Tool", open=False):
307
+ with gr.Row():
308
+ tool_target_img = gr.Image(type="pil", label="Target Image to Swap / Enhance", sources=["upload", "clipboard"], height=200)
309
+ tool_ref_img = gr.Image(type="pil", label="Reference Face Image (Optional)", sources=["upload", "clipboard"], height=200)
310
+ with gr.Row():
311
+ tool_gender_dropdown = gr.Dropdown(
312
+ choices=["Any / All Faces", "Female Faces Only", "Male Faces Only"],
313
+ value="Any / All Faces",
314
+ label="👥 Target Gender to Swap"
315
+ )
316
+ tool_gfpgan_checkbox = gr.Checkbox(
317
+ label="✨ Enhance Face Detail (GFPGAN v1.4)",
318
+ value=True,
319
+ info="Sharpens blurry facial details via GFPGAN restoration"
320
+ )
321
+ with gr.Row():
322
+ tool_swap_btn = gr.Button("🚀 Swap Single Image Face (API / CPU)", variant="primary")
323
+ tool_enhance_only_btn = gr.Button("✨ Enhance Face Only (GFPGAN v1.4)", variant="secondary")
324
+
325
+ tool_result_img = gr.Image(type="pil", label="Result Image (Swapped / Enhanced)", interactive=False, height=220)
326
+ tool_send_btn = gr.Button("📸 Send Result Image to Main Video Input", variant="secondary")
327
+
328
+ def run_standalone_swap(target_img, ref_img, gender, enhance_gfpgan):
329
+ if target_img is None:
330
+ raise gr.Error("Please upload a target image to swap.")
331
+ return face_swapper.swap_face_in_single_image(target_img, ref_img, gender, enhance_with_gfpgan=enhance_gfpgan)
332
+
333
+ def run_standalone_face_enhance(target_img):
334
+ if target_img is None:
335
+ raise gr.Error("Please upload an image to enhance.")
336
+ res = face_swapper.call_sulphur_enhance_face_api(target_img)
337
+ if res is None:
338
+ raise gr.Error("Sulphur AI GFPGAN Face Enhance API is offline or not configured.")
339
+ return res
340
+
341
+ tool_swap_btn.click(
342
+ fn=run_standalone_swap,
343
+ inputs=[tool_target_img, tool_ref_img, tool_gender_dropdown, tool_gfpgan_checkbox],
344
+ outputs=[tool_result_img]
345
+ )
346
+
347
+ tool_enhance_only_btn.click(
348
+ fn=run_standalone_face_enhance,
349
+ inputs=[tool_target_img],
350
+ outputs=[tool_result_img]
351
+ )
352
+
353
+ tool_send_btn.click(
354
+ fn=lambda img: img,
355
+ inputs=[tool_result_img],
356
+ outputs=[input_image_component]
357
+ )
358
+
359
+ with gr.Accordion("💎 Remote VIP GPU RIFE Ultra Acceleration (Exclusive High-Speed Engine)", open=True):
360
+ gr.HTML(load_template("rife_vip.html"))
361
+ enable_vip_rife_checkbox = gr.Checkbox(
362
+ label="⚡ Enable VIP Remote RIFE Acceleration (Bypasses Local CPU)",
363
+ value=False,
364
+ info="Offloads 2x, 4x, or 8x frame rate extension to dedicated VIP GPU server with 0 GPU load."
365
+ )
366
+ vip_password_input = gr.Textbox(
367
+ label="🔑 VIP Password Access Key",
368
+ type="password",
369
+ placeholder="Enter VIP Password to unlock remote GPU RIFE",
370
+ value="",
371
+ info="Required to authorization remote GPU RIFE acceleration."
372
+ )
373
+ with gr.Row():
374
+ vip_rife_multiplier_radio = gr.Radio(
375
+ choices=["2x (32 FPS / 2x Duration)", "4x (64 FPS / 4x Duration)", "8x (128 FPS / 8x Duration)"],
376
+ value="2x (32 FPS / 2x Duration)",
377
+ label="🚀 VIP RIFE Multiplier",
378
+ scale=3
379
+ )
380
+ vip_rife_mode_dropdown = gr.Dropdown(
381
+ choices=[
382
+ "High-FPS Motion Smoothness (FPS Boost)",
383
+ "Slow-Motion / Extend Duration (Multiplied Seconds)"
384
+ ],
385
+ value="High-FPS Motion Smoothness (FPS Boost)",
386
+ label="⚙️ VIP RIFE Output Mode",
387
+ scale=3
388
+ )
389
+
390
+ ui_inputs = [
391
+ input_image_component, last_image_component, prompt_input, steps_slider,
392
+ negative_prompt_input, duration_seconds_input,
393
+ guidance_scale_input, guidance_scale_2_input, seed_input, randomize_seed_checkbox,
394
+ quality_slider, scheduler_dropdown, flow_shift_slider, frame_multi,
395
+ motion_extension_dropdown,
396
+ safe_mode_checkbox,
397
+ custom_lora_url_input,
398
+ custom_lora_scale_input,
399
+ enable_prompt_relay_checkbox,
400
+ relay_prompt_schedule_input,
401
+ ref_face_component,
402
+ target_gender_dropdown,
403
+ play_result_video,
404
+ custom_filename_input,
405
+ noise_temperature_slider,
406
+ enable_vip_rife_checkbox,
407
+ vip_rife_multiplier_radio,
408
+ vip_rife_mode_dropdown,
409
+ vip_password_input
410
+ ]
411
+
412
+ generate_button.click(
413
+ fn=generate_video,
414
+ inputs=ui_inputs,
415
+ outputs=[video_output, file_output, seed_input, gpu_report_box]
416
+ )
417
+
418
+ grab_frame_btn.click(
419
+ fn=extract_frame,
420
+ inputs=[video_output, timestamp_box],
421
+ outputs=[input_image_component, timestamp_box],
422
+ js=get_timestamp_js
423
+ )
424
+
425
+ grab_last_frame_btn.click(
426
+ fn=extract_frame,
427
+ inputs=[video_output, timestamp_box],
428
+ outputs=[last_image_component, timestamp_box],
429
+ js=get_timestamp_js
430
+ )
431
+
432
+ grab_swap_target_btn.click(
433
+ fn=extract_frame,
434
+ inputs=[video_output, timestamp_box],
435
+ outputs=[tool_target_img, timestamp_box],
436
+ js=get_timestamp_js
437
+ )
438
+
439
+ gr.HTML(load_template("footer.html"))
440
+
441
+ return demo