Spaces:
Running on Zero
Running on Zero
Upload 9 files
Browse files- app.py +10 -3
- packages.txt +1 -0
- requirements.txt +13 -10
- 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 |
-
|
| 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:
|
| 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 |
-
"#
|
| 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,
|
| 129 |
-
parts[1] or None,
|
| 130 |
-
parts[2] or None,
|
| 131 |
-
parts[3] or None,
|
| 132 |
-
parts[4] or None,
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1002 |
-
return normalized if normalized else "__unknown__"
|
| 1003 |
-
|
| 1004 |
|
| 1005 |
def _nsfw_remote_inc_total(ip: str) -> Optional[int]:
|
| 1006 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1301 |
-
|
| 1302 |
-
|
| 1303 |
-
|
| 1304 |
-
|
| 1305 |
-
|
| 1306 |
-
|
| 1307 |
-
|
| 1308 |
-
|
| 1309 |
-
|
| 1310 |
-
|
| 1311 |
-
|
| 1312 |
-
|
| 1313 |
-
|
| 1314 |
-
|
| 1315 |
-
|
| 1316 |
-
|
| 1317 |
-
|
| 1318 |
-
|
| 1319 |
-
|
| 1320 |
-
|
| 1321 |
-
|
| 1322 |
-
|
| 1323 |
-
|
| 1324 |
-
|
| 1325 |
-
|
| 1326 |
-
|
| 1327 |
-
|
| 1328 |
-
|
| 1329 |
-
|
| 1330 |
-
|
| 1331 |
-
|
| 1332 |
-
|
| 1333 |
-
|
| 1334 |
-
|
| 1335 |
-
|
| 1336 |
-
|
| 1337 |
-
|
| 1338 |
-
|
| 1339 |
-
|
| 1340 |
-
|
| 1341 |
-
|
| 1342 |
-
|
| 1343 |
-
|
| 1344 |
-
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
|
| 1348 |
-
|
| 1349 |
-
|
| 1350 |
-
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
|
| 1356 |
-
|
| 1357 |
-
|
| 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
|
| 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
|