Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,12 @@ uv.lock
# venvstacks build artifact: regenerated from venvstacks.toml + pyproject.toml
# by packaging/build.py:_generate_venvstacks_toml() on every Swift app build.
packaging/_venvstacks_resolved.toml
.claude/

# Video generation artifacts (job outputs live under {base_path}, never in-repo;
# these guard against measurement/test runs writing into the worktree)
*.mp4
*.mp4.metadata.json
video-jobs/
video-artifacts/
p0_video/
26 changes: 26 additions & 0 deletions docs/upstream-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,29 @@ Qwen-Gemma / oQ)的相关度。
破坏部分 HTTP 客户端 / Copilot CLI

> 下次 review 上游 open PR 时,把结论(引入 / 跳过)回填到对应小节。

---

## 2026-06-11 分化标记: 视频生成引擎 (fmlx 自有, 永不回流)

feat/video-engine 引入文生视频引擎 (Wan2.2 T2V A14B via mlx-gen, 设计
docs/video-generation-engine-spec.md). 这是 fmlx 与上游的有意分化,
不向上游 PR. 对上游同源文件的补丁面 (cherry-pick 撞冲突时参考):

- model_discovery.py: ModelType/EngineType Literal + model_index.json
识别分支 + _register_model 视频臂与跳过过滤
- engine_pool.py: Literal + 映射 + get_engine 入口 video 拒绝臂 +
_load_engine 防御臂
- server.py: video 路由挂载 / pre-pool 400 / 默认模型 chat-capable 过滤 /
ModelInfo.model_type / lifespan 构造与关停 VideoJobManager
- process_memory_enforcer.py: 视频内存租约 (acquire/set pid/release +
ceiling 扣减 + 动态 ceiling 加回)
- settings.py: VideoSettings section + huggingface.disable_xet
- admin/routes.py: valid_types/type_to_engine + 列表与删除门放宽 +
global-settings video 字段
- cli.py: HF_HUB_DISABLE_XET 注入
- exceptions.py: ModelTypeNotLoadableError

全新文件 (无冲突面): omlx/video/*, omlx/api/video_models.py,
omlx/api/video_routes.py, tests/test_video_*.py,
scripts/video_p0_measure.py.
627 changes: 627 additions & 0 deletions docs/video-generation-engine-spec.md

Large diffs are not rendered by default.

19 changes: 18 additions & 1 deletion omlx/admin/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -767,5 +767,22 @@
"cluster.router.unknown": "unknown",
"cluster.healthy.yes": "yes",
"cluster.healthy.no": "no",
"cluster.save.failed": "Save failed"
"cluster.save.failed": "Save failed",
"settings": {
"video": {
"title": "Video Generation",
"enabled": "Enable video generation",
"enabled_desc": "Serve POST /v1/videos via the subprocess worker. Requires the video worker venv.",
"memory_lease": "Memory lease (GB)",
"memory_lease_desc": "Reserved against the memory ceiling while a job runs; co-resident LLMs throttle accordingly.",
"default_steps": "Default denoise steps",
"default_fps": "Default FPS",
"max_queued_jobs": "Max queued jobs",
"job_timeout": "Job timeout (seconds)",
"artifacts_max_gb": "Artifact storage cap (GB)",
"artifacts_max_gb_desc": "Oldest video files are purged beyond this; job records are kept.",
"worker_python": "Worker python path",
"worker_python_desc": "Python of the isolated video venv. Empty = default path."
}
}
}
19 changes: 18 additions & 1 deletion omlx/admin/i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -742,5 +742,22 @@
"cluster.router.unknown": "неизвестно",
"cluster.healthy.yes": "да",
"cluster.healthy.no": "нет",
"cluster.save.failed": "Не удалось сохранить"
"cluster.save.failed": "Не удалось сохранить",
"settings": {
"video": {
"title": "Генерация видео",
"enabled": "Включить генерацию видео",
"enabled_desc": "Обслуживать POST /v1/videos через подпроцесс-воркер. Требуется venv видео-воркера.",
"memory_lease": "Резерв памяти (ГБ)",
"memory_lease_desc": "Резервируется из лимита памяти на время задачи; LLM соответственно замедляются.",
"default_steps": "Шаги диффузии по умолчанию",
"default_fps": "FPS по умолчанию",
"max_queued_jobs": "Макс. задач в очереди",
"job_timeout": "Таймаут задачи (сек)",
"artifacts_max_gb": "Лимит хранения (ГБ)",
"artifacts_max_gb_desc": "Старые видеофайлы удаляются сверх лимита; записи задач сохраняются.",
"worker_python": "Путь к python воркера",
"worker_python_desc": "Python изолированного видео-venv. Пусто = путь по умолчанию."
}
}
}
19 changes: 18 additions & 1 deletion omlx/admin/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -765,5 +765,22 @@
"cluster.router.unknown": "未知",
"cluster.healthy.yes": "是",
"cluster.healthy.no": "否",
"cluster.save.failed": "保存失败"
"cluster.save.failed": "保存失败",
"settings": {
"video": {
"title": "视频生成",
"enabled": "启用视频生成",
"enabled_desc": "通过子进程 worker 提供 POST /v1/videos。需要先安装视频 worker venv。",
"memory_lease": "内存租约 (GB)",
"memory_lease_desc": "任务运行期间从内存上限中预留;共驻的 LLM 会相应限流。",
"default_steps": "默认去噪步数",
"default_fps": "默认帧率",
"max_queued_jobs": "最大排队任务数",
"job_timeout": "任务超时 (秒)",
"artifacts_max_gb": "产物存储上限 (GB)",
"artifacts_max_gb_desc": "超限时清除最旧的视频文件;任务记录保留。",
"worker_python": "Worker python 路径",
"worker_python_desc": "独立视频 venv 的 python。留空用默认路径。"
}
}
}
63 changes: 55 additions & 8 deletions omlx/admin/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,21 @@ class GlobalSettingsRequest(BaseModel):
# Idle timeout settings. null disables the global fallback.
idle_timeout_seconds: int | None = Field(default=None, ge=60)

# Video generation settings (docs/video-generation-engine-spec.md 4.5)
video_enabled: bool | None = None
video_worker_python: str | None = None
video_memory_lease_gb: float | None = Field(default=None, gt=0)
video_max_queued_jobs: int | None = Field(default=None, ge=1)
video_job_timeout_seconds: int | None = Field(default=None, ge=60)
video_progress_stall_timeout_seconds: int | None = Field(default=None, ge=30)
video_default_steps: int | None = Field(default=None, ge=1)
video_default_fps: int | None = Field(default=None, ge=1)
video_max_frames: int | None = Field(default=None, ge=5)
video_max_steps: int | None = Field(default=None, ge=1)
video_max_pixels_per_frame: int | None = Field(default=None, ge=256)
video_artifacts_max_count: int | None = Field(default=None, ge=1)
video_artifacts_max_gb: float | None = Field(default=None, gt=0)

# Auth settings
api_key: str | None = None
skip_api_key_verification: bool | None = None
Expand Down Expand Up @@ -1857,7 +1872,7 @@ async def update_model_settings(
)
current_settings.model_alias = alias_value
if "model_type_override" in sent:
valid_types = {"llm", "vlm", "embedding", "reranker", "audio_stt", "audio_tts", "audio_sts"}
valid_types = {"llm", "vlm", "embedding", "reranker", "audio_stt", "audio_tts", "audio_sts", "video"}
# Treat empty string as None (auto-detect)
override_value = request.model_type_override or None
if override_value is not None and override_value not in valid_types:
Expand All @@ -1875,6 +1890,7 @@ async def update_model_settings(
"audio_stt": "audio_stt",
"audio_tts": "audio_tts",
"audio_sts": "audio_sts",
"video": "video",
}
if override_value:
entry.model_type = override_value
Expand Down Expand Up @@ -2849,6 +2865,7 @@ async def get_global_settings(is_admin: bool = Depends(require_admin)):
"idle_timeout": {
"idle_timeout_seconds": global_settings.idle_timeout.idle_timeout_seconds,
},
"video": global_settings.video.to_dict(),
}


Expand Down Expand Up @@ -3268,6 +3285,30 @@ async def update_global_settings(
else:
logger.info("Idle timeout disabled")

# Apply video settings (Live for caps/timeouts; enabled flips per-request
# gating immediately because handlers read settings.video each call.
# worker_python/memory_lease affect the NEXT job dispatch.)
_video_fields = {
"video_enabled": "enabled",
"video_worker_python": "worker_python",
"video_memory_lease_gb": "memory_lease_gb",
"video_max_queued_jobs": "max_queued_jobs",
"video_job_timeout_seconds": "job_timeout_seconds",
"video_progress_stall_timeout_seconds": "progress_stall_timeout_seconds",
"video_default_steps": "default_steps",
"video_default_fps": "default_fps",
"video_max_frames": "max_frames",
"video_max_steps": "max_steps",
"video_max_pixels_per_frame": "max_pixels_per_frame",
"video_artifacts_max_count": "artifacts_max_count",
"video_artifacts_max_gb": "artifacts_max_gb",
}
for req_field, attr in _video_fields.items():
value = getattr(request, req_field, None)
if value is not None:
setattr(global_settings.video, attr, value)
runtime_applied.append(req_field)

# Apply auth settings (API key change)
if request.api_key is not None:
from ..server import _server_state
Expand Down Expand Up @@ -4465,7 +4506,7 @@ async def list_hf_models(is_admin: bool = Depends(require_admin)):

model_dirs = global_settings.model.get_model_dirs(global_settings.base_path)

from ..model_discovery import _resolve_hf_cache_entry
from ..model_discovery import _is_model_dir, _resolve_hf_cache_entry

def _add_model(model_path: Path, model_name: str) -> None:
if model_name in seen_names:
Expand All @@ -4492,23 +4533,25 @@ def _add_model(model_path: Path, model_name: str) -> None:
if not subdir.is_dir() or subdir.name.startswith("."):
continue

if (subdir / "config.json").exists():
# _is_model_dir accepts config.json or model_index.json roots
# (diffusers-layout video models) and excludes adapters.
if _is_model_dir(subdir):
# Level 1: direct model folder
_add_model(subdir, subdir.name)
else:
# HF Hub cache entry: models--Org--Name/snapshots/<hash>/
hf_resolved = _resolve_hf_cache_entry(subdir)
if hf_resolved is not None:
snapshot_path, model_name = hf_resolved
if (snapshot_path / "config.json").exists():
if _is_model_dir(snapshot_path):
_add_model(snapshot_path, model_name)
continue

# Level 2: organization folder — scan children
for child in sorted(subdir.iterdir()):
if not child.is_dir() or child.name.startswith("."):
continue
if (child / "config.json").exists():
if _is_model_dir(child):
_add_model(child, child.name)

return {"models": models}
Expand All @@ -4528,14 +4571,18 @@ async def delete_hf_model(

model_dirs = global_settings.model.get_model_dirs(global_settings.base_path)

# Search for model across all directories in both flat and org-folder layouts
# Search for model across all directories in both flat and org-folder
# layouts. _is_model_dir accepts config.json or model_index.json roots
# (diffusers-layout video models must be deletable too).
from ..model_discovery import _is_model_dir

model_path = None
parent_model_dir = None
for model_dir in model_dirs:
if not model_dir.exists():
continue
candidate = model_dir / model_name
if candidate.is_dir() and (candidate / "config.json").exists():
if candidate.is_dir() and _is_model_dir(candidate):
model_path = candidate
parent_model_dir = model_dir
break
Expand All @@ -4544,7 +4591,7 @@ async def delete_hf_model(
if not subdir.is_dir() or subdir.name.startswith("."):
continue
candidate = subdir / model_name
if candidate.is_dir() and (candidate / "config.json").exists():
if candidate.is_dir() and _is_model_dir(candidate):
model_path = candidate
parent_model_dir = model_dir
break
Expand Down
10 changes: 10 additions & 0 deletions omlx/admin/static/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
integrations: { copilot_model: null, codex_model: null, opencode_model: null, openclaw_model: null, pi_model: null, openclaw_tools_profile: 'full' },
ui: { language: 'en' },
idle_timeout: { idle_timeout_seconds: null },
video: { enabled: false, worker_python: '', memory_lease_gb: 36, max_queued_jobs: 4, job_timeout_seconds: 7200, progress_stall_timeout_seconds: 600, default_steps: 20, default_fps: 16, max_frames: 121, max_steps: 50, max_pixels_per_frame: 921600, artifacts_max_count: 50, artifacts_max_gb: 50 },
system: { total_memory_bytes: 0, total_memory: '', auto_model_memory: '', ssd_total_bytes: 0, ssd_total: '' },
},

Expand Down Expand Up @@ -781,6 +782,7 @@
claude_code: { ...this.globalSettings.claude_code, ...data.claude_code },
integrations: { ...this.globalSettings.integrations, ...data.integrations },
idle_timeout: { ...this.globalSettings.idle_timeout, ...data.idle_timeout },
video: { ...this.globalSettings.video, ...data.video },
system: { ...this.globalSettings.system, ...data.system },
};
this.globalSettings.ui = data.ui || { language: 'en' };
Expand Down Expand Up @@ -884,6 +886,14 @@
...(this.globalSettings.auth.api_key ? { api_key: this.globalSettings.auth.api_key } : {}),
skip_api_key_verification: this.globalSettings.auth.skip_api_key_verification,
idle_timeout_seconds: this.globalSettings.idle_timeout?.idle_timeout_seconds ?? null,
video_enabled: this.globalSettings.video?.enabled ?? null,
video_worker_python: this.globalSettings.video?.worker_python || null,
video_memory_lease_gb: this.globalSettings.video?.memory_lease_gb ?? null,
video_max_queued_jobs: this.globalSettings.video?.max_queued_jobs ?? null,
video_job_timeout_seconds: this.globalSettings.video?.job_timeout_seconds ?? null,
video_default_steps: this.globalSettings.video?.default_steps ?? null,
video_default_fps: this.globalSettings.video?.default_fps ?? null,
video_artifacts_max_gb: this.globalSettings.video?.artifacts_max_gb ?? null,
}),
});

Expand Down
1 change: 1 addition & 0 deletions omlx/admin/templates/dashboard/_modal_model_settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ <h3 class="text-xs font-bold uppercase tracking-widest text-neutral-400 mb-5">{{
<option value="audio_stt">Audio STT</option>
<option value="audio_tts">Audio TTS</option>
<option value="audio_sts">Audio STS</option>
<option value="video">Video</option>
</select>
</div>
<div x-show="reasoningParsers.length > 0">
Expand Down
Loading