Akuyakufree commited on
Commit
3d51071
·
verified ·
1 Parent(s): 794949d

Upload 9 files

Browse files
Files changed (4) hide show
  1. app.py +10 -3
  2. packages.txt +1 -0
  3. requirements.txt +13 -10
  4. src/app_lib.py +177 -860
app.py CHANGED
@@ -10,6 +10,14 @@ from typing import Any, Dict
10
  _CONFIG_MARKER = b"OMNICFG1"
11
 
12
 
 
 
 
 
 
 
 
 
13
  def _collect_config_values(namespace: Dict[str, Any]) -> Dict[str, Any]:
14
  data: Dict[str, Any] = {}
15
  for key, value in namespace.items():
@@ -77,7 +85,6 @@ def _materialize_private_config_from_env() -> None:
77
  except Exception as exc:
78
  print(f"[startup] warning: build private config failed: {type(exc).__name__}: {exc}")
79
 
80
- # Local/dev fallback: if local private source exists, pack it to src/config.pyc.
81
  local_source_candidates = (
82
  repo_dir / "tools_local" / "config.py",
83
  repo_dir / "src_codes" / "src" / "config.py",
@@ -93,7 +100,6 @@ def _materialize_private_config_from_env() -> None:
93
  except Exception as exc:
94
  print(f"[startup] warning: pack local private config failed: {type(exc).__name__}: {exc}")
95
 
96
- # Compatibility migration: keep existing source-less pyc workable, then repack once.
97
  if not pyc_target.exists():
98
  return
99
  try:
@@ -165,6 +171,7 @@ def _log_model_config_status() -> None:
165
 
166
 
167
  if __name__ == "__main__":
 
168
  _log_model_config_status()
169
  print("[startup] runtime preparation:")
170
  if callable(kickoff_runtime_prepare_background):
@@ -177,4 +184,4 @@ if __name__ == "__main__":
177
  else:
178
  print(ensure_models_ready_on_startup())
179
  demo = build_demo()
180
- demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))
 
10
  _CONFIG_MARKER = b"OMNICFG1"
11
 
12
 
13
+ def _apply_frame_interpolation_settings() -> None:
14
+ os.environ["OMNI_BASE_FPS"] = "12"
15
+ os.environ["OMNI_FRAME_MULTIPLIER"] = "2"
16
+ os.environ["OMNI_CRF"] = "0"
17
+ os.environ["OMNI_ALLOWED_FPS"] = "16,32,64,128"
18
+ print("[startup] Frame interpolation settings applied: BASE_FPS=12, MULTIPLIER=2, CRF=auto, ALLOWED_FPS=16,32,64,128")
19
+
20
+
21
  def _collect_config_values(namespace: Dict[str, Any]) -> Dict[str, Any]:
22
  data: Dict[str, Any] = {}
23
  for key, value in namespace.items():
 
85
  except Exception as exc:
86
  print(f"[startup] warning: build private config failed: {type(exc).__name__}: {exc}")
87
 
 
88
  local_source_candidates = (
89
  repo_dir / "tools_local" / "config.py",
90
  repo_dir / "src_codes" / "src" / "config.py",
 
100
  except Exception as exc:
101
  print(f"[startup] warning: pack local private config failed: {type(exc).__name__}: {exc}")
102
 
 
103
  if not pyc_target.exists():
104
  return
105
  try:
 
171
 
172
 
173
  if __name__ == "__main__":
174
+ _apply_frame_interpolation_settings()
175
  _log_model_config_status()
176
  print("[startup] runtime preparation:")
177
  if callable(kickoff_runtime_prepare_background):
 
184
  else:
185
  print(ensure_models_ready_on_startup())
186
  demo = build_demo()
187
+ demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")), ssr_mode=False)
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ffmpeg
requirements.txt CHANGED
@@ -1,21 +1,20 @@
1
  gradio==5.44.1
2
  spaces>=0.34.0
 
 
3
  huggingface_hub==0.34.4
4
  comfyui-frontend-package==1.36.13
5
  comfyui-workflow-templates==0.7.69
6
  comfyui-embedded-docs==0.3.1
7
- comfy-kitchen>=0.2.5
8
  torch
9
  torchsde
10
  torchvision
11
  torchaudio
12
- torchao==0.17.0
13
- peft==0.19.1
14
- diffusers==0.38.0
15
- transformers==4.57.6
16
- accelerate>=1.13.0
17
  numpy>=1.25.0
18
  einops
 
 
 
19
  tokenizers>=0.13.3
20
  sentencepiece
21
  safetensors>=0.4.2
@@ -53,14 +52,18 @@ imageio-ffmpeg>=0.5.1
53
  alembic
54
  SQLAlchemy
55
  av>=14.2.0
 
56
  kornia>=0.7.1
57
  spandrel
58
  pydantic~=2.0
59
  pydantic-settings~=2.0
60
- redis>=5.0.8
61
-
62
  opencv-python-headless==4.12.0.88
63
- sageattention
64
- onnx
65
  glitch-this
 
 
 
 
 
 
66
  scikit-image
 
1
  gradio==5.44.1
2
  spaces>=0.34.0
3
+ gradio==5.44.1
4
+ spaces>=0.34.0
5
  huggingface_hub==0.34.4
6
  comfyui-frontend-package==1.36.13
7
  comfyui-workflow-templates==0.7.69
8
  comfyui-embedded-docs==0.3.1
 
9
  torch
10
  torchsde
11
  torchvision
12
  torchaudio
 
 
 
 
 
13
  numpy>=1.25.0
14
  einops
15
+ transformers>=4.50.3
16
+ accelerate>=1.0.1
17
+ diffusers>=0.35.0
18
  tokenizers>=0.13.3
19
  sentencepiece
20
  safetensors>=0.4.2
 
52
  alembic
53
  SQLAlchemy
54
  av>=14.2.0
55
+ comfy-kitchen>=0.2.5
56
  kornia>=0.7.1
57
  spandrel
58
  pydantic~=2.0
59
  pydantic-settings~=2.0
 
 
60
  opencv-python-headless==4.12.0.88
61
+ redis>=5.0.8
 
62
  glitch-this
63
+ PyOpenGL
64
+ PyOpenGL_accelerate
65
+ glfw
66
+ onnx
67
+ onnxruntime
68
+ sageattention
69
  scikit-image
src/app_lib.py CHANGED
@@ -1,5 +1,3 @@
1
- # Auto-generated bundle. DO NOT EDIT.
2
- # Source-of-truth lives in src_codes/src/app_lib.py
3
  import os
4
  import json
5
  import re
@@ -19,14 +17,14 @@ from typing import Dict, List, Optional, Tuple, Any
19
 
20
  import gradio as gr
21
  import spaces
 
 
22
 
23
  try:
24
  import redis
25
- except Exception: # pragma: no cover
26
  redis = None
27
 
28
- # PRIVACY BUILD: Redis usage tracking is disabled entirely.
29
- # No per-IP counters, no country stats, no NSFW hit tracking.
30
  redis = None
31
 
32
  os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
@@ -53,12 +51,9 @@ WORKFLOW_FILES = ("video_t2v_api.json", "video_i2v_api.json", "video_v2v_api.jso
53
  WORKFLOW_PACK_MARKER = b"OMNIWF1"
54
  WORKFLOW_PACK_SUFFIX = ".pack"
55
 
56
- # PUBLIC BUILD: built-in model manifest used when the OMNI_VIDEOS env var is
57
- # not set, so a duplicated Space works with no secrets. All four files are on
58
- # public Hugging Face repos. Override by setting OMNI_VIDEOS if desired.
59
  DEFAULT_OMNI_VIDEOS = (
60
  "wan_2.1_vae.safetensors@Comfy-Org/Wan_2.1_ComfyUI_repackaged@split_files/vae/wan_2.1_vae.safetensors"
61
- "#umt5_xxl_fp8_e4m3fn_scaled.safetensors@Comfy-Org/Wan_2.1_ComfyUI_repackaged@split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors"
62
  "#wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8High.safetensors@jorgmikel76/wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8High@wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8High.safetensors"
63
  "#wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8Low.safetensors@jorgmikel76/wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8High@wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8Low.safetensors"
64
  )
@@ -72,6 +67,22 @@ _RUNTIME_PREP_RUNNING = False
72
  _RUNTIME_PREP_STATUS = "runtime preparation not started"
73
  _PREP_IO_LOCK = threading.Lock()
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  def _split_parts(value: Optional[str] = None) -> Tuple[str, str, str, str]:
77
  raw = (value if value is not None else os.getenv("OMNI_VIDEOS") or DEFAULT_OMNI_VIDEOS).strip()
@@ -80,7 +91,6 @@ def _split_parts(value: Optional[str] = None) -> Tuple[str, str, str, str]:
80
  raise ValueError("OMNI_VIDEOS must have 4 non-empty parts separated by '#'.")
81
  return parts[0], parts[1], parts[2], parts[3]
82
 
83
-
84
  def _parse_entry(entry: str) -> Tuple[str, Optional[str], Optional[str]]:
85
  parts = (entry or "").strip().split("@", 2)
86
  filename = parts[0].strip() if parts and parts[0].strip() else ""
@@ -88,7 +98,6 @@ def _parse_entry(entry: str) -> Tuple[str, Optional[str], Optional[str]]:
88
  repo_relpath = parts[2].strip() if len(parts) >= 3 and parts[2].strip() else None
89
  return filename, repo_id, repo_relpath
90
 
91
-
92
  def parse_model_names(value: Optional[str] = None) -> Dict[str, str]:
93
  a, b, c, d = _split_parts(value)
94
  vae, _, _ = _parse_entry(a)
@@ -104,7 +113,6 @@ def parse_model_names(value: Optional[str] = None) -> Dict[str, str]:
104
  "unet_q6kl": unet_q6kl,
105
  }
106
 
107
-
108
  def parse_model_entries(value: Optional[str] = None) -> Dict[str, Tuple[str, Optional[str], Optional[str]]]:
109
  a, b, c, d = _split_parts(value)
110
  return {
@@ -114,7 +122,6 @@ def parse_model_entries(value: Optional[str] = None) -> Dict[str, Tuple[str, Opt
114
  "unet_q6kl": _parse_entry(d),
115
  }
116
 
117
-
118
  def _split_one_key(
119
  value: Optional[str] = None,
120
  ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]]:
@@ -125,14 +132,13 @@ def _split_one_key(
125
  while len(parts) < 5:
126
  parts.append("")
127
  return (
128
- parts[0] or None, # hf token
129
- parts[1] or None, # xai/openrouter token
130
- parts[2] or None, # legacy api base
131
- parts[3] or None, # worker api token
132
- parts[4] or None, # github token
133
  )
134
 
135
-
136
  def _get_hf_token() -> Optional[str]:
137
  hf_from_one_key, _, _, _, _ = _split_one_key()
138
  if hf_from_one_key:
@@ -143,7 +149,6 @@ def _get_hf_token() -> Optional[str]:
143
  return value
144
  return None
145
 
146
-
147
  def _get_worker_api_token() -> Optional[str]:
148
  _, _, _, worker_from_one_key, _ = _split_one_key()
149
  if worker_from_one_key:
@@ -154,12 +159,10 @@ def _get_worker_api_token() -> Optional[str]:
154
  return value
155
  return None
156
 
157
-
158
  def _get_legacy_api_base() -> Optional[str]:
159
  _, _, legacy_api_base, _, _ = _split_one_key()
160
  return legacy_api_base
161
 
162
-
163
  def _get_github_token() -> Optional[str]:
164
  _, _, _, _, github_from_one_key = _split_one_key()
165
  if github_from_one_key:
@@ -170,22 +173,18 @@ def _get_github_token() -> Optional[str]:
170
  return value
171
  return None
172
 
173
-
174
  def _model_root_dir() -> Path:
175
  src_dir = Path(__file__).resolve().parent
176
  repo_dir = src_dir.parent
177
  return repo_dir / "ComfyUIVideo" / "models"
178
 
179
-
180
  def _repo_dir() -> Path:
181
  src_dir = Path(__file__).resolve().parent
182
  return src_dir.parent
183
 
184
-
185
  def _comfy_dir() -> Path:
186
  return _repo_dir() / "ComfyUIVideo"
187
 
188
-
189
  def _load_app_config():
190
  path = _repo_dir() / "src" / "config.pyc"
191
  if not path.exists():
@@ -194,7 +193,6 @@ def _load_app_config():
194
  raw = path.read_bytes()
195
  marker = b"OMNICFG1"
196
 
197
- # Preferred format: packed binary config payload in `src/config.pyc`.
198
  if raw.startswith(marker):
199
  payload = raw[len(marker) :]
200
  try:
@@ -207,7 +205,6 @@ def _load_app_config():
207
  print(f"[config] loaded packed private config: {path}")
208
  return types.SimpleNamespace(**data)
209
 
210
- # Compatibility format: standard Python sourceless bytecode.
211
  loader = importlib.machinery.SourcelessFileLoader("omni_video_factory_private_config", str(path))
212
  spec = importlib.util.spec_from_loader("omni_video_factory_private_config", loader)
213
  module = importlib.util.module_from_spec(spec) if spec else None
@@ -232,38 +229,30 @@ def _load_app_config():
232
  print(f"[config] loaded sourceless private config: {path}")
233
  return types.SimpleNamespace(**data)
234
 
235
-
236
  APP_CONFIG = _load_app_config()
237
 
238
-
239
  def _runtime_git_repo() -> str:
240
  repo = (os.getenv("OMNI_RUNTIME_GIT_REPO") or "selfitcamera/ComfyUIVideo").strip()
241
  return repo.removesuffix(".git")
242
 
243
-
244
  def _runtime_git_revision() -> str:
245
- # Pin default runtime revision to a known-good commit; env can still override.
246
  return (os.getenv("OMNI_RUNTIME_GIT_REF") or "17b9fb3").strip()
247
 
248
-
249
  def _is_commit_hash(ref: str) -> bool:
250
  ref = (ref or "").strip()
251
  if len(ref) < 7 or len(ref) > 40:
252
  return False
253
  return all(ch in "0123456789abcdefABCDEF" for ch in ref)
254
 
255
-
256
  def _runtime_git_user() -> str:
257
  return (os.getenv("OMNI_RUNTIME_GIT_USER") or "selfitcamera").strip()
258
 
259
-
260
  def _workflow_repo_id() -> str:
261
  env_space_id = (os.getenv("SPACE_ID") or "").strip()
262
  if env_space_id:
263
  return env_space_id
264
  return (os.getenv("OMNI_WORKFLOW_REPO") or "FrameAI4687/AI-Video-0213-02").strip()
265
 
266
-
267
  def _runtime_git_clone_url() -> str:
268
  github_token = _get_github_token()
269
  repo = _runtime_git_repo()
@@ -271,7 +260,6 @@ def _runtime_git_clone_url() -> str:
271
  return f"https://{_runtime_git_user()}:{github_token}@github.com/{repo}.git"
272
  return f"https://github.com/{repo}.git"
273
 
274
-
275
  def _redact_sensitive(text: str) -> str:
276
  out = text
277
  for secret in (_get_hf_token(), _get_worker_api_token(), _get_github_token()):
@@ -279,11 +267,9 @@ def _redact_sensitive(text: str) -> str:
279
  out = out.replace(secret, "***")
280
  return out
281
 
282
-
283
  def _llm_api_base() -> str:
284
  return (os.getenv("OMNI_LLM_API_BASE") or "https://omnifilm.net").strip().rstrip("/")
285
 
286
-
287
  def _llm_api_base_candidates() -> List[str]:
288
  candidates: List[str] = []
289
  env_base = (os.getenv("OMNI_LLM_API_BASE") or "").strip().rstrip("/")
@@ -292,7 +278,6 @@ def _llm_api_base_candidates() -> List[str]:
292
  candidates.append("https://omnifilm.net")
293
  legacy_base = (_get_legacy_api_base() or "").strip().rstrip("/")
294
  if legacy_base.startswith("http://") or legacy_base.startswith("https://"):
295
- # ONE_KEY third segment may include a path; keep only scheme+host as fallback.
296
  parts = legacy_base.split("/", 3)
297
  if len(parts) >= 3:
298
  candidates.append(parts[0] + "//" + parts[2])
@@ -304,7 +289,6 @@ def _llm_api_base_candidates() -> List[str]:
304
  seen.add(base)
305
  return ordered
306
 
307
-
308
  def _http_json(
309
  method: str,
310
  url: str,
@@ -349,7 +333,6 @@ def _http_json(
349
  return _decode_json(raw)
350
  except urllib.error.HTTPError as exc:
351
  detail = exc.read().decode("utf-8", errors="replace")
352
- # Cloudflare 1010 may block urllib requests in Spaces while curl still works.
353
  if int(getattr(exc, "code", 0) or 0) == 403 and "1010" in detail:
354
  try:
355
  return _http_json_via_curl()
@@ -359,7 +342,6 @@ def _http_json(
359
  except Exception as exc:
360
  raise RuntimeError(f"{type(exc).__name__}: {exc}") from exc
361
 
362
-
363
  def _extract_result_text(obj) -> str:
364
  def _strip_answer_wrapper(text: str) -> str:
365
  raw = (text or "").strip()
@@ -384,7 +366,6 @@ def _extract_result_text(obj) -> str:
384
  with_sep = [t for t in texts if "¥" in t]
385
  return max(with_sep or texts, key=len)
386
  if isinstance(obj, dict):
387
- # Prefer common chat completion fields first.
388
  if "choices" in obj and isinstance(obj["choices"], list):
389
  for choice in obj["choices"]:
390
  if isinstance(choice, dict):
@@ -405,7 +386,6 @@ def _extract_result_text(obj) -> str:
405
  return max(with_sep or texts, key=len)
406
  return ""
407
 
408
-
409
  _NSFW_PIPELINE = None
410
  _NSFW_STATE_LOCK = threading.Lock()
411
  _USAGE_STATE_LOCK = threading.Lock()
@@ -415,7 +395,6 @@ _REDIS_STATE_LOCK = threading.Lock()
415
  _REDIS_CLIENT = None
416
  _REDIS_MEMORY_LAST_CHECK_TS = 0.0
417
 
418
-
419
  def _redis_url() -> str:
420
  for key in ("REDIS_KEY", "REDIS_URL", "OMNI_REDIS_URL"):
421
  value = (os.getenv(key) or "").strip()
@@ -423,11 +402,9 @@ def _redis_url() -> str:
423
  return value
424
  return ""
425
 
426
-
427
  def _redis_enabled() -> bool:
428
  return bool(_redis_url())
429
 
430
-
431
  def _runtime_boot_marker() -> str:
432
  host = (os.getenv("HOSTNAME") or "unknown-host").strip() or "unknown-host"
433
  proc_boot = ""
@@ -442,7 +419,6 @@ def _runtime_boot_marker() -> str:
442
  return f"{host}:{proc_boot}"
443
  return host
444
 
445
-
446
  def _redis_scan_delete(client, pattern: str) -> int:
447
  total = 0
448
  cursor = 0
@@ -454,7 +430,6 @@ def _redis_scan_delete(client, pattern: str) -> int:
454
  break
455
  return total
456
 
457
-
458
  def _redis_prepare_usage_on_boot(client) -> None:
459
  marker_key = "ovf:meta:boot_marker"
460
  marker = _runtime_boot_marker()
@@ -462,36 +437,28 @@ def _redis_prepare_usage_on_boot(client) -> None:
462
  old = str(client.get(marker_key) or "")
463
  except Exception:
464
  old = ""
465
-
466
  if old == marker:
467
  return
468
-
469
  cleared_usage = 0
470
  try:
471
  cleared_usage += _redis_scan_delete(client, "ovf:usage:*")
472
  cleared_usage += _redis_scan_delete(client, "ovf:usage_window:*")
473
  except Exception as exc:
474
- print(f"[redis] usage reset skipped: {type(exc).__name__}: {exc}")
475
-
476
  try:
477
  client.set(marker_key, marker)
478
  except Exception:
479
  pass
480
 
481
- print(f"[redis] usage counters reset on startup. cleared={cleared_usage} marker={marker}")
482
-
483
-
484
  def _redis_maybe_flush_all(client) -> None:
485
  global _REDIS_MEMORY_LAST_CHECK_TS
486
  now = time.time()
487
  interval_default = int(getattr(APP_CONFIG, "REDIS_MEMORY_CHECK_INTERVAL_SECONDS", 120))
488
  interval = max(15, int((os.getenv("REDIS_MEMORY_CHECK_INTERVAL_SECONDS") or str(interval_default)).strip() or str(interval_default)))
489
-
490
  with _REDIS_STATE_LOCK:
491
  if (now - float(_REDIS_MEMORY_LAST_CHECK_TS or 0.0)) < interval:
492
  return
493
  _REDIS_MEMORY_LAST_CHECK_TS = now
494
-
495
  try:
496
  info = client.info(section="memory")
497
  used = int(info.get("used_memory") or 0)
@@ -500,30 +467,24 @@ def _redis_maybe_flush_all(client) -> None:
500
  max_memory = int((os.getenv("REDIS_MEMORY_LIMIT_BYTES") or str(30 * 1024 * 1024)).strip() or str(30 * 1024 * 1024))
501
  if max_memory <= 0:
502
  return
503
-
504
  ratio = float(used) / float(max_memory)
505
  threshold_default = float(getattr(APP_CONFIG, "REDIS_FLUSH_ALL_RATIO", 0.95))
506
  threshold = float((os.getenv("REDIS_FLUSH_ALL_RATIO") or str(threshold_default)).strip() or str(threshold_default))
507
  threshold = min(0.999, max(0.6, threshold))
508
-
509
  if ratio >= threshold:
510
  client.flushdb()
511
- print(f"[redis] memory high ({used}/{max_memory}, ratio={ratio:.3f}). flushdb executed.")
512
  except Exception as exc:
513
- print(f"[redis] memory check skipped: {type(exc).__name__}: {exc}")
514
-
515
 
516
  def _redis_client():
517
  global _REDIS_CLIENT
518
  if _REDIS_CLIENT is not None:
519
  return _REDIS_CLIENT
520
-
521
  if redis is None:
522
  return None
523
  url = _redis_url()
524
  if not url:
525
  return None
526
-
527
  with _REDIS_STATE_LOCK:
528
  if _REDIS_CLIENT is not None:
529
  return _REDIS_CLIENT
@@ -543,30 +504,22 @@ def _redis_client():
543
  client.ping()
544
  _redis_prepare_usage_on_boot(client)
545
  _REDIS_CLIENT = client
546
- print(f"[redis] connected. max_connections={max_connections}")
547
  except Exception as exc:
548
- print(f"[redis] disabled: {type(exc).__name__}: {exc}")
549
  _REDIS_CLIENT = None
550
  return _REDIS_CLIENT
551
 
552
-
553
  def _nsfw_enabled() -> bool:
554
- # NSFW filter disabled: this project is distributed without the content filter.
555
  return False
556
 
557
-
558
  def _nsfw_policy_applies(mode: str) -> bool:
559
- # NSFW filter disabled.
560
  return False
561
 
562
-
563
  def _normalize_ip(ip: str) -> str:
564
  ip = (ip or "").strip()
565
  if not ip:
566
  return ""
567
  if "," in ip:
568
  ip = ip.split(",", 1)[0].strip()
569
- # Strip :port for IPv4-like literals.
570
  if "." in ip and ip.count(":") == 1:
571
  host, _, maybe_port = ip.rpartition(":")
572
  if host and maybe_port.isdigit():
@@ -575,7 +528,6 @@ def _normalize_ip(ip: str) -> str:
575
  ip = ip[1:-1].strip()
576
  return ip if 0 < len(ip) <= 128 else ""
577
 
578
-
579
  def _request_ip(request: Any = None) -> str:
580
  if request is None:
581
  return ""
@@ -599,16 +551,13 @@ def _request_ip(request: Any = None) -> str:
599
  pass
600
  return ""
601
 
602
-
603
  def _usage_entry_key(ip: str) -> str:
604
  key = _normalize_ip(ip)
605
  return key if key else "__unknown__"
606
 
607
-
608
  def _usage_key(ip: str) -> str:
609
  return f"ovf:usage:{_usage_entry_key(ip)}"
610
 
611
-
612
  def _usage_country_stats_key(metric: str = "video") -> str:
613
  m = str(metric or "video").strip().lower()
614
  if m == "auto":
@@ -617,136 +566,39 @@ def _usage_country_stats_key(metric: str = "video") -> str:
617
  return "ovf:usage:global:country_nsfw_total"
618
  return "ovf:usage:global:country_video_total"
619
 
620
-
621
  def _usage_global_video_total_key() -> str:
622
  return "ovf:usage:global:video_total"
623
 
624
-
625
  def _usage_country_ip_set_key(country_code: str) -> str:
626
  return f"ovf:usage:global:country_ips:{country_code}"
627
 
628
-
629
  def _stats_country(country_code: str) -> str:
630
  cc = (country_code or "").strip().upper()
631
  if len(cc) == 2 and cc.isalpha():
632
  return cc
633
  return "UNKNOWN"
634
 
635
-
636
  def _usage_window_key(ip: str, window_seconds: int, now: float) -> str:
637
  bucket = int(float(now) // max(1, int(window_seconds)))
638
  return f"ovf:usage_window:auto:{_usage_entry_key(ip)}:{bucket}"
639
 
640
-
641
  def _geo_key(ip: str) -> str:
642
  return f"ovf:geo:{_usage_entry_key(ip)}"
643
 
644
-
645
  def _usage_ttl_seconds() -> int:
646
  return max(300, int((os.getenv("REDIS_USAGE_TTL_SECONDS") or str(7 * 24 * 3600)).strip() or str(7 * 24 * 3600)))
647
 
648
-
649
  def _geo_ttl_seconds() -> int:
650
  return max(3600, int((os.getenv("REDIS_GEO_TTL_SECONDS") or str(180 * 24 * 3600)).strip() or str(180 * 24 * 3600)))
651
 
652
-
653
  def _set_local_country_cache(ip: str, country: str, now_ts: Optional[float] = None) -> None:
654
  now_ts = float(now_ts if now_ts is not None else time.time())
655
  with _USAGE_STATE_LOCK:
656
  _COUNTRY_CACHE[ip] = country
657
  _COUNTRY_CACHE_TS[ip] = now_ts
658
 
659
-
660
  def _request_country(request: Any = None, ip: str = "") -> str:
661
- # PRIVACY BUILD: no header-country reads, no external geo lookups (ipwho.is/ipapi.co).
662
  return ""
663
- keys = tuple(
664
- getattr(
665
- APP_CONFIG,
666
- "IP_COUNTRY_HEADER_KEYS",
667
- (
668
- "cf-ipcountry",
669
- "x-vercel-ip-country",
670
- "x-hf-country",
671
- "x-hf-ip-country",
672
- "x-country-code",
673
- ),
674
- )
675
- or ()
676
- )
677
-
678
- headers = {}
679
- if request is not None:
680
- try:
681
- headers = {str(k).lower(): str(v) for k, v in dict(getattr(request, "headers", {}) or {}).items()}
682
- except Exception:
683
- headers = {}
684
-
685
- normalized_ip = _normalize_ip(ip or _request_ip(request))
686
-
687
- for key in keys:
688
- v = (headers.get(str(key).lower()) or "").strip().upper()
689
- if len(v) == 2 and v.isalpha():
690
- if normalized_ip:
691
- _set_local_country_cache(normalized_ip, v)
692
- client = _redis_client()
693
- if client is not None:
694
- try:
695
- pipe = client.pipeline()
696
- pipe.hset(_geo_key(normalized_ip), mapping={"country": v, "updated_at": str(int(time.time()))})
697
- pipe.expire(_geo_key(normalized_ip), _geo_ttl_seconds())
698
- pipe.execute()
699
- except Exception as exc:
700
- print(f"[redis] geo save from header skipped: {type(exc).__name__}: {exc}")
701
- return v
702
-
703
- if not normalized_ip:
704
- return ""
705
-
706
- now = time.time()
707
- ttl = max(60, int(getattr(APP_CONFIG, "IP_COUNTRY_CACHE_TTL_SECONDS")))
708
- with _USAGE_STATE_LOCK:
709
- if (now - float(_COUNTRY_CACHE_TS.get(normalized_ip, 0.0))) < ttl:
710
- return str(_COUNTRY_CACHE.get(normalized_ip, "") or "").upper()
711
-
712
- client = _redis_client()
713
- if client is not None:
714
- try:
715
- cached = str(client.hget(_geo_key(normalized_ip), "country") or "").strip().upper()
716
- if len(cached) == 2 and cached.isalpha():
717
- _set_local_country_cache(normalized_ip, cached, now)
718
- return cached
719
- except Exception as exc:
720
- print(f"[redis] geo read skipped: {type(exc).__name__}: {exc}")
721
-
722
- if not bool(getattr(APP_CONFIG, "IP_COUNTRY_ENABLE_GEO_LOOKUP")):
723
- return ""
724
- if normalized_ip.startswith(("10.", "192.168.", "127.")) or normalized_ip in {"::1"}:
725
- return ""
726
-
727
- timeout = max(1, int(getattr(APP_CONFIG, "IP_COUNTRY_LOOKUP_TIMEOUT_SECONDS")))
728
- country = ""
729
- for url in (f"https://ipwho.is/{normalized_ip}", f"https://ipapi.co/{normalized_ip}/json/"):
730
- try:
731
- data = _http_json("GET", url, timeout=timeout)
732
- country = str((data.get("country_code") or "")).strip().upper()
733
- if len(country) == 2 and country.isalpha():
734
- break
735
- country = ""
736
- except Exception:
737
- country = ""
738
-
739
- _set_local_country_cache(normalized_ip, country, now)
740
- if country and client is not None:
741
- try:
742
- pipe = client.pipeline()
743
- pipe.hset(_geo_key(normalized_ip), mapping={"country": country, "updated_at": str(int(now))})
744
- pipe.expire(_geo_key(normalized_ip), _geo_ttl_seconds())
745
- pipe.execute()
746
- except Exception as exc:
747
- print(f"[redis] geo save skipped: {type(exc).__name__}: {exc}")
748
- return country
749
-
750
 
751
  def _snapshot_usage_counts(ip: str) -> Tuple[int, int, int]:
752
  client = _redis_client()
@@ -760,15 +612,12 @@ def _snapshot_usage_counts(ip: str) -> Tuple[int, int, int]:
760
  int(vals[2] or 0),
761
  )
762
  except Exception as exc:
763
- print(f"[redis] usage snapshot unavailable: {type(exc).__name__}: {exc}")
764
  return (0, 0, 0)
765
 
766
-
767
  def _usage_country(ip: str, fallback_country_code: str = "") -> str:
768
  cc = (fallback_country_code or "").strip().upper()
769
  if cc:
770
  return cc
771
-
772
  client = _redis_client()
773
  if client is not None:
774
  try:
@@ -777,445 +626,72 @@ def _usage_country(ip: str, fallback_country_code: str = "") -> str:
777
  return cc
778
  except Exception:
779
  pass
780
-
781
  return ""
782
 
783
-
784
  def _log_usage_snapshot(event: str, ip: str, country_code: str = "") -> None:
785
- # PRIVACY BUILD: no console logging of IPs/countries.
786
  return
787
- a, v, n = _snapshot_usage_counts(ip)
788
- ip_text = _normalize_ip(ip) or "unknown"
789
- cc = _usage_country(ip, country_code)
790
- print(
791
- f"用量统计 事件={event} IP={ip_text} 国家={cc or 'unknown'} "
792
- f"自动分镜次数={a} 视频生成次数={v} NSFW次数={n}"
793
- )
794
-
795
 
796
  def _log_global_country_video_stats_if_needed(client, global_total: int) -> None:
797
- interval_default = int(getattr(APP_CONFIG, "COUNTRY_GENERATION_STATS_INTERVAL", 20))
798
- interval = max(1, int((os.getenv("COUNTRY_GENERATION_STATS_INTERVAL") or str(interval_default)).strip() or str(interval_default)))
799
- if global_total <= 0 or (global_total % interval) != 0:
800
- return
801
- try:
802
- raw_video = client.hgetall(_usage_country_stats_key("video")) or {}
803
- raw_auto = client.hgetall(_usage_country_stats_key("auto")) or {}
804
- raw_nsfw = client.hgetall(_usage_country_stats_key("nsfw")) or {}
805
- except Exception as exc:
806
- print(f"[redis] global country stats skipped: {type(exc).__name__}: {exc}")
807
- return
808
-
809
- country_set = set()
810
- for raw in (raw_video, raw_auto, raw_nsfw):
811
- for cc in raw.keys():
812
- country_set.add(_stats_country(str(cc)))
813
-
814
- stats: List[Tuple[str, int, int, int, int]] = []
815
- if country_set:
816
- pipe = client.pipeline()
817
- ordered_countries = sorted(country_set)
818
- for cc in ordered_countries:
819
- pipe.scard(_usage_country_ip_set_key(cc))
820
- ip_counts = pipe.execute()
821
- ip_count_map = {}
822
- for idx, cc in enumerate(ordered_countries):
823
- try:
824
- ip_count_map[cc] = int(ip_counts[idx] or 0)
825
- except Exception:
826
- ip_count_map[cc] = 0
827
- else:
828
- ip_count_map = {}
829
-
830
- for cc in country_set:
831
- try:
832
- video_count = int(raw_video.get(cc, 0) or 0)
833
- except Exception:
834
- video_count = 0
835
- try:
836
- auto_count = int(raw_auto.get(cc, 0) or 0)
837
- except Exception:
838
- auto_count = 0
839
- try:
840
- nsfw_count = int(raw_nsfw.get(cc, 0) or 0)
841
- except Exception:
842
- nsfw_count = 0
843
- ip_count = int(ip_count_map.get(cc, 0) or 0)
844
- if video_count > 0 or auto_count > 0 or nsfw_count > 0:
845
- stats.append((cc, video_count, auto_count, nsfw_count, ip_count))
846
-
847
- stats.sort(key=lambda item: (item[1], item[2], item[3]), reverse=True)
848
- if not stats:
849
- return
850
-
851
- print("=" * 60)
852
- print(f"📊 Global generation by country (total={global_total}, interval={interval})")
853
- for cc, video_count, auto_count, nsfw_count, ip_count in stats:
854
- pct = (float(video_count) / float(global_total)) * 100.0 if global_total > 0 else 0.0
855
- print(
856
- f" {cc}: video={video_count} ({pct:.1f}%), "
857
- f"auto-prompt={auto_count}, nsfw={nsfw_count}, unique_ip={ip_count}"
858
- )
859
- print("=" * 60)
860
-
861
 
862
  def _allow_auto_prompt_and_record(ip: str, country_code: str = "") -> Tuple[bool, str]:
863
- country_code = (country_code or "").strip().upper()
864
- now = time.time()
865
- window_seconds = max(1, int(getattr(APP_CONFIG, "AUTO_PROMPT_WINDOW_SECONDS")))
866
- per_window_limit = max(1, int(getattr(APP_CONFIG, "AUTO_PROMPT_MAX_PER_WINDOW")))
867
- country_limits = dict(
868
- getattr(APP_CONFIG, "AUTO_PROMPT_COUNTRY_TOTAL_LIMITS") or {}
869
- )
870
- limit_msg = str(getattr(APP_CONFIG, "AUTO_PROMPT_LIMIT_MESSAGE", "") or "").strip()
871
- if (not limit_msg) or any("\u4e00" <= ch <= "\u9fff" for ch in limit_msg):
872
- limit_msg = "Auto-prompt is temporarily unavailable. Please enter an English prompt manually."
873
-
874
- client = _redis_client()
875
- if client is None:
876
- print("[redis] auto-prompt limit skipped: redis unavailable")
877
- return True, ""
878
-
879
- try:
880
- _redis_maybe_flush_all(client)
881
- usage_key = _usage_key(ip)
882
- window_key = _usage_window_key(ip, window_seconds, now)
883
-
884
- window_count = int(client.incr(window_key) or 0)
885
- if window_count == 1:
886
- client.expire(window_key, window_seconds + 5)
887
- if window_count > per_window_limit:
888
- try:
889
- client.decr(window_key)
890
- except Exception:
891
- pass
892
- return False, limit_msg
893
-
894
- cc = country_code
895
- if not cc:
896
- cc = str(client.hget(usage_key, "country") or "").strip().upper()
897
-
898
- total_limit = None
899
- if cc:
900
- try:
901
- total_limit = int(country_limits.get(cc))
902
- except Exception:
903
- total_limit = None
904
- if total_limit is not None and total_limit >= 0:
905
- current_total = int(client.hget(usage_key, "auto_prompt_total") or 0)
906
- if current_total >= int(total_limit):
907
- try:
908
- client.decr(window_key)
909
- except Exception:
910
- pass
911
- return False, limit_msg
912
-
913
- stats_country = _stats_country(cc)
914
- pipe = client.pipeline()
915
- if country_code:
916
- pipe.hset(usage_key, "country", country_code)
917
- pipe.hincrby(usage_key, "auto_prompt_total", 1)
918
- pipe.hincrby(_usage_country_stats_key("auto"), stats_country, 1)
919
- pipe.sadd(_usage_country_ip_set_key(stats_country), _usage_entry_key(ip))
920
- pipe.expire(usage_key, _usage_ttl_seconds())
921
- pipe.execute()
922
- return True, ""
923
- except Exception as exc:
924
- print(f"[redis] auto-prompt limit skipped on error: {type(exc).__name__}: {exc}")
925
- return True, ""
926
-
927
 
928
  def _record_video_generation(ip: str, country_code: str = "") -> None:
929
- country_code = (country_code or "").strip().upper()
930
- client = _redis_client()
931
- if client is None:
932
- print("[redis] record video skipped: redis unavailable")
933
- return
934
- try:
935
- _redis_maybe_flush_all(client)
936
- usage_key = _usage_key(ip)
937
- stats_country = _stats_country(country_code)
938
- pipe = client.pipeline()
939
- if country_code:
940
- pipe.hset(usage_key, "country", country_code)
941
- pipe.hincrby(usage_key, "video_total", 1)
942
- pipe.hincrby(_usage_country_stats_key("video"), stats_country, 1)
943
- pipe.sadd(_usage_country_ip_set_key(stats_country), _usage_entry_key(ip))
944
- pipe.incr(_usage_global_video_total_key())
945
- pipe.expire(usage_key, _usage_ttl_seconds())
946
- results = pipe.execute()
947
- # `incr(global_total)` is always the penultimate pipeline op (before expire).
948
- global_total = int(results[-2] or 0)
949
- _log_global_country_video_stats_if_needed(client, global_total)
950
- except Exception as exc:
951
- print(f"[redis] record video skipped on error: {type(exc).__name__}: {exc}")
952
-
953
 
954
  def _record_nsfw_hit(ip: str, country_code: str = "") -> int:
955
- country_code = (country_code or "").strip().upper()
956
- client = _redis_client()
957
- if client is None:
958
- print("[redis] record nsfw skipped: redis unavailable")
959
- return 0
960
- try:
961
- _redis_maybe_flush_all(client)
962
- usage_key = _usage_key(ip)
963
- stats_country = _stats_country(country_code)
964
- pipe = client.pipeline()
965
- if country_code:
966
- pipe.hset(usage_key, "country", country_code)
967
- pipe.hincrby(usage_key, "nsfw_total", 1)
968
- pipe.hincrby(_usage_country_stats_key("nsfw"), stats_country, 1)
969
- pipe.sadd(_usage_country_ip_set_key(stats_country), _usage_entry_key(ip))
970
- pipe.expire(usage_key, _usage_ttl_seconds())
971
- results = pipe.execute()
972
- total = int(results[1] or 0)
973
- return total
974
- except Exception as exc:
975
- print(f"[redis] record nsfw skipped on error: {type(exc).__name__}: {exc}")
976
- return 0
977
-
978
 
979
  def _nsfw_counter_endpoint() -> Optional[str]:
980
- candidates = [
981
- (os.getenv("SELFIT_NSFW_ENDPOINT") or "").strip(),
982
- (os.getenv("OMNI_NSFW_ENDPOINT") or "").strip(),
983
- str(getattr(APP_CONFIG, "NSFW_COUNTER_ENDPOINT") or "").strip(),
984
- (_get_legacy_api_base() or "").strip(),
985
- ]
986
- for raw in candidates:
987
- if not raw:
988
- continue
989
- if not raw.startswith("http://") and not raw.startswith("https://"):
990
- continue
991
- url = raw.rstrip("/")
992
- if not url.endswith("/nsfw_vf"):
993
- # If caller only gave host, append default endpoint path.
994
- if url.count("/") <= 2:
995
- url = url + "/nsfw_vf"
996
- return url
997
  return None
998
 
999
-
1000
  def _nsfw_counter_key(ip: str) -> str:
1001
- normalized = _normalize_ip(ip)
1002
- return normalized if normalized else "__unknown__"
1003
-
1004
 
1005
  def _nsfw_remote_inc_total(ip: str) -> Optional[int]:
1006
- endpoint = _nsfw_counter_endpoint()
1007
- ip = _normalize_ip(ip)
1008
- if not endpoint or not ip:
1009
- return None
1010
- timeout = int(getattr(APP_CONFIG, "NSFW_REMOTE_TIMEOUT_SECONDS"))
1011
- data = _http_json("POST", endpoint, payload={"ip": ip}, timeout=timeout)
1012
- if int(data.get("code", -1)) != 0:
1013
- raise RuntimeError(f"nsfw inc failed: {data.get('message')}")
1014
- return int(((data.get("data") or {}).get("nsfw_total") or 0))
1015
-
1016
 
1017
  def _nsfw_remote_register_nsfw(ip: str) -> Optional[int]:
1018
- key = _nsfw_counter_key(ip)
1019
- if key != "__unknown__":
1020
- try:
1021
- total = _nsfw_remote_inc_total(key)
1022
- if total is not None:
1023
- return int(total)
1024
- except Exception as exc:
1025
- print(f"[nsfw] remote inc failed: {type(exc).__name__}: {exc}")
1026
- if not bool(getattr(APP_CONFIG, "NSFW_FAIL_OPEN")):
1027
- raise
1028
  return None
1029
 
1030
-
1031
  def _nsfw_keyword_match(label: str) -> bool:
1032
- text = (label or "").strip().lower()
1033
- keywords = tuple(getattr(APP_CONFIG, "NSFW_LABEL_KEYWORDS"))
1034
- return any(k and k in text for k in keywords)
1035
-
1036
 
1037
  def _get_nsfw_pipeline():
1038
- global _NSFW_PIPELINE
1039
- with _NSFW_STATE_LOCK:
1040
- if _NSFW_PIPELINE is None:
1041
- from transformers import pipeline
1042
-
1043
- model_id = (os.getenv("NSFW_MODEL_ID") or getattr(APP_CONFIG, "NSFW_MODEL_ID")).strip()
1044
- _NSFW_PIPELINE = pipeline("image-classification", model=model_id, device=-1)
1045
- return _NSFW_PIPELINE
1046
-
1047
 
1048
  def _nsfw_predict_label_from_pil(pil_image) -> str:
1049
- pipe = _get_nsfw_pipeline()
1050
- out = pipe(pil_image, top_k=1)
1051
- if not out:
1052
- return "unknown"
1053
- best = out[0] if isinstance(out, list) else out
1054
- return str(best.get("label") or "unknown")
1055
-
1056
 
1057
  def _video_frames_for_nsfw(video_path: str):
1058
- try:
1059
- import cv2
1060
- from PIL import Image
1061
- except Exception:
1062
- return []
1063
- if not video_path or not os.path.exists(video_path):
1064
- return []
1065
-
1066
- cap = cv2.VideoCapture(video_path)
1067
- if not cap.isOpened():
1068
- return []
1069
- frames = []
1070
- try:
1071
- frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
1072
- ratios = tuple(getattr(APP_CONFIG, "NSFW_SAMPLE_RATIOS"))
1073
- if frame_count > 0:
1074
- idxs = []
1075
- for ratio in ratios[:2]:
1076
- try:
1077
- idx = int(frame_count * float(ratio))
1078
- except Exception:
1079
- idx = int(frame_count * 0.5)
1080
- idxs.append(max(0, min(frame_count - 1, idx)))
1081
- for idx in idxs:
1082
- cap.set(cv2.CAP_PROP_POS_FRAMES, int(idx))
1083
- ok, frame = cap.read()
1084
- if ok and frame is not None:
1085
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
1086
- frames.append(Image.fromarray(frame))
1087
- if frames:
1088
- return frames
1089
-
1090
- # Fallback for formats where CAP_PROP_FRAME_COUNT is unreliable.
1091
- raw_frames = []
1092
- while True:
1093
- ok, frame = cap.read()
1094
- if not ok or frame is None:
1095
- break
1096
- raw_frames.append(frame)
1097
- if len(raw_frames) > 512:
1098
- break
1099
- if not raw_frames:
1100
- return []
1101
- total = len(raw_frames)
1102
- idxs = []
1103
- for ratio in ratios[:2]:
1104
- try:
1105
- idx = int(total * float(ratio))
1106
- except Exception:
1107
- idx = int(total * 0.5)
1108
- idxs.append(max(0, min(total - 1, idx)))
1109
- for idx in idxs:
1110
- frame = cv2.cvtColor(raw_frames[idx], cv2.COLOR_BGR2RGB)
1111
- frames.append(Image.fromarray(frame))
1112
- return frames
1113
- finally:
1114
- try:
1115
- cap.release()
1116
- except Exception:
1117
- pass
1118
-
1119
 
1120
  def _nsfw_check_video(video_path: str) -> Tuple[bool, str]:
1121
- frames = _video_frames_for_nsfw(video_path)
1122
- if not frames:
1123
- return False, "nsfw=unknown(frame_extract_failed)"
1124
-
1125
- labels = []
1126
- names = ("mid", "p80")
1127
- for i, frame in enumerate(frames[:2]):
1128
- label = _nsfw_predict_label_from_pil(frame)
1129
- labels.append((names[i] if i < len(names) else f"f{i}", label))
1130
- flagged = any(_nsfw_keyword_match(lbl) for _, lbl in labels)
1131
- detail = ", ".join(f"{k}={v}" for k, v in labels) if labels else "none"
1132
- return flagged, f"nsfw={flagged} ({detail})"
1133
-
1134
 
1135
  def _nsfw_blur_preview_from_video(video_path: str) -> Optional[str]:
1136
- frames = _video_frames_for_nsfw(video_path)
1137
- if not frames:
1138
- return None
1139
- target = frames[1] if len(frames) > 1 else frames[0]
1140
- try:
1141
- from PIL import ImageFilter
1142
- except Exception:
1143
- return None
1144
- try:
1145
- base = target.convert("RGB")
1146
- blurred = base.filter(ImageFilter.GaussianBlur(radius=32))
1147
- merged = base.blend(blurred, alpha=0.8)
1148
- out_path = _output_dir() / f"nsfw_blur_{int(time.time() * 1000)}.jpg"
1149
- merged.save(out_path, format="JPEG", quality=88)
1150
- return str(out_path)
1151
- except Exception:
1152
- return None
1153
-
1154
 
1155
  def _nsfw_warning_card_html() -> str:
1156
- return (
1157
- "<div style='margin-top:10px;padding:0;'>"
1158
- "<div style='border:1px solid #f59e0b;border-radius:16px;overflow:hidden;"
1159
- "box-shadow:0 6px 18px rgba(245,158,11,0.18);background:#fff7ed;'>"
1160
- "<div style='background:linear-gradient(135deg,#f59e0b 0%,#f97316 100%);padding:10px 14px;'>"
1161
- "<div style='color:white;font-weight:700;font-size:14px;'>⚠️ NSFW Warning</div>"
1162
- "</div>"
1163
- "<div style='padding:12px 14px;color:#7c2d12;font-size:13px;line-height:1.55;'>"
1164
- "<div>Sensitive content detected. Please adjust prompts for safer output.</div>"
1165
- "<div style='margin-top:8px;color:#92400e;'>According to Hugging Face content policy "
1166
- "(<a href='https://huggingface.co/content-policy' target='_blank' "
1167
- "style='color:#b45309;font-weight:700;text-decoration:none;'>https://huggingface.co/content-policy</a>), "
1168
- "this Space includes mandatory safety filtering. Repeated inappropriate content may cause your account "
1169
- "to lose access to this Space.</div>"
1170
- "</div>"
1171
- "</div>"
1172
- "</div>"
1173
- )
1174
-
1175
 
1176
  def _nsfw_blocked_card_html() -> str:
1177
- return (
1178
- "<div style='margin-top:10px;padding:0;'>"
1179
- "<div style='border:1px solid #ef4444;border-radius:16px;overflow:hidden;"
1180
- "box-shadow:0 8px 24px rgba(239,68,68,0.18);background:#fef2f2;'>"
1181
- "<div style='background:linear-gradient(135deg,#ef4444 0%,#dc2626 100%);padding:10px 14px;'>"
1182
- "<div style='color:white;font-weight:700;font-size:14px;'>🚫 NSFW Blocked</div>"
1183
- "</div>"
1184
- "<div style='padding:12px 14px;color:#7f1d1d;font-size:13px;line-height:1.55;'>"
1185
- "<div>This output has been hidden by policy. Please switch to a safer prompt.</div>"
1186
- "<div style='margin-top:8px;color:#991b1b;'>According to Hugging Face content policy "
1187
- "(<a href='https://huggingface.co/content-policy' target='_blank' "
1188
- "style='color:#b91c1c;font-weight:700;text-decoration:none;'>https://huggingface.co/content-policy</a>), "
1189
- "this Space includes mandatory safety filtering. Repeated inappropriate content may cause your account "
1190
- "to lose access to this Space.</div>"
1191
- "<div style='margin-top:8px;color:#991b1b;'>A blurred preview may be shown below.</div>"
1192
- "</div>"
1193
- "</div>"
1194
- "</div>"
1195
- )
1196
-
1197
 
1198
  def _nsfw_card_update_for_status(status_text: str):
1199
- text = str(status_text or "")
1200
- if "NSFW blocked:" in text:
1201
- return gr.update(value=_nsfw_blocked_card_html(), visible=True)
1202
- if "NSFW warning:" in text:
1203
- return gr.update(value=_nsfw_warning_card_html(), visible=True)
1204
  return gr.update(value="", visible=False)
1205
 
1206
-
1207
  def _nsfw_preview_update(path: Optional[str]):
1208
  p = str(path or "").strip()
1209
  if p and Path(p).exists():
1210
  return gr.update(value=p, visible=True)
1211
  return gr.update(value=None, visible=False)
1212
 
1213
-
1214
  def _public_generation_status(status_text: str) -> str:
1215
  lines = [str(raw or "").strip() for raw in str(status_text or "").splitlines() if str(raw or "").strip()]
1216
  if not lines:
1217
  return "ZeroGPU elapsed: --"
1218
-
1219
  elapsed_line = "ZeroGPU elapsed: --"
1220
  detail_lines: List[str] = []
1221
  for line in lines:
@@ -1223,64 +699,22 @@ def _public_generation_status(status_text: str) -> str:
1223
  elapsed_line = line
1224
  else:
1225
  detail_lines.append(line)
1226
-
1227
  if not detail_lines:
1228
  return elapsed_line
1229
-
1230
  detail = detail_lines[0]
1231
  if len(detail) > 320:
1232
  detail = detail[:317] + "..."
1233
  return f"{detail}\n{elapsed_line}"
1234
 
1235
-
1236
  def _should_show_like_tip(ip: str) -> bool:
1237
- normalized_ip = _normalize_ip(ip)
1238
- if not normalized_ip:
1239
- return False
1240
- _auto_total, video_total, _nsfw_total = _snapshot_usage_counts(normalized_ip)
1241
- return int(video_total) >= 2
1242
-
1243
 
1244
  def _download_upsell_card_html(show_like_tip: bool = False) -> str:
1245
- like_tip_html = ""
1246
- if show_like_tip:
1247
- like_tip_html = (
1248
- "<div style='margin-bottom:8px;font-weight:700;color:#1f1f7a;'>"
1249
- "If this Space helps you, please give us a ❤️ on Hugging Face."
1250
- "</div>"
1251
- )
1252
- return (
1253
- "<div style='margin-top:10px;padding:0;'>"
1254
- "<div style='border:1px solid #c7d2fe;border-radius:16px;overflow:hidden;"
1255
- "box-shadow:0 8px 22px rgba(99,102,241,0.16);background:linear-gradient(135deg,#eef2ff 0%,#e0e7ff 100%);'>"
1256
- "<div style='padding:14px 16px;color:#312e81;font-size:13px;line-height:1.7;text-align:center;'>"
1257
- + like_tip_html
1258
- + "This Space is currently in Lite mode.<br/>Visit our Official Site for unbound creativity, commercial licensing, and 20s cinematic video."
1259
- "</div>"
1260
- "<div style='display:flex;justify-content:center;padding:0 16px 16px 16px;'>"
1261
- "<a href='https://omnieditor.net/image-to-video' target='_blank' style='display:inline-flex;"
1262
- "align-items:center;justify-content:center;min-width:280px;padding:12px 36px;border-radius:12px;"
1263
- "background:linear-gradient(135deg,#ff8a65 0%,#f6c453 100%);color:white;text-decoration:none;"
1264
- "font-weight:800;font-size:14px;box-shadow:0 6px 18px rgba(245,158,11,0.28);'>"
1265
- "Visit Official Site"
1266
- "</a>"
1267
- "</div>"
1268
- "</div>"
1269
- "</div>"
1270
- )
1271
-
1272
 
1273
  def _download_upsell_update(video_path: Optional[str], status_text: str = "", show_like_tip: bool = False):
1274
- text = str(status_text or "")
1275
- if "NSFW blocked:" in text or "NSFW warning:" in text:
1276
- return gr.update(value=_download_upsell_card_html(show_like_tip=show_like_tip), visible=True)
1277
-
1278
- p = str(video_path or "").strip()
1279
- if p and Path(p).exists():
1280
- return gr.update(value=_download_upsell_card_html(show_like_tip=show_like_tip), visible=True)
1281
  return gr.update(value="", visible=False)
1282
 
1283
-
1284
  def _normalize_scene_parts(content: str, scene_count: int, fallback_prompt: str) -> List[str]:
1285
  parts = [p.strip() for p in (content or "").split("¥") if p.strip()]
1286
  if not parts:
@@ -1290,79 +724,70 @@ def _normalize_scene_parts(content: str, scene_count: int, fallback_prompt: str)
1290
  normalized.append(normalized[-1])
1291
  return normalized
1292
 
1293
-
1294
  def _llm_generate_scene_prompt_text(
1295
  mode: str,
1296
  prompt_text: str,
1297
  scene_count: int,
1298
  seconds_per_scene: int,
1299
  ) -> str:
1300
- # PRIVACY BUILD: no external LLM API (omnifilm.net) - prompt text never leaves this server.
1301
- # Auto-scene falls back to local scene splitting.
1302
- text, _note = _ensure_scene_prompt_text(prompt_text or "", scene_count)
1303
- return text
1304
- worker_token = _get_worker_api_token()
1305
- if not worker_token:
1306
- raise RuntimeError("WORKER_API_TOKEN is missing")
1307
-
1308
- mode_norm = (mode or "").strip().lower()
1309
- preset = "i2v-prompt" if mode_norm == "i2v" else "t2v-prompt"
1310
- payload = {
1311
- "preset": preset,
1312
- "payload": {
1313
- "prompt_text": (prompt_text or "").strip(),
1314
- "scene_count": max(1, min(int(scene_count or 1), 4)),
1315
- "seconds_per_scene": max(1, int(seconds_per_scene or 5)),
1316
- },
1317
- "priority": 0,
1318
- }
1319
- last_err = "unknown llm error"
1320
- for base in _llm_api_base_candidates():
1321
- headers = {
1322
- "Authorization": f"Bearer {worker_token}",
1323
- "Origin": base,
1324
- "Referer": base + "/",
1325
- }
1326
- try:
1327
- submit_url = f"{base}/api/llm-tasks/submit"
1328
- submit_data = _http_json("POST", submit_url, payload=payload, headers=headers, timeout=30)
1329
- if int(submit_data.get("code", -1)) != 0:
1330
- raise RuntimeError(f"submit failed: {submit_data.get('message')}")
1331
- task_id = (
1332
- (submit_data.get("data") or {}).get("task_id")
1333
- or (submit_data.get("data") or {}).get("id")
1334
- )
1335
- if not task_id:
1336
- raise RuntimeError("submit succeeded but task_id missing")
1337
-
1338
- status_url = f"{base}/api/llm-tasks/status/{task_id}"
1339
- deadline = time.time() + 45
1340
- while time.time() < deadline:
1341
- status_data = _http_json("GET", status_url, headers=headers, timeout=30)
1342
- if int(status_data.get("code", -1)) != 0:
1343
- raise RuntimeError(f"status failed: {status_data.get('message')}")
1344
- task = (status_data.get("data") or {}).get("task") or {}
1345
- status = str(task.get("status") or "").lower().strip()
1346
- if status == "succeeded":
1347
- result_obj = task.get("result")
1348
- if result_obj is None and isinstance(task.get("result_json"), str):
1349
- try:
1350
- result_obj = json.loads(task["result_json"])
1351
- except Exception:
1352
- result_obj = task["result_json"]
1353
- text = _extract_result_text(result_obj)
1354
- if not text:
1355
- raise RuntimeError("llm succeeded but result text is empty")
1356
- return text
1357
- if status in {"failed", "dead", "cancelled"}:
1358
- raise RuntimeError(task.get("error_text") or f"llm task {status}")
1359
- time.sleep(1.0)
1360
- raise RuntimeError("llm task timeout (45s)")
1361
- except Exception as exc:
1362
- last_err = f"{base}: {exc}"
1363
- continue
1364
- raise RuntimeError(last_err)
1365
-
1366
 
1367
  def _auto_prompt_for_mode(mode: str, prompt_text: str, scene_count: int, seconds_per_scene: int) -> Tuple[str, str]:
1368
  prompt_text = (prompt_text or "").strip()
@@ -1370,7 +795,6 @@ def _auto_prompt_for_mode(mode: str, prompt_text: str, scene_count: int, seconds
1370
  scenes = _normalize_scene_parts(llm_text, scene_count, prompt_text)
1371
  return "¥".join(scenes), f"auto-prompt ok: {scene_count} scenes"
1372
 
1373
-
1374
  def _ensure_scene_prompt_text(prompt_text: str, scene_count: int) -> Tuple[str, str]:
1375
  prompt_text = (prompt_text or "").strip()
1376
  if scene_count <= 1:
@@ -1378,7 +802,6 @@ def _ensure_scene_prompt_text(prompt_text: str, scene_count: int) -> Tuple[str,
1378
  scenes = _normalize_scene_parts(prompt_text, scene_count, prompt_text)
1379
  return "¥".join(scenes), "scene prompt: expanded from base prompt"
1380
 
1381
-
1382
  def _normalize_scene_inputs(scene_count: int, base_prompt: str, scenes: List[str]) -> List[str]:
1383
  scene_count = max(1, min(int(scene_count), 4))
1384
  base_prompt = (base_prompt or "").strip()
@@ -1393,7 +816,6 @@ def _normalize_scene_inputs(scene_count: int, base_prompt: str, scenes: List[str
1393
  normalized[i] = normalized[i - 1] if i > 0 and normalized[i - 1] else base_prompt
1394
  return normalized[:scene_count]
1395
 
1396
-
1397
  def _scene_values_for_ui(scene_count: int, scene_array: List[str]) -> Tuple[str, str, str, str]:
1398
  vals = list(scene_array or [])
1399
  while len(vals) < 4:
@@ -1404,19 +826,8 @@ def _scene_values_for_ui(scene_count: int, scene_array: List[str]) -> Tuple[str,
1404
  vals[count] = vals[count] if count < len(vals) else ""
1405
  return vals[0], vals[1], vals[2], vals[3]
1406
 
1407
-
1408
  def _ui_generate_scenes(mode: str, prompt_text: str, scene_count: int, seconds_per_scene: int, request: Any = None):
1409
  count = max(1, min(int(scene_count or 1), 4))
1410
- ip = _request_ip(request)
1411
- cc = _request_country(request, ip=ip)
1412
- ok, deny_msg = _allow_auto_prompt_and_record(ip, cc)
1413
- if not ok:
1414
- _log_usage_snapshot("auto-prompt受限", ip, cc)
1415
- fallback = _normalize_scene_inputs(count, "", ["", "", "", ""])
1416
- s1, s2, s3, s4 = _scene_values_for_ui(count, fallback)
1417
- return s1, s2, s3, s4, deny_msg
1418
-
1419
- _log_usage_snapshot("auto-prompt执行", ip, cc)
1420
  try:
1421
  joined, note = _auto_prompt_for_mode(mode, prompt_text, count, int(seconds_per_scene or 3))
1422
  scenes = _normalize_scene_parts(joined, count, prompt_text)
@@ -1427,11 +838,9 @@ def _ui_generate_scenes(mode: str, prompt_text: str, scene_count: int, seconds_p
1427
  s1, s2, s3, s4 = _scene_values_for_ui(count, fallback)
1428
  return s1, s2, s3, s4, f"auto-prompt failed: {exc}"
1429
 
1430
-
1431
  def _workflow_pack_path(wf_dir: Path, workflow_name: str) -> Path:
1432
  return wf_dir / f"{workflow_name}{WORKFLOW_PACK_SUFFIX}"
1433
 
1434
-
1435
  def _decode_packed_workflow(raw: bytes) -> str:
1436
  if not raw.startswith(WORKFLOW_PACK_MARKER):
1437
  raise RuntimeError("invalid workflow pack marker")
@@ -1439,16 +848,13 @@ def _decode_packed_workflow(raw: bytes) -> str:
1439
  decoded = zlib.decompress(payload)
1440
  return decoded.decode("utf-8")
1441
 
1442
-
1443
  def _materialize_workflow_from_pack(wf_dir: Path, workflow_name: str) -> Tuple[bool, str]:
1444
  json_path = wf_dir / workflow_name
1445
  if json_path.exists():
1446
  return True, "workflow json already exists"
1447
-
1448
  packed_path = _workflow_pack_path(wf_dir, workflow_name)
1449
  if not packed_path.exists():
1450
  return False, f"packed workflow not found: {packed_path}"
1451
-
1452
  try:
1453
  text = _decode_packed_workflow(packed_path.read_bytes())
1454
  data = json.loads(text)
@@ -1458,7 +864,6 @@ def _materialize_workflow_from_pack(wf_dir: Path, workflow_name: str) -> Tuple[b
1458
  except Exception as exc:
1459
  return False, f"failed to materialize {workflow_name}: {type(exc).__name__}: {exc}"
1460
 
1461
-
1462
  def _materialize_packed_workflows(wf_dir: Path) -> None:
1463
  if not wf_dir.exists():
1464
  return
@@ -1467,20 +872,15 @@ def _materialize_packed_workflows(wf_dir: Path) -> None:
1467
  ok, detail = _materialize_workflow_from_pack(wf_dir, name)
1468
  if ok and detail.startswith("materialized"):
1469
  notes.append(detail)
1470
- if notes:
1471
- print(f"[workflow] packed workflows restored in {wf_dir}: {notes}")
1472
-
1473
 
1474
  def _download_workflow_files_from_hf(dest_dir: Path) -> str:
1475
  try:
1476
  from huggingface_hub import hf_hub_download
1477
  except Exception as exc:
1478
  return f"workflow hf download unavailable: {type(exc).__name__}: {exc}"
1479
-
1480
  repo_id = _workflow_repo_id()
1481
  token = _get_hf_token()
1482
  dest_dir.mkdir(parents=True, exist_ok=True)
1483
-
1484
  got = []
1485
  misses = []
1486
  candidate_filenames = {
@@ -1492,7 +892,6 @@ def _download_workflow_files_from_hf(dest_dir: Path) -> str:
1492
  )
1493
  for name in WORKFLOW_FILES
1494
  }
1495
-
1496
  for name in WORKFLOW_FILES:
1497
  resolved = False
1498
  for candidate in candidate_filenames[name]:
@@ -1520,15 +919,10 @@ def _download_workflow_files_from_hf(dest_dir: Path) -> str:
1520
  continue
1521
  if not resolved:
1522
  misses.append(name)
1523
-
1524
  if misses:
1525
- return (
1526
- f"workflow hf download partial from space:{repo_id}; "
1527
- f"downloaded={got or 'none'} missing={misses}"
1528
- )
1529
  return f"workflow hf download ok from space:{repo_id}; downloaded={got}"
1530
 
1531
-
1532
  def _expected_model_paths() -> Dict[str, Path]:
1533
  entries = parse_model_entries()
1534
  root = _model_root_dir()
@@ -1537,13 +931,11 @@ def _expected_model_paths() -> Dict[str, Path]:
1537
  out[key] = root / MODEL_KEY_TO_SUBDIR[key] / filename
1538
  return out
1539
 
1540
-
1541
  def check_required_models_status() -> str:
1542
  try:
1543
  entries = parse_model_entries()
1544
  except Exception as exc:
1545
  return f"OMNI_VIDEOS invalid: {type(exc).__name__}: {exc}"
1546
-
1547
  root = _model_root_dir()
1548
  lines = [f"model root: {root}"]
1549
  for key, path in _expected_model_paths().items():
@@ -1553,18 +945,15 @@ def check_required_models_status() -> str:
1553
  lines.append(f"- {key}: {'ok' if exists else 'missing'} -> {path.name} ({source})")
1554
  return "\n".join(lines)
1555
 
1556
-
1557
  def download_missing_models() -> str:
1558
  try:
1559
  from huggingface_hub import hf_hub_download
1560
  except Exception as exc:
1561
  return f"huggingface_hub unavailable: {type(exc).__name__}: {exc}"
1562
-
1563
  try:
1564
  entries = parse_model_entries()
1565
  except Exception as exc:
1566
  return f"OMNI_VIDEOS invalid: {type(exc).__name__}: {exc}"
1567
-
1568
  token = _get_hf_token()
1569
  results = []
1570
  for key, path in _expected_model_paths().items():
@@ -1585,8 +974,6 @@ def download_missing_models() -> str:
1585
  local_dir=str(path.parent),
1586
  )
1587
  )
1588
- # Some repos use nested paths like "split_files/vae/xxx.safetensors".
1589
- # We always copy to the exact expected filename/location.
1590
  if downloaded.resolve() != path.resolve():
1591
  shutil.copy2(downloaded, path)
1592
  saved = path
@@ -1597,20 +984,17 @@ def download_missing_models() -> str:
1597
  results.append(f"- {key}: download failed ({type(exc).__name__}: {exc})")
1598
  return "\n".join(results)
1599
 
1600
-
1601
  def ensure_models_ready_on_startup() -> str:
1602
  try:
1603
  entries = parse_model_entries()
1604
  except Exception as exc:
1605
  return f"skip auto-download: OMNI_VIDEOS invalid ({type(exc).__name__}: {exc})"
1606
-
1607
  missing_keys = []
1608
  for key, path in _expected_model_paths().items():
1609
  if not path.exists():
1610
  missing_keys.append(key)
1611
  if not missing_keys:
1612
  return "all required models already present"
1613
-
1614
  lines = [f"missing models at startup: {', '.join(missing_keys)}"]
1615
  lines.append(download_missing_models())
1616
  final_missing = [key for key, path in _expected_model_paths().items() if not path.exists()]
@@ -1620,7 +1004,6 @@ def ensure_models_ready_on_startup() -> str:
1620
  lines.append("all required models are ready")
1621
  return "\n".join(lines)
1622
 
1623
-
1624
  def _set_model_prep_state(*, running: Optional[bool] = None, status: Optional[str] = None) -> None:
1625
  global _MODEL_PREP_RUNNING, _MODEL_PREP_STATUS
1626
  with _MODEL_PREP_LOCK:
@@ -1629,12 +1012,10 @@ def _set_model_prep_state(*, running: Optional[bool] = None, status: Optional[st
1629
  if status is not None:
1630
  _MODEL_PREP_STATUS = status
1631
 
1632
-
1633
  def _get_model_prep_state() -> Tuple[bool, str]:
1634
  with _MODEL_PREP_LOCK:
1635
  return _MODEL_PREP_RUNNING, _MODEL_PREP_STATUS
1636
 
1637
-
1638
  def _run_model_prep_job() -> None:
1639
  try:
1640
  with _PREP_IO_LOCK:
@@ -1643,30 +1024,24 @@ def _run_model_prep_job() -> None:
1643
  result = f"startup model preparation failed: {type(exc).__name__}: {exc}"
1644
  _set_model_prep_state(running=False, status=result)
1645
 
1646
-
1647
  def kickoff_model_prepare_background() -> str:
1648
  if _all_models_present():
1649
  _set_model_prep_state(running=False, status="all required models already present")
1650
  return "all required models already present"
1651
-
1652
  running, status = _get_model_prep_state()
1653
  if running:
1654
  return f"background model preparation already running\n{status}"
1655
-
1656
  _set_model_prep_state(running=True, status="background model preparation started")
1657
  worker = threading.Thread(target=_run_model_prep_job, name="model-prep", daemon=True)
1658
  worker.start()
1659
  return "background model preparation started"
1660
 
1661
-
1662
  def ensure_models_ready_for_generation() -> Tuple[bool, str]:
1663
  if _all_models_present():
1664
  return True, "all required models are ready"
1665
-
1666
  running, status = _get_model_prep_state()
1667
  if running:
1668
  return False, "models are preparing in background\n" + status
1669
-
1670
  _set_model_prep_state(running=True, status="on-demand model preparation started")
1671
  try:
1672
  with _PREP_IO_LOCK:
@@ -1674,12 +1049,10 @@ def ensure_models_ready_for_generation() -> Tuple[bool, str]:
1674
  except Exception as exc:
1675
  result = f"on-demand model preparation failed: {type(exc).__name__}: {exc}"
1676
  _set_model_prep_state(running=False, status=result)
1677
-
1678
  if _all_models_present():
1679
  return True, result
1680
  return False, result
1681
 
1682
-
1683
  def _set_runtime_prep_state(*, running: Optional[bool] = None, status: Optional[str] = None) -> None:
1684
  global _RUNTIME_PREP_RUNNING, _RUNTIME_PREP_STATUS
1685
  with _RUNTIME_PREP_LOCK:
@@ -1688,12 +1061,10 @@ def _set_runtime_prep_state(*, running: Optional[bool] = None, status: Optional[
1688
  if status is not None:
1689
  _RUNTIME_PREP_STATUS = status
1690
 
1691
-
1692
  def _get_runtime_prep_state() -> Tuple[bool, str]:
1693
  with _RUNTIME_PREP_LOCK:
1694
  return _RUNTIME_PREP_RUNNING, _RUNTIME_PREP_STATUS
1695
 
1696
-
1697
  def _has_comfy_runtime() -> bool:
1698
  comfy_dir = _comfy_dir()
1699
  required = (
@@ -1706,7 +1077,6 @@ def _has_comfy_runtime() -> bool:
1706
  )
1707
  return all(path.exists() for path in required)
1708
 
1709
-
1710
  def _runtime_missing_paths() -> list[str]:
1711
  comfy_dir = _comfy_dir()
1712
  required = (
@@ -1719,7 +1089,6 @@ def _runtime_missing_paths() -> list[str]:
1719
  )
1720
  return [str(path.relative_to(comfy_dir)) for path in required if not path.exists()]
1721
 
1722
-
1723
  def _run_git(cmd: list[str], cwd: Optional[Path] = None) -> Tuple[bool, str]:
1724
  try:
1725
  result = subprocess.run(
@@ -1731,14 +1100,12 @@ def _run_git(cmd: list[str], cwd: Optional[Path] = None) -> Tuple[bool, str]:
1731
  )
1732
  except Exception as exc:
1733
  return False, f"{type(exc).__name__}: {exc}"
1734
-
1735
  merged = "\n".join([result.stdout.strip(), result.stderr.strip()]).strip()
1736
  merged = _redact_sensitive(merged)
1737
  if result.returncode != 0:
1738
  return False, merged or f"git command failed (exit={result.returncode})"
1739
  return True, merged
1740
 
1741
-
1742
  def _runtime_git_head(comfy_dir: Path) -> str:
1743
  ok, detail = _run_git(["git", "-C", str(comfy_dir), "rev-parse", "--short", "HEAD"])
1744
  if not ok:
@@ -1748,30 +1115,22 @@ def _runtime_git_head(comfy_dir: Path) -> str:
1748
  return "unknown"
1749
  return detail.splitlines()[-1]
1750
 
1751
-
1752
  def _download_runtime_on_demand() -> str:
1753
- # PUBLIC BUILD: the Comfy runtime repo (selfitcamera/ComfyUIVideo) is
1754
- # public, so cloning works anonymously without a GitHub token.
1755
  repo = _runtime_git_repo()
1756
  revision = _runtime_git_revision()
1757
  comfy_dir = _comfy_dir()
1758
  models_dir = comfy_dir / "models"
1759
  models_backup = _repo_dir() / ".runtime_models_backup"
1760
  clone_url = _runtime_git_clone_url()
1761
- print(f"[runtime] syncing Comfy runtime from github:{repo}@{revision}")
1762
-
1763
  if models_backup.exists():
1764
  shutil.rmtree(models_backup, ignore_errors=True)
1765
-
1766
  if comfy_dir.exists() and not (comfy_dir / ".git").exists():
1767
  if models_dir.exists():
1768
  try:
1769
  shutil.move(str(models_dir), str(models_backup))
1770
- print("[runtime] preserved existing models directory before git clone.")
1771
  except Exception as exc:
1772
  return f"runtime clone failed while preserving models: {type(exc).__name__}: {exc}"
1773
  shutil.rmtree(comfy_dir, ignore_errors=True)
1774
-
1775
  if not comfy_dir.exists():
1776
  comfy_dir.parent.mkdir(parents=True, exist_ok=True)
1777
  if _is_commit_hash(revision):
@@ -1808,7 +1167,6 @@ def _download_runtime_on_demand() -> str:
1808
  ok, detail = _run_git(["git", "-C", str(comfy_dir), "reset", "--hard", "FETCH_HEAD"])
1809
  if not ok:
1810
  return f"runtime sync failed (reset): {detail}"
1811
-
1812
  if models_backup.exists():
1813
  restored_models_dir = comfy_dir / "models"
1814
  try:
@@ -1818,18 +1176,13 @@ def _download_runtime_on_demand() -> str:
1818
  else:
1819
  restored_models_dir.parent.mkdir(parents=True, exist_ok=True)
1820
  shutil.move(str(models_backup), str(restored_models_dir))
1821
- print("[runtime] restored preserved models directory after git sync.")
1822
  except Exception as exc:
1823
  return f"runtime sync finished but restoring models failed: {type(exc).__name__}: {exc}"
1824
-
1825
  if _has_comfy_runtime():
1826
- print(f"[runtime] current runtime commit: {_runtime_git_head(comfy_dir)}")
1827
- print("[runtime] Comfy runtime git sync finished.")
1828
  return f"runtime ready from github:{repo}@{revision}"
1829
  missing = _runtime_missing_paths()
1830
  return "runtime git sync finished but files are still missing: " + ", ".join(missing)
1831
 
1832
-
1833
  def _run_runtime_prep_job() -> None:
1834
  try:
1835
  with _PREP_IO_LOCK:
@@ -1838,30 +1191,24 @@ def _run_runtime_prep_job() -> None:
1838
  result = f"startup runtime preparation failed: {type(exc).__name__}: {exc}"
1839
  _set_runtime_prep_state(running=False, status=result)
1840
 
1841
-
1842
  def kickoff_runtime_prepare_background() -> str:
1843
  if _has_comfy_runtime():
1844
  _set_runtime_prep_state(running=False, status="runtime already present")
1845
  return "runtime already present"
1846
-
1847
  running, status = _get_runtime_prep_state()
1848
  if running:
1849
  return f"background runtime preparation already running\n{status}"
1850
-
1851
  _set_runtime_prep_state(running=True, status="background runtime preparation started")
1852
  worker = threading.Thread(target=_run_runtime_prep_job, name="runtime-prep", daemon=True)
1853
  worker.start()
1854
  return "background runtime preparation started"
1855
 
1856
-
1857
  def ensure_runtime_ready_for_generation() -> Tuple[bool, str]:
1858
  if _has_comfy_runtime():
1859
  return True, "runtime already present"
1860
-
1861
  running, status = _get_runtime_prep_state()
1862
  if running:
1863
  return False, "runtime is preparing in background\n" + status
1864
-
1865
  _set_runtime_prep_state(running=True, status="on-demand runtime preparation started")
1866
  try:
1867
  with _PREP_IO_LOCK:
@@ -1869,18 +1216,15 @@ def ensure_runtime_ready_for_generation() -> Tuple[bool, str]:
1869
  except Exception as exc:
1870
  result = f"on-demand runtime preparation failed: {type(exc).__name__}: {exc}"
1871
  _set_runtime_prep_state(running=False, status=result)
1872
-
1873
  if _has_comfy_runtime():
1874
  return True, result
1875
  return False, result
1876
 
1877
-
1878
  def resolve_placeholders(obj):
1879
  names = parse_model_names()
1880
  resolved = _deep_resolve_placeholders(obj, names)
1881
  return _apply_model_name_aliases(resolved, names)
1882
 
1883
-
1884
  def _install_comfyapi_src_shims() -> None:
1885
  if "src" not in sys.modules:
1886
  pkg = types.ModuleType("src")
@@ -1890,8 +1234,7 @@ def _install_comfyapi_src_shims() -> None:
1890
  if mod is None:
1891
  mod = types.ModuleType("src.model_names")
1892
  sys.modules["src.model_names"] = mod
1893
- mod.resolve_placeholders = resolve_placeholders # type: ignore[attr-defined]
1894
-
1895
 
1896
  def _drop_conflicting_modules(base_dir: Path) -> None:
1897
  checks = {
@@ -1914,7 +1257,6 @@ def _drop_conflicting_modules(base_dir: Path) -> None:
1914
  if key == root_name or key.startswith(root_name + "."):
1915
  del sys.modules[key]
1916
 
1917
-
1918
  def _get_comfy_client():
1919
  global _COMFY_CLIENT
1920
  if _COMFY_CLIENT is not None:
@@ -1926,6 +1268,21 @@ def _get_comfy_client():
1926
 
1927
  _install_comfyapi_src_shims()
1928
  _drop_conflicting_modules(comfy_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1929
  comfy_dir_str = str(comfy_dir)
1930
  if comfy_dir_str not in sys.path:
1931
  sys.path.insert(0, comfy_dir_str)
@@ -1953,14 +1310,12 @@ def _get_comfy_client():
1953
  )
1954
  return _COMFY_CLIENT
1955
 
1956
-
1957
  def _resolve_model_name(value: str, names: Dict[str, str]) -> str:
1958
  key = PLACEHOLDER_TO_KEY.get((value or "").strip())
1959
  if not key:
1960
  return value
1961
  return names.get(key, value)
1962
 
1963
-
1964
  def _deep_resolve_placeholders(obj, names: Dict[str, str]):
1965
  if isinstance(obj, dict):
1966
  return {k: _deep_resolve_placeholders(v, names) for k, v in obj.items()}
@@ -1970,7 +1325,6 @@ def _deep_resolve_placeholders(obj, names: Dict[str, str]):
1970
  return _resolve_model_name(obj, names)
1971
  return obj
1972
 
1973
-
1974
  def _pick_unet_key(node: Dict, original_name: str) -> str:
1975
  title = ((node.get("_meta") or {}).get("title") or "")
1976
  text = f"{title} {original_name}".lower()
@@ -1978,7 +1332,6 @@ def _pick_unet_key(node: Dict, original_name: str) -> str:
1978
  return "unet_q6kl"
1979
  return "unet_q6kh"
1980
 
1981
-
1982
  def _apply_model_name_aliases(obj, names: Dict[str, str]):
1983
  if not isinstance(obj, dict):
1984
  return obj
@@ -1989,19 +1342,16 @@ def _apply_model_name_aliases(obj, names: Dict[str, str]):
1989
  inputs = node.get("inputs")
1990
  if not isinstance(inputs, dict):
1991
  continue
1992
-
1993
  if "vae_name" in inputs and isinstance(inputs.get("vae_name"), str):
1994
  inputs["vae_name"] = names["vae"]
1995
  if "clip_name" in inputs and isinstance(inputs.get("clip_name"), str):
1996
  inputs["clip_name"] = names["text_encoder"]
1997
-
1998
  if "unet_name" in inputs and isinstance(inputs.get("unet_name"), str):
1999
  if class_type in ("UnetLoaderGGUF", "UnetLoaderGGUFAdvanced", "UNETLoader"):
2000
  key = _pick_unet_key(node, inputs["unet_name"])
2001
  inputs["unet_name"] = names[key]
2002
  return obj
2003
 
2004
-
2005
  def _collect_placeholders(obj, out: set[str]) -> None:
2006
  if isinstance(obj, dict):
2007
  for v in obj.values():
@@ -2014,23 +1364,17 @@ def _collect_placeholders(obj, out: set[str]) -> None:
2014
  if isinstance(obj, str) and obj in PLACEHOLDER_TO_KEY:
2015
  out.add(obj)
2016
 
2017
-
2018
  def _workflow_dir() -> Path:
2019
  repo_dir = _repo_dir()
2020
  candidates = (
2021
  repo_dir / "src" / "workflows",
2022
  )
2023
-
2024
  for wf_dir in candidates:
2025
  _materialize_packed_workflows(wf_dir)
2026
  if all((wf_dir / name).exists() for name in WORKFLOW_FILES):
2027
- print(f"[workflow] using workflow dir: {wf_dir}")
2028
  return wf_dir
2029
-
2030
  primary = candidates[0]
2031
  primary.mkdir(parents=True, exist_ok=True)
2032
-
2033
- # Last-resort discovery in case Space checkout layout changes unexpectedly.
2034
  found = {}
2035
  search_roots = [repo_dir / "src", repo_dir / "workflows", repo_dir]
2036
  for name in WORKFLOW_FILES:
@@ -2041,7 +1385,6 @@ def _workflow_dir() -> Path:
2041
  if matches:
2042
  found[name] = matches[0]
2043
  break
2044
-
2045
  if len(found) == len(WORKFLOW_FILES):
2046
  parents = {}
2047
  for path in found.values():
@@ -2052,35 +1395,20 @@ def _workflow_dir() -> Path:
2052
  if not dst_path.exists() and src_path.exists():
2053
  shutil.copy2(src_path, dst_path)
2054
  if all((primary / name).exists() for name in WORKFLOW_FILES):
2055
- print(f"[workflow] discovered fallback files and rebuilt: {primary}")
2056
  return primary
2057
  if all((best_parent / name).exists() for name in WORKFLOW_FILES):
2058
- print(f"[workflow] using discovered workflow dir: {best_parent}")
2059
  return best_parent
2060
-
2061
- hf_status = _download_workflow_files_from_hf(primary)
2062
- print(f"[workflow] {hf_status}")
2063
- if all((primary / name).exists() for name in WORKFLOW_FILES):
2064
- print(f"[workflow] rebuilt workflows from hf into: {primary}")
2065
- return primary
2066
-
2067
- print("[workflow] missing workflow files; checked candidates:")
2068
- for wf_dir in candidates:
2069
- missing = [name for name in WORKFLOW_FILES if not (wf_dir / name).exists()]
2070
- print(f"[workflow] - {wf_dir} missing={missing}")
2071
  return primary
2072
 
2073
-
2074
  def validate_workflow_placeholders() -> str:
2075
  try:
2076
  names = parse_model_names()
2077
  except Exception as exc:
2078
  return f"OMNI_VIDEOS invalid: {type(exc).__name__}: {exc}"
2079
-
2080
  wf_dir = _workflow_dir()
2081
  missing = []
2082
  unresolved_msgs = []
2083
-
2084
  for wf_name in WORKFLOW_FILES:
2085
  wf_path = wf_dir / wf_name
2086
  if not wf_path.exists():
@@ -2091,7 +1419,6 @@ def validate_workflow_placeholders() -> str:
2091
  except Exception as exc:
2092
  unresolved_msgs.append(f"{wf_name}: invalid json ({type(exc).__name__})")
2093
  continue
2094
-
2095
  resolved = _deep_resolve_placeholders(data, names)
2096
  unresolved = set()
2097
  _collect_placeholders(resolved, unresolved)
@@ -2099,17 +1426,14 @@ def validate_workflow_placeholders() -> str:
2099
  unresolved_msgs.append(f"{wf_name}: unresolved placeholders={sorted(unresolved)}")
2100
  else:
2101
  unresolved_msgs.append(f"{wf_name}: ok")
2102
-
2103
  lines = []
2104
  if missing:
2105
  lines.append(f"missing workflow files: {', '.join(missing)}")
2106
  lines.extend(unresolved_msgs)
2107
  return "\n".join(lines) if lines else "no workflow files found"
2108
 
2109
-
2110
  def _env_status_text() -> str:
2111
  lines = []
2112
-
2113
  raw = (os.getenv("OMNI_VIDEOS") or "").strip()
2114
  if not raw:
2115
  lines.append("- OMNI_VIDEOS: missing")
@@ -2119,27 +1443,23 @@ def _env_status_text() -> str:
2119
  lines.append(f"- OMNI_VIDEOS: ok ({parsed['vae']}, {parsed['text_encoder']})")
2120
  except Exception as exc:
2121
  lines.append(f"- OMNI_VIDEOS: invalid ({type(exc).__name__})")
2122
-
2123
  one_key = (os.getenv("ONE_KEY") or "").strip()
2124
  lines.append("- ONE_KEY: configured" if one_key else "- ONE_KEY: missing")
2125
  lines.append("- WORKER_API_TOKEN: configured" if _get_worker_api_token() else "- WORKER_API_TOKEN: missing")
2126
  lines.append("- GITHUB_TOKEN: configured" if _get_github_token() else "- GITHUB_TOKEN: missing")
2127
  return "\n".join(lines)
2128
 
2129
-
2130
  @spaces.GPU(duration=20)
2131
  def healthcheck_gpu(name: str) -> str:
2132
  who = (name or "").strip() or "world"
2133
  return f"Omni-Video-Factory bootstrap is running. Hello, {who}!"
2134
 
2135
-
2136
  def _all_models_present() -> bool:
2137
  try:
2138
  return all(path.exists() for path in _expected_model_paths().values())
2139
  except Exception:
2140
  return False
2141
 
2142
-
2143
  def _maybe_file_path(value) -> str:
2144
  if isinstance(value, str):
2145
  return value
@@ -2150,13 +1470,11 @@ def _maybe_file_path(value) -> str:
2150
  return got
2151
  return ""
2152
 
2153
-
2154
  def _output_dir() -> Path:
2155
  out = _repo_dir() / "datas" / "outputs"
2156
  out.mkdir(parents=True, exist_ok=True)
2157
  return out
2158
 
2159
-
2160
  def _scene_count() -> int:
2161
  default_scene = int(getattr(APP_CONFIG, "DEFAULT_SCENE_COUNT"))
2162
  raw = (os.getenv("OMNI_SCENE_COUNT") or str(default_scene)).strip()
@@ -2166,7 +1484,6 @@ def _scene_count() -> int:
2166
  value = default_scene
2167
  return max(1, min(value, 4))
2168
 
2169
-
2170
  def _vid_resolution_default() -> int:
2171
  raw = (os.getenv("OMNI_VID_RES") or "384").strip()
2172
  try:
@@ -2177,13 +1494,11 @@ def _vid_resolution_default() -> int:
2177
  return 384
2178
  return value
2179
 
2180
-
2181
  def _aspect_ratio_default() -> str:
2182
  raw = (os.getenv("OMNI_ASPECT_RATIO") or "3:4").strip()
2183
  allowed = {"16:9", "4:3", "1:1", "3:4", "9:16"}
2184
  return raw if raw in allowed else "3:4"
2185
 
2186
-
2187
  def _seconds_per_scene_default() -> int:
2188
  raw = (os.getenv("OMNI_SECONDS_PER_SCENE") or "3").strip()
2189
  try:
@@ -2192,7 +1507,6 @@ def _seconds_per_scene_default() -> int:
2192
  value = 3
2193
  return 5 if value >= 5 else 3
2194
 
2195
-
2196
  def _allowed_scene_counts(resolution: int) -> List[int]:
2197
  rules = {
2198
  384: [1, 2, 3, 4],
@@ -2200,7 +1514,6 @@ def _allowed_scene_counts(resolution: int) -> List[int]:
2200
  }
2201
  return rules.get(int(resolution), [1])
2202
 
2203
-
2204
  def _normalize_generation_options(
2205
  mode: str,
2206
  scene_count: Optional[int],
@@ -2220,27 +1533,21 @@ def _normalize_generation_options(
2220
  scenes = _scene_count()
2221
  if scenes not in allowed_scenes:
2222
  scenes = allowed_scenes[-1]
2223
-
2224
  secs = int(seconds_per_scene) if seconds_per_scene else _seconds_per_scene_default()
2225
  secs = 5 if secs >= 5 else 3
2226
- # Keep v2v aligned with UI options even though it does not use scene branches.
2227
  if mode == "v2v":
2228
  scenes = 1
2229
-
2230
  notes = f"config: res={res}, scenes={scenes}, seconds_per_scene={secs}"
2231
  return scenes, res, secs, notes
2232
 
2233
-
2234
  def _frames_for_seconds(seconds_per_scene: int) -> int:
2235
  return 81 if int(seconds_per_scene) >= 5 else 49
2236
 
2237
-
2238
  def _round_to_multiple(value: int, multiple: int = 16) -> int:
2239
  if multiple <= 1:
2240
  return int(value)
2241
  return int((int(value) + multiple - 1) // multiple * multiple)
2242
 
2243
-
2244
  def _compute_t2v_dims(aspect_ratio: str, resolution: int) -> Tuple[int, int]:
2245
  res = int(resolution)
2246
  aspect_map = {
@@ -2251,15 +1558,12 @@ def _compute_t2v_dims(aspect_ratio: str, resolution: int) -> Tuple[int, int]:
2251
  "9:16": (9, 16),
2252
  }
2253
  w_ratio, h_ratio = aspect_map.get(aspect_ratio, (3, 4))
2254
- # Keep target pixel area anchored to resolution^2, then apply aspect ratio.
2255
  target_area = max(256, int(res) * int(res))
2256
  width = int((target_area * (w_ratio / h_ratio)) ** 0.5)
2257
  height = int((target_area * (h_ratio / w_ratio)) ** 0.5)
2258
  return _round_to_multiple(width, 16), _round_to_multiple(height, 16)
2259
 
2260
-
2261
  def _gpu_duration_default() -> int:
2262
- # Fixed override for ZeroGPU request duration.
2263
  default_fixed = int(getattr(APP_CONFIG, "GPU_DEFAULT_FIXED_SECONDS"))
2264
  raw = (os.getenv("OMNI_GPU_SECONDS") or str(default_fixed)).strip()
2265
  try:
@@ -2270,7 +1574,6 @@ def _gpu_duration_default() -> int:
2270
  max_secs = int(getattr(APP_CONFIG, "GPU_MAX_SECONDS"))
2271
  return max(min_secs, min(value, max_secs))
2272
 
2273
-
2274
  def _estimated_runtime_seconds(
2275
  mode: str,
2276
  scene_count: Optional[int],
@@ -2283,7 +1586,6 @@ def _estimated_runtime_seconds(
2283
  resolution=resolution,
2284
  seconds_per_scene=seconds_per_scene,
2285
  )
2286
- # Packed config is JSON-based, so dict keys may become strings ("384"/"512"/"800").
2287
  raw_base_by_res = dict(getattr(APP_CONFIG, "GPU_BASE_SECONDS_BY_RES") or {})
2288
  base_by_res: Dict[int, float] = {}
2289
  for key, value in raw_base_by_res.items():
@@ -2299,7 +1601,6 @@ def _estimated_runtime_seconds(
2299
  mode_multiplier = float(mode_multiplier_map.get(str(mode or "").lower().strip(), 1.0))
2300
  return max(10.0, base * scene_multiplier * seconds_multiplier * mode_multiplier)
2301
 
2302
-
2303
  def _dynamic_gpu_duration(
2304
  mode: str,
2305
  prompt: str,
@@ -2312,9 +1613,8 @@ def _dynamic_gpu_duration(
2312
  aspect_ratio: str = "3:4",
2313
  client_ip: Optional[str] = None,
2314
  client_country: Optional[str] = None,
 
2315
  ) -> int:
2316
- _ = client_ip
2317
- _ = client_country
2318
  raw_fixed = (os.getenv("OMNI_GPU_SECONDS") or "").strip()
2319
  if raw_fixed:
2320
  try:
@@ -2323,7 +1623,6 @@ def _dynamic_gpu_duration(
2323
  return max(min_secs, min(int(raw_fixed), max_secs))
2324
  except (TypeError, ValueError):
2325
  pass
2326
-
2327
  estimated = _estimated_runtime_seconds(mode, scene_count, resolution, seconds_per_scene)
2328
  ratio_default = float(getattr(APP_CONFIG, "GPU_BUFFER_RATIO_DEFAULT"))
2329
  extra_default = float(getattr(APP_CONFIG, "GPU_BUFFER_SECONDS_DEFAULT"))
@@ -2342,6 +1641,34 @@ def _dynamic_gpu_duration(
2342
  max_secs = int(getattr(APP_CONFIG, "GPU_MAX_SECONDS"))
2343
  return max(min_secs, min(requested, max_secs))
2344
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2345
 
2346
  @spaces.GPU(duration=_dynamic_gpu_duration)
2347
  def run_generation_real(
@@ -2356,7 +1683,15 @@ def run_generation_real(
2356
  aspect_ratio: str = "3:4",
2357
  client_ip: Optional[str] = None,
2358
  client_country: Optional[str] = None,
 
2359
  ):
 
 
 
 
 
 
 
2360
  started_at = time.perf_counter()
2361
  warming_up_message = "warming up, please retry in 15-30 seconds"
2362
 
@@ -2370,10 +1705,7 @@ def run_generation_real(
2370
  prompt = (prompt or "").strip()
2371
  if not prompt:
2372
  return _status_text("prompt is empty"), None, None
2373
-
2374
- _record_video_generation(client_ip or "", client_country or "")
2375
- _log_usage_snapshot("点击生成", client_ip or "", client_country or "")
2376
-
2377
  models_ok, prep_status = ensure_models_ready_for_generation()
2378
  if not models_ok:
2379
  return (
@@ -2463,35 +1795,8 @@ def run_generation_real(
2463
  return _status_text(f"{mode} finished but output missing: {final_path}"), None, None
2464
 
2465
  status_lines: List[str] = []
2466
- if _nsfw_enabled() and _nsfw_policy_applies(mode):
2467
- try:
2468
- is_nsfw, _nsfw_detail = _nsfw_check_video(final_path)
2469
- if is_nsfw:
2470
- nsfw_total = _record_nsfw_hit(client_ip or "", client_country or "")
2471
- _log_usage_snapshot("NSFW命中", client_ip or "", client_country or "")
2472
- block_after = int(getattr(APP_CONFIG, "NSFW_BLOCK_AFTER_HITS"))
2473
- warn_after = int(getattr(APP_CONFIG, "NSFW_WARN_AFTER_HITS"))
2474
- if int(nsfw_total) >= max(1, warn_after):
2475
- status_lines.append("NSFW warning: policy filter triggered.")
2476
-
2477
- local_should_block = int(nsfw_total) >= max(1, block_after)
2478
- remote_total = _nsfw_remote_register_nsfw(client_ip or "")
2479
- remote_limit = int(getattr(APP_CONFIG, "NSFW_REMOTE_BLOCK_AFTER_TOTAL"))
2480
- remote_should_block = (remote_total is not None) and (int(remote_total) > int(remote_limit))
2481
-
2482
- if local_should_block or remote_should_block:
2483
- preview_path = _nsfw_blur_preview_from_video(final_path)
2484
- try:
2485
- Path(final_path).unlink(missing_ok=True)
2486
- except Exception:
2487
- pass
2488
- status_lines.append("NSFW blocked: video has been hidden by policy.")
2489
- return _status_text("\n".join(status_lines)), None, preview_path
2490
- except Exception as exc:
2491
- status_lines.append(f"NSFW check error: {type(exc).__name__}: {exc}")
2492
  return _status_text("\n".join(status_lines)), final_path, None
2493
 
2494
-
2495
  def build_demo() -> gr.Blocks:
2496
  banner_subtitle = "Please give us a ❤️ if you find it helpful. No usage limits beyond your ZeroGPU daily quota."
2497
  manual_prompt_min_chars = 10
@@ -2510,7 +1815,7 @@ def build_demo() -> gr.Blocks:
2510
  f"""
2511
  <div style="text-align: center; margin: 20px auto 10px auto; max-width: 800px;">
2512
  <h1 style="color: #2c3e50; margin: 0; font-size: 3.5em; font-weight: 800; letter-spacing: 3px; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);">
2513
- 🎬 {getattr(APP_CONFIG, "APP_TITLE")}
2514
  </h1>
2515
  </div>
2516
  <style>
@@ -2616,6 +1921,7 @@ def build_demo() -> gr.Blocks:
2616
  seconds_per_scene,
2617
  resolution,
2618
  aspect_ratio,
 
2619
  base_prompt,
2620
  s1,
2621
  s2,
@@ -2639,6 +1945,7 @@ def build_demo() -> gr.Blocks:
2639
  str(aspect_ratio),
2640
  ip,
2641
  cc,
 
2642
  )
2643
  show_like_tip = _should_show_like_tip(ip)
2644
  return (
@@ -2653,6 +1960,7 @@ def build_demo() -> gr.Blocks:
2653
  scene_count,
2654
  seconds_per_scene,
2655
  resolution,
 
2656
  image_file,
2657
  base_prompt,
2658
  s1,
@@ -2677,6 +1985,7 @@ def build_demo() -> gr.Blocks:
2677
  "3:4",
2678
  ip,
2679
  cc,
 
2680
  )
2681
  show_like_tip = _should_show_like_tip(ip)
2682
  return (
@@ -2692,6 +2001,7 @@ def build_demo() -> gr.Blocks:
2692
  seconds_per_scene,
2693
  resolution,
2694
  aspect_ratio,
 
2695
  base_prompt,
2696
  s1,
2697
  s2,
@@ -2707,6 +2017,7 @@ def build_demo() -> gr.Blocks:
2707
  seconds_per_scene,
2708
  resolution,
2709
  aspect_ratio,
 
2710
  base_prompt,
2711
  s1,
2712
  s2,
@@ -2719,6 +2030,7 @@ def build_demo() -> gr.Blocks:
2719
  scene_count,
2720
  seconds_per_scene,
2721
  resolution,
 
2722
  image_file,
2723
  base_prompt,
2724
  s1,
@@ -2734,6 +2046,7 @@ def build_demo() -> gr.Blocks:
2734
  scene_count,
2735
  seconds_per_scene,
2736
  resolution,
 
2737
  image_file,
2738
  base_prompt,
2739
  s1,
@@ -2743,7 +2056,7 @@ def build_demo() -> gr.Blocks:
2743
  request,
2744
  )
2745
 
2746
- def _submit_v2v(prompt, video_file, resolution, seconds_per_scene, aspect_ratio, request: gr.Request):
2747
  ip = _request_ip(request)
2748
  cc = _request_country(request, ip=ip)
2749
  status, video, preview = run_generation_real(
@@ -2758,6 +2071,7 @@ def build_demo() -> gr.Blocks:
2758
  str(aspect_ratio),
2759
  ip,
2760
  cc,
 
2761
  )
2762
  show_like_tip = _should_show_like_tip(ip)
2763
  return (
@@ -2780,6 +2094,7 @@ def build_demo() -> gr.Blocks:
2780
  scene_count_i2v = gr.Radio([1, 2, 3, 4], value=default_scene, label="Scene Count")
2781
  seconds_i2v = gr.Radio([3, 5], value=_seconds_per_scene_default(), label="Seconds per Scene")
2782
  resolution_i2v = gr.Radio([384, 512], value=default_resolution, label="Resolution")
 
2783
  resolution_note_i2v = gr.Markdown(_resolution_note_text(default_resolution))
2784
 
2785
  image_i2v = gr.File(label="Image file", file_types=["image"])
@@ -2836,12 +2151,12 @@ def build_demo() -> gr.Blocks:
2836
  )
2837
  run_btn_i2v_manual.click(
2838
  _submit_i2v_manual,
2839
- inputs=[scene_count_i2v, seconds_i2v, resolution_i2v, image_i2v, base_prompt_i2v, s1_i2v_manual, s2_i2v_manual, s3_i2v_manual, s4_i2v_manual],
2840
  outputs=[status_i2v, out_video_i2v, nsfw_card_i2v, nsfw_preview_i2v, download_card_i2v],
2841
  )
2842
  run_btn_i2v_auto.click(
2843
  _submit_i2v,
2844
- inputs=[scene_count_i2v, seconds_i2v, resolution_i2v, image_i2v, base_prompt_i2v, s1_i2v_auto, s2_i2v_auto, s3_i2v_auto, s4_i2v_auto],
2845
  outputs=[status_i2v, out_video_i2v, nsfw_card_i2v, nsfw_preview_i2v, download_card_i2v],
2846
  )
2847
 
@@ -2851,6 +2166,7 @@ def build_demo() -> gr.Blocks:
2851
  scene_count_t2v = gr.Radio([1, 2, 3, 4], value=default_scene, label="Scene Count")
2852
  seconds_t2v = gr.Radio([3, 5], value=_seconds_per_scene_default(), label="Seconds per Scene")
2853
  resolution_t2v = gr.Radio([384, 512], value=default_resolution, label="Resolution")
 
2854
  resolution_note_t2v = gr.Markdown(_resolution_note_text(default_resolution))
2855
  aspect_ratio_t2v = gr.Radio(["16:9", "4:3", "1:1", "3:4", "9:16"], value=default_ratio, label="Aspect Ratio")
2856
  dims_hint_t2v = gr.Markdown(f"T2V dims: **{d_w}x{d_h}**")
@@ -2909,12 +2225,12 @@ def build_demo() -> gr.Blocks:
2909
  )
2910
  run_btn_t2v_manual.click(
2911
  _submit_t2v_manual,
2912
- inputs=[scene_count_t2v, seconds_t2v, resolution_t2v, aspect_ratio_t2v, base_prompt_t2v, s1_t2v_manual, s2_t2v_manual, s3_t2v_manual, s4_t2v_manual],
2913
  outputs=[status_t2v, out_video_t2v, nsfw_card_t2v, nsfw_preview_t2v, download_card_t2v],
2914
  )
2915
  run_btn_t2v_auto.click(
2916
  _submit_t2v,
2917
- inputs=[scene_count_t2v, seconds_t2v, resolution_t2v, aspect_ratio_t2v, base_prompt_t2v, s1_t2v_auto, s2_t2v_auto, s3_t2v_auto, s4_t2v_auto],
2918
  outputs=[status_t2v, out_video_t2v, nsfw_card_t2v, nsfw_preview_t2v, download_card_t2v],
2919
  )
2920
 
@@ -2924,6 +2240,7 @@ def build_demo() -> gr.Blocks:
2924
  seconds_v2v = gr.Radio([3, 5], value=_seconds_per_scene_default(), label="Additional Seconds")
2925
  resolution_v2v = gr.Radio([384, 512], value=default_resolution, label="Resolution")
2926
  aspect_ratio_v2v = gr.Radio(["16:9", "4:3", "1:1", "3:4", "9:16"], value=default_ratio, label="Aspect Ratio")
 
2927
  prompt_v2v = gr.Textbox(label="Prompt", value="Enhance this clip with cinematic lighting", lines=3)
2928
  video_v2v = gr.File(label="Video file", file_types=["video"])
2929
  run_btn_v2v = gr.Button("Submit Generate V2V", variant="primary")
@@ -2937,7 +2254,7 @@ def build_demo() -> gr.Blocks:
2937
 
2938
  run_btn_v2v.click(
2939
  _submit_v2v,
2940
- inputs=[prompt_v2v, video_v2v, resolution_v2v, seconds_v2v, aspect_ratio_v2v],
2941
  outputs=[status_v2v, out_video_v2v, nsfw_card_v2v, nsfw_preview_v2v, download_card_v2v],
2942
  )
2943
  gr.HTML(
@@ -2994,4 +2311,4 @@ def build_demo() -> gr.Blocks:
2994
  </div>
2995
  """
2996
  )
2997
- return demo
 
 
 
1
  import os
2
  import json
3
  import re
 
17
 
18
  import gradio as gr
19
  import spaces
20
+ import torch
21
+ from transformers import AutoModelForCausalLM, AutoTokenizer
22
 
23
  try:
24
  import redis
25
+ except Exception:
26
  redis = None
27
 
 
 
28
  redis = None
29
 
30
  os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
 
51
  WORKFLOW_PACK_MARKER = b"OMNIWF1"
52
  WORKFLOW_PACK_SUFFIX = ".pack"
53
 
 
 
 
54
  DEFAULT_OMNI_VIDEOS = (
55
  "wan_2.1_vae.safetensors@Comfy-Org/Wan_2.1_ComfyUI_repackaged@split_files/vae/wan_2.1_vae.safetensors"
56
+ "#nsfw_wan_umt5-xxl_fp8_scaled.safetensors@geceff/Wan2.2-Custom-Models-GGUF@text_encoders/nsfw_wan_umt5-xxl_fp8_scaled.safetensors"
57
  "#wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8High.safetensors@jorgmikel76/wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8High@wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8High.safetensors"
58
  "#wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8Low.safetensors@jorgmikel76/wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8High@wan22EnhancedNSFWCameraPrompt_nsfwFASTMOVEFP8Low.safetensors"
59
  )
 
67
  _RUNTIME_PREP_STATUS = "runtime preparation not started"
68
  _PREP_IO_LOCK = threading.Lock()
69
 
70
+ _QWEN_MODEL = None
71
+ _QWEN_TOKENIZER = None
72
+ _QWEN_LOCK = threading.Lock()
73
+
74
+ def _load_qwen_model():
75
+ global _QWEN_MODEL, _QWEN_TOKENIZER
76
+ with _QWEN_LOCK:
77
+ if _QWEN_MODEL is None:
78
+ model_id = "Qwen/Qwen3-0.6B"
79
+ _QWEN_TOKENIZER = AutoTokenizer.from_pretrained(model_id)
80
+ _QWEN_MODEL = AutoModelForCausalLM.from_pretrained(
81
+ model_id,
82
+ dtype=torch.float32,
83
+ device_map="cpu"
84
+ )
85
+ return _QWEN_MODEL, _QWEN_TOKENIZER
86
 
87
  def _split_parts(value: Optional[str] = None) -> Tuple[str, str, str, str]:
88
  raw = (value if value is not None else os.getenv("OMNI_VIDEOS") or DEFAULT_OMNI_VIDEOS).strip()
 
91
  raise ValueError("OMNI_VIDEOS must have 4 non-empty parts separated by '#'.")
92
  return parts[0], parts[1], parts[2], parts[3]
93
 
 
94
  def _parse_entry(entry: str) -> Tuple[str, Optional[str], Optional[str]]:
95
  parts = (entry or "").strip().split("@", 2)
96
  filename = parts[0].strip() if parts and parts[0].strip() else ""
 
98
  repo_relpath = parts[2].strip() if len(parts) >= 3 and parts[2].strip() else None
99
  return filename, repo_id, repo_relpath
100
 
 
101
  def parse_model_names(value: Optional[str] = None) -> Dict[str, str]:
102
  a, b, c, d = _split_parts(value)
103
  vae, _, _ = _parse_entry(a)
 
113
  "unet_q6kl": unet_q6kl,
114
  }
115
 
 
116
  def parse_model_entries(value: Optional[str] = None) -> Dict[str, Tuple[str, Optional[str], Optional[str]]]:
117
  a, b, c, d = _split_parts(value)
118
  return {
 
122
  "unet_q6kl": _parse_entry(d),
123
  }
124
 
 
125
  def _split_one_key(
126
  value: Optional[str] = None,
127
  ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], Optional[str]]:
 
132
  while len(parts) < 5:
133
  parts.append("")
134
  return (
135
+ parts[0] or None,
136
+ parts[1] or None,
137
+ parts[2] or None,
138
+ parts[3] or None,
139
+ parts[4] or None,
140
  )
141
 
 
142
  def _get_hf_token() -> Optional[str]:
143
  hf_from_one_key, _, _, _, _ = _split_one_key()
144
  if hf_from_one_key:
 
149
  return value
150
  return None
151
 
 
152
  def _get_worker_api_token() -> Optional[str]:
153
  _, _, _, worker_from_one_key, _ = _split_one_key()
154
  if worker_from_one_key:
 
159
  return value
160
  return None
161
 
 
162
  def _get_legacy_api_base() -> Optional[str]:
163
  _, _, legacy_api_base, _, _ = _split_one_key()
164
  return legacy_api_base
165
 
 
166
  def _get_github_token() -> Optional[str]:
167
  _, _, _, _, github_from_one_key = _split_one_key()
168
  if github_from_one_key:
 
173
  return value
174
  return None
175
 
 
176
  def _model_root_dir() -> Path:
177
  src_dir = Path(__file__).resolve().parent
178
  repo_dir = src_dir.parent
179
  return repo_dir / "ComfyUIVideo" / "models"
180
 
 
181
  def _repo_dir() -> Path:
182
  src_dir = Path(__file__).resolve().parent
183
  return src_dir.parent
184
 
 
185
  def _comfy_dir() -> Path:
186
  return _repo_dir() / "ComfyUIVideo"
187
 
 
188
  def _load_app_config():
189
  path = _repo_dir() / "src" / "config.pyc"
190
  if not path.exists():
 
193
  raw = path.read_bytes()
194
  marker = b"OMNICFG1"
195
 
 
196
  if raw.startswith(marker):
197
  payload = raw[len(marker) :]
198
  try:
 
205
  print(f"[config] loaded packed private config: {path}")
206
  return types.SimpleNamespace(**data)
207
 
 
208
  loader = importlib.machinery.SourcelessFileLoader("omni_video_factory_private_config", str(path))
209
  spec = importlib.util.spec_from_loader("omni_video_factory_private_config", loader)
210
  module = importlib.util.module_from_spec(spec) if spec else None
 
229
  print(f"[config] loaded sourceless private config: {path}")
230
  return types.SimpleNamespace(**data)
231
 
 
232
  APP_CONFIG = _load_app_config()
233
 
 
234
  def _runtime_git_repo() -> str:
235
  repo = (os.getenv("OMNI_RUNTIME_GIT_REPO") or "selfitcamera/ComfyUIVideo").strip()
236
  return repo.removesuffix(".git")
237
 
 
238
  def _runtime_git_revision() -> str:
 
239
  return (os.getenv("OMNI_RUNTIME_GIT_REF") or "17b9fb3").strip()
240
 
 
241
  def _is_commit_hash(ref: str) -> bool:
242
  ref = (ref or "").strip()
243
  if len(ref) < 7 or len(ref) > 40:
244
  return False
245
  return all(ch in "0123456789abcdefABCDEF" for ch in ref)
246
 
 
247
  def _runtime_git_user() -> str:
248
  return (os.getenv("OMNI_RUNTIME_GIT_USER") or "selfitcamera").strip()
249
 
 
250
  def _workflow_repo_id() -> str:
251
  env_space_id = (os.getenv("SPACE_ID") or "").strip()
252
  if env_space_id:
253
  return env_space_id
254
  return (os.getenv("OMNI_WORKFLOW_REPO") or "FrameAI4687/AI-Video-0213-02").strip()
255
 
 
256
  def _runtime_git_clone_url() -> str:
257
  github_token = _get_github_token()
258
  repo = _runtime_git_repo()
 
260
  return f"https://{_runtime_git_user()}:{github_token}@github.com/{repo}.git"
261
  return f"https://github.com/{repo}.git"
262
 
 
263
  def _redact_sensitive(text: str) -> str:
264
  out = text
265
  for secret in (_get_hf_token(), _get_worker_api_token(), _get_github_token()):
 
267
  out = out.replace(secret, "***")
268
  return out
269
 
 
270
  def _llm_api_base() -> str:
271
  return (os.getenv("OMNI_LLM_API_BASE") or "https://omnifilm.net").strip().rstrip("/")
272
 
 
273
  def _llm_api_base_candidates() -> List[str]:
274
  candidates: List[str] = []
275
  env_base = (os.getenv("OMNI_LLM_API_BASE") or "").strip().rstrip("/")
 
278
  candidates.append("https://omnifilm.net")
279
  legacy_base = (_get_legacy_api_base() or "").strip().rstrip("/")
280
  if legacy_base.startswith("http://") or legacy_base.startswith("https://"):
 
281
  parts = legacy_base.split("/", 3)
282
  if len(parts) >= 3:
283
  candidates.append(parts[0] + "//" + parts[2])
 
289
  seen.add(base)
290
  return ordered
291
 
 
292
  def _http_json(
293
  method: str,
294
  url: str,
 
333
  return _decode_json(raw)
334
  except urllib.error.HTTPError as exc:
335
  detail = exc.read().decode("utf-8", errors="replace")
 
336
  if int(getattr(exc, "code", 0) or 0) == 403 and "1010" in detail:
337
  try:
338
  return _http_json_via_curl()
 
342
  except Exception as exc:
343
  raise RuntimeError(f"{type(exc).__name__}: {exc}") from exc
344
 
 
345
  def _extract_result_text(obj) -> str:
346
  def _strip_answer_wrapper(text: str) -> str:
347
  raw = (text or "").strip()
 
366
  with_sep = [t for t in texts if "¥" in t]
367
  return max(with_sep or texts, key=len)
368
  if isinstance(obj, dict):
 
369
  if "choices" in obj and isinstance(obj["choices"], list):
370
  for choice in obj["choices"]:
371
  if isinstance(choice, dict):
 
386
  return max(with_sep or texts, key=len)
387
  return ""
388
 
 
389
  _NSFW_PIPELINE = None
390
  _NSFW_STATE_LOCK = threading.Lock()
391
  _USAGE_STATE_LOCK = threading.Lock()
 
395
  _REDIS_CLIENT = None
396
  _REDIS_MEMORY_LAST_CHECK_TS = 0.0
397
 
 
398
  def _redis_url() -> str:
399
  for key in ("REDIS_KEY", "REDIS_URL", "OMNI_REDIS_URL"):
400
  value = (os.getenv(key) or "").strip()
 
402
  return value
403
  return ""
404
 
 
405
  def _redis_enabled() -> bool:
406
  return bool(_redis_url())
407
 
 
408
  def _runtime_boot_marker() -> str:
409
  host = (os.getenv("HOSTNAME") or "unknown-host").strip() or "unknown-host"
410
  proc_boot = ""
 
419
  return f"{host}:{proc_boot}"
420
  return host
421
 
 
422
  def _redis_scan_delete(client, pattern: str) -> int:
423
  total = 0
424
  cursor = 0
 
430
  break
431
  return total
432
 
 
433
  def _redis_prepare_usage_on_boot(client) -> None:
434
  marker_key = "ovf:meta:boot_marker"
435
  marker = _runtime_boot_marker()
 
437
  old = str(client.get(marker_key) or "")
438
  except Exception:
439
  old = ""
 
440
  if old == marker:
441
  return
 
442
  cleared_usage = 0
443
  try:
444
  cleared_usage += _redis_scan_delete(client, "ovf:usage:*")
445
  cleared_usage += _redis_scan_delete(client, "ovf:usage_window:*")
446
  except Exception as exc:
447
+ pass
 
448
  try:
449
  client.set(marker_key, marker)
450
  except Exception:
451
  pass
452
 
 
 
 
453
  def _redis_maybe_flush_all(client) -> None:
454
  global _REDIS_MEMORY_LAST_CHECK_TS
455
  now = time.time()
456
  interval_default = int(getattr(APP_CONFIG, "REDIS_MEMORY_CHECK_INTERVAL_SECONDS", 120))
457
  interval = max(15, int((os.getenv("REDIS_MEMORY_CHECK_INTERVAL_SECONDS") or str(interval_default)).strip() or str(interval_default)))
 
458
  with _REDIS_STATE_LOCK:
459
  if (now - float(_REDIS_MEMORY_LAST_CHECK_TS or 0.0)) < interval:
460
  return
461
  _REDIS_MEMORY_LAST_CHECK_TS = now
 
462
  try:
463
  info = client.info(section="memory")
464
  used = int(info.get("used_memory") or 0)
 
467
  max_memory = int((os.getenv("REDIS_MEMORY_LIMIT_BYTES") or str(30 * 1024 * 1024)).strip() or str(30 * 1024 * 1024))
468
  if max_memory <= 0:
469
  return
 
470
  ratio = float(used) / float(max_memory)
471
  threshold_default = float(getattr(APP_CONFIG, "REDIS_FLUSH_ALL_RATIO", 0.95))
472
  threshold = float((os.getenv("REDIS_FLUSH_ALL_RATIO") or str(threshold_default)).strip() or str(threshold_default))
473
  threshold = min(0.999, max(0.6, threshold))
 
474
  if ratio >= threshold:
475
  client.flushdb()
 
476
  except Exception as exc:
477
+ pass
 
478
 
479
  def _redis_client():
480
  global _REDIS_CLIENT
481
  if _REDIS_CLIENT is not None:
482
  return _REDIS_CLIENT
 
483
  if redis is None:
484
  return None
485
  url = _redis_url()
486
  if not url:
487
  return None
 
488
  with _REDIS_STATE_LOCK:
489
  if _REDIS_CLIENT is not None:
490
  return _REDIS_CLIENT
 
504
  client.ping()
505
  _redis_prepare_usage_on_boot(client)
506
  _REDIS_CLIENT = client
 
507
  except Exception as exc:
 
508
  _REDIS_CLIENT = None
509
  return _REDIS_CLIENT
510
 
 
511
  def _nsfw_enabled() -> bool:
 
512
  return False
513
 
 
514
  def _nsfw_policy_applies(mode: str) -> bool:
 
515
  return False
516
 
 
517
  def _normalize_ip(ip: str) -> str:
518
  ip = (ip or "").strip()
519
  if not ip:
520
  return ""
521
  if "," in ip:
522
  ip = ip.split(",", 1)[0].strip()
 
523
  if "." in ip and ip.count(":") == 1:
524
  host, _, maybe_port = ip.rpartition(":")
525
  if host and maybe_port.isdigit():
 
528
  ip = ip[1:-1].strip()
529
  return ip if 0 < len(ip) <= 128 else ""
530
 
 
531
  def _request_ip(request: Any = None) -> str:
532
  if request is None:
533
  return ""
 
551
  pass
552
  return ""
553
 
 
554
  def _usage_entry_key(ip: str) -> str:
555
  key = _normalize_ip(ip)
556
  return key if key else "__unknown__"
557
 
 
558
  def _usage_key(ip: str) -> str:
559
  return f"ovf:usage:{_usage_entry_key(ip)}"
560
 
 
561
  def _usage_country_stats_key(metric: str = "video") -> str:
562
  m = str(metric or "video").strip().lower()
563
  if m == "auto":
 
566
  return "ovf:usage:global:country_nsfw_total"
567
  return "ovf:usage:global:country_video_total"
568
 
 
569
  def _usage_global_video_total_key() -> str:
570
  return "ovf:usage:global:video_total"
571
 
 
572
  def _usage_country_ip_set_key(country_code: str) -> str:
573
  return f"ovf:usage:global:country_ips:{country_code}"
574
 
 
575
  def _stats_country(country_code: str) -> str:
576
  cc = (country_code or "").strip().upper()
577
  if len(cc) == 2 and cc.isalpha():
578
  return cc
579
  return "UNKNOWN"
580
 
 
581
  def _usage_window_key(ip: str, window_seconds: int, now: float) -> str:
582
  bucket = int(float(now) // max(1, int(window_seconds)))
583
  return f"ovf:usage_window:auto:{_usage_entry_key(ip)}:{bucket}"
584
 
 
585
  def _geo_key(ip: str) -> str:
586
  return f"ovf:geo:{_usage_entry_key(ip)}"
587
 
 
588
  def _usage_ttl_seconds() -> int:
589
  return max(300, int((os.getenv("REDIS_USAGE_TTL_SECONDS") or str(7 * 24 * 3600)).strip() or str(7 * 24 * 3600)))
590
 
 
591
  def _geo_ttl_seconds() -> int:
592
  return max(3600, int((os.getenv("REDIS_GEO_TTL_SECONDS") or str(180 * 24 * 3600)).strip() or str(180 * 24 * 3600)))
593
 
 
594
  def _set_local_country_cache(ip: str, country: str, now_ts: Optional[float] = None) -> None:
595
  now_ts = float(now_ts if now_ts is not None else time.time())
596
  with _USAGE_STATE_LOCK:
597
  _COUNTRY_CACHE[ip] = country
598
  _COUNTRY_CACHE_TS[ip] = now_ts
599
 
 
600
  def _request_country(request: Any = None, ip: str = "") -> str:
 
601
  return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
 
603
  def _snapshot_usage_counts(ip: str) -> Tuple[int, int, int]:
604
  client = _redis_client()
 
612
  int(vals[2] or 0),
613
  )
614
  except Exception as exc:
 
615
  return (0, 0, 0)
616
 
 
617
  def _usage_country(ip: str, fallback_country_code: str = "") -> str:
618
  cc = (fallback_country_code or "").strip().upper()
619
  if cc:
620
  return cc
 
621
  client = _redis_client()
622
  if client is not None:
623
  try:
 
626
  return cc
627
  except Exception:
628
  pass
 
629
  return ""
630
 
 
631
  def _log_usage_snapshot(event: str, ip: str, country_code: str = "") -> None:
 
632
  return
 
 
 
 
 
 
 
 
633
 
634
  def _log_global_country_video_stats_if_needed(client, global_total: int) -> None:
635
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
 
637
  def _allow_auto_prompt_and_record(ip: str, country_code: str = "") -> Tuple[bool, str]:
638
+ return True, ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
  def _record_video_generation(ip: str, country_code: str = "") -> None:
641
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
 
643
  def _record_nsfw_hit(ip: str, country_code: str = "") -> int:
644
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
645
 
646
  def _nsfw_counter_endpoint() -> Optional[str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
  return None
648
 
 
649
  def _nsfw_counter_key(ip: str) -> str:
650
+ return "__unknown__"
 
 
651
 
652
  def _nsfw_remote_inc_total(ip: str) -> Optional[int]:
653
+ return None
 
 
 
 
 
 
 
 
 
654
 
655
  def _nsfw_remote_register_nsfw(ip: str) -> Optional[int]:
 
 
 
 
 
 
 
 
 
 
656
  return None
657
 
 
658
  def _nsfw_keyword_match(label: str) -> bool:
659
+ return False
 
 
 
660
 
661
  def _get_nsfw_pipeline():
662
+ return None
 
 
 
 
 
 
 
 
663
 
664
  def _nsfw_predict_label_from_pil(pil_image) -> str:
665
+ return "unknown"
 
 
 
 
 
 
666
 
667
  def _video_frames_for_nsfw(video_path: str):
668
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
 
670
  def _nsfw_check_video(video_path: str) -> Tuple[bool, str]:
671
+ return False, ""
 
 
 
 
 
 
 
 
 
 
 
 
672
 
673
  def _nsfw_blur_preview_from_video(video_path: str) -> Optional[str]:
674
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
675
 
676
  def _nsfw_warning_card_html() -> str:
677
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
 
679
  def _nsfw_blocked_card_html() -> str:
680
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
 
682
  def _nsfw_card_update_for_status(status_text: str):
 
 
 
 
 
683
  return gr.update(value="", visible=False)
684
 
 
685
  def _nsfw_preview_update(path: Optional[str]):
686
  p = str(path or "").strip()
687
  if p and Path(p).exists():
688
  return gr.update(value=p, visible=True)
689
  return gr.update(value=None, visible=False)
690
 
 
691
  def _public_generation_status(status_text: str) -> str:
692
  lines = [str(raw or "").strip() for raw in str(status_text or "").splitlines() if str(raw or "").strip()]
693
  if not lines:
694
  return "ZeroGPU elapsed: --"
 
695
  elapsed_line = "ZeroGPU elapsed: --"
696
  detail_lines: List[str] = []
697
  for line in lines:
 
699
  elapsed_line = line
700
  else:
701
  detail_lines.append(line)
 
702
  if not detail_lines:
703
  return elapsed_line
 
704
  detail = detail_lines[0]
705
  if len(detail) > 320:
706
  detail = detail[:317] + "..."
707
  return f"{detail}\n{elapsed_line}"
708
 
 
709
  def _should_show_like_tip(ip: str) -> bool:
710
+ return False
 
 
 
 
 
711
 
712
  def _download_upsell_card_html(show_like_tip: bool = False) -> str:
713
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
 
715
  def _download_upsell_update(video_path: Optional[str], status_text: str = "", show_like_tip: bool = False):
 
 
 
 
 
 
 
716
  return gr.update(value="", visible=False)
717
 
 
718
  def _normalize_scene_parts(content: str, scene_count: int, fallback_prompt: str) -> List[str]:
719
  parts = [p.strip() for p in (content or "").split("¥") if p.strip()]
720
  if not parts:
 
724
  normalized.append(normalized[-1])
725
  return normalized
726
 
 
727
  def _llm_generate_scene_prompt_text(
728
  mode: str,
729
  prompt_text: str,
730
  scene_count: int,
731
  seconds_per_scene: int,
732
  ) -> str:
733
+ if scene_count <= 1:
734
+ text, _note = _ensure_scene_prompt_text(prompt_text or "", scene_count)
735
+ return text
736
+
737
+ model, tokenizer = _load_qwen_model()
738
+ system_prompt = (
739
+ "You are a cinematic video prompt generator. "
740
+ f"You MUST output EXACTLY {scene_count} scenes. "
741
+ "Separate each scene using the '¥' character. "
742
+ "Example format: Scene 1 description ¥ Scene 2 description"
743
+ )
744
+ user_prompt = f"Expand this prompt into {scene_count} continuous cinematic scenes: {prompt_text}"
745
+ messages = [
746
+ {"role": "system", "content": system_prompt},
747
+ {"role": "user", "content": user_prompt}
748
+ ]
749
+ text_input = tokenizer.apply_chat_template(
750
+ messages,
751
+ tokenize=False,
752
+ add_generation_prompt=True
753
+ )
754
+ model_inputs = tokenizer([text_input], return_tensors="pt")
755
+
756
+ # Increase max_new_tokens to allow the model to finish its reasoning process
757
+ generated_ids = model.generate(
758
+ **model_inputs,
759
+ max_new_tokens=1536
760
+ )
761
+ generated_ids = [
762
+ output_ids[len(input_ids):] for input_ids, output_ids in zip(model_inputs.input_ids, generated_ids)
763
+ ]
764
+ response = tokenizer.batch_decode(generated_ids, skip_special_tokens=True)[0]
765
+
766
+ # Strictly remove the <think> section
767
+ if "</think>" in response:
768
+ response = response.split("</think>")[-1].strip()
769
+ else:
770
+ import re
771
+ response = re.sub(r'<think>.*', '', response, flags=re.DOTALL).strip()
772
+
773
+ # Fallback to the original prompt if the response is empty
774
+ if not response.strip():
775
+ response = " ¥ ".join([prompt_text] * scene_count)
776
+
777
+ import re
778
+ if '¥' not in response and scene_count > 1:
779
+ parts = re.split(r'(?i)scene\s*\d+:', response)
780
+ parts = [p.strip() for p in parts if p.strip()]
781
+ if len(parts) >= scene_count:
782
+ response = ' ¥ '.join(parts[:scene_count])
783
+ else:
784
+ sentences = [s.strip() for s in response.split('.') if s.strip()]
785
+ if len(sentences) >= scene_count:
786
+ chunk_size = max(1, len(sentences) // scene_count)
787
+ chunks = [' '.join(sentences[i:i+chunk_size]) for i in range(0, len(sentences), chunk_size)]
788
+ response = ' ¥ '.join(chunks[:scene_count])
789
+
790
+ return response
 
 
 
 
 
 
 
 
791
 
792
  def _auto_prompt_for_mode(mode: str, prompt_text: str, scene_count: int, seconds_per_scene: int) -> Tuple[str, str]:
793
  prompt_text = (prompt_text or "").strip()
 
795
  scenes = _normalize_scene_parts(llm_text, scene_count, prompt_text)
796
  return "¥".join(scenes), f"auto-prompt ok: {scene_count} scenes"
797
 
 
798
  def _ensure_scene_prompt_text(prompt_text: str, scene_count: int) -> Tuple[str, str]:
799
  prompt_text = (prompt_text or "").strip()
800
  if scene_count <= 1:
 
802
  scenes = _normalize_scene_parts(prompt_text, scene_count, prompt_text)
803
  return "¥".join(scenes), "scene prompt: expanded from base prompt"
804
 
 
805
  def _normalize_scene_inputs(scene_count: int, base_prompt: str, scenes: List[str]) -> List[str]:
806
  scene_count = max(1, min(int(scene_count), 4))
807
  base_prompt = (base_prompt or "").strip()
 
816
  normalized[i] = normalized[i - 1] if i > 0 and normalized[i - 1] else base_prompt
817
  return normalized[:scene_count]
818
 
 
819
  def _scene_values_for_ui(scene_count: int, scene_array: List[str]) -> Tuple[str, str, str, str]:
820
  vals = list(scene_array or [])
821
  while len(vals) < 4:
 
826
  vals[count] = vals[count] if count < len(vals) else ""
827
  return vals[0], vals[1], vals[2], vals[3]
828
 
 
829
  def _ui_generate_scenes(mode: str, prompt_text: str, scene_count: int, seconds_per_scene: int, request: Any = None):
830
  count = max(1, min(int(scene_count or 1), 4))
 
 
 
 
 
 
 
 
 
 
831
  try:
832
  joined, note = _auto_prompt_for_mode(mode, prompt_text, count, int(seconds_per_scene or 3))
833
  scenes = _normalize_scene_parts(joined, count, prompt_text)
 
838
  s1, s2, s3, s4 = _scene_values_for_ui(count, fallback)
839
  return s1, s2, s3, s4, f"auto-prompt failed: {exc}"
840
 
 
841
  def _workflow_pack_path(wf_dir: Path, workflow_name: str) -> Path:
842
  return wf_dir / f"{workflow_name}{WORKFLOW_PACK_SUFFIX}"
843
 
 
844
  def _decode_packed_workflow(raw: bytes) -> str:
845
  if not raw.startswith(WORKFLOW_PACK_MARKER):
846
  raise RuntimeError("invalid workflow pack marker")
 
848
  decoded = zlib.decompress(payload)
849
  return decoded.decode("utf-8")
850
 
 
851
  def _materialize_workflow_from_pack(wf_dir: Path, workflow_name: str) -> Tuple[bool, str]:
852
  json_path = wf_dir / workflow_name
853
  if json_path.exists():
854
  return True, "workflow json already exists"
 
855
  packed_path = _workflow_pack_path(wf_dir, workflow_name)
856
  if not packed_path.exists():
857
  return False, f"packed workflow not found: {packed_path}"
 
858
  try:
859
  text = _decode_packed_workflow(packed_path.read_bytes())
860
  data = json.loads(text)
 
864
  except Exception as exc:
865
  return False, f"failed to materialize {workflow_name}: {type(exc).__name__}: {exc}"
866
 
 
867
  def _materialize_packed_workflows(wf_dir: Path) -> None:
868
  if not wf_dir.exists():
869
  return
 
872
  ok, detail = _materialize_workflow_from_pack(wf_dir, name)
873
  if ok and detail.startswith("materialized"):
874
  notes.append(detail)
 
 
 
875
 
876
  def _download_workflow_files_from_hf(dest_dir: Path) -> str:
877
  try:
878
  from huggingface_hub import hf_hub_download
879
  except Exception as exc:
880
  return f"workflow hf download unavailable: {type(exc).__name__}: {exc}"
 
881
  repo_id = _workflow_repo_id()
882
  token = _get_hf_token()
883
  dest_dir.mkdir(parents=True, exist_ok=True)
 
884
  got = []
885
  misses = []
886
  candidate_filenames = {
 
892
  )
893
  for name in WORKFLOW_FILES
894
  }
 
895
  for name in WORKFLOW_FILES:
896
  resolved = False
897
  for candidate in candidate_filenames[name]:
 
919
  continue
920
  if not resolved:
921
  misses.append(name)
 
922
  if misses:
923
+ return f"workflow hf download partial from space:{repo_id}; downloaded={got or 'none'} missing={misses}"
 
 
 
924
  return f"workflow hf download ok from space:{repo_id}; downloaded={got}"
925
 
 
926
  def _expected_model_paths() -> Dict[str, Path]:
927
  entries = parse_model_entries()
928
  root = _model_root_dir()
 
931
  out[key] = root / MODEL_KEY_TO_SUBDIR[key] / filename
932
  return out
933
 
 
934
  def check_required_models_status() -> str:
935
  try:
936
  entries = parse_model_entries()
937
  except Exception as exc:
938
  return f"OMNI_VIDEOS invalid: {type(exc).__name__}: {exc}"
 
939
  root = _model_root_dir()
940
  lines = [f"model root: {root}"]
941
  for key, path in _expected_model_paths().items():
 
945
  lines.append(f"- {key}: {'ok' if exists else 'missing'} -> {path.name} ({source})")
946
  return "\n".join(lines)
947
 
 
948
  def download_missing_models() -> str:
949
  try:
950
  from huggingface_hub import hf_hub_download
951
  except Exception as exc:
952
  return f"huggingface_hub unavailable: {type(exc).__name__}: {exc}"
 
953
  try:
954
  entries = parse_model_entries()
955
  except Exception as exc:
956
  return f"OMNI_VIDEOS invalid: {type(exc).__name__}: {exc}"
 
957
  token = _get_hf_token()
958
  results = []
959
  for key, path in _expected_model_paths().items():
 
974
  local_dir=str(path.parent),
975
  )
976
  )
 
 
977
  if downloaded.resolve() != path.resolve():
978
  shutil.copy2(downloaded, path)
979
  saved = path
 
984
  results.append(f"- {key}: download failed ({type(exc).__name__}: {exc})")
985
  return "\n".join(results)
986
 
 
987
  def ensure_models_ready_on_startup() -> str:
988
  try:
989
  entries = parse_model_entries()
990
  except Exception as exc:
991
  return f"skip auto-download: OMNI_VIDEOS invalid ({type(exc).__name__}: {exc})"
 
992
  missing_keys = []
993
  for key, path in _expected_model_paths().items():
994
  if not path.exists():
995
  missing_keys.append(key)
996
  if not missing_keys:
997
  return "all required models already present"
 
998
  lines = [f"missing models at startup: {', '.join(missing_keys)}"]
999
  lines.append(download_missing_models())
1000
  final_missing = [key for key, path in _expected_model_paths().items() if not path.exists()]
 
1004
  lines.append("all required models are ready")
1005
  return "\n".join(lines)
1006
 
 
1007
  def _set_model_prep_state(*, running: Optional[bool] = None, status: Optional[str] = None) -> None:
1008
  global _MODEL_PREP_RUNNING, _MODEL_PREP_STATUS
1009
  with _MODEL_PREP_LOCK:
 
1012
  if status is not None:
1013
  _MODEL_PREP_STATUS = status
1014
 
 
1015
  def _get_model_prep_state() -> Tuple[bool, str]:
1016
  with _MODEL_PREP_LOCK:
1017
  return _MODEL_PREP_RUNNING, _MODEL_PREP_STATUS
1018
 
 
1019
  def _run_model_prep_job() -> None:
1020
  try:
1021
  with _PREP_IO_LOCK:
 
1024
  result = f"startup model preparation failed: {type(exc).__name__}: {exc}"
1025
  _set_model_prep_state(running=False, status=result)
1026
 
 
1027
  def kickoff_model_prepare_background() -> str:
1028
  if _all_models_present():
1029
  _set_model_prep_state(running=False, status="all required models already present")
1030
  return "all required models already present"
 
1031
  running, status = _get_model_prep_state()
1032
  if running:
1033
  return f"background model preparation already running\n{status}"
 
1034
  _set_model_prep_state(running=True, status="background model preparation started")
1035
  worker = threading.Thread(target=_run_model_prep_job, name="model-prep", daemon=True)
1036
  worker.start()
1037
  return "background model preparation started"
1038
 
 
1039
  def ensure_models_ready_for_generation() -> Tuple[bool, str]:
1040
  if _all_models_present():
1041
  return True, "all required models are ready"
 
1042
  running, status = _get_model_prep_state()
1043
  if running:
1044
  return False, "models are preparing in background\n" + status
 
1045
  _set_model_prep_state(running=True, status="on-demand model preparation started")
1046
  try:
1047
  with _PREP_IO_LOCK:
 
1049
  except Exception as exc:
1050
  result = f"on-demand model preparation failed: {type(exc).__name__}: {exc}"
1051
  _set_model_prep_state(running=False, status=result)
 
1052
  if _all_models_present():
1053
  return True, result
1054
  return False, result
1055
 
 
1056
  def _set_runtime_prep_state(*, running: Optional[bool] = None, status: Optional[str] = None) -> None:
1057
  global _RUNTIME_PREP_RUNNING, _RUNTIME_PREP_STATUS
1058
  with _RUNTIME_PREP_LOCK:
 
1061
  if status is not None:
1062
  _RUNTIME_PREP_STATUS = status
1063
 
 
1064
  def _get_runtime_prep_state() -> Tuple[bool, str]:
1065
  with _RUNTIME_PREP_LOCK:
1066
  return _RUNTIME_PREP_RUNNING, _RUNTIME_PREP_STATUS
1067
 
 
1068
  def _has_comfy_runtime() -> bool:
1069
  comfy_dir = _comfy_dir()
1070
  required = (
 
1077
  )
1078
  return all(path.exists() for path in required)
1079
 
 
1080
  def _runtime_missing_paths() -> list[str]:
1081
  comfy_dir = _comfy_dir()
1082
  required = (
 
1089
  )
1090
  return [str(path.relative_to(comfy_dir)) for path in required if not path.exists()]
1091
 
 
1092
  def _run_git(cmd: list[str], cwd: Optional[Path] = None) -> Tuple[bool, str]:
1093
  try:
1094
  result = subprocess.run(
 
1100
  )
1101
  except Exception as exc:
1102
  return False, f"{type(exc).__name__}: {exc}"
 
1103
  merged = "\n".join([result.stdout.strip(), result.stderr.strip()]).strip()
1104
  merged = _redact_sensitive(merged)
1105
  if result.returncode != 0:
1106
  return False, merged or f"git command failed (exit={result.returncode})"
1107
  return True, merged
1108
 
 
1109
  def _runtime_git_head(comfy_dir: Path) -> str:
1110
  ok, detail = _run_git(["git", "-C", str(comfy_dir), "rev-parse", "--short", "HEAD"])
1111
  if not ok:
 
1115
  return "unknown"
1116
  return detail.splitlines()[-1]
1117
 
 
1118
  def _download_runtime_on_demand() -> str:
 
 
1119
  repo = _runtime_git_repo()
1120
  revision = _runtime_git_revision()
1121
  comfy_dir = _comfy_dir()
1122
  models_dir = comfy_dir / "models"
1123
  models_backup = _repo_dir() / ".runtime_models_backup"
1124
  clone_url = _runtime_git_clone_url()
 
 
1125
  if models_backup.exists():
1126
  shutil.rmtree(models_backup, ignore_errors=True)
 
1127
  if comfy_dir.exists() and not (comfy_dir / ".git").exists():
1128
  if models_dir.exists():
1129
  try:
1130
  shutil.move(str(models_dir), str(models_backup))
 
1131
  except Exception as exc:
1132
  return f"runtime clone failed while preserving models: {type(exc).__name__}: {exc}"
1133
  shutil.rmtree(comfy_dir, ignore_errors=True)
 
1134
  if not comfy_dir.exists():
1135
  comfy_dir.parent.mkdir(parents=True, exist_ok=True)
1136
  if _is_commit_hash(revision):
 
1167
  ok, detail = _run_git(["git", "-C", str(comfy_dir), "reset", "--hard", "FETCH_HEAD"])
1168
  if not ok:
1169
  return f"runtime sync failed (reset): {detail}"
 
1170
  if models_backup.exists():
1171
  restored_models_dir = comfy_dir / "models"
1172
  try:
 
1176
  else:
1177
  restored_models_dir.parent.mkdir(parents=True, exist_ok=True)
1178
  shutil.move(str(models_backup), str(restored_models_dir))
 
1179
  except Exception as exc:
1180
  return f"runtime sync finished but restoring models failed: {type(exc).__name__}: {exc}"
 
1181
  if _has_comfy_runtime():
 
 
1182
  return f"runtime ready from github:{repo}@{revision}"
1183
  missing = _runtime_missing_paths()
1184
  return "runtime git sync finished but files are still missing: " + ", ".join(missing)
1185
 
 
1186
  def _run_runtime_prep_job() -> None:
1187
  try:
1188
  with _PREP_IO_LOCK:
 
1191
  result = f"startup runtime preparation failed: {type(exc).__name__}: {exc}"
1192
  _set_runtime_prep_state(running=False, status=result)
1193
 
 
1194
  def kickoff_runtime_prepare_background() -> str:
1195
  if _has_comfy_runtime():
1196
  _set_runtime_prep_state(running=False, status="runtime already present")
1197
  return "runtime already present"
 
1198
  running, status = _get_runtime_prep_state()
1199
  if running:
1200
  return f"background runtime preparation already running\n{status}"
 
1201
  _set_runtime_prep_state(running=True, status="background runtime preparation started")
1202
  worker = threading.Thread(target=_run_runtime_prep_job, name="runtime-prep", daemon=True)
1203
  worker.start()
1204
  return "background runtime preparation started"
1205
 
 
1206
  def ensure_runtime_ready_for_generation() -> Tuple[bool, str]:
1207
  if _has_comfy_runtime():
1208
  return True, "runtime already present"
 
1209
  running, status = _get_runtime_prep_state()
1210
  if running:
1211
  return False, "runtime is preparing in background\n" + status
 
1212
  _set_runtime_prep_state(running=True, status="on-demand runtime preparation started")
1213
  try:
1214
  with _PREP_IO_LOCK:
 
1216
  except Exception as exc:
1217
  result = f"on-demand runtime preparation failed: {type(exc).__name__}: {exc}"
1218
  _set_runtime_prep_state(running=False, status=result)
 
1219
  if _has_comfy_runtime():
1220
  return True, result
1221
  return False, result
1222
 
 
1223
  def resolve_placeholders(obj):
1224
  names = parse_model_names()
1225
  resolved = _deep_resolve_placeholders(obj, names)
1226
  return _apply_model_name_aliases(resolved, names)
1227
 
 
1228
  def _install_comfyapi_src_shims() -> None:
1229
  if "src" not in sys.modules:
1230
  pkg = types.ModuleType("src")
 
1234
  if mod is None:
1235
  mod = types.ModuleType("src.model_names")
1236
  sys.modules["src.model_names"] = mod
1237
+ mod.resolve_placeholders = resolve_placeholders
 
1238
 
1239
  def _drop_conflicting_modules(base_dir: Path) -> None:
1240
  checks = {
 
1257
  if key == root_name or key.startswith(root_name + "."):
1258
  del sys.modules[key]
1259
 
 
1260
  def _get_comfy_client():
1261
  global _COMFY_CLIENT
1262
  if _COMFY_CLIENT is not None:
 
1268
 
1269
  _install_comfyapi_src_shims()
1270
  _drop_conflicting_modules(comfy_dir)
1271
+
1272
+ try:
1273
+ config_path = comfy_dir / "custom_nodes" / "was-node-suite-comfyui" / "was_suite_config.json"
1274
+ if config_path.parent.exists():
1275
+ config_data = {}
1276
+ if config_path.exists():
1277
+ try:
1278
+ config_data = json.loads(config_path.read_text(encoding="utf-8"))
1279
+ except Exception:
1280
+ pass
1281
+ config_data["ffmpeg_bin_path"] = "/usr/bin/ffmpeg"
1282
+ config_path.write_text(json.dumps(config_data, indent=4), encoding="utf-8")
1283
+ except Exception:
1284
+ pass
1285
+
1286
  comfy_dir_str = str(comfy_dir)
1287
  if comfy_dir_str not in sys.path:
1288
  sys.path.insert(0, comfy_dir_str)
 
1310
  )
1311
  return _COMFY_CLIENT
1312
 
 
1313
  def _resolve_model_name(value: str, names: Dict[str, str]) -> str:
1314
  key = PLACEHOLDER_TO_KEY.get((value or "").strip())
1315
  if not key:
1316
  return value
1317
  return names.get(key, value)
1318
 
 
1319
  def _deep_resolve_placeholders(obj, names: Dict[str, str]):
1320
  if isinstance(obj, dict):
1321
  return {k: _deep_resolve_placeholders(v, names) for k, v in obj.items()}
 
1325
  return _resolve_model_name(obj, names)
1326
  return obj
1327
 
 
1328
  def _pick_unet_key(node: Dict, original_name: str) -> str:
1329
  title = ((node.get("_meta") or {}).get("title") or "")
1330
  text = f"{title} {original_name}".lower()
 
1332
  return "unet_q6kl"
1333
  return "unet_q6kh"
1334
 
 
1335
  def _apply_model_name_aliases(obj, names: Dict[str, str]):
1336
  if not isinstance(obj, dict):
1337
  return obj
 
1342
  inputs = node.get("inputs")
1343
  if not isinstance(inputs, dict):
1344
  continue
 
1345
  if "vae_name" in inputs and isinstance(inputs.get("vae_name"), str):
1346
  inputs["vae_name"] = names["vae"]
1347
  if "clip_name" in inputs and isinstance(inputs.get("clip_name"), str):
1348
  inputs["clip_name"] = names["text_encoder"]
 
1349
  if "unet_name" in inputs and isinstance(inputs.get("unet_name"), str):
1350
  if class_type in ("UnetLoaderGGUF", "UnetLoaderGGUFAdvanced", "UNETLoader"):
1351
  key = _pick_unet_key(node, inputs["unet_name"])
1352
  inputs["unet_name"] = names[key]
1353
  return obj
1354
 
 
1355
  def _collect_placeholders(obj, out: set[str]) -> None:
1356
  if isinstance(obj, dict):
1357
  for v in obj.values():
 
1364
  if isinstance(obj, str) and obj in PLACEHOLDER_TO_KEY:
1365
  out.add(obj)
1366
 
 
1367
  def _workflow_dir() -> Path:
1368
  repo_dir = _repo_dir()
1369
  candidates = (
1370
  repo_dir / "src" / "workflows",
1371
  )
 
1372
  for wf_dir in candidates:
1373
  _materialize_packed_workflows(wf_dir)
1374
  if all((wf_dir / name).exists() for name in WORKFLOW_FILES):
 
1375
  return wf_dir
 
1376
  primary = candidates[0]
1377
  primary.mkdir(parents=True, exist_ok=True)
 
 
1378
  found = {}
1379
  search_roots = [repo_dir / "src", repo_dir / "workflows", repo_dir]
1380
  for name in WORKFLOW_FILES:
 
1385
  if matches:
1386
  found[name] = matches[0]
1387
  break
 
1388
  if len(found) == len(WORKFLOW_FILES):
1389
  parents = {}
1390
  for path in found.values():
 
1395
  if not dst_path.exists() and src_path.exists():
1396
  shutil.copy2(src_path, dst_path)
1397
  if all((primary / name).exists() for name in WORKFLOW_FILES):
 
1398
  return primary
1399
  if all((best_parent / name).exists() for name in WORKFLOW_FILES):
 
1400
  return best_parent
1401
+ _download_workflow_files_from_hf(primary)
 
 
 
 
 
 
 
 
 
 
1402
  return primary
1403
 
 
1404
  def validate_workflow_placeholders() -> str:
1405
  try:
1406
  names = parse_model_names()
1407
  except Exception as exc:
1408
  return f"OMNI_VIDEOS invalid: {type(exc).__name__}: {exc}"
 
1409
  wf_dir = _workflow_dir()
1410
  missing = []
1411
  unresolved_msgs = []
 
1412
  for wf_name in WORKFLOW_FILES:
1413
  wf_path = wf_dir / wf_name
1414
  if not wf_path.exists():
 
1419
  except Exception as exc:
1420
  unresolved_msgs.append(f"{wf_name}: invalid json ({type(exc).__name__})")
1421
  continue
 
1422
  resolved = _deep_resolve_placeholders(data, names)
1423
  unresolved = set()
1424
  _collect_placeholders(resolved, unresolved)
 
1426
  unresolved_msgs.append(f"{wf_name}: unresolved placeholders={sorted(unresolved)}")
1427
  else:
1428
  unresolved_msgs.append(f"{wf_name}: ok")
 
1429
  lines = []
1430
  if missing:
1431
  lines.append(f"missing workflow files: {', '.join(missing)}")
1432
  lines.extend(unresolved_msgs)
1433
  return "\n".join(lines) if lines else "no workflow files found"
1434
 
 
1435
  def _env_status_text() -> str:
1436
  lines = []
 
1437
  raw = (os.getenv("OMNI_VIDEOS") or "").strip()
1438
  if not raw:
1439
  lines.append("- OMNI_VIDEOS: missing")
 
1443
  lines.append(f"- OMNI_VIDEOS: ok ({parsed['vae']}, {parsed['text_encoder']})")
1444
  except Exception as exc:
1445
  lines.append(f"- OMNI_VIDEOS: invalid ({type(exc).__name__})")
 
1446
  one_key = (os.getenv("ONE_KEY") or "").strip()
1447
  lines.append("- ONE_KEY: configured" if one_key else "- ONE_KEY: missing")
1448
  lines.append("- WORKER_API_TOKEN: configured" if _get_worker_api_token() else "- WORKER_API_TOKEN: missing")
1449
  lines.append("- GITHUB_TOKEN: configured" if _get_github_token() else "- GITHUB_TOKEN: missing")
1450
  return "\n".join(lines)
1451
 
 
1452
  @spaces.GPU(duration=20)
1453
  def healthcheck_gpu(name: str) -> str:
1454
  who = (name or "").strip() or "world"
1455
  return f"Omni-Video-Factory bootstrap is running. Hello, {who}!"
1456
 
 
1457
  def _all_models_present() -> bool:
1458
  try:
1459
  return all(path.exists() for path in _expected_model_paths().values())
1460
  except Exception:
1461
  return False
1462
 
 
1463
  def _maybe_file_path(value) -> str:
1464
  if isinstance(value, str):
1465
  return value
 
1470
  return got
1471
  return ""
1472
 
 
1473
  def _output_dir() -> Path:
1474
  out = _repo_dir() / "datas" / "outputs"
1475
  out.mkdir(parents=True, exist_ok=True)
1476
  return out
1477
 
 
1478
  def _scene_count() -> int:
1479
  default_scene = int(getattr(APP_CONFIG, "DEFAULT_SCENE_COUNT"))
1480
  raw = (os.getenv("OMNI_SCENE_COUNT") or str(default_scene)).strip()
 
1484
  value = default_scene
1485
  return max(1, min(value, 4))
1486
 
 
1487
  def _vid_resolution_default() -> int:
1488
  raw = (os.getenv("OMNI_VID_RES") or "384").strip()
1489
  try:
 
1494
  return 384
1495
  return value
1496
 
 
1497
  def _aspect_ratio_default() -> str:
1498
  raw = (os.getenv("OMNI_ASPECT_RATIO") or "3:4").strip()
1499
  allowed = {"16:9", "4:3", "1:1", "3:4", "9:16"}
1500
  return raw if raw in allowed else "3:4"
1501
 
 
1502
  def _seconds_per_scene_default() -> int:
1503
  raw = (os.getenv("OMNI_SECONDS_PER_SCENE") or "3").strip()
1504
  try:
 
1507
  value = 3
1508
  return 5 if value >= 5 else 3
1509
 
 
1510
  def _allowed_scene_counts(resolution: int) -> List[int]:
1511
  rules = {
1512
  384: [1, 2, 3, 4],
 
1514
  }
1515
  return rules.get(int(resolution), [1])
1516
 
 
1517
  def _normalize_generation_options(
1518
  mode: str,
1519
  scene_count: Optional[int],
 
1533
  scenes = _scene_count()
1534
  if scenes not in allowed_scenes:
1535
  scenes = allowed_scenes[-1]
 
1536
  secs = int(seconds_per_scene) if seconds_per_scene else _seconds_per_scene_default()
1537
  secs = 5 if secs >= 5 else 3
 
1538
  if mode == "v2v":
1539
  scenes = 1
 
1540
  notes = f"config: res={res}, scenes={scenes}, seconds_per_scene={secs}"
1541
  return scenes, res, secs, notes
1542
 
 
1543
  def _frames_for_seconds(seconds_per_scene: int) -> int:
1544
  return 81 if int(seconds_per_scene) >= 5 else 49
1545
 
 
1546
  def _round_to_multiple(value: int, multiple: int = 16) -> int:
1547
  if multiple <= 1:
1548
  return int(value)
1549
  return int((int(value) + multiple - 1) // multiple * multiple)
1550
 
 
1551
  def _compute_t2v_dims(aspect_ratio: str, resolution: int) -> Tuple[int, int]:
1552
  res = int(resolution)
1553
  aspect_map = {
 
1558
  "9:16": (9, 16),
1559
  }
1560
  w_ratio, h_ratio = aspect_map.get(aspect_ratio, (3, 4))
 
1561
  target_area = max(256, int(res) * int(res))
1562
  width = int((target_area * (w_ratio / h_ratio)) ** 0.5)
1563
  height = int((target_area * (h_ratio / w_ratio)) ** 0.5)
1564
  return _round_to_multiple(width, 16), _round_to_multiple(height, 16)
1565
 
 
1566
  def _gpu_duration_default() -> int:
 
1567
  default_fixed = int(getattr(APP_CONFIG, "GPU_DEFAULT_FIXED_SECONDS"))
1568
  raw = (os.getenv("OMNI_GPU_SECONDS") or str(default_fixed)).strip()
1569
  try:
 
1574
  max_secs = int(getattr(APP_CONFIG, "GPU_MAX_SECONDS"))
1575
  return max(min_secs, min(value, max_secs))
1576
 
 
1577
  def _estimated_runtime_seconds(
1578
  mode: str,
1579
  scene_count: Optional[int],
 
1586
  resolution=resolution,
1587
  seconds_per_scene=seconds_per_scene,
1588
  )
 
1589
  raw_base_by_res = dict(getattr(APP_CONFIG, "GPU_BASE_SECONDS_BY_RES") or {})
1590
  base_by_res: Dict[int, float] = {}
1591
  for key, value in raw_base_by_res.items():
 
1601
  mode_multiplier = float(mode_multiplier_map.get(str(mode or "").lower().strip(), 1.0))
1602
  return max(10.0, base * scene_multiplier * seconds_multiplier * mode_multiplier)
1603
 
 
1604
  def _dynamic_gpu_duration(
1605
  mode: str,
1606
  prompt: str,
 
1613
  aspect_ratio: str = "3:4",
1614
  client_ip: Optional[str] = None,
1615
  client_country: Optional[str] = None,
1616
+ target_fps: int = 16,
1617
  ) -> int:
 
 
1618
  raw_fixed = (os.getenv("OMNI_GPU_SECONDS") or "").strip()
1619
  if raw_fixed:
1620
  try:
 
1623
  return max(min_secs, min(int(raw_fixed), max_secs))
1624
  except (TypeError, ValueError):
1625
  pass
 
1626
  estimated = _estimated_runtime_seconds(mode, scene_count, resolution, seconds_per_scene)
1627
  ratio_default = float(getattr(APP_CONFIG, "GPU_BUFFER_RATIO_DEFAULT"))
1628
  extra_default = float(getattr(APP_CONFIG, "GPU_BUFFER_SECONDS_DEFAULT"))
 
1641
  max_secs = int(getattr(APP_CONFIG, "GPU_MAX_SECONDS"))
1642
  return max(min_secs, min(requested, max_secs))
1643
 
1644
+ def _patch_workflow_settings(target_fps: int) -> None:
1645
+ wf_dir = _workflow_dir()
1646
+ for wf_name in WORKFLOW_FILES:
1647
+ wf_path = wf_dir / wf_name
1648
+ if not wf_path.exists():
1649
+ continue
1650
+ try:
1651
+ data = json.loads(wf_path.read_text(encoding="utf-8"))
1652
+ changed = False
1653
+ for key, node in data.items():
1654
+ class_type = str(node.get("class_type", ""))
1655
+ if class_type == "VHS_VideoCombine":
1656
+ if "inputs" in node:
1657
+ if "frame_rate" in node["inputs"] and node["inputs"]["frame_rate"] != target_fps:
1658
+ node["inputs"]["frame_rate"] = target_fps
1659
+ changed = True
1660
+ if "crf" in node["inputs"] and node["inputs"]["crf"] != 0:
1661
+ node["inputs"]["crf"] = 0
1662
+ changed = True
1663
+ if "VFI" in class_type or class_type == "RIFE VFI":
1664
+ if "inputs" in node:
1665
+ if "multiplier" in node["inputs"] and node["inputs"]["multiplier"] != 2:
1666
+ node["inputs"]["multiplier"] = 2
1667
+ changed = True
1668
+ if changed:
1669
+ wf_path.write_text(json.dumps(data, indent=4), encoding="utf-8")
1670
+ except Exception:
1671
+ pass
1672
 
1673
  @spaces.GPU(duration=_dynamic_gpu_duration)
1674
  def run_generation_real(
 
1683
  aspect_ratio: str = "3:4",
1684
  client_ip: Optional[str] = None,
1685
  client_country: Optional[str] = None,
1686
+ target_fps: int = 16,
1687
  ):
1688
+ os.environ["OMNI_TARGET_FPS"] = str(target_fps)
1689
+ os.environ["OMNI_BASE_FPS"] = "16"
1690
+ os.environ["OMNI_FRAME_MULTIPLIER"] = "2"
1691
+ os.environ["OMNI_CRF"] = "0"
1692
+
1693
+ _patch_workflow_settings(target_fps)
1694
+
1695
  started_at = time.perf_counter()
1696
  warming_up_message = "warming up, please retry in 15-30 seconds"
1697
 
 
1705
  prompt = (prompt or "").strip()
1706
  if not prompt:
1707
  return _status_text("prompt is empty"), None, None
1708
+
 
 
 
1709
  models_ok, prep_status = ensure_models_ready_for_generation()
1710
  if not models_ok:
1711
  return (
 
1795
  return _status_text(f"{mode} finished but output missing: {final_path}"), None, None
1796
 
1797
  status_lines: List[str] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1798
  return _status_text("\n".join(status_lines)), final_path, None
1799
 
 
1800
  def build_demo() -> gr.Blocks:
1801
  banner_subtitle = "Please give us a ❤️ if you find it helpful. No usage limits beyond your ZeroGPU daily quota."
1802
  manual_prompt_min_chars = 10
 
1815
  f"""
1816
  <div style="text-align: center; margin: 20px auto 10px auto; max-width: 800px;">
1817
  <h1 style="color: #2c3e50; margin: 0; font-size: 3.5em; font-weight: 800; letter-spacing: 3px; text-shadow: 2px 2px 4px rgba(0,0,0,0.1);">
1818
+ 🎬 {getattr(APP_CONFIG, "APP_TITLE", "Omni Video Factory")}
1819
  </h1>
1820
  </div>
1821
  <style>
 
1921
  seconds_per_scene,
1922
  resolution,
1923
  aspect_ratio,
1924
+ target_fps,
1925
  base_prompt,
1926
  s1,
1927
  s2,
 
1945
  str(aspect_ratio),
1946
  ip,
1947
  cc,
1948
+ int(target_fps),
1949
  )
1950
  show_like_tip = _should_show_like_tip(ip)
1951
  return (
 
1960
  scene_count,
1961
  seconds_per_scene,
1962
  resolution,
1963
+ target_fps,
1964
  image_file,
1965
  base_prompt,
1966
  s1,
 
1985
  "3:4",
1986
  ip,
1987
  cc,
1988
+ int(target_fps),
1989
  )
1990
  show_like_tip = _should_show_like_tip(ip)
1991
  return (
 
2001
  seconds_per_scene,
2002
  resolution,
2003
  aspect_ratio,
2004
+ target_fps,
2005
  base_prompt,
2006
  s1,
2007
  s2,
 
2017
  seconds_per_scene,
2018
  resolution,
2019
  aspect_ratio,
2020
+ target_fps,
2021
  base_prompt,
2022
  s1,
2023
  s2,
 
2030
  scene_count,
2031
  seconds_per_scene,
2032
  resolution,
2033
+ target_fps,
2034
  image_file,
2035
  base_prompt,
2036
  s1,
 
2046
  scene_count,
2047
  seconds_per_scene,
2048
  resolution,
2049
+ target_fps,
2050
  image_file,
2051
  base_prompt,
2052
  s1,
 
2056
  request,
2057
  )
2058
 
2059
+ def _submit_v2v(prompt, video_file, resolution, seconds_per_scene, aspect_ratio, target_fps, request: gr.Request):
2060
  ip = _request_ip(request)
2061
  cc = _request_country(request, ip=ip)
2062
  status, video, preview = run_generation_real(
 
2071
  str(aspect_ratio),
2072
  ip,
2073
  cc,
2074
+ int(target_fps),
2075
  )
2076
  show_like_tip = _should_show_like_tip(ip)
2077
  return (
 
2094
  scene_count_i2v = gr.Radio([1, 2, 3, 4], value=default_scene, label="Scene Count")
2095
  seconds_i2v = gr.Radio([3, 5], value=_seconds_per_scene_default(), label="Seconds per Scene")
2096
  resolution_i2v = gr.Radio([384, 512], value=default_resolution, label="Resolution")
2097
+ target_fps_i2v = gr.Radio([16, 32, 64, 128], value=16, label="Target FPS (Frame Interpolation)")
2098
  resolution_note_i2v = gr.Markdown(_resolution_note_text(default_resolution))
2099
 
2100
  image_i2v = gr.File(label="Image file", file_types=["image"])
 
2151
  )
2152
  run_btn_i2v_manual.click(
2153
  _submit_i2v_manual,
2154
+ inputs=[scene_count_i2v, seconds_i2v, resolution_i2v, target_fps_i2v, image_i2v, base_prompt_i2v, s1_i2v_manual, s2_i2v_manual, s3_i2v_manual, s4_i2v_manual],
2155
  outputs=[status_i2v, out_video_i2v, nsfw_card_i2v, nsfw_preview_i2v, download_card_i2v],
2156
  )
2157
  run_btn_i2v_auto.click(
2158
  _submit_i2v,
2159
+ inputs=[scene_count_i2v, seconds_i2v, resolution_i2v, target_fps_i2v, image_i2v, base_prompt_i2v, s1_i2v_auto, s2_i2v_auto, s3_i2v_auto, s4_i2v_auto],
2160
  outputs=[status_i2v, out_video_i2v, nsfw_card_i2v, nsfw_preview_i2v, download_card_i2v],
2161
  )
2162
 
 
2166
  scene_count_t2v = gr.Radio([1, 2, 3, 4], value=default_scene, label="Scene Count")
2167
  seconds_t2v = gr.Radio([3, 5], value=_seconds_per_scene_default(), label="Seconds per Scene")
2168
  resolution_t2v = gr.Radio([384, 512], value=default_resolution, label="Resolution")
2169
+ target_fps_t2v = gr.Radio([16, 32, 64, 128], value=16, label="Target FPS (Frame Interpolation)")
2170
  resolution_note_t2v = gr.Markdown(_resolution_note_text(default_resolution))
2171
  aspect_ratio_t2v = gr.Radio(["16:9", "4:3", "1:1", "3:4", "9:16"], value=default_ratio, label="Aspect Ratio")
2172
  dims_hint_t2v = gr.Markdown(f"T2V dims: **{d_w}x{d_h}**")
 
2225
  )
2226
  run_btn_t2v_manual.click(
2227
  _submit_t2v_manual,
2228
+ inputs=[scene_count_t2v, seconds_t2v, resolution_t2v, aspect_ratio_t2v, target_fps_t2v, base_prompt_t2v, s1_t2v_manual, s2_t2v_manual, s3_t2v_manual, s4_t2v_manual],
2229
  outputs=[status_t2v, out_video_t2v, nsfw_card_t2v, nsfw_preview_t2v, download_card_t2v],
2230
  )
2231
  run_btn_t2v_auto.click(
2232
  _submit_t2v,
2233
+ inputs=[scene_count_t2v, seconds_t2v, resolution_t2v, aspect_ratio_t2v, target_fps_t2v, base_prompt_t2v, s1_t2v_auto, s2_t2v_auto, s3_t2v_auto, s4_t2v_auto],
2234
  outputs=[status_t2v, out_video_t2v, nsfw_card_t2v, nsfw_preview_t2v, download_card_t2v],
2235
  )
2236
 
 
2240
  seconds_v2v = gr.Radio([3, 5], value=_seconds_per_scene_default(), label="Additional Seconds")
2241
  resolution_v2v = gr.Radio([384, 512], value=default_resolution, label="Resolution")
2242
  aspect_ratio_v2v = gr.Radio(["16:9", "4:3", "1:1", "3:4", "9:16"], value=default_ratio, label="Aspect Ratio")
2243
+ target_fps_v2v = gr.Radio([16, 32, 64, 128], value=16, label="Target FPS (Frame Interpolation)")
2244
  prompt_v2v = gr.Textbox(label="Prompt", value="Enhance this clip with cinematic lighting", lines=3)
2245
  video_v2v = gr.File(label="Video file", file_types=["video"])
2246
  run_btn_v2v = gr.Button("Submit Generate V2V", variant="primary")
 
2254
 
2255
  run_btn_v2v.click(
2256
  _submit_v2v,
2257
+ inputs=[prompt_v2v, video_v2v, resolution_v2v, seconds_v2v, aspect_ratio_v2v, target_fps_v2v],
2258
  outputs=[status_v2v, out_video_v2v, nsfw_card_v2v, nsfw_preview_v2v, download_card_v2v],
2259
  )
2260
  gr.HTML(
 
2311
  </div>
2312
  """
2313
  )
2314
+ return demo