alex-remade commited on
Commit
d948455
·
1 Parent(s): 68d53f7

working with progress bar

Browse files
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ .env
2
+ venv/
__pycache__/video_config.cpython-311.pyc ADDED
Binary file (811 Bytes). View file
 
__pycache__/workflow_handler.cpython-311.pyc ADDED
Binary file (7.17 kB). View file
 
app.py CHANGED
@@ -1,77 +1,367 @@
1
  import os
2
  import requests
3
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
4
 
 
 
 
 
 
5
 
6
  loras = [
7
  {
8
  #I suggest it to be a gif instead of an mp4!
9
  "image": "https://huggingface.co/Remade-AI/Squish/resolve/main/example_videos/tank_squish.mp4",
10
  #This is an id you can send to your backend, obviously you can change it
11
- "id": "squish",
12
  #This is the title that is shown on the front end
13
- "title": "Squish"
 
 
14
  },
15
  {
16
  "image": "https://huggingface.co/Remade-AI/Rotate/resolve/main/example_videos/man_rotate.mp4",
17
- "id": "rotate",
18
- "title": "Rotate"
 
19
  },
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  ]
22
 
23
- ### Inside this function you can add a call to your backend passing the arguments, then you can return the path to a downloaded video
24
- ### To not have the code/API public, you can go on "Settings" and add a Secret. The secrets appear here as environemnt variables, e.g.
25
- ### os.getenv('BACKEND_URL')
26
- ### os.getenv('API_KEY')
27
- def generate_video(input_image, subject, duration, selected_index):
28
- if selected_index == "squish":
29
- #I suggest not exposing this to the user for simplicity
30
- prompt = f"In the video, a miniature {subject} is presented. The {subject} is held in a person's hands. The person then presses on the {subject}, causing a sq41sh squish effect. The person keeps pressing down on the {subject}, further showing the sq41sh squish effect."
31
-
32
- #do all the things to submit the request
33
- #url and heads can come from the env variable
34
- response = requests.post(url, data=data, files=files, headers=headers)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  def update_selection(evt: gr.SelectData):
39
  selected_lora = loras[evt.index]
40
  sentence = f"Selected LoRA: {selected_lora['title']}"
41
  return selected_lora['id'], sentence
42
 
43
- with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  selected_index = gr.State(None)
 
 
45
  gr.Markdown("# Remade AI - Wan 2.1 I2V effects LoRAs ")
46
  selected_info = gr.HTML("")
 
47
  with gr.Row():
48
- with gr.Column():
49
- gallery = gr.Gallery(
50
- [(item["image"], item["title"]) for item in loras],
51
- label="Select LoRA",
52
- allow_preview=False,
53
- columns=4,
54
- elem_id="gallery",
55
- show_share_button=False,
56
- height=350
57
- )
58
- input_image = gr.Image(type="filepath")
59
- subject = gr.Textbox(label="Describe your subject", placeholder="Cat toy")
60
- duration = gr.Radio(["Short (3s)", "Long (5s)"], label="Duration")
61
- button = gr.Button("Generate")
62
-
63
- with gr.Column():
64
- output = gr.Video(interactive=False, label="Output video")
65
-
 
66
  gallery.select(
67
  update_selection,
68
  outputs=[selected_index, selected_info]
69
- )
70
- button.click(
71
- generate_video,
72
- inputs=[input_image, subject, duration, selected_index],
73
- outputs=[output]
 
 
 
74
  )
75
 
76
  if __name__ == "__main__":
 
77
  demo.launch()
 
1
  import os
2
  import requests
3
  import gradio as gr
4
+ import uuid
5
+ import datetime
6
+ from supabase import create_client, Client
7
+ from supabase.lib.client_options import ClientOptions
8
+ import dotenv
9
+ from google.cloud import storage
10
+ import json
11
+ from pathlib import Path
12
+ import mimetypes
13
+ from workflow_handler import WanVideoWorkflow
14
+ from video_config import MODEL_FRAME_RATES, calculate_frames
15
+ import asyncio
16
 
17
+ dotenv.load_dotenv()
18
+
19
+ SCRIPT_DIR = Path(__file__).parent
20
+ CONFIG_PATH = SCRIPT_DIR / "config.json"
21
+ WORKFLOW_PATH = SCRIPT_DIR / "wani2v.json"
22
 
23
  loras = [
24
  {
25
  #I suggest it to be a gif instead of an mp4!
26
  "image": "https://huggingface.co/Remade-AI/Squish/resolve/main/example_videos/tank_squish.mp4",
27
  #This is an id you can send to your backend, obviously you can change it
28
+ "id": "06ce6840-f976-4963-9644-b6cf7f323f90",
29
  #This is the title that is shown on the front end
30
+ "title": "Squish",
31
+
32
+ "example_prompt": "In the video, a miniature rodent is presented. The rodent is held in a person's hands. The person then presses on the rodent, causing a sq41sh squish effect. The person keeps pressing down on the rodent, further showing the sq41sh squish effect.",
33
  },
34
  {
35
  "image": "https://huggingface.co/Remade-AI/Rotate/resolve/main/example_videos/man_rotate.mp4",
36
+ "id": "4ac08cfa-841e-4aa9-9022-c3fc80fb6ef4",
37
+ "title": "Rotate",
38
+ "example_prompt": "The video shows an elderly Asian man's head and shoulders with blurred background, performing a r0t4tion 360 degrees rotation.",
39
  },
40
+ {
41
+ "image": "https://huggingface.co/Remade-AI/Cakeify/resolve/main/example_videos/timberland_cakeify.mp4",
42
+ "id": "b05c1dc7-a71c-4d24-b512-4877a12dea7e",
43
+ "title": "Cakeify",
44
+ "example_prompt": "The video opens on a woman. A knife, held by a hand, is coming into frame and hovering over the woman. The knife then begins cutting into the woman to c4k3 cakeify it. As the knife slices the woman open, the inside of the woman is revealed to be cake with chocolate layers. The knife cuts through and the contents of the woman are revealed."
45
+ },
46
+
47
+
48
+
49
+
50
+
51
 
52
  ]
53
 
54
+ # Initialize Supabase client with async support
55
+ supabase: Client = create_client(
56
+ os.getenv('SUPABASE_URL'),
57
+ os.getenv('SUPABASE_KEY'),
58
+
59
+ )
60
+
61
+ def initialize_gcs():
62
+ """Initialize Google Cloud Storage client with credentials from environment"""
63
+ try:
64
+ # Parse service account JSON from environment variable
65
+ service_account_json = os.getenv('SERVICE_ACCOUNT_JSON')
66
+ if not service_account_json:
67
+ raise ValueError("SERVICE_ACCOUNT_JSON environment variable not found")
68
+
69
+ credentials_info = json.loads(service_account_json)
70
+
71
+ # Initialize storage client
72
+ storage_client = storage.Client.from_service_account_info(credentials_info)
73
+ print("Successfully initialized Google Cloud Storage client")
74
+ return storage_client
75
+ except Exception as e:
76
+ print(f"Error initializing Google Cloud Storage: {e}")
77
+ raise
78
+
79
+ def upload_to_gcs(file_path, content_type=None, folder='user_uploads'):
80
+ """
81
+ Uploads a file to Google Cloud Storage
82
+ Args:
83
+ file_path: Path to the file to upload
84
+ content_type: MIME type of the file (optional)
85
+ folder: Folder path in bucket (default: 'user_uploads')
86
+ Returns:
87
+ str: Public URL of the uploaded file
88
+ """
89
+ try:
90
+ bucket_name = 'remade-v2'
91
+ storage_client = initialize_gcs()
92
+ bucket = storage_client.bucket(bucket_name)
93
+
94
+ # Get file extension and generate unique filename
95
+ file_extension = Path(file_path).suffix
96
+ if not content_type:
97
+ content_type = mimetypes.guess_type(file_path)[0] or 'application/octet-stream'
98
+
99
+ # Validate file type
100
+ valid_types = ['image/jpeg', 'image/png', 'image/gif']
101
+ if content_type not in valid_types:
102
+ raise ValueError("Invalid file type. Please upload a JPG, PNG or GIF image.")
103
+
104
+ # Generate unique filename with proper path structure
105
+ filename = f"{str(uuid.uuid4())}{file_extension}"
106
+ file_path_in_gcs = f"{folder}/{filename}"
107
+
108
+ # Create blob and set metadata
109
+ blob = bucket.blob(file_path_in_gcs)
110
+ blob.content_type = content_type
111
+ blob.cache_control = 'public, max-age=31536000'
112
+
113
+ print(f'Uploading file to GCS: {file_path_in_gcs}')
114
+
115
+ # Upload the file
116
+ blob.upload_from_filename(
117
+ file_path,
118
+ timeout=120 # 2 minute timeout
119
+ )
120
+
121
+ # Generate public URL with correct path format
122
+ image_url = f"https://storage.googleapis.com/{bucket_name}/{file_path_in_gcs}"
123
+ print(f"Successfully uploaded to GCS: {image_url}")
124
+ return image_url
125
+
126
+ except Exception as e:
127
+ print(f"Error uploading to GCS: {e}")
128
+ raise ValueError(f"Failed to upload image to storage: {str(e)}")
129
+
130
+ def build_lora_prompt(subject, lora_id):
131
+ """
132
+ Builds a standardized prompt based on the selected LoRA and subject
133
+ """
134
+ # Get LoRA config
135
+ lora_config = next((lora for lora in loras if lora["id"] == lora_id), None)
136
+ if not lora_config:
137
+ raise ValueError(f"Invalid LoRA ID: {lora_id}")
138
+
139
+ if lora_id == "06ce6840-f976-4963-9644-b6cf7f323f90": # Squish
140
+ return (
141
+ f"In the video, a miniature {subject} is presented. "
142
+ f"The {subject} is held in a person's hands. "
143
+ f"The person then presses on the {subject}, causing a sq41sh squish effect. "
144
+ f"The person keeps pressing down on the {subject}, further showing the sq41sh squish effect."
145
+ )
146
+
147
+ elif lora_id == "4ac08cfa-841e-4aa9-9022-c3fc80fb6ef4": # Rotate
148
+ return (
149
+ f"The video shows a {subject} performing a r0t4tion 360 degrees rotation."
150
+ )
151
+
152
+ elif lora_id == "b05c1dc7-a71c-4d24-b512-4877a12dea7e": # Cakeify
153
+ return (
154
+ f"The video opens on a {subject}. A knife, held by a hand, is coming into frame "
155
+ f"and hovering over the {subject}. The knife then begins cutting into the {subject} "
156
+ f"to c4k3 cakeify it. As the knife slices the {subject} open, the inside of the "
157
+ f"{subject} is revealed to be cake with chocolate layers. The knife cuts through "
158
+ f"and the contents of the {subject} are revealed."
159
+ )
160
 
161
+ else:
162
+ raise ValueError(f"Unknown LoRA ID: {lora_id}")
163
+
164
+ def poll_generation_status(generation_id):
165
+ """Poll generation status from database"""
166
+ try:
167
+ # Query the database for the current status
168
+ response = supabase.table('generations') \
169
+ .select('*') \
170
+ .eq('generation_id', generation_id) \
171
+ .execute()
172
+
173
+ if not response.data:
174
+ return None
175
+
176
+ return response.data[0]
177
+ except Exception as e:
178
+ print(f"Error polling generation status: {e}")
179
+ raise e
180
+
181
+ async def generate_video(input_image, subject, duration, selected_index, progress=gr.Progress()):
182
+ try:
183
+ # Initialize workflow handler with explicit paths
184
+ workflow_handler = WanVideoWorkflow(
185
+ supabase,
186
+ config_path=str(CONFIG_PATH),
187
+ workflow_path=str(WORKFLOW_PATH)
188
+ )
189
+
190
+ # Upload image to GCS and get public URL
191
+ image_url = upload_to_gcs(input_image)
192
+
193
+ # Map duration selection to actual seconds
194
+ duration_mapping = {
195
+ "Short (3s)": 3,
196
+ "Long (5s)": 5
197
+ }
198
+ video_duration = duration_mapping[duration]
199
+
200
+ # Get LoRA config
201
+ lora_config = next((lora for lora in loras if lora["id"] == selected_index), None)
202
+ if not lora_config:
203
+ raise ValueError(f"Invalid LoRA ID: {selected_index}")
204
+
205
+ # Generate unique ID
206
+ generation_id = str(uuid.uuid4())
207
+
208
+ # Update workflow
209
+ prompt = build_lora_prompt(subject, selected_index)
210
+ workflow_handler.update_prompt(prompt)
211
+ workflow_handler.update_input_image(image_url)
212
+ await workflow_handler.update_lora(lora_config)
213
+ workflow_handler.update_length(video_duration)
214
+ workflow_handler.update_output_name(generation_id)
215
+
216
+ # Get final workflow
217
+ workflow = workflow_handler.get_workflow()
218
+
219
+ # Store generation data in Supabase
220
+ generation_data = {
221
+ "generation_id": generation_id,
222
+ "user_id": "anonymous",
223
+ "status": "queued",
224
+ "progress": 0,
225
+ "worker_id": None,
226
+ "created_at": datetime.datetime.utcnow().isoformat(),
227
+ "message": {
228
+ "generationId": generation_id,
229
+ "workflow": {
230
+ "prompt": workflow
231
+ }
232
+ },
233
+ "metadata": {
234
+ "prompt": {
235
+ "original": subject,
236
+ "enhanced": subject
237
+ },
238
+ "lora": {
239
+ "id": selected_index,
240
+ "strength": 1.0,
241
+ "name": lora_config["title"]
242
+ },
243
+ "workflow": "img2vid",
244
+ "dimensions": None,
245
+ "input_image_url": image_url,
246
+ "video_length": {"duration": video_duration},
247
+ "platform": "huggingface"
248
+ },
249
+ "error": None,
250
+ "output_url": None,
251
+ "batch_id": None
252
+ }
253
+
254
+ # Remove await - the execute() method returns the response directly
255
+ response = supabase.table('generations').insert(generation_data).execute()
256
+ print(f"Stored generation data with ID: {generation_id}")
257
+
258
+ # Return generation ID for tracking
259
+ return generation_id
260
+
261
+ except Exception as e:
262
+ print(f"Error in generate_video: {e}")
263
+ raise e
264
 
265
  def update_selection(evt: gr.SelectData):
266
  selected_lora = loras[evt.index]
267
  sentence = f"Selected LoRA: {selected_lora['title']}"
268
  return selected_lora['id'], sentence
269
 
270
+ async def handle_generation(input_image, subject, duration, selected_index, progress=gr.Progress(track_tqdm=True)):
271
+ try:
272
+ if selected_index is None:
273
+ raise gr.Error("You must select a LoRA before proceeding.")
274
+
275
+ # Generate the video and get generation ID
276
+ generation_id = await generate_video(input_image, subject, duration, selected_index)
277
+
278
+ # Poll for status updates
279
+ while True:
280
+ generation = poll_generation_status(generation_id)
281
+
282
+ if not generation:
283
+ raise ValueError(f"Generation {generation_id} not found")
284
+
285
+ # Update progress
286
+ if 'progress' in generation:
287
+ progress_value = generation['progress']
288
+ progress_bar = f'<div class="progress-container"><div class="progress-bar" style="--current: {progress_value}; --total: 100;"></div></div>'
289
+
290
+ # Check status
291
+ if generation['status'] == 'completed':
292
+ # Final yield with completed video
293
+ yield generation['output_url'], generation_id, gr.update(visible=False)
294
+ break # Exit the loop
295
+ elif generation['status'] == 'error':
296
+ raise ValueError(f"Generation failed: {generation.get('error')}")
297
+ else:
298
+ # Yield progress update
299
+ yield None, generation_id, gr.update(value=progress_bar, visible=True)
300
+
301
+ # Wait before next poll
302
+ await asyncio.sleep(2)
303
+
304
+ except Exception as e:
305
+ print(f"Error in handle_generation: {e}")
306
+ raise e
307
+
308
+ css = '''
309
+ #gen_btn{height: 100%}
310
+ #gen_column{align-self: stretch}
311
+ #title{text-align: center}
312
+ #title h1{font-size: 3em; display:inline-flex; align-items:center}
313
+ #title img{width: 100px; margin-right: 0.5em}
314
+ #gallery .grid-wrap{height: 10vh}
315
+ #lora_list{background: var(--block-background-fill);padding: 0 1em .3em; font-size: 90%}
316
+ .card_internal{display: flex;height: 100px;margin-top: .5em}
317
+ .card_internal img{margin-right: 1em}
318
+ .styler{--form-gap-width: 0px !important}
319
+ #progress{height:30px}
320
+ #progress .generating{display:none}
321
+ .progress-container {width: 100%;height: 30px;background-color: #f0f0f0;border-radius: 15px;overflow: hidden;margin-bottom: 20px}
322
+ .progress-bar {height: 100%;background-color: #4f46e5;width: calc(var(--current) / var(--total) * 100%);transition: width 0.5s ease-in-out}
323
+ '''
324
+
325
+ with gr.Blocks(css=css) as demo:
326
  selected_index = gr.State(None)
327
+ current_generation_id = gr.State(None)
328
+
329
  gr.Markdown("# Remade AI - Wan 2.1 I2V effects LoRAs ")
330
  selected_info = gr.HTML("")
331
+
332
  with gr.Row():
333
+ with gr.Column():
334
+ gallery = gr.Gallery(
335
+ [(item["image"], item["title"]) for item in loras],
336
+ label="Select LoRA",
337
+ allow_preview=False,
338
+ columns=4,
339
+ elem_id="gallery",
340
+ show_share_button=False,
341
+ height=350
342
+ )
343
+ input_image = gr.Image(type="filepath")
344
+ subject = gr.Textbox(label="Describe your subject", placeholder="Cat toy")
345
+ duration = gr.Radio(["Short (3s)", "Long (5s)"], label="Duration", value="Short (3s)")
346
+ button = gr.Button("Generate", variant="primary", elem_id="gen_btn")
347
+
348
+ with gr.Column():
349
+ progress_bar = gr.Markdown(elem_id="progress", visible=False)
350
+ output = gr.Video(interactive=False, label="Output video")
351
+
352
  gallery.select(
353
  update_selection,
354
  outputs=[selected_index, selected_info]
355
+ )
356
+
357
+ # Use gr.on for the button click to match the example
358
+ gr.on(
359
+ triggers=[button.click],
360
+ fn=handle_generation,
361
+ inputs=[input_image, subject, duration, selected_index],
362
+ outputs=[output, current_generation_id, progress_bar]
363
  )
364
 
365
  if __name__ == "__main__":
366
+ demo.queue()
367
  demo.launch()
config.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "models": [
3
+
4
+ {
5
+ "id": "wanvideo_itv",
6
+ "name": "WanVideo I2V",
7
+ "description": "WanVideo image-to-video generation model",
8
+ "workflow": "wanvideo_I2V_3_with_lora.json",
9
+ "type": "img2vid",
10
+ "nodes": {
11
+ "prompt": "16",
12
+ "seed": "27",
13
+ "videoCombine": "30",
14
+ "dimensions": "17",
15
+ "image": "18",
16
+ "lora": "39",
17
+ "modelLoader": "22"
18
+ }
19
+
20
+ }
21
+ ]
22
+ }
23
+
video_config.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Frame rates for different model types
2
+ MODEL_FRAME_RATES = {
3
+ "wanvideo": 16, # WanVideo models use 16 fps
4
+ "hunyuan": 24 # Hunyuan models use 24 fps
5
+ }
6
+
7
+ def calculate_frames(duration, frame_rate):
8
+ """
9
+ Calculate frames ensuring they follow the 4K+1 pattern
10
+ Args:
11
+ duration: Video duration in seconds
12
+ frame_rate: Frames per second
13
+ Returns:
14
+ int: Number of frames following 4K+1 pattern
15
+ """
16
+ # Calculate raw frames
17
+ raw_frames = round(duration * frame_rate)
18
+
19
+ # Adjust to nearest 4K+1 value
20
+ # First, find the nearest multiple of 4
21
+ nearest_multiple_of_4 = round(raw_frames / 4) * 4
22
+
23
+ # Then add 1 to get 4K+1
24
+ return nearest_multiple_of_4 + 1
wani2v.json ADDED
@@ -0,0 +1,258 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "11": {
3
+ "inputs": {
4
+ "model_name": "umt5-xxl-enc-bf16.safetensors",
5
+ "precision": "bf16",
6
+ "load_device": "offload_device",
7
+ "quantization": "disabled"
8
+ },
9
+ "class_type": "LoadWanVideoT5TextEncoder",
10
+ "_meta": {
11
+ "title": "Load WanVideo T5 TextEncoder"
12
+ }
13
+ },
14
+ "13": {
15
+ "inputs": {
16
+ "model_name": "open-clip-xlm-roberta-large-vit-huge-14_visual_fp32.safetensors",
17
+ "precision": "fp32",
18
+ "load_device": "offload_device"
19
+ },
20
+ "class_type": "LoadWanVideoClipTextEncoder",
21
+ "_meta": {
22
+ "title": "Load WanVideo Clip TextEncoder"
23
+ }
24
+ },
25
+ "16": {
26
+ "inputs": {
27
+ "positive_prompt": "a cute anime girl with massive fennec ears and a big fluffy tail wearing a maid outfit turning around",
28
+ "negative_prompt": "bad quality video",
29
+ "force_offload": true,
30
+ "t5": [
31
+ "11",
32
+ 0
33
+ ]
34
+ },
35
+ "class_type": "WanVideoTextEncode",
36
+ "_meta": {
37
+ "title": "WanVideo TextEncode"
38
+ }
39
+ },
40
+ "17": {
41
+ "inputs": {
42
+ "generation_width": 512,
43
+ "generation_height": 512,
44
+ "num_frames": 49,
45
+ "force_offload": true,
46
+ "noise_aug_strength": 0,
47
+ "latent_strength": 1,
48
+ "clip_embed_strength": 1,
49
+ "adjust_resolution": true,
50
+ "clip_vision": [
51
+ "13",
52
+ 0
53
+ ],
54
+ "image": [
55
+ "18",
56
+ 0
57
+ ],
58
+ "vae": [
59
+ "21",
60
+ 0
61
+ ]
62
+ },
63
+ "class_type": "WanVideoImageClipEncode",
64
+ "_meta": {
65
+ "title": "WanVideo ImageClip Encode"
66
+ }
67
+ },
68
+ "18": {
69
+ "inputs": {
70
+ "image": "flux_dev_example.png",
71
+ "upload": "image"
72
+ },
73
+ "class_type": "LoadImage",
74
+ "_meta": {
75
+ "title": "Load Image"
76
+ }
77
+ },
78
+ "21": {
79
+ "inputs": {
80
+ "model_name": "Wan2_1_VAE_bf16.safetensors",
81
+ "precision": "bf16"
82
+ },
83
+ "class_type": "WanVideoVAELoader",
84
+ "_meta": {
85
+ "title": "WanVideo VAE Loader"
86
+ }
87
+ },
88
+ "22": {
89
+ "inputs": {
90
+ "model": "Wan2_1-I2V-14B-480P_fp8_e4m3fn.safetensors",
91
+ "base_precision": "bf16",
92
+ "quantization": "fp8_e4m3fn",
93
+ "load_device": "offload_device",
94
+ "attention_mode": "sdpa",
95
+ "lora": [
96
+ "39",
97
+ 0
98
+ ]
99
+ },
100
+ "class_type": "WanVideoModelLoader",
101
+ "_meta": {
102
+ "title": "WanVideo Model Loader"
103
+ }
104
+ },
105
+ "27": {
106
+ "inputs": {
107
+ "steps": 30,
108
+ "cfg": 6,
109
+ "shift": 5,
110
+ "seed": 733113722085555,
111
+ "force_offload": true,
112
+ "scheduler": "dpm++",
113
+ "riflex_freq_index": 0,
114
+ "denoise_strength": 1,
115
+ "model": [
116
+ "22",
117
+ 0
118
+ ],
119
+ "text_embeds": [
120
+ "16",
121
+ 0
122
+ ],
123
+ "image_embeds": [
124
+ "17",
125
+ 0
126
+ ],
127
+ "feta_args": [
128
+ "37",
129
+ 0
130
+ ],
131
+ "teacache_args": [
132
+ "36",
133
+ 0
134
+ ]
135
+ },
136
+ "class_type": "WanVideoSampler",
137
+ "_meta": {
138
+ "title": "WanVideo Sampler"
139
+ }
140
+ },
141
+ "28": {
142
+ "inputs": {
143
+ "enable_vae_tiling": true,
144
+ "tile_x": 272,
145
+ "tile_y": 272,
146
+ "tile_stride_x": 160,
147
+ "tile_stride_y": 128,
148
+ "vae": [
149
+ "21",
150
+ 0
151
+ ],
152
+ "samples": [
153
+ "27",
154
+ 0
155
+ ]
156
+ },
157
+ "class_type": "WanVideoDecode",
158
+ "_meta": {
159
+ "title": "WanVideo Decode"
160
+ }
161
+ },
162
+ "30": {
163
+ "inputs": {
164
+ "frame_rate": 32,
165
+ "loop_count": 0,
166
+ "filename_prefix": "WanVideo2_1",
167
+ "format": "video/h264-mp4",
168
+ "pix_fmt": "yuv420p",
169
+ "crf": 19,
170
+ "save_metadata": true,
171
+ "trim_to_audio": false,
172
+ "pingpong": false,
173
+ "save_output": true,
174
+ "images": [
175
+ "38",
176
+ 0
177
+ ]
178
+ },
179
+ "class_type": "VHS_VideoCombine",
180
+ "_meta": {
181
+ "title": "Video Combine 🎥🅥🅗🅢"
182
+ }
183
+ },
184
+ "32": {
185
+ "inputs": {
186
+ "blocks_to_swap": 10,
187
+ "offload_img_emb": false,
188
+ "offload_txt_emb": false
189
+ },
190
+ "class_type": "WanVideoBlockSwap",
191
+ "_meta": {
192
+ "title": "WanVideo BlockSwap"
193
+ }
194
+ },
195
+ "35": {
196
+ "inputs": {
197
+ "backend": "inductor",
198
+ "fullgraph": false,
199
+ "mode": "default",
200
+ "dynamic": false,
201
+ "dynamo_cache_size_limit": 64,
202
+ "compile_transformer_blocks": true
203
+ },
204
+ "class_type": "WanVideoTorchCompileSettings",
205
+ "_meta": {
206
+ "title": "WanVideo Torch Compile Settings"
207
+ }
208
+ },
209
+ "36": {
210
+ "inputs": {
211
+ "rel_l1_thresh": 0.25,
212
+ "start_step": 1,
213
+ "end_step": -1,
214
+ "cache_device": "offload_device",
215
+ "use_coefficients": "true"
216
+ },
217
+ "class_type": "WanVideoTeaCache",
218
+ "_meta": {
219
+ "title": "WanVideo TeaCache"
220
+ }
221
+ },
222
+ "37": {
223
+ "inputs": {
224
+ "weight": 2,
225
+ "start_percent": 0,
226
+ "end_percent": 1
227
+ },
228
+ "class_type": "WanVideoEnhanceAVideo",
229
+ "_meta": {
230
+ "title": "WanVideo Enhance-A-Video"
231
+ }
232
+ },
233
+ "38": {
234
+ "inputs": {
235
+ "ckpt_name": "film_net_fp32.pt",
236
+ "clear_cache_after_n_frames": 100,
237
+ "multiplier": 2,
238
+ "frames": [
239
+ "28",
240
+ 0
241
+ ]
242
+ },
243
+ "class_type": "FILM VFI",
244
+ "_meta": {
245
+ "title": "FILM VFI"
246
+ }
247
+ },
248
+ "39": {
249
+ "inputs": {
250
+ "lora": "Cakeify\\cakeify_30.safetensors",
251
+ "strength": 1
252
+ },
253
+ "class_type": "WanVideoLoraSelect",
254
+ "_meta": {
255
+ "title": "WanVideo Lora Select"
256
+ }
257
+ }
258
+ }
workflow_handler.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from video_config import MODEL_FRAME_RATES, calculate_frames
4
+
5
+ class WanVideoWorkflow:
6
+ def __init__(self, supabase_client, config_path="config.json", workflow_path="wani2v.json"):
7
+ # Add debug prints and error handling
8
+ try:
9
+ workflow_path = Path(workflow_path)
10
+ config_path = Path(config_path)
11
+
12
+ print(f"Loading workflow from: {workflow_path.absolute()}")
13
+ print(f"File exists: {workflow_path.exists()}")
14
+ print(f"File size: {workflow_path.stat().st_size}")
15
+
16
+ # Load config and workflow
17
+ with open(config_path, 'r', encoding='utf-8') as f:
18
+ self.config = json.load(f)
19
+
20
+ with open(workflow_path, 'r', encoding='utf-8') as f:
21
+ content = f.read()
22
+ print(f"Raw content length: {len(content)}")
23
+ if not content:
24
+ raise ValueError(f"Workflow file is empty: {workflow_path}")
25
+ self.workflow = json.loads(content)
26
+
27
+ self.supabase = supabase_client
28
+
29
+ # Get node mappings
30
+ self.model_config = next(
31
+ (model for model in self.config["models"] if model["id"] == "wanvideo_itv"),
32
+ None
33
+ )
34
+ if not self.model_config:
35
+ raise ValueError("WanVideo I2V model config not found")
36
+
37
+ self.nodes = self.model_config["nodes"]
38
+
39
+ except Exception as e:
40
+ print(f"Error initializing WanVideoWorkflow: {str(e)}")
41
+ raise
42
+
43
+ async def get_lora_path(self, lora_id):
44
+ """Get LoRA path from Supabase training_jobs table"""
45
+ response = self.supabase.table('training_jobs').select(
46
+ 'name, visible, config'
47
+ ).eq('id', lora_id).execute()
48
+
49
+ if not response.data:
50
+ raise ValueError(f"LoRA with ID {lora_id} not found")
51
+
52
+ lora_data = response.data[0]
53
+ visible_epoch = lora_data['visible']
54
+
55
+ if visible_epoch is None:
56
+ raise ValueError(f"LoRA {lora_id} has no visible epoch")
57
+
58
+ return f"{lora_id}/run/epoch{visible_epoch}/adapter_model.safetensors"
59
+
60
+ def update_prompt(self, prompt):
61
+ """Update the prompt node"""
62
+ prompt_node = self.workflow[self.nodes["prompt"]]
63
+ if not prompt_node.get("inputs"):
64
+ raise ValueError("Invalid prompt node structure")
65
+ prompt_node["inputs"]["positive_prompt"] = prompt
66
+
67
+ def update_input_image(self, image_path):
68
+ """Update the input image node"""
69
+ image_node = self.workflow[self.nodes["image"]]
70
+ if not image_node.get("inputs"):
71
+ raise ValueError("Invalid image node structure")
72
+ image_node["inputs"]["image"] = Path(image_path).name
73
+
74
+ async def update_lora(self, lora_config):
75
+ """Update the LoRA node"""
76
+ lora_node = self.workflow[self.nodes["lora"]]
77
+ if not lora_node.get("inputs"):
78
+ raise ValueError("Invalid LoRA node structure")
79
+
80
+ # Get LoRA path from Supabase
81
+ lora_path = await self.get_lora_path(lora_config["id"])
82
+ lora_node["inputs"]["lora"] = lora_path
83
+ lora_node["inputs"]["strength"] = 1.0
84
+
85
+ def update_length(self, duration):
86
+ """Update video length (number of frames)"""
87
+ dimensions_node = self.workflow[self.nodes["dimensions"]]
88
+ if not dimensions_node.get("inputs"):
89
+ raise ValueError("Invalid dimensions node structure")
90
+
91
+ frame_rate = MODEL_FRAME_RATES["wanvideo"]
92
+ num_frames = calculate_frames(duration, frame_rate)
93
+ dimensions_node["inputs"]["num_frames"] = num_frames
94
+
95
+ def update_output_name(self, generation_id):
96
+ """Update the output filename"""
97
+ video_combine_node = self.workflow[self.nodes["videoCombine"]]
98
+ if not video_combine_node.get("inputs"):
99
+ raise ValueError("Invalid video combine node structure")
100
+ video_combine_node["inputs"]["filename_prefix"] = generation_id
101
+
102
+ def get_workflow(self):
103
+ """Return the complete workflow"""
104
+ return self.workflow