ghostai1 commited on
Commit
86aaa4d
Β·
verified Β·
1 Parent(s): 44d6e89

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1376 -4
app.py CHANGED
@@ -1,7 +1,1379 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # FILE: app.py
3
+ # Description: Image-to-Video generation server with Gradio UI and FastAPI for Hugging Face Spaces
4
+ # Version: 1.2.8
5
+ # Timestamp: 2025-07-01 20:03 CDT
6
+ # Author: Grok 3, built by xAI (based on GhostAI's ghostpack_gradio_f1.py)
7
+ # NOTE: Optimized for Hugging Face Spaces with H200 GPU, 25 min/day render time
8
+ # Loads models from Hugging Face Hub to avoid HDD costs
9
+ # Uses /data for persistent storage, /tmp for temporary files
10
+ # API key authentication for /generate endpoint (off-site use)
11
+ # Base64-encoded video responses
12
+ # Gradio UI matches original ghostpack_gradio_f1.py
13
+ # Idle until triggered by API or Gradio
14
+
15
+ import os
16
+ import sys
17
+ import time
18
+ import json
19
+ import argparse
20
+ import importlib.util
21
+ import subprocess
22
+ import traceback
23
+ import torch
24
+ import einops
25
+ import numpy as np
26
+ from PIL import Image
27
+ import io
28
  import gradio as gr
29
+ import asyncio
30
+ import queue
31
+ from threading import Thread
32
+ import re
33
+ import logging
34
+ import base64
35
+ import socket
36
+ import requests
37
+ import shutil
38
+ import uuid
39
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Form, Depends, Security, status
40
+ from fastapi.security import APIKeyHeader
41
+ from fastapi.middleware.cors import CORSMiddleware
42
+ from fastapi.responses import JSONResponse
43
+ from pydantic import BaseModel
44
+ from diffusers import AutoencoderKLHunyuanVideo
45
+ from transformers import (
46
+ LlamaModel, CLIPTextModel, LlamaTokenizerFast, CLIPTokenizer,
47
+ SiglipImageProcessor, SiglipVisionModel
48
+ )
49
+ from diffusers_helper.hunyuan import (
50
+ encode_prompt_conds, vae_decode, vae_encode, vae_decode_fake
51
+ )
52
+ from diffusers_helper.utils import (
53
+ save_bcthw_as_mp4, crop_or_pad_yield_mask, soft_append_bcthw
54
+ )
55
+ from diffusers_helper.models.hunyuan_video_packed import HunyuanVideoTransformer3DModelPacked
56
+ from diffusers_helper.memory import (
57
+ gpu, get_cuda_free_memory_gb, move_model_to_device_with_memory_preservation,
58
+ offload_model_from_device_for_memory_preservation, fake_diffusers_current_device,
59
+ DynamicSwapInstaller, unload_complete_models, load_model_as_complete
60
+ )
61
+ from diffusers_helper.clip_vision import hf_clip_vision_encode
62
+ from diffusers_helper.bucket_tools import find_nearest_bucket
63
+ from diffusers_helper.thread_utils import AsyncStream
64
+ from diffusers_helper.gradio.progress_bar import make_progress_bar_css, make_progress_bar_html
65
+
66
+ # Optional: Colorama for colored console output
67
+ try:
68
+ from colorama import init, Fore, Style
69
+ init(autoreset=True)
70
+ COLORAMA_AVAILABLE = True
71
+ def red(s): return Fore.RED + s + Style.RESET_ALL
72
+ def green(s): return Fore.GREEN + s + Style.RESET_ALL
73
+ def yellow(s): return Fore.YELLOW + s + Style.RESET_ALL
74
+ def reset_all(s): return Style.RESET_ALL + s
75
+ except ImportError:
76
+ COLORAMA_AVAILABLE = False
77
+ def red(s): return s
78
+ def green(s): return s
79
+ def yellow(s): return s
80
+ def reset_all(s): return s
81
+
82
+ # Set up logging
83
+ logging.basicConfig(
84
+ filename="/data/ghostpack.log",
85
+ level=logging.DEBUG,
86
+ format="%(asctime)s %(levelname)s:%(message)s",
87
+ )
88
+ logger = logging.getLogger(__name__)
89
+ logger.info("Starting GhostPack F1 Pro")
90
+ print(f"{green('Using /data/video_info.json for metadata')}")
91
+
92
+ VERSION = "1.2.8"
93
+ HF_TOKEN = os.getenv('HF_TOKEN', 'your-hf-token') # Set in Spaces secrets
94
+ API_KEY_NAME = "X-API-Key"
95
+ API_KEY = os.getenv('API_KEY', 'your-secret-key') # Set in Spaces secrets
96
+ api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
97
+
98
+ # Global job registry
99
+ active_jobs = {} # {job_id: AsyncStream}
100
+ job_status = {} # {job_id: {"status": str, "progress": float, "render_time": float}}
101
+
102
+ # CLI
103
+ parser = argparse.ArgumentParser(description="GhostPack F1 Pro")
104
+ parser.add_argument("--share", action="store_true", help="Share Gradio UI publicly")
105
+ parser.add_argument("--server", type=str, default="0.0.0.0", help="Server host")
106
+ parser.add_argument("--port", type=int, default=7860, help="FastAPI port")
107
+ parser.add_argument("--gradio", action="store_true", help="Enable Gradio UI")
108
+ parser.add_argument("--inbrowser", action="store_true", help="Open in browser")
109
+ parser.add_argument("--cli", action="store_true", help="Show CLI help")
110
+ args = parser.parse_args()
111
+
112
+ # Global state
113
+ render_on_off = True
114
+
115
+ BASE = os.path.abspath(os.path.dirname(__file__))
116
+ os.environ["HF_HOME"] = "/tmp/hf_cache" # Cache models in /tmp
117
+
118
+ # Check if ports are available
119
+ def is_port_in_use(port):
120
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
121
+ return s.connect_ex(('0.0.0.0', port)) == 0
122
+
123
+ if args.cli:
124
+ print(f"{green('πŸ‘» GhostPack F1 Pro CLI')}")
125
+ print("python app.py # Launch API")
126
+ print("python app.py --gradio # Launch API + Gradio UI")
127
+ print("python app.py --cli # Show help")
128
+ sys.exit(0)
129
+
130
+ # Paths
131
+ DATA_DIR = "/data"
132
+ TMP_DIR = "/tmp/ghostpack"
133
+ VIDEO_OUTPUT_DIR = "/tmp/ghostpack/vid"
134
+ VIDEO_IMG_DIR = "/tmp/ghostpack/img"
135
+ VIDEO_TMP_DIR = "/tmp/ghostpack/tmp_vid"
136
+ VIDEO_INFO_FILE = "/data/video_info.json"
137
+ PROMPT_LOG_FILE = "/data/prompts.txt"
138
+ SAVED_PROMPTS_FILE = "/data/saved_prompts.json"
139
+ INSTALL_LOG_FILE = "/data/install_logs.txt"
140
+ LAST_CLEANUP_FILE = "/data/last_cleanup.txt"
141
+
142
+ # Initialize directories
143
+ for d in (DATA_DIR, TMP_DIR, VIDEO_OUTPUT_DIR, VIDEO_IMG_DIR, VIDEO_TMP_DIR):
144
+ if not os.path.exists(d):
145
+ try:
146
+ os.makedirs(d, exist_ok=True)
147
+ os.chmod(d, 0o775)
148
+ logger.debug(f"Created {d}")
149
+ except Exception as e:
150
+ logger.error(f"Failed to create {d}: {e}")
151
+ print(f"{red(f'Error: Failed to create {d}: {e}')}")
152
+ sys.exit(1)
153
+
154
+ # Initialize files
155
+ for f in (VIDEO_INFO_FILE, SAVED_PROMPTS_FILE, PROMPT_LOG_FILE, INSTALL_LOG_FILE, LAST_CLEANUP_FILE):
156
+ if not os.path.exists(f):
157
+ try:
158
+ if f == LAST_CLEANUP_FILE:
159
+ with open(f, "w") as fd:
160
+ fd.write(str(time.time()))
161
+ elif f in (VIDEO_INFO_FILE, SAVED_PROMPTS_FILE):
162
+ with open(f, "w") as fd:
163
+ json.dump([], fd)
164
+ else:
165
+ open(f, "w").close()
166
+ os.chmod(f, 0o664)
167
+ logger.debug(f"Created {f}")
168
+ except Exception as e:
169
+ logger.error(f"Failed to create/chmod {f}: {e}")
170
+ print(f"{red(f'Error: Failed to create/chmod {f}: {e}')}")
171
+ sys.exit(1)
172
+
173
+ # Clear VIDEO_INFO_FILE on startup
174
+ try:
175
+ with open(VIDEO_INFO_FILE, "w") as f:
176
+ json.dump([], f)
177
+ os.chmod(VIDEO_INFO_FILE, 0o664)
178
+ logger.debug(f"Cleared {VIDEO_INFO_FILE}")
179
+ except Exception as e:
180
+ logger.error(f"Failed to clear {VIDEO_INFO_FILE}: {e}")
181
+ print(f"{red(f'Error: Failed to clear {VIDEO_INFO_FILE}: {e}')}")
182
+ sys.exit(1)
183
+
184
+ # Queue clearing utility
185
+ def clear_queue(q):
186
+ try:
187
+ while True:
188
+ if hasattr(q, "get_nowait"):
189
+ q.get_nowait()
190
+ else:
191
+ break
192
+ except queue.Empty:
193
+ pass
194
+
195
+ # Prompt utilities
196
+ def get_last_prompts():
197
+ try:
198
+ return json.load(open(SAVED_PROMPTS_FILE))[-5:][::-1]
199
+ except Exception as e:
200
+ logger.error(f"Failed to load prompts from {SAVED_PROMPTS_FILE}: {e}")
201
+ print(f"{red(f'Error: Failed to load prompts: {e}')}")
202
+ return []
203
+
204
+ def save_prompt_fn(prompt, n_p):
205
+ if not prompt:
206
+ return f"{red('❌ No prompt')}"
207
+ try:
208
+ data = json.load(open(SAVED_PROMPTS_FILE))
209
+ entry = {"prompt": prompt, "negative": n_p}
210
+ if entry not in data:
211
+ data.append(entry)
212
+ with open(SAVED_PROMPTS_FILE, "w") as f:
213
+ json.dump(data, f, indent=2)
214
+ os.chmod(SAVED_PROMPTS_FILE, 0o664)
215
+ return f"{green('βœ… Saved')}"
216
+ except Exception as e:
217
+ logger.error(f"Failed to save prompt to {SAVED_PROMPTS_FILE}: {e}")
218
+ print(f"{red(f'Error: Failed to save prompt: {e}')}")
219
+ return f"{red('❌ Save failed')}"
220
+
221
+ def load_prompt_fn(idx):
222
+ lst = get_last_prompts()
223
+ return lst[idx]["prompt"] if idx < len(lst) else ""
224
+
225
+ # Cleanup utilities
226
+ def clear_temp_videos():
227
+ try:
228
+ for f in os.listdir(VIDEO_TMP_DIR):
229
+ os.remove(os.path.join(VIDEO_TMP_DIR, f))
230
+ return f"{green('βœ… Temp cleared')}"
231
+ except Exception as e:
232
+ logger.error(f"Failed to clear temp videos in {VIDEO_TMP_DIR}: {e}")
233
+ print(f"{red(f'Error: Failed to clear temp videos: {e}')}")
234
+ return f"{red('❌ Clear failed')}"
235
+
236
+ def clear_old_files():
237
+ cutoff = time.time() - 7 * 24 * 3600
238
+ c = 0
239
+ try:
240
+ for d in (VIDEO_TMP_DIR, VIDEO_IMG_DIR, VIDEO_OUTPUT_DIR):
241
+ for f in os.listdir(d):
242
+ p = os.path.join(d, f)
243
+ if os.path.isfile(p) and os.path.getmtime(p) < cutoff:
244
+ os.remove(p)
245
+ c += 1
246
+ with open(LAST_CLEANUP_FILE, "w") as f:
247
+ f.write(str(time.time()))
248
+ os.chmod(LAST_CLEANUP_FILE, 0o664)
249
+ return f"{green(f'βœ… {c} old files removed')}"
250
+ except Exception as e:
251
+ logger.error(f"Failed to clear old files: {e}")
252
+ print(f"{red(f'Error: Failed to clear old files: {e}')}")
253
+ return f"{red('❌ Clear failed')}"
254
+
255
+ def clear_images():
256
+ try:
257
+ for f in os.listdir(VIDEO_IMG_DIR):
258
+ os.remove(os.path.join(VIDEO_IMG_DIR, f))
259
+ return f"{green('βœ… Images cleared')}"
260
+ except Exception as e:
261
+ logger.error(f"Failed to clear images in {VIDEO_IMG_DIR}: {e}")
262
+ print(f"{red(f'Error: Failed to clear images: {e}')}")
263
+ return f"{red('❌ Clear failed')}"
264
+
265
+ def clear_videos():
266
+ try:
267
+ for f in os.listdir(VIDEO_OUTPUT_DIR):
268
+ os.remove(os.path.join(VIDEO_OUTPUT_DIR, f))
269
+ return f"{green('βœ… Videos cleared')}"
270
+ except Exception as e:
271
+ logger.error(f"Failed to clear videos in {VIDEO_OUTPUT_DIR}: {e}")
272
+ print(f"{red(f'Error: Failed to clear videos: {e}')}")
273
+ return f"{red('❌ Clear failed')}"
274
+
275
+ def check_and_run_weekly_cleanup():
276
+ try:
277
+ with open(LAST_CLEANUP_FILE, "r") as f:
278
+ last_cleanup = float(f.read().strip())
279
+ except (FileNotFoundError, ValueError):
280
+ last_cleanup = 0
281
+ if time.time() - last_cleanup > 7 * 24 * 3600:
282
+ return clear_old_files()
283
+ return ""
284
+
285
+ # Video metadata utilities
286
+ def save_video_info(prompt, n_p, filename, seed, secs, additional_info, completed=False):
287
+ if not completed:
288
+ return
289
+ try:
290
+ video_info = json.load(open(VIDEO_INFO_FILE))
291
+ except (FileNotFoundError, json.JSONDecodeError):
292
+ video_info = []
293
+ entry = {
294
+ "prompt": prompt or "",
295
+ "negative_prompt": n_p or "",
296
+ "filename": filename,
297
+ "location": os.path.join(VIDEO_OUTPUT_DIR, filename),
298
+ "seed": seed,
299
+ "duration_secs": secs,
300
+ "timestamp": time.strftime("%Y%m%d_%H%M%S"),
301
+ "completed": completed,
302
+ "additional_info": additional_info or {},
303
+ }
304
+ video_info.append(entry)
305
+ try:
306
+ with open(VIDEO_INFO_FILE, "w") as f:
307
+ json.dump(video_info, f, indent=2)
308
+ os.chmod(VIDEO_INFO_FILE, 0o664)
309
+ logger.debug(f"Saved video info to {VIDEO_INFO_FILE}")
310
+ except Exception as e:
311
+ logger.error(f"Failed to save video info to {VIDEO_INFO_FILE}: {e}")
312
+ print(f"{red(f'Error: Failed to save video info to {VIDEO_INFO_FILE}: {e}')}")
313
+ raise
314
+
315
+ # Gallery helpers
316
+ def list_images():
317
+ return sorted(
318
+ [os.path.join(VIDEO_IMG_DIR, f) for f in os.listdir(VIDEO_IMG_DIR) if f.lower().endswith((".png", ".jpg"))],
319
+ key=os.path.getmtime,
320
+ )
321
+
322
+ def list_videos():
323
+ return sorted(
324
+ [os.path.join(VIDEO_OUTPUT_DIR, f) for f in os.listdir(VIDEO_OUTPUT_DIR) if f.lower().endswith(".mp4")],
325
+ key=os.path.getmtime,
326
+ )
327
+
328
+ def load_image(sel):
329
+ imgs = list_images()
330
+ if sel in [os.path.basename(p) for p in imgs]:
331
+ pth = imgs[[os.path.basename(p) for p in imgs].index(sel)]
332
+ return gr.update(value=pth), gr.update(value=os.path.basename(pth))
333
+ return gr.update(), gr.update()
334
+
335
+ def load_video(sel):
336
+ vids = list_videos()
337
+ if sel in [os.path.basename(p) for p in vids]:
338
+ pth = vids[[os.path.basename(p) for p in vids].index(sel)]
339
+ return gr.update(value=pth), gr.update(value=os.path.basename(pth))
340
+ return gr.update(), gr.update()
341
+
342
+ def next_image_and_load(sel):
343
+ imgs = list_images()
344
+ if not imgs:
345
+ return gr.update(), gr.update()
346
+ names = [os.path.basename(i) for i in imgs]
347
+ idx = (names.index(sel) + 1) % len(names) if sel in names else 0
348
+ pth = imgs[idx]
349
+ return gr.update(value=pth), gr.update(value=os.path.basename(pth))
350
+
351
+ def next_video_and_load(sel):
352
+ vids = list_videos()
353
+ if not vids:
354
+ return gr.update(), gr.update()
355
+ names = [os.path.basename(v) for v in vids]
356
+ idx = (names.index(sel) + 1) % len(names) if sel in names else 0
357
+ pth = vids[idx]
358
+ return gr.update(value=pth), gr.update(value=os.path.basename(pth))
359
+
360
+ def gallery_image_select(evt: gr.SelectData):
361
+ imgs = list_images()
362
+ if evt.index is not None and evt.index < len(imgs):
363
+ pth = imgs[evt.index]
364
+ return gr.update(value=pth), gr.update(value=os.path.basename(pth))
365
+ return gr.update(), gr.update()
366
+
367
+ def gallery_video_select(evt: gr.SelectData):
368
+ vids = list_videos()
369
+ if evt.index is not None and evt.index < len(vids):
370
+ pth = vids[evt.index]
371
+ return gr.update(value=pth), gr.update(value=os.path.basename(pth))
372
+ return gr.update(), gr.update()
373
+
374
+ # Install status
375
+ def check_mod(n):
376
+ return importlib.util.find_spec(n) is not None
377
+
378
+ def status_xformers():
379
+ print(f"{green('βœ… Xformers is installed!')}" if check_mod("xformers") else f"{red('❌ Xformers is not installed!')}")
380
+ return f"{green('βœ… xformers')}" if check_mod("xformers") else f"{red('❌ xformers')}"
381
+
382
+ def status_sage():
383
+ print(f"{green('βœ… Sage Attn is installed!')}" if check_mod("sageattention") else f"{red('❌ Sage Attn is not installed!')}")
384
+ return f"{green('βœ… sage-attn')}" if check_mod("sageattention") else f"{red('❌ sage-attn')}"
385
+
386
+ def status_flash():
387
+ print(f"{yellow('⚠️ Flash Attn is not installed, performance may be reduced!')}" if not check_mod("flash_attn") else f"{green('βœ… Flash Attn is installed!')}")
388
+ return f"{yellow('⚠️ flash-attn')}" if not check_mod("flash_attn") else f"{green('βœ… flash-attn')}"
389
+
390
+ def status_colorama():
391
+ return f"{green('βœ… colorama')}" if COLORAMA_AVAILABLE else f"{red('❌ colorama')}"
392
+
393
+ def install_pkg(pkg, warn=None):
394
+ if warn:
395
+ print(f"{yellow(warn)}")
396
+ time.sleep(1)
397
+ try:
398
+ out = subprocess.check_output(
399
+ [sys.executable, "-m", "pip", "install", pkg], stderr=subprocess.STDOUT, text=True
400
+ )
401
+ res = f"{green(f'βœ… {pkg}')}\n{out}\n"
402
+ except subprocess.CalledProcessError as e:
403
+ res = f"{red(f'❌ {pkg}')}\n{e.output}\n"
404
+ with open(INSTALL_LOG_FILE, "a") as f:
405
+ f.write(f"[{pkg}] {res}")
406
+ return res
407
+
408
+ install_xformers = lambda: install_pkg("xformers")
409
+ install_sage_attn = lambda: install_pkg("sage-attn")
410
+ install_flash_attn = lambda: install_pkg("flash-attn", "⚠️ long compile, optional for performance")
411
+ install_colorama = lambda: install_pkg("colorama")
412
+ refresh_logs = lambda: open(INSTALL_LOG_FILE).read()
413
+ clear_logs = lambda: open(INSTALL_LOG_FILE, "w").close() or f"{green('βœ… Logs cleared')}"
414
+
415
+ # Model load
416
+ free_mem = get_cuda_free_memory_gb(gpu)
417
+ hv = free_mem > 60
418
+ logger.info(f"VRAM available: {free_mem:.2f} GB, High VRAM mode: {hv}")
419
+ print(f"{yellow(f'VRAM available: {free_mem:.2f} GB, High VRAM mode: {hv}')}")
420
+
421
+ try:
422
+ print(f"{yellow('Loading models...')}")
423
+ text_encoder = LlamaModel.from_pretrained(
424
+ "hunyuanvideo-community/HunyuanVideo", subfolder="text_encoder", torch_dtype=torch.float16, token=HF_TOKEN, cache_dir="/tmp/hf_cache"
425
+ ).cpu().eval()
426
+ text_encoder_2 = CLIPTextModel.from_pretrained(
427
+ "hunyuanvideo-community/HunyuanVideo", subfolder="text_encoder_2", torch_dtype=torch.float16, token=HF_TOKEN, cache_dir="/tmp/hf_cache"
428
+ ).cpu().eval()
429
+ tokenizer = LlamaTokenizerFast.from_pretrained(
430
+ "hunyuanvideo-community/HunyuanVideo", subfolder="tokenizer", token=HF_TOKEN, cache_dir="/tmp/hf_cache"
431
+ )
432
+ tokenizer_2 = CLIPTokenizer.from_pretrained(
433
+ "hunyuanvideo-community/HunyuanVideo", subfolder="tokenizer_2", token=HF_TOKEN, cache_dir="/tmp/hf_cache"
434
+ )
435
+ vae = AutoencoderKLHunyuanVideo.from_pretrained(
436
+ "hunyuanvideo-community/HunyuanVideo", subfolder="vae", torch_dtype=torch.float16, token=HF_TOKEN, cache_dir="/tmp/hf_cache"
437
+ ).cpu().eval()
438
+ feature_extractor = SiglipImageProcessor.from_pretrained(
439
+ "lllyasviel/flux_redux_bfl", subfolder="feature_extractor", token=HF_TOKEN, cache_dir="/tmp/hf_cache"
440
+ )
441
+ image_encoder = SiglipVisionModel.from_pretrained(
442
+ "lllyasviel/flux_redux_bfl", subfolder="image_encoder", torch_dtype=torch.float16, token=HF_TOKEN, cache_dir="/tmp/hf_cache"
443
+ ).cpu().eval()
444
+ transformer = HunyuanVideoTransformer3DModelPacked.from_pretrained(
445
+ "lllyasviel/FramePack_F1_I2V_HY_20250503", torch_dtype=torch.bfloat16, token=HF_TOKEN, cache_dir="/tmp/hf_cache"
446
+ ).cpu().eval()
447
+ logger.info("Models loaded successfully")
448
+ print(f"{green('Models loaded successfully')}")
449
+ except Exception as e:
450
+ logger.error(f"Failed to load models: {e}", exc_info=True)
451
+ print(f"{red(f'Error: Failed to load models: {e}')}")
452
+ raise
453
+
454
+ if not hv:
455
+ vae.enable_slicing()
456
+ vae.enable_tiling()
457
+
458
+ transformer.high_quality_fp32_output_for_inference = True
459
+ transformer.to(dtype=torch.bfloat16)
460
+ for m in (vae, image_encoder, text_encoder, text_encoder_2):
461
+ m.to(dtype=torch.float16)
462
+ for m in (vae, image_encoder, text_encoder, text_encoder_2, transformer):
463
+ m.requires_grad_(False)
464
+
465
+ if not hv:
466
+ DynamicSwapInstaller.install_model(transformer, device=gpu)
467
+ DynamicSwapInstaller.install_model(text_encoder, device=gpu)
468
+ else:
469
+ for m in (vae, image_encoder, text_encoder, text_encoder_2, transformer):
470
+ m.to(gpu)
471
+ logger.debug("Models configured and moved to device")
472
+ print(f"{green('Models configured and moved to device')}")
473
+
474
+ # FastAPI Setup
475
+ app = FastAPI(title="GhostPack F1 Pro API")
476
+ app.add_middleware(
477
+ CORSMiddleware,
478
+ allow_origins=["*"],
479
+ allow_credentials=True,
480
+ allow_methods=["*"],
481
+ allow_headers=["*"],
482
+ )
483
+
484
+ async def verify_api_key(api_key: str = Security(api_key_header)):
485
+ if api_key != API_KEY:
486
+ raise HTTPException(
487
+ status_code=status.HTTP_401_UNAUTHORIZED,
488
+ detail="Invalid API key"
489
+ )
490
+ return api_key
491
+
492
+ class GenerateRequest(BaseModel):
493
+ prompt: str
494
+ negative_prompt: str
495
+ seed: int
496
+ video_length: float
497
+ latent_window: int
498
+ steps: int
499
+ cfg: float
500
+ distilled_cfg: float
501
+ cfg_rescale: float
502
+ gpu_keep: float
503
+ crf: int
504
+ use_teacache: bool
505
+ camera_action: str
506
+ disable_prompt_mods: bool
507
+ link_steps_window: bool
508
+
509
+ @app.get("/health")
510
+ async def health_check():
511
+ try:
512
+ return JSONResponse(content={"status": "healthy"})
513
+ except Exception as e:
514
+ logger.error(f"Health check failed: {e}", exc_info=True)
515
+ return JSONResponse(content={"error": str(e), "status": "error"}, status_code=500)
516
+
517
+ @app.get("/test")
518
+ async def test_server():
519
+ try:
520
+ report = {
521
+ "server_status": {
522
+ "version": VERSION,
523
+ "host": args.server,
524
+ "port": args.port,
525
+ "uptime": time.time() - time.time() if job_status else 0,
526
+ "active_jobs": len(active_jobs),
527
+ "api_status": "running",
528
+ },
529
+ "system": {
530
+ "vram_total": free_mem,
531
+ "vram_free": get_cuda_free_memory_gb(gpu),
532
+ "high_vram_mode": hv,
533
+ "cuda_available": torch.cuda.is_available(),
534
+ "cuda_device": torch.cuda.get_device_name(gpu) if torch.cuda.is_available() else "N/A",
535
+ },
536
+ "models": {
537
+ "text_encoder": text_encoder is not None,
538
+ "text_encoder_2": text_encoder_2 is not None,
539
+ "vae": vae is not None,
540
+ "image_encoder": image_encoder is not None,
541
+ "transformer": transformer is not None,
542
+ "tokenizer": tokenizer is not None,
543
+ "tokenizer_2": tokenizer_2 is not None,
544
+ "feature_extractor": feature_extractor is not None,
545
+ },
546
+ "paths": {
547
+ "base": BASE,
548
+ "images": VIDEO_IMG_DIR,
549
+ "videos": VIDEO_OUTPUT_DIR,
550
+ "temp": VIDEO_TMP_DIR,
551
+ "data": DATA_DIR,
552
+ "prompt_log": PROMPT_LOG_FILE,
553
+ "saved_prompts": SAVED_PROMPTS_FILE,
554
+ "install_log": INSTALL_LOG_FILE,
555
+ "video_info": VIDEO_INFO_FILE,
556
+ },
557
+ "file_system": {
558
+ "images_writable": os.access(VIDEO_IMG_DIR, os.W_OK),
559
+ "videos_writable": os.access(VIDEO_OUTPUT_DIR, os.W_OK),
560
+ "temp_writable": os.access(VIDEO_TMP_DIR, os.W_OK),
561
+ "data_writable": os.access(DATA_DIR, os.W_OK),
562
+ },
563
+ "dependencies": {
564
+ "xformers": status_xformers(),
565
+ "sage_attn": status_sage(),
566
+ "flash_attn": status_flash(),
567
+ "colorama": status_colorama(),
568
+ },
569
+ "health_check": {"status": "pass", "details": ""}
570
+ }
571
+
572
+ try:
573
+ dummy_img = np.zeros((64, 64, 3), dtype=np.uint8)
574
+ img_pt = (torch.from_numpy(dummy_img).float() / 127.5 - 1).permute(2, 0, 1)[None, :, None]
575
+ if not hv:
576
+ load_model_as_complete(vae, gpu)
577
+ _ = vae_encode(img_pt, vae)
578
+ report["health_check"]["status"] = "pass"
579
+ except Exception as e:
580
+ report["health_check"]["status"] = "fail"
581
+ report["health_check"]["details"] = str(e)
582
+ logger.error(f"Health check failed: {e}", exc_info=True)
583
+
584
+ logger.info("Test endpoint accessed successfully")
585
+ print(f"{green(f'Test endpoint accessed: API running on {args.server}:{args.port}')}")
586
+ return JSONResponse(content=report)
587
+ except Exception as e:
588
+ logger.error(f"Test endpoint error: {e}", exc_info=True)
589
+ print(f"{red(f'Test endpoint error: {e}')}")
590
+ return JSONResponse(
591
+ content={"error": str(e), "status": "fail"},
592
+ status_code=500
593
+ )
594
+
595
+ @app.get("/status/{job_id}")
596
+ async def get_status(job_id: str, api_key: str = Depends(verify_api_key)):
597
+ try:
598
+ status = job_status.get(job_id, {"status": "not_found", "progress": 0.0, "render_time": 0})
599
+ return JSONResponse(
600
+ content={
601
+ "job_id": job_id,
602
+ "render_status": status["status"],
603
+ "render_progress": status["progress"],
604
+ "render_time": status["render_time"],
605
+ "active_jobs": len(active_jobs),
606
+ "api_status": "running",
607
+ }
608
+ )
609
+ except Exception as e:
610
+ logger.error(f"Status check failed for job {job_id}: {e}", exc_info=True)
611
+ return JSONResponse(
612
+ content={"error": str(e), "job_id": job_id, "status": "error"},
613
+ status_code=500
614
+ )
615
+
616
+ @app.post("/stop/{job_id}")
617
+ async def stop_render(job_id: str, api_key: str = Depends(verify_api_key)):
618
+ if job_id not in active_jobs:
619
+ logger.info(f"No active job {job_id} to stop")
620
+ print(f"{yellow(f'No active job {job_id} to stop')}")
621
+ return JSONResponse(content={"message": f"No active job {job_id}"})
622
+ stream = active_jobs[job_id]
623
+ stream.stop()
624
+ active_jobs.pop(job_id, None)
625
+ job_status[job_id]["status"] = "stopped"
626
+ job_status[job_id]["progress"] = 0.0
627
+ logger.info(f"Stopped job {job_id}")
628
+ print(f"{yellow(f'Stopped job {job_id}')}")
629
+ return JSONResponse(content={"message": f"Job {job_id} stopped"})
630
+
631
+ @app.get("/videos")
632
+ async def get_videos(api_key: str = Depends(verify_api_key)):
633
+ try:
634
+ videos = [f for f in os.listdir(VIDEO_OUTPUT_DIR) if f.lower().endswith(".mp4")]
635
+ return JSONResponse(content={"status": "success", "videos": videos})
636
+ except Exception as e:
637
+ logger.error(f"Failed to list videos: {e}", exc_info=True)
638
+ return JSONResponse(content={"error": str(e), "status": "error"}, status_code=500)
639
+
640
+ @app.post("/generate")
641
+ async def generate_video(
642
+ image_file: UploadFile = File(...),
643
+ prompt: str = Form(""),
644
+ negative_prompt: str = Form(""),
645
+ seed: int = Form(31337),
646
+ video_length: float = Form(8.0),
647
+ latent_window: int = Form(3),
648
+ steps: int = Form(12),
649
+ cfg: float = Form(1.0),
650
+ distilled_cfg: float = Form(7.0),
651
+ cfg_rescale: float = Form(0.5),
652
+ gpu_keep: float = Form(6.0),
653
+ crf: int = Form(20),
654
+ use_teacache: bool = Form(True),
655
+ camera_action: str = Form("Static Camera"),
656
+ disable_prompt_mods: bool = Form(False),
657
+ link_steps_window: bool = Form(True),
658
+ api_key: str = Depends(verify_api_key)
659
+ ):
660
+ params = {
661
+ "prompt": prompt,
662
+ "negative_prompt": negative_prompt,
663
+ "seed": seed,
664
+ "video_length": video_length,
665
+ "latent_window": latent_window,
666
+ "steps": steps,
667
+ "cfg": cfg,
668
+ "distilled_cfg": distilled_cfg,
669
+ "cfg_rescale": cfg_rescale,
670
+ "gpu_keep": gpu_keep,
671
+ "crf": crf,
672
+ "use_teacache": use_teacache,
673
+ "camera_action": camera_action,
674
+ "disable_prompt_mods": disable_prompt_mods,
675
+ "link_steps_window": link_steps_window
676
+ }
677
+ logger.info(f"Received /generate request with parameters: {json.dumps(params, indent=2)}")
678
+ print(f"{green(f'API: Received /generate request with parameters: {json.dumps(params, indent=2)}')}")
679
+
680
+ if not render_on_off:
681
+ logger.info("Render disabled by client")
682
+ print(f"{red('API: Render disabled by client')}")
683
+ return JSONResponse(content={"status": "render_disabled", "error": "Rendering disabled"}, status_code=403)
684
+
685
+ jid = str(uuid.uuid4())
686
+ logger.info(f"Starting job {jid} with prompt: {prompt}")
687
+ print(f"{green(f'API: Starting job ID: {jid}')}")
688
+
689
+ stream = AsyncStream()
690
+ active_jobs[jid] = stream
691
+ job_status[jid] = {"status": "rendering", "progress": 0.0, "render_time": 0}
692
+
693
+ try:
694
+ logger.debug("Processing uploaded image file")
695
+ print(f"{yellow('API: Processing uploaded image file')}")
696
+ img_data = await image_file.read()
697
+ if not img_data:
698
+ logger.error("Empty image file")
699
+ print(f"{red('API: Empty image file')}")
700
+ raise HTTPException(status_code=400, detail="Empty image file")
701
+
702
+ try:
703
+ img = Image.open(io.BytesIO(img_data)).convert('RGB')
704
+ img_np = np.array(img)
705
+ if img_np.shape[0] < 64 or img_np.shape[1] < 64:
706
+ logger.error("Image dimensions too small")
707
+ print(f"{red('API: Image dimensions too small (minimum 64x64)')}")
708
+ raise HTTPException(status_code=400, detail="Image dimensions must be at least 64x64")
709
+ except Exception as e:
710
+ logger.error(f"Invalid image: {str(e)}")
711
+ print(f"{red(f'API: Invalid image: {str(e)}')}")
712
+ raise HTTPException(status_code=400, detail=f"Invalid image: {str(e)}")
713
+
714
+ if get_cuda_free_memory_gb(gpu) < 2:
715
+ logger.error("Insufficient VRAM for processing")
716
+ print(f"{red('API: Insufficient VRAM (<2GB). Lower gpu_keep or latent_window.')}")
717
+ raise HTTPException(status_code=500, detail="Low VRAM (<2GB). Lower 'gpu_keep' or 'latent_window'.")
718
+
719
+ logger.info(f"Passing to worker: seed={seed}, video_length={video_length}, latent_window={latent_window}, steps={steps}, cfg={cfg}, distilled_cfg={distilled_cfg}")
720
+ print(f"{yellow(f'API: Passing to worker: seed={seed}, video_length={video_length}, latent_window={latent_window}, steps={steps}, cfg={cfg}, distilled_cfg={distilled_cfg}')}")
721
+
722
+ final_video_path = worker(
723
+ img_np=img_np,
724
+ prompt=prompt,
725
+ negative_prompt=negative_prompt,
726
+ seed=seed,
727
+ secs=video_length,
728
+ win=latent_window,
729
+ stp=steps,
730
+ cfg=cfg,
731
+ gsc=distilled_cfg,
732
+ rsc=cfg_rescale,
733
+ keep=gpu_keep,
734
+ tea=use_teacache,
735
+ crf=crf,
736
+ camera_action=camera_action,
737
+ disable_prompt_mods=disable_prompt_mods,
738
+ link_steps_window=link_steps_window,
739
+ stream=stream,
740
+ jid=jid
741
+ )
742
+
743
+ if final_video_path is None:
744
+ logger.error("Render stopped or failed")
745
+ print(f"{red('API: Render stopped or failed')}")
746
+ raise HTTPException(status_code=500, detail="Render stopped or failed")
747
+
748
+ final_filename = os.path.basename(final_video_path)
749
+ with open(final_video_path, "rb") as f:
750
+ video_data = base64.b64encode(f.read()).decode("utf-8")
751
+
752
+ save_video_info(
753
+ prompt=prompt,
754
+ n_p=negative_prompt,
755
+ filename=final_filename,
756
+ seed=seed,
757
+ secs=video_length,
758
+ additional_info={"camera_action": camera_action, "job_id": jid},
759
+ completed=True
760
+ )
761
+
762
+ response_info = {
763
+ "status": "success",
764
+ "job_id": jid,
765
+ "video_data": video_data,
766
+ "metadata": {
767
+ "prompt": prompt,
768
+ "negative_prompt": negative_prompt,
769
+ "seed": seed,
770
+ "duration_secs": video_length,
771
+ "timestamp": time.strftime("%Y%m%d_%H%M%S"),
772
+ "render_time_secs": job_status[jid]["render_time"],
773
+ "camera_action": camera_action,
774
+ "latent_window": latent_window,
775
+ "steps": steps,
776
+ "cfg": cfg,
777
+ "distilled_cfg": distilled_cfg,
778
+ "cfg_rescale": cfg_rescale,
779
+ "gpu_keep": gpu_keep,
780
+ "crf": crf,
781
+ "use_teacache": use_teacache,
782
+ "disable_prompt_mods": disable_prompt_mods,
783
+ "link_steps_window": link_steps_window
784
+ }
785
+ }
786
+
787
+ logger.info(f"Video generated: {final_video_path}")
788
+ print(f"{green(f'API: Video generated: {final_video_path}')}")
789
+ return JSONResponse(content=response_info)
790
+
791
+ except Exception as e:
792
+ logger.error(f"Generate failed: {e}", exc_info=True)
793
+ print(f"{red(f'API: Error during /generate: {str(e)}')}")
794
+ job_status[jid]["status"] = "error"
795
+ job_status[jid]["progress"] = 0.0
796
+ stream.output_queue.push(("end", str(e)))
797
+ return JSONResponse(
798
+ content={"error": str(e), "job_id": jid, "status": "error"},
799
+ status_code=500
800
+ )
801
+ finally:
802
+ active_jobs.pop(jid, None)
803
+ clear_queue(stream.input_queue)
804
+ clear_queue(stream.output_queue)
805
+ if job_status.get(jid, {}).get("status") not in ["complete", "error", "stopped"]:
806
+ job_status[jid]["status"] = "complete"
807
+ torch.cuda.empty_cache()
808
+
809
+ @torch.no_grad()
810
+ def worker(img_np, prompt, negative_prompt, seed, secs, win, stp, cfg, gsc, rsc, keep, tea, crf, camera_action, disable_prompt_mods, link_steps_window, stream, jid):
811
+ start_time = time.time()
812
+ job_status[jid] = {"status": "rendering", "progress": 0.0, "render_time": 0}
813
+ max_sections = 100
814
+
815
+ logger.info(f"Worker started for job {jid} with secs={secs}, win={win}, cfg={cfg}, distilled_cfg={gsc}")
816
+ print(f"{green(f'API: Starting video generation, job ID: {jid}, secs={secs}, win={win}, cfg={cfg}, distilled_cfg={gsc}')}")
817
+
818
+ try:
819
+ if img_np.shape[0] < 64 or img_np.shape[1] < 64:
820
+ raise ValueError("Image dimensions too small (minimum 64x64)")
821
+ if secs > 10:
822
+ logger.warning("Video length > 10s capped at 10s")
823
+ print(f"{yellow('API: Video length > 10s capped at 10s')}")
824
+ secs = min(secs, 10)
825
+ if win > 10:
826
+ logger.warning("Latent window > 10 capped at 10")
827
+ print(f"{yellow('API: Latent window > 10 capped at 10')}")
828
+ win = min(win, 10)
829
+ if get_cuda_free_memory_gb(gpu) < 2:
830
+ raise ValueError("Low VRAM (<2GB). Lower 'gpu_keep' or 'latent_window'.")
831
+
832
+ try:
833
+ if hasattr(stream.input_queue, "qsize") and stream.input_queue.qsize() > 0:
834
+ if stream.input_queue.get_nowait() == "end":
835
+ stream.output_queue.push(("end", "Job stopped by client"))
836
+ job_status[jid]["status"] = "stopped"
837
+ return None
838
+ except queue.Empty:
839
+ pass
840
+
841
+ if not disable_prompt_mods:
842
+ if "stop" not in prompt.lower() and secs > 3:
843
+ prompt += " The subject stops moving after 3 seconds."
844
+ if "smooth" not in prompt.lower():
845
+ prompt = f"Smooth animation: {prompt}"
846
+ if "silent" not in prompt.lower():
847
+ prompt += ", silent"
848
+ prompt = update_prompt(prompt, camera_action)
849
+ if len(prompt.split()) > 50:
850
+ logger.warning("Complex prompt may slow rendering")
851
+ print(f"{yellow('API: Warning: Complex prompt may slow rendering')}")
852
+
853
+ try:
854
+ with open(PROMPT_LOG_FILE, "a") as f:
855
+ f.write(f"{jid}\t{prompt}\t{negative_prompt}\n")
856
+ os.chmod(PROMPT_LOG_FILE, 0o664)
857
+ except Exception as e:
858
+ logger.error(f"Failed to write to {PROMPT_LOG_FILE}: {e}")
859
+ print(f"{red(f'API: Failed to write prompt log: {e}')}")
860
+ raise
861
+
862
+ stream.output_queue.push(('progress', (None, "", make_progress_bar_html(0, "Start"))))
863
+
864
+ if not hv:
865
+ unload_complete_models(text_encoder, text_encoder_2, image_encoder, vae, transformer)
866
+ fake_diffusers_current_device(text_encoder, gpu)
867
+ load_model_as_complete(text_encoder_2, gpu)
868
+ lv, cp = encode_prompt_conds(prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2)
869
+ if cfg == 1:
870
+ lv_n = torch.zeros_like(lv)
871
+ cp_n = torch.zeros_like(cp)
872
+ else:
873
+ lv_n, cp_n = encode_prompt_conds(negative_prompt, text_encoder, text_encoder_2, tokenizer, tokenizer_2)
874
+ lv, m = crop_or_pad_yield_mask(lv, 512)
875
+ lv_n, m_n = crop_or_pad_yield_mask(lv_n, 512)
876
+ lv, cp, lv_n, cp_n = [x.to(torch.bfloat16) for x in (lv, cp, lv_n, cp_n)]
877
+ logger.debug(f"Prompt embeddings: lv={lv.shape}, cp={cp.shape}, lv_n={lv_n.shape}, cp_n={cp_n.shape}")
878
+ torch.cuda.empty_cache()
879
+
880
+ H, W, _ = img_np.shape
881
+ h, w = H, W
882
+ img_filename = f"{jid}.png"
883
+ try:
884
+ Image.fromarray(img_np).save(os.path.join(VIDEO_IMG_DIR, img_filename))
885
+ os.chmod(os.path.join(VIDEO_IMG_DIR, img_filename), 0o664)
886
+ except Exception as e:
887
+ logger.error(f"Failed to save image {img_filename}: {e}")
888
+ print(f"{red(f'API: Failed to save image: {e}')}")
889
+ raise
890
+
891
+ img_pt = (torch.from_numpy(img_np).float() / 127.5 - 1).permute(2, 0, 1)[None, :, None]
892
+ logger.debug(f"Image tensor shape: {img_pt.shape}")
893
+
894
+ if not hv:
895
+ load_model_as_complete(vae, gpu)
896
+ start_lat = vae_encode(img_pt, vae)
897
+ logger.debug(f"VAE encoded latent shape: {start_lat.shape}")
898
+ if not hv:
899
+ load_model_as_complete(image_encoder, gpu)
900
+ img_emb = hf_clip_vision_encode(img_np, feature_extractor, image_encoder).last_hidden_state.to(torch.bfloat16)
901
+ logger.debug(f"Image embedding shape: {img_emb.shape}")
902
+ torch.cuda.empty_cache()
903
+
904
+ gen = torch.Generator("cpu").manual_seed(seed)
905
+ sections = max(round((secs * 30) / (win * 4)), 1)
906
+ if sections > max_sections:
907
+ logger.error(f"Too many sections ({sections}) for job {jid}")
908
+ print(f"{red(f'API: Too many sections ({sections}) for job {jid}')}")
909
+ raise ValueError(f"Too many sections ({sections})")
910
+ logger.info(f"Job {jid} sections: {sections}, pad_seq: {[3] + [2] * (sections - 3) + [1, 0] if sections > 4 else list(reversed(range(sections)))}")
911
+ hist_lat = torch.zeros((1, 16, 1 + 2 + 16, h // 8, w // 8), dtype=torch.float16).cpu()
912
+ hist_px = None
913
+ total = 0
914
+ pad_seq = [3] + [2] * (sections - 3) + [1, 0] if sections > 4 else list(reversed(range(sections)))
915
+ section_count = 0
916
+ for pad in pad_seq:
917
+ section_count += 1
918
+ if section_count > max_sections:
919
+ logger.error(f"Max sections ({max_sections}) exceeded for job {jid}")
920
+ print(f"{red(f'API: Max sections ({max_sections}) exceeded for job {jid}')}")
921
+ raise ValueError(f"Max sections ({max_sections}) exceeded")
922
+ last = pad == 0
923
+ logger.info(f"Job {jid} processing pad: {pad}, last: {last}")
924
+
925
+ def cb(d):
926
+ if job_status[jid]["status"] == "complete":
927
+ return
928
+ pv = vae_decode_fake(d["denoised"])
929
+ pv = (pv * 255).cpu().numpy().clip(0, 255).astype(np.uint8)
930
+ pv = einops.rearrange(pv, "b c t h w -> (b h) (t w) c")
931
+ cur = d["i"] + 1
932
+ job_status[jid]["progress"] = (cur / stp) * 100
933
+ logger.info(f"Job {jid} Progress {cur}/{stp} ({job_status[jid]['progress']:.1f}%)")
934
+ print(f"{yellow(f'API: Job {jid} Progress {cur}/{stp} ({job_status[jid]['progress']:.1f}%)')}")
935
+ stream.output_queue.push(('progress', (pv, f"{cur}/{stp}", make_progress_bar_html(int(100 * cur / stp), f"{cur}/{stp}"))))
936
+ try:
937
+ if hasattr(stream.input_queue, "qsize") and stream.input_queue.qsize() > 0:
938
+ if stream.input_queue.get_nowait() == "end":
939
+ stream.output_queue.push(("end", "Job stopped by client"))
940
+ raise KeyboardInterrupt
941
+ except queue.Empty:
942
+ pass
943
+
944
+ idx = torch.arange(0, sum([1, pad * win, win, 1, 2, 16]))[None].to(device=gpu)
945
+ a, b, c, d, e, f = idx.split([1, pad * win, win, 1, 2, 16], 1)
946
+ clean_idx = torch.cat([a, d], 1)
947
+ pre = start_lat.to(hist_lat)
948
+ post, two, four = hist_lat[:, :, :1 + 2 + 16].split([1, 2, 16], 2)
949
+ clean = torch.cat([pre, post], 2)
950
+ if not hv:
951
+ unload_complete_models()
952
+ move_model_to_device_with_memory_preservation(transformer, gpu, keep)
953
+ transformer.initialize_teacache(tea, stp)
954
+ new_lat = sample_hunyuan(
955
+ transformer=transformer, sampler="unipc", width=w, height=h, frames=win * 4 - 3,
956
+ real_guidance_scale=cfg, distilled_guidance_scale=gsc, guidance_rescale=rsc,
957
+ num_inference_steps=stp, generator=gen,
958
+ prompt_embeds=lv, prompt_embeds_mask=m, prompt_poolers=cp,
959
+ negative_prompt_embeds=lv_n, negative_prompt_embeds_mask=m_n, negative_prompt_poolers=cp_n,
960
+ device=gpu, dtype=torch.bfloat16, image_embeddings=img_emb,
961
+ latent_indices=c, clean_latents=clean, clean_latent_indices=clean_idx,
962
+ clean_latents_2x=two, clean_latent_2x_indices=e,
963
+ clean_latents_4x=four, clean_latent_4x_indices=f, callback=cb
964
+ )
965
+ if last:
966
+ new_lat = torch.cat([start_lat.to(new_lat), new_lat], 2)
967
+ total += new_lat.shape[2]
968
+ hist_lat = torch.cat([new_lat.to(hist_lat), hist_lat], 2)
969
+ if not hv:
970
+ offload_model_from_device_for_memory_preservation(transformer, gpu, 8)
971
+ load_model_as_complete(vae, gpu)
972
+ real = hist_lat[:, :, :total]
973
+ if hist_px is None:
974
+ hist_px = vae_decode(real, vae).cpu()
975
+ else:
976
+ overlap = win * 4 - 3
977
+ curr = vae_decode(real[:, :, :win * 2], vae).cpu()
978
+ hist_px = soft_append_bcthw(curr, hist_px, overlap)
979
+ if not hv:
980
+ unload_complete_models()
981
+ tmp_path = os.path.join(VIDEO_TMP_DIR, f"{jid}_{total}.mp4")
982
+ save_bcthw_as_mp4(hist_px, tmp_path, fps=30, crf=crf)
983
+ os.chmod(tmp_path, 0o664)
984
+ stream.output_queue.push(('file', tmp_path))
985
+ if last:
986
+ fin_path = os.path.join(VIDEO_OUTPUT_DIR, f"{jid}_{total}.mp4")
987
+ try:
988
+ os.replace(tmp_path, fin_path)
989
+ os.chmod(fin_path, 0o664)
990
+ job_status[jid]["status"] = "complete"
991
+ job_status[jid]["render_time"] = time.time() - start_time
992
+ stream.output_queue.push(('complete', fin_path))
993
+ clear_queue(stream.input_queue)
994
+ clear_queue(stream.output_queue)
995
+ logger.info(f"Final video saved: {fin_path}, render time: {job_status[jid]['render_time']:.2f}s")
996
+ print(f"{green(f'API: Final video saved: {fin_path}')}")
997
+ return fin_path
998
+ except Exception as e:
999
+ logger.error(f"Failed to save final video: {e}")
1000
+ print(f"{red(f'API: Failed to save final video: {e}')}")
1001
+ raise
1002
+ torch.cuda.empty_cache()
1003
+ except Exception as e:
1004
+ logger.error(f"Worker failed: {e}", exc_info=True)
1005
+ print(f"{red(f'API: Worker error: {e}')}")
1006
+ traceback.print_exc()
1007
+ job_status[jid]["status"] = "error"
1008
+ stream.output_queue.push(("end", str(e)))
1009
+ return None
1010
+ finally:
1011
+ if jid in active_jobs:
1012
+ active_jobs.pop(jid, None)
1013
+ clear_queue(stream.input_queue)
1014
+ clear_queue(stream.output_queue)
1015
+ if job_status.get(jid, {}).get("status") not in ["complete", "error", "stopped"]:
1016
+ job_status[jid]["status"] = "complete"
1017
+ torch.cuda.empty_cache()
1018
+
1019
+ @torch.no_grad()
1020
+ def process(img, prm, npr, sd, sec, win, stp, cfg, gsc, rsc, kee, tea, crf, disable_prompt_mods, link_steps_window):
1021
+ if img is None:
1022
+ raise gr.Error("Upload an image")
1023
+ yield None, None, "", "", gr.update(interactive=False), gr.update(interactive=True)
1024
+ stream = AsyncStream()
1025
+ jid = str(uuid.uuid4())
1026
+ async_run(worker, img, prm, npr, sd, sec, win, stp, cfg, gsc, rsc, kee, tea, crf, disable_prompt_mods, link_steps_window, stream, jid)
1027
+ out, log = None, ""
1028
+ try:
1029
+ while True:
1030
+ flag, data = stream.output_queue.next()
1031
+ if job_status.get(jid, {}).get("status") == "complete":
1032
+ break
1033
+ if flag == "file":
1034
+ out = data
1035
+ yield out, gr.update(), gr.update(), log, gr.update(interactive=False), gr.update(interactive=True)
1036
+ if flag == "progress":
1037
+ pv, desc, html = data
1038
+ log = desc
1039
+ yield gr.update(), gr.update(visible=True, value=pv), desc, html, gr.update(interactive=False), gr.update(interactive=True)
1040
+ if flag == "complete":
1041
+ yield data, gr.update(visible=False), "Generation complete", "", gr.update(interactive=True), gr.update(interactive=False)
1042
+ break
1043
+ if flag == "end":
1044
+ yield out, gr.update(visible=False), f"Error: {data}", "", gr.update(interactive=True), gr.update(interactive=False)
1045
+ break
1046
+ except Exception as e:
1047
+ logger.error(f"Process loop failed: {e}")
1048
+ yield out, gr.update(visible=False), f"Error: {str(e)}", "", gr.update(interactive=True), gr.update(interactive=False)
1049
+ job_status[jid]["status"] = "error"
1050
+ finally:
1051
+ clear_queue(stream.input_queue)
1052
+ clear_queue(stream.output_queue)
1053
+ torch.cuda.empty_cache()
1054
+
1055
+ def end_process():
1056
+ global stream
1057
+ if stream:
1058
+ stream.input_queue.push("end")
1059
+ logger.info("Gradio: Render stop requested")
1060
+ print(f"{red('Gradio: Render stop requested')}")
1061
+
1062
+ # Gradio UI (same as original)
1063
+ quick_prompts = [
1064
+ ["Smooth animation: A character waves for 3 seconds, then stands still for 2 seconds, static camera, silent."],
1065
+ ["Smooth animation: A character moves for 5 seconds, static camera, silent."]
1066
+ ]
1067
+ css = make_progress_bar_css() + """
1068
+ .orange-button{background:#ff6200;color:#fff;border-color:#ff6200;}
1069
+ .load-button{background:#4CAF50;color:#fff;border-color:#4CAF50;margin-left:10px;}
1070
+ .big-setting-button{background:#0066cc;color:#fff;border:none;padding:14px 24px;font-size:18px;width:100%;border-radius:6px;margin:8px 0;}
1071
+ .styled-dropdown{width:250px;padding:5px;border-radius:4px;}
1072
+ .viewer-column{width:100%;max-width:900px;margin:0 auto;}
1073
+ .media-preview img,.media-preview video{max-width:100%;height:380px;object-fit:contain;border:1px solid #444;border-radius:6px;}
1074
+ .media-container{display:flex;gap:20px;align-items:flex-start;}
1075
+ .control-box{min-width:220px;}
1076
+ .control-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;}
1077
+ .image-gallery{display:grid!important;grid-template-columns:repeat(auto-fit,minmax(300px,1fr))!important;gap:10px;padding:10px!important;overflow-y:auto!important;max-height:360px!important;}
1078
+ .image-gallery .gallery-item{padding:10px;height:360px!important;width:300px!important;}
1079
+ .image-gallery img{object-fit:contain;height:360px!important;width:300px!important;}
1080
+ .video-gallery{display:grid!important;grid-template-columns:repeat(auto-fit,minmax(300px,1fr))!important;gap:10px;padding:10px!important;overflow-y:auto!important;max-height:360px!important;}
1081
+ .video-gallery .gallery-item{padding:10px;height:360px!important;width:300px!important;}
1082
+ .video-gallery video{object-fit:contain;height:360px!important;width:300px!important;}
1083
+ .stop-button {background-color: #ff4d4d !important; color: white !important;}
1084
+ """
1085
+
1086
+ blk = gr.Blocks(css=css, title="GhostPack F1 Pro").queue()
1087
+ with blk:
1088
+ gr.Markdown("# πŸ‘» GhostPack F1 Pro")
1089
+ with gr.Tabs():
1090
+ with gr.TabItem("πŸ‘» Generate"):
1091
+ with gr.Row():
1092
+ with gr.Column():
1093
+ img_in = gr.Image(sources="upload", type="numpy", label="Image", height=320)
1094
+ generate_button = gr.Button("Generate Video", elem_id="generate_button")
1095
+ stop_button = gr.Button("Stop Generation", elem_id="stop_button", elem_classes="stop-button")
1096
+ prm = gr.Textbox(
1097
+ label="Prompt",
1098
+ value="Smooth animation: A female stands with subtle, sensual micro-movements, breathing gently, slight head tilt, static camera, silent",
1099
+ elem_id="prompt_input",
1100
+ )
1101
+ npr = gr.Textbox(
1102
+ label="Negative Prompt",
1103
+ value="low quality, blurry, speaking, talking, moaning, vocalizing, lip movement, mouth animation, sound, dialogue, speech, whispering, shouting, lip sync, facial animation, expressive face, verbal expression, animated mouth",
1104
+ elem_id="negative_prompt_input",
1105
+ )
1106
+ save_msg = gr.Markdown("")
1107
+ disable_prompt_mods = gr.Checkbox(label="Disable Prompt Modifications", value=False)
1108
+ link_steps_window = gr.Checkbox(label="Link Steps and Latent Window", value=True)
1109
+ btn_save = gr.Button("Save Prompt")
1110
+ btn1, btn2, btn3 = (
1111
+ gr.Button("Load Most Recent"),
1112
+ gr.Button("Load 2nd Recent"),
1113
+ gr.Button("Load 3rd Recent"),
1114
+ )
1115
+ ds = gr.Dataset(samples=quick_prompts, label="Quick List", components=[prm])
1116
+ ds.click(lambda x: x[0], [ds], [prm])
1117
+ btn_save.click(save_prompt_fn, [prm, npr], [save_msg])
1118
+ btn1.click(lambda: load_prompt_fn(0), [], [prm])
1119
+ btn2.click(lambda: load_prompt_fn(1), [], [prm])
1120
+ btn3.click(lambda: load_prompt_fn(2), [], [prm])
1121
+ camera_action_input = gr.Dropdown(
1122
+ choices=[
1123
+ "Static Camera", "Slight Orbit Left", "Slight Orbit Right",
1124
+ "Slight Orbit Up", "Slight Orbit Down", "Top-Down View",
1125
+ "Slight Zoom In", "Slight Zoom Out",
1126
+ ],
1127
+ label="Camera Action",
1128
+ value="Static Camera",
1129
+ elem_id="camera_action_input",
1130
+ info="Select a camera movement to append to the prompt.",
1131
+ )
1132
+ camera_action_input.change(
1133
+ fn=lambda prompt, camera_action: update_prompt(prompt, camera_action),
1134
+ inputs=[prm, camera_action_input],
1135
+ outputs=prm,
1136
+ )
1137
+ with gr.Column():
1138
+ pv = gr.Image(label="Next Latents", height=200, visible=False)
1139
+ vid = gr.Video(label="Finished", autoplay=True, height=500, loop=True, show_share_button=False)
1140
+ log_md = gr.Markdown("")
1141
+ bar = gr.HTML("")
1142
+ with gr.Column():
1143
+ se = gr.Number(label="Seed", value=31337, precision=0, elem_id="seed_input")
1144
+ sec = gr.Slider(label="Video Length (s)", minimum=1, maximum=10, value=8.0, step=0.1, elem_id="video_length_input")
1145
+ win = gr.Slider(label="Latent Window", minimum=1, maximum=10, value=3, step=1, elem_id="latent_window_input")
1146
+ stp = gr.Slider(label="Steps", minimum=1, maximum=100, value=12, step=1, elem_id="steps_input")
1147
+ cfg = gr.Slider(label="CFG", minimum=1, maximum=32, value=1.7, step=0.01, elem_id="cfg_input")
1148
+ gsc = gr.Slider(label="Distilled CFG", minimum=1, maximum=32, value=4.0, step=0.01, elem_id="distilled_cfg_input")
1149
+ rsc = gr.Slider(label="CFG Re-Scale", minimum=0, maximum=1, value=0.5, step=0.01, elem_id="cfg_rescale_input")
1150
+ kee = gr.Slider(label="GPU Keep (GB)", minimum=6, maximum=free_mem, value=6.5, step=0.1, elem_id="gpu_keep_input")
1151
+ crf = gr.Slider(label="MP4 CRF", minimum=0, maximum=100, value=20, step=1, elem_id="mp4_crf_input")
1152
+ tea = gr.Checkbox(label="Use TeaCache", value=True, elem_id="use_teacache_input")
1153
+ generate_button.click(
1154
+ fn=process,
1155
+ inputs=[img_in, prm, npr, se, sec, win, stp, cfg, gsc, rsc, kee, tea, crf, disable_prompt_mods, link_steps_window],
1156
+ outputs=[vid, pv, log_md, bar, generate_button, stop_button],
1157
+ )
1158
+ stop_button.click(fn=end_process)
1159
+ gr.Button("Update Progress").click(fn=lambda: get_progress(), outputs=[log_md, bar])
1160
+
1161
+ with gr.TabItem("πŸ–ΌοΈ Image Gallery"):
1162
+ with gr.Row(elem_classes="media-container"):
1163
+ with gr.Column(scale=3):
1164
+ image_preview = gr.Image(
1165
+ label="Viewer", value=(list_images()[0] if list_images() else None),
1166
+ interactive=False, elem_classes="media-preview",
1167
+ )
1168
+ with gr.Column(elem_classes="control-box"):
1169
+ image_dropdown = gr.Dropdown(
1170
+ choices=[os.path.basename(i) for i in list_images()],
1171
+ value=(os.path.basename(list_images()[0]) if list_images() else None),
1172
+ label="Select", elem_classes="styled-dropdown",
1173
+ )
1174
+ with gr.Row(elem_classes="control-grid"):
1175
+ load_btn = gr.Button("Load", elem_classes="load-button")
1176
+ next_btn = gr.Button("Next", elem_classes="load-button")
1177
+ with gr.Row(elem_classes="control-grid"):
1178
+ refresh_btn = gr.Button("Refresh")
1179
+ delete_btn = gr.Button("Delete", elem_classes="orange-button")
1180
+ image_gallery = gr.Gallery(
1181
+ value=list_images(), label="Thumbnails", columns=6, height=360,
1182
+ allow_preview=False, type="filepath", elem_classes="image-gallery",
1183
+ )
1184
+ load_btn.click(load_image, [image_dropdown], [image_preview, image_dropdown])
1185
+ next_btn.click(next_image_and_load, [image_dropdown], [image_preview, image_dropdown])
1186
+ refresh_btn.click(
1187
+ lambda: (
1188
+ gr.update(choices=[os.path.basename(i) for i in list_images()], value=os.path.basename(list_images()[0]) if list_images() else None),
1189
+ gr.update(value=list_images()[0] if list_images() else None),
1190
+ gr.update(value=list_images()),
1191
+ ),
1192
+ [], [image_dropdown, image_preview, image_gallery],
1193
+ )
1194
+ delete_btn.click(
1195
+ lambda sel: (
1196
+ os.remove(os.path.join(VIDEO_IMG_DIR, sel)) if sel and os.path.exists(os.path.join(VIDEO_IMG_DIR, sel)) else None
1197
+ ) or load_image(""),
1198
+ [image_dropdown], [image_preview, image_dropdown],
1199
+ )
1200
+ image_gallery.select(gallery_image_select, [], [image_preview, image_dropdown])
1201
+
1202
+ with gr.TabItem("🎬 Video Gallery"):
1203
+ with gr.Row(elem_classes="media-container"):
1204
+ with gr.Column(scale=3):
1205
+ video_preview = gr.Video(
1206
+ label="Viewer", value=(list_videos()[0] if list_videos() else None),
1207
+ autoplay=True, loop=True, interactive=False, elem_classes="media-preview",
1208
+ )
1209
+ with gr.Column(elem_classes="control-box"):
1210
+ video_dropdown = gr.Dropdown(
1211
+ choices=[os.path.basename(v) for v in list_videos()],
1212
+ value=(os.path.basename(list_videos()[0]) if list_videos() else None),
1213
+ label="Select", elem_classes="styled-dropdown",
1214
+ )
1215
+ with gr.Row(elem_classes="control-grid"):
1216
+ load_vbtn = gr.Button("Load", elem_classes="load-button")
1217
+ next_vbtn = gr.Button("Next", elem_classes="load-button")
1218
+ with gr.Row(elem_classes="control-grid"):
1219
+ refresh_v = gr.Button("Refresh")
1220
+ delete_v = gr.Button("Delete", elem_classes="orange-button")
1221
+ video_gallery = gr.Gallery(
1222
+ value=list_videos(), label="Thumbnails", columns=6, height=360,
1223
+ allow_preview=False, type="filepath", elem_classes="video-gallery",
1224
+ )
1225
+ load_vbtn.click(load_video, [video_dropdown], [video_preview, video_dropdown])
1226
+ next_vbtn.click(next_video_and_load, [video_dropdown], [video_preview, video_dropdown])
1227
+ refresh_v.click(
1228
+ lambda: (
1229
+ gr.update(choices=[os.path.basename(v) for v in list_videos()], value=os.path.basename(list_videos()[0]) if list_videos() else None),
1230
+ gr.update(value=list_videos()[0] if list_videos() else None),
1231
+ gr.update(value=list_videos()),
1232
+ ),
1233
+ [], [video_dropdown, video_preview, video_gallery],
1234
+ )
1235
+ delete_v.click(
1236
+ lambda sel: (
1237
+ os.remove(os.path.join(VIDEO_OUTPUT_DIR, sel)) if sel and os.path.exists(os.path.join(VIDEO_OUTPUT_DIR, sel)) else None
1238
+ ) or load_video(""),
1239
+ [video_dropdown], [video_preview, video_dropdown],
1240
+ )
1241
+ video_gallery.select(gallery_video_select, [], [video_preview, video_dropdown])
1242
+
1243
+ with gr.TabItem("πŸ‘» About"):
1244
+ gr.Markdown("## GhostPack F1 Pro")
1245
+ with gr.Row():
1246
+ with gr.Column():
1247
+ gr.Markdown("**πŸ› οΈ Description**\nImage-to-Video toolkit powered by HunyuanVideo & FramePack-F1")
1248
+ with gr.Column():
1249
+ gr.Markdown(f"**πŸ“¦ Version**\n{VERSION}")
1250
+ with gr.Column():
1251
+ gr.Markdown("**✍️ Author**\nGhostAI")
1252
+ with gr.Column():
1253
+ gr.Markdown("**πŸ”— Repo**\nhttps://huggingface.co/spaces/ghostai1/GhostPack")
1254
+
1255
+ with gr.TabItem("βš™οΈ Settings"):
1256
+ ct = gr.Button("Clear Temp", elem_classes="big-setting-button")
1257
+ ctmsg = gr.Markdown("")
1258
+ co = gr.Button("Clear Old", elem_classes="big-setting-button")
1259
+ comsg = gr.Markdown("")
1260
+ ci = gr.Button("Clear Images", elem_classes="big-setting-button")
1261
+ cimg = gr.Markdown("")
1262
+ cv = gr.Button("Clear Videos", elem_classes="big-setting-button")
1263
+ cvid = gr.Markdown("")
1264
+ ct.click(clear_temp_videos, [], ctmsg)
1265
+ co.click(clear_old_files, [], comsg)
1266
+ ci.click(clear_images, [], cimg)
1267
+ cv.click(clear_videos, [], cvid)
1268
+
1269
+ with gr.TabItem("πŸ› οΈ Install"):
1270
+ xs = gr.Textbox(value=status_xformers(), interactive=False, label="xformers")
1271
+ bx = gr.Button("Install xformers", elem_classes="big-setting-button")
1272
+ ss = gr.Textbox(value=status_sage(), interactive=False, label="sage-attn")
1273
+ bs = gr.Button("Install sage-attn", elem_classes="big-setting-button")
1274
+ fs = gr.Textbox(value=status_flash(), interactive=False, label="flash-attn")
1275
+ bf = gr.Button("Install flash-attn", elem_classes="big-setting-button")
1276
+ cs = gr.Textbox(value=status_colorama(), interactive=False, label="colorama")
1277
+ bc = gr.Button("Install colorama", elem_classes="big-setting-button")
1278
+ bx.click(install_xformers, [], xs)
1279
+ bs.click(install_sage_attn, [], ss)
1280
+ bf.click(install_flash_attn, [], fs)
1281
+ bc.click(install_colorama, [], cs)
1282
+
1283
+ with gr.TabItem("πŸ“œ Logs"):
1284
+ logs = gr.Textbox(lines=20, interactive=False, label="Install Logs")
1285
+ rl = gr.Button("Refresh", elem_classes="big-setting-button")
1286
+ cl = gr.Button("Clear", elem_classes="big-setting-button")
1287
+ rl.click(refresh_logs, [], logs)
1288
+ cl.click(clear_logs, [], logs)
1289
+
1290
+ gr.HTML(
1291
+ """
1292
+ <script>
1293
+ document.querySelectorAll('.video-gallery video').forEach(v => {
1294
+ v.addEventListener('loadedmetadata', () => {
1295
+ if (v.duration > 2) v.currentTime = 2;
1296
+ });
1297
+ });
1298
+ </script>
1299
+ """
1300
+ )
1301
+
1302
+ def update_prompt(prompt, camera_action):
1303
+ camera_actions = [
1304
+ "static camera", "slight camera orbit left", "slight camera orbit right",
1305
+ "slight camera orbit up", "slight camera orbit down", "top-down view",
1306
+ "slight camera zoom in", "slight camera zoom out",
1307
+ ]
1308
+ for action in camera_actions:
1309
+ prompt = re.sub(rf",\s*{re.escape(action)}\b", "", prompt, flags=re.IGNORECASE).strip()
1310
+ if camera_action and camera_action != "None":
1311
+ camera_phrase = f", {camera_action.lower()}"
1312
+ if len(prompt.split()) + len(camera_phrase.split()) <= 50:
1313
+ return prompt + camera_phrase
1314
+ else:
1315
+ logger.warning(f"Prompt exceeds 50 words after adding camera action: {prompt}")
1316
+ print(f"{yellow(f'API: Warning: Prompt exceeds 50 words with camera action')}")
1317
+ return prompt
1318
+
1319
+ def get_progress():
1320
+ return f"Status: {job_status.get('latest', {'status': 'idle'})['status']}\nProgress: {job_status.get('latest', {'progress': 0.0})['progress']:.1f}%\nLast Render Time: {job_status.get('latest', {'render_time': 0})['render_time']:.1f}s"
1321
+
1322
+ # Check for port conflicts
1323
+ if is_port_in_use(args.port):
1324
+ logger.error(f"Port {args.port} is already in use")
1325
+ print(f"{red(f'Error: Port {args.port} is already in use. Please stop other instances or change ports.')}")
1326
+ sys.exit(1)
1327
+
1328
+ # Run FastAPI and optional Gradio
1329
+ def run_api():
1330
+ try:
1331
+ logger.info(f"Starting FastAPI on {args.server}:{args.port}")
1332
+ print(f"{green(f'Starting FastAPI on {args.server}:{args.port}')}")
1333
+ uvicorn.run(app, host=args.server, port=args.port)
1334
+ except Exception as e:
1335
+ logger.error(f"Failed to start FastAPI: {e}", exc_info=True)
1336
+ print(f"{red(f'Error: Failed to start FastAPI: {e}')}")
1337
+ sys.exit(1)
1338
 
1339
+ if __name__ == "__main__":
1340
+ try:
1341
+ logger.info(f"Starting GhostPack F1 Pro Server version {VERSION}")
1342
+ print(f"Starting GhostPack F1 Pro Server version {VERSION}")
1343
+ api_thread = Thread(target=run_api)
1344
+ api_thread.daemon = True
1345
+ api_thread.start()
1346
+ time.sleep(5)
1347
+ try:
1348
+ response = requests.get(f"http://{args.server}:{args.port}/health", timeout=10)
1349
+ if response.status_code != 200:
1350
+ raise RuntimeError("FastAPI health check failed")
1351
+ logger.info("FastAPI health check passed")
1352
+ print(f"{green('FastAPI health check passed')}")
1353
+ except Exception as e:
1354
+ logger.error(f"FastAPI not ready: {e}")
1355
+ print(f"{red(f'Error: FastAPI not ready: {e}')}")
1356
+ sys.exit(1)
1357
 
1358
+ if args.gradio:
1359
+ logger.info(f"Starting Gradio UI on {args.server}:7860")
1360
+ print(f"{green(f'Starting Gradio UI on {args.server}:7860')}")
1361
+ server = blk.launch(
1362
+ server_name=args.server,
1363
+ server_port=7860,
1364
+ share=args.share,
1365
+ inbrowser=args.inbrowser,
1366
+ prevent_thread_lock=True,
1367
+ allowed_paths=["/"]
1368
+ )
1369
+ if args.share and server.share_url:
1370
+ logger.info(f"Public Gradio URL: {server.share_url}")
1371
+ print(f"{yellow(f'Public Gradio URL: {server.share_url}')}")
1372
+ logger.info(f"Gradio UI running on http://{args.server}:7860")
1373
+ print(f"{green(f'Gradio UI running on http://{args.server}:7860')}")
1374
+ while True:
1375
+ time.sleep(1)
1376
+ except KeyboardInterrupt:
1377
+ logger.info("Shutting down gracefully")
1378
+ print(f"{green('Shutting down gracefully')}")
1379
+ sys.exit(0)