Files
hao-ai-lab-FastVideo/apps/dreamverse/server/mock_server.py
T
SolitaryThinker aef113a861 [fix] dreamverse-monorepo: carry generic-merged files product-local
Source: Dreamverse repo at will/integrate-public-fastvideo @ ec8ef92.\n\nCarry the generic-merged backend modules as Dreamverse product-local code because the public streaming APIs are not drop-in compatible with the product runtime yet. Revert the Phase 2 public compatibility shims so FastVideo's public streaming surface returns to the pre-migration state.\n\nCo-authored-by: Junda (David) Su <90978028+Davids048@users.noreply.github.com>\nCo-authored-by: Matthew Noto <99706358+RandNMR73@users.noreply.github.com>\nCo-authored-by: XOR-op <17672363+XOR-op@users.noreply.github.com>\nCo-authored-by: Zhang Peiyuan <42993249+jzhang38@users.noreply.github.com>
2026-05-07 13:37:25 -07:00

1162 lines
44 KiB
Python

# pyright: reportMissingTypeArgument=false, reportConstantRedefinition=false, reportGeneralTypeIssues=false
# ruff: noqa: UP022, F841, SIM105
# mypy: ignore-errors
"""
Protocol-compatible mock server for the current LTX2 streaming UI.
This mock speaks the same websocket API shape as `server/main.py` for:
- `session_init_v2`
- `append_prompt`
- `simple_generate`
- prompt window reset / rewrite control messages
It streams a small fragmented MP4 clip generated by ffmpeg so the frontend can
exercise the real AV pipeline without needing a GPU backend.
"""
from __future__ import annotations
import asyncio
from contextlib import asynccontextmanager
import json
import logging
import os
import shutil
import subprocess
import uuid
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from config import FRONTEND_STATIC_DIR_CANDIDATES, GENERATION_SEGMENT_CAP
from session_init_image import cleanup_session_init_image, persist_session_init_image
LATENCY_MS = 200
SESSION_TIMEOUT_SECONDS = 300
MOCK_FRAME_WIDTH = 640
MOCK_FRAME_HEIGHT = 352
MOCK_FPS = 24
MOCK_DURATION_SECONDS = 5.0
MOCK_STREAM_CHUNK_SIZE_BYTES = 256 * 1024
MOCK_STREAM_CHUNK_DELAY_MS = 15
MOCK_AV_MIME = 'video/mp4; codecs="avc1.42E01E"'
FFMPEG_BIN = shutil.which(os.getenv("FASTVIDEO_FFMPEG_BIN", "ffmpeg"))
MOCK_SEGMENT_BYTES: bytes | None = None
class _HeartbeatAccessLogFilter(logging.Filter):
"""Drop noisy access logs for frequent health/readiness probes."""
def filter(self, record: logging.LogRecord) -> bool:
message = record.getMessage()
return ('"GET /healthz ' not in message and '"GET /readyz ' not in message)
def _install_heartbeat_log_filter() -> None:
access_logger = logging.getLogger("uvicorn.access")
for existing in access_logger.filters:
if isinstance(existing, _HeartbeatAccessLogFilter):
return
access_logger.addFilter(_HeartbeatAccessLogFilter())
def _build_mock_segment_bytes() -> bytes:
if not FFMPEG_BIN:
raise RuntimeError("ffmpeg is required to run the mock streaming server.")
command = [
FFMPEG_BIN,
"-hide_banner",
"-loglevel",
"error",
"-f",
"lavfi",
"-i",
f"testsrc2=size={MOCK_FRAME_WIDTH}x{MOCK_FRAME_HEIGHT}:rate={MOCK_FPS}",
"-t",
f"{MOCK_DURATION_SECONDS}",
"-an",
"-c:v",
"libx264",
"-preset",
"ultrafast",
"-tune",
"zerolatency",
"-pix_fmt",
"yuv420p",
"-profile:v",
"baseline",
"-level",
"3.0",
"-movflags",
"+empty_moov+default_base_moof+frag_keyframe",
"-frag_duration",
"250000",
"-f",
"mp4",
"pipe:1",
]
result = subprocess.run(
command,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if not result.stdout:
raise RuntimeError("Mock ffmpeg command did not return MP4 bytes.")
return result.stdout
class PromptSubmission:
def __init__(self, prompt_id: str, raw_prompt: str):
self.prompt_id = prompt_id
self.raw_prompt = raw_prompt
class ReadyPrompt:
def __init__(
self,
*,
prompt: str,
source: str,
prompt_id: str | None = None,
seed_prompt_index: int | None = None,
loop_iteration: int | None = None,
):
self.prompt = prompt
self.source = source
self.prompt_id = prompt_id
self.seed_prompt_index = seed_prompt_index
self.loop_iteration = loop_iteration
@asynccontextmanager
async def lifespan(app: FastAPI):
global MOCK_SEGMENT_BYTES
del app
print(f"Mock server starting with {LATENCY_MS}ms simulated latency...", flush=True)
MOCK_SEGMENT_BYTES = _build_mock_segment_bytes()
print(
"Mock server ready "
f"({len(MOCK_SEGMENT_BYTES) / (1024 * 1024):.2f} MiB fMP4 clip)",
flush=True,
)
yield
MOCK_SEGMENT_BYTES = None
print("Mock server shutting down...", flush=True)
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/healthz")
async def healthz():
return {
"status": "ok",
"service": "ltx2-streaming-mock-server",
}
@app.get("/readyz")
async def readyz():
return {
"status": "ready",
"service": "ltx2-streaming-mock-server",
}
@app.get("/status")
async def status():
return {
"queue_size": 0,
"available_gpus": 1,
"total_gpus": 1,
}
async def _stream_mock_segment(
ws_send_json,
ws_send_bytes,
*,
segment_idx: int,
) -> None:
if not MOCK_SEGMENT_BYTES:
raise RuntimeError("Mock segment bytes were not initialized.")
stream_id = f"mock-stream-{segment_idx}-{uuid.uuid4().hex[:8]}"
await ws_send_json({
"type": "media_init",
"segment_idx": segment_idx,
"mime": MOCK_AV_MIME,
"stream_id": stream_id,
"mode": "av_fmp4",
})
chunk_count = 0
for start in range(0, len(MOCK_SEGMENT_BYTES), MOCK_STREAM_CHUNK_SIZE_BYTES):
end = start + MOCK_STREAM_CHUNK_SIZE_BYTES
await ws_send_bytes(MOCK_SEGMENT_BYTES[start:end])
chunk_count += 1
if MOCK_STREAM_CHUNK_DELAY_MS > 0:
await asyncio.sleep(MOCK_STREAM_CHUNK_DELAY_MS / 1000.0)
await ws_send_json({
"type": "media_segment_complete",
"segment_idx": segment_idx,
"stream_id": stream_id,
"chunks": chunk_count,
})
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
client_id = str(uuid.uuid4())
print(f"Client {client_id[:8]} connected")
send_lock = asyncio.Lock()
stop_event = asyncio.Event()
session_init_image = None
async def ws_send_json(payload: dict) -> None:
async with send_lock:
await websocket.send_json(payload)
async def ws_send_bytes(payload: bytes) -> None:
async with send_lock:
await websocket.send_bytes(payload)
async def session_timeout() -> None:
await asyncio.sleep(SESSION_TIMEOUT_SECONDS)
if stop_event.is_set():
return
stop_event.set()
try:
await ws_send_json({
"type": "session_timeout",
"message": f"Session expired after {SESSION_TIMEOUT_SECONDS} seconds",
})
await websocket.close(code=1000, reason="Session timeout")
except Exception:
pass
try:
await ws_send_json({
"type": "queue_status",
"position": 0,
"total_gpus": 1,
"available_gpus": 1,
})
try:
init_data = await asyncio.wait_for(websocket.receive_json(), timeout=10.0)
except asyncio.TimeoutError:
init_data = {}
init_type = init_data.get("type")
preset_id = init_data.get("preset_id")
preset_label = str(init_data.get("preset_label") or "").strip()
initial_rollout_prompt = str(init_data.get("initial_rollout_prompt") or "").strip()
enhancement_enabled = bool(init_data.get("enhancement_enabled", True))
auto_extension_enabled = bool(init_data.get("auto_extension_enabled", False))
loop_generation_enabled = bool(init_data.get("loop_generation_enabled", False))
single_clip_mode = bool(init_data.get("single_clip_mode", False))
generation_paused = False
if init_type == "session_init_v2":
incoming_prompts = init_data.get("curated_prompts", [])
elif init_type == "select_model":
incoming_prompts = init_data.get("segment_prompts", [])
else:
incoming_prompts = []
curated_prompts = [prompt.strip() for prompt in incoming_prompts if isinstance(prompt, str) and prompt.strip()]
generation_paused = bool(initial_rollout_prompt and not single_clip_mode and len(curated_prompts) == 0)
try:
session_init_image = persist_session_init_image(init_data.get("initial_image"))
except ValueError as exc:
await ws_send_json({
"type": "error",
"message": str(exc),
})
await websocket.close(code=1003, reason="Invalid initial image")
return
timeout_task = asyncio.create_task(session_timeout())
await ws_send_json({
"type": "gpu_assigned",
"gpu_id": 0,
"session_timeout": SESSION_TIMEOUT_SECONDS,
})
raw_prompt_queue: asyncio.Queue[PromptSubmission] = asyncio.Queue()
ready_prompt_queue: asyncio.Queue[ReadyPrompt] = asyncio.Queue()
seed_prompt_memory = list(curated_prompts)
curated_idx = 0
segment_idx = 0
loop_iteration = 0 if generation_paused else 1
prompt_sources_blocked = False
pending_seed_reset = False
pending_seed_reset_reason = ""
pending_simple_submission: PromptSubmission | None = None
single_clip_waiting_for_request = False
rollout_waiting_for_rewrite = False
initial_rollout_waiting_for_rewrite = generation_paused
segment_generation_active = False
rewrite_restart_pending = False
project_active = True
project_stream_started = False
pending_project_end = False
def replace_session_image(initial_image_payload: object) -> None:
nonlocal session_init_image
next_session_image = persist_session_init_image(initial_image_payload)
previous_session_image = session_init_image
session_init_image = next_session_image
if previous_session_image is not None:
cleanup_session_init_image(previous_session_image)
async def send_stream_start(seed_reason: str) -> None:
await ws_send_json({
"type": "ltx2_stream_start",
"total_segments": len(curated_prompts),
"preset_id": preset_id,
"stream_mode": "av_fmp4",
"live_mode": True,
"loop_generation_enabled": loop_generation_enabled,
"loop_iteration": loop_iteration,
"generation_segment_cap": 0,
})
if seed_reason == "init":
await ws_send_json({
"type": "seed_prompts_updated",
"prompts": seed_prompt_memory,
"preset_id": preset_id,
"preset_label": preset_label,
"reason": "init",
"seed_prompt_count": len(seed_prompt_memory),
})
await ws_send_json({
"type": "loop_generation_updated",
"enabled": loop_generation_enabled,
})
await ws_send_json({
"type": "generation_paused_updated",
"paused": generation_paused,
})
else:
await ws_send_json({
"type": "seed_prompts_reset_applied",
"reason": seed_reason or "manual_reset",
"loop_iteration": loop_iteration,
"seed_prompt_count": len(seed_prompt_memory),
"dropped_user_raw_queue": 0,
"dropped_user_ready_queue": 0,
})
async def promote_submission_to_ready(submission: PromptSubmission, ) -> None:
nonlocal prompt_sources_blocked
await ws_send_json({
"type": "prompt_received",
"prompt_id": submission.prompt_id,
"queue_depth": raw_prompt_queue.qsize() + 1,
})
if enhancement_enabled:
await ws_send_json({
"type": "prompt_enhancing",
"prompt_id": submission.prompt_id,
})
await asyncio.sleep(min(LATENCY_MS, 120) / 1000.0)
final_prompt = submission.raw_prompt
await ready_prompt_queue.put(
ReadyPrompt(
prompt=final_prompt,
source="user_enhanced",
prompt_id=submission.prompt_id,
loop_iteration=loop_iteration,
))
await ws_send_json({
"type": "prompt_ready",
"prompt_id": submission.prompt_id,
"prompt": final_prompt,
"source": "user_enhanced",
"latency_ms": float(min(LATENCY_MS, 120)),
})
else:
final_prompt = submission.raw_prompt
await ready_prompt_queue.put(
ReadyPrompt(
prompt=final_prompt,
source="user_raw",
prompt_id=submission.prompt_id,
loop_iteration=loop_iteration,
))
await ws_send_json({
"type": "prompt_ready",
"prompt_id": submission.prompt_id,
"prompt": final_prompt,
"source": "user_raw",
"latency_ms": 0.0,
})
if prompt_sources_blocked:
prompt_sources_blocked = False
await ws_send_json({
"type": "prompt_sources_resumed",
"segment_idx": segment_idx + 1,
"source": "user_prompt",
"prompt_id": submission.prompt_id,
})
async def apply_project_init_payload(payload: dict[str, object], ) -> bool:
nonlocal preset_id
nonlocal preset_label
nonlocal initial_rollout_prompt
nonlocal enhancement_enabled
nonlocal auto_extension_enabled
nonlocal loop_generation_enabled
nonlocal single_clip_mode
nonlocal generation_paused
nonlocal seed_prompt_memory
nonlocal curated_prompts
nonlocal curated_idx
nonlocal segment_idx
nonlocal pending_seed_reset
nonlocal pending_seed_reset_reason
nonlocal pending_simple_submission
nonlocal single_clip_waiting_for_request
nonlocal rollout_waiting_for_rewrite
nonlocal initial_rollout_waiting_for_rewrite
nonlocal rewrite_restart_pending
nonlocal loop_iteration
nonlocal project_active
nonlocal project_stream_started
nonlocal pending_project_end
next_initial_rollout_prompt = str(payload.get("initial_rollout_prompt") or "").strip()
next_preset_id = str(payload.get("preset_id") or "").strip()
next_preset_label = str(payload.get("preset_label") or "").strip()
next_curated_prompts = [
prompt.strip() for prompt in payload.get("curated_prompts", [])
if isinstance(prompt, str) and prompt.strip()
]
try:
replace_session_image(payload.get("initial_image"))
except ValueError as exc:
await ws_send_json({
"type": "error",
"message": str(exc),
})
return False
if next_preset_id:
preset_id = next_preset_id
if next_preset_label:
preset_label = next_preset_label
initial_rollout_prompt = next_initial_rollout_prompt
enhancement_enabled = bool(payload.get("enhancement_enabled", True))
auto_extension_enabled = bool(payload.get("auto_extension_enabled", False))
loop_generation_enabled = bool(payload.get("loop_generation_enabled", False))
single_clip_mode = bool(payload.get("single_clip_mode", False))
seed_prompt_memory = list(next_curated_prompts)
curated_prompts = list(seed_prompt_memory)
curated_idx = 0
segment_idx = 0
pending_seed_reset = False
pending_seed_reset_reason = ""
pending_simple_submission = None
single_clip_waiting_for_request = False
rollout_waiting_for_rewrite = False
generation_paused = bool(initial_rollout_prompt and not single_clip_mode and len(curated_prompts) == 0)
initial_rollout_waiting_for_rewrite = generation_paused
rewrite_restart_pending = False
loop_iteration = 0
project_active = True
project_stream_started = False
pending_project_end = False
return True
def drain_queue_nowait(queue: asyncio.Queue) -> int:
drained = 0
while True:
try:
queue.get_nowait()
drained += 1
except asyncio.QueueEmpty:
break
return drained
async def enter_project_idle() -> None:
nonlocal seed_prompt_memory
nonlocal curated_prompts
nonlocal curated_idx
nonlocal segment_idx
nonlocal generation_paused
nonlocal pending_seed_reset
nonlocal pending_seed_reset_reason
nonlocal pending_simple_submission
nonlocal single_clip_waiting_for_request
nonlocal rollout_waiting_for_rewrite
nonlocal initial_rollout_waiting_for_rewrite
nonlocal rewrite_restart_pending
nonlocal initial_rollout_prompt
nonlocal project_active
nonlocal project_stream_started
nonlocal pending_project_end
dropped_raw = drain_queue_nowait(raw_prompt_queue)
dropped_ready = drain_queue_nowait(ready_prompt_queue)
seed_prompt_memory = []
curated_prompts = []
curated_idx = 0
segment_idx = 0
generation_paused = False
pending_seed_reset = False
pending_seed_reset_reason = ""
pending_simple_submission = None
single_clip_waiting_for_request = False
rollout_waiting_for_rewrite = False
initial_rollout_waiting_for_rewrite = False
rewrite_restart_pending = False
initial_rollout_prompt = ""
project_active = False
pending_project_end = False
if project_stream_started:
project_stream_started = False
await ws_send_json({"type": "ltx2_stream_complete"})
await ws_send_json({
"type": "project_idle",
"dropped_user_raw_queue": dropped_raw,
"dropped_user_ready_queue": dropped_ready,
})
async def reader_loop() -> None:
nonlocal preset_id
nonlocal preset_label
nonlocal enhancement_enabled
nonlocal auto_extension_enabled
nonlocal loop_generation_enabled
nonlocal single_clip_mode
nonlocal generation_paused
nonlocal seed_prompt_memory
nonlocal curated_prompts
nonlocal pending_seed_reset
nonlocal pending_seed_reset_reason
nonlocal pending_simple_submission
nonlocal prompt_sources_blocked
nonlocal rollout_waiting_for_rewrite
nonlocal initial_rollout_waiting_for_rewrite
nonlocal rewrite_restart_pending
nonlocal project_active
nonlocal pending_project_end
while not stop_event.is_set():
try:
data = await websocket.receive_json()
except WebSocketDisconnect:
stop_event.set()
return
except Exception:
stop_event.set()
return
msg_type = data.get("type")
if msg_type == "leave":
stop_event.set()
return
if msg_type == "end_project_keep_session":
loop_generation_enabled = False
generation_paused = False
rollout_waiting_for_rewrite = False
initial_rollout_waiting_for_rewrite = False
rewrite_restart_pending = False
pending_project_end = True
continue
if msg_type == "project_init_v1":
if project_active or pending_project_end:
await ws_send_json({
"type":
"error",
"message": ("Current project is still ending. "
"Wait for the reset to finish before starting another project."),
})
continue
init_applied = await apply_project_init_payload(data)
if not init_applied:
continue
if initial_rollout_waiting_for_rewrite:
await ws_send_json({
"type": "rewrite_seed_prompts_started",
"model": "mock-rewrite-model",
})
seed_prompt_memory = [f"{initial_rollout_prompt} [segment {index}]" for index in range(1, 7)]
curated_prompts = list(seed_prompt_memory)
await ws_send_json({
"type":
"seed_prompts_updated",
"prompts":
seed_prompt_memory,
"preset_id":
preset_id,
"preset_label":
preset_label,
"reason":
"rewrite",
"fallback_used":
False,
"error":
None,
"model":
"mock-rewrite-model",
"latency_ms":
float(min(LATENCY_MS, 150)),
"raw_llm_output":
json.dumps({
"id": preset_id or "current_rollout",
"label": preset_label or "Current rollout",
"segment_prompts": seed_prompt_memory,
}),
})
await ws_send_json({
"type": "rewrite_seed_prompts_complete",
"fallback_used": False,
"error": None,
"preset_id": preset_id,
"preset_label": preset_label,
"model": "mock-rewrite-model",
"latency_ms": float(min(LATENCY_MS, 150)),
})
pending_seed_reset = True
pending_seed_reset_reason = "initial_rewrite"
generation_paused = False
initial_rollout_waiting_for_rewrite = False
await ws_send_json({
"type": "generation_paused_updated",
"paused": generation_paused,
})
else:
pending_seed_reset = True
pending_seed_reset_reason = "project_init"
continue
if not project_active:
if msg_type in {
"append_prompt",
"rewrite_seed_prompts",
"reset_to_seed_prompts",
"restart_generation",
}:
await ws_send_json({
"type": "error",
"message": "Start a new project before sending prompts.",
})
continue
if msg_type == "append_prompt":
if ((rollout_waiting_for_rewrite or initial_rollout_waiting_for_rewrite) and not single_clip_mode):
await ws_send_json({
"type": "error",
"message": ("Use Rewrite to create or restart the rollout."),
})
continue
raw_prompt = str(data.get("prompt", "")).strip()
if not raw_prompt:
await ws_send_json({
"type": "error",
"message": "append_prompt requires a non-empty prompt",
})
continue
await promote_submission_to_ready(
PromptSubmission(
prompt_id=str(data.get("prompt_id") or uuid.uuid4()),
raw_prompt=raw_prompt,
))
continue
if msg_type == "simple_generate":
raw_prompt = str(data.get("prompt", "")).strip()
if not raw_prompt:
await ws_send_json({
"type": "error",
"message": "simple_generate requires a non-empty prompt",
})
continue
try:
replace_session_image(data.get("initial_image"))
except ValueError as exc:
await ws_send_json({
"type": "error",
"message": str(exc),
})
continue
next_preset_id = str(data.get("preset_id") or "").strip()
if next_preset_id:
preset_id = next_preset_id
next_preset_label = str(data.get("preset_label") or "").strip()
if next_preset_label:
preset_label = next_preset_label
enhancement_enabled = bool(data.get("enhancement_enabled", True))
auto_extension_enabled = False
loop_generation_enabled = False
single_clip_mode = True
generation_paused = False
pending_seed_reset = True
pending_seed_reset_reason = "simple_generate"
seed_prompt_memory = ([] if enhancement_enabled else [raw_prompt])
curated_prompts = list(seed_prompt_memory)
pending_simple_submission = (PromptSubmission(
prompt_id=str(data.get("prompt_id") or uuid.uuid4()),
raw_prompt=raw_prompt,
) if enhancement_enabled else None)
continue
if msg_type == "rewrite_seed_prompts":
rewrite_instruction = str(data.get("rewrite_instruction", "")).strip()
if len(seed_prompt_memory) == 0 and not (initial_rollout_waiting_for_rewrite
and rewrite_instruction):
await ws_send_json({
"type": "error",
"message": "No seed prompts available to rewrite.",
})
continue
if (segment_generation_active and not single_clip_mode and not rollout_waiting_for_rewrite
and not initial_rollout_waiting_for_rewrite):
rewrite_restart_pending = True
await ws_send_json({
"type": "rewrite_seed_prompts_started",
"model": "mock-rewrite-model",
})
rollout_id = preset_id or "current_rollout"
rollout_label = preset_label or "Current rollout"
if seed_prompt_memory:
rewritten_prompts = [
f"{prompt} [{rewrite_instruction or 'mock rewrite'}]" for prompt in seed_prompt_memory
]
else:
base_prompt = rewrite_instruction or "mock rewrite"
rewritten_prompts = [f"{base_prompt} [segment {index}]" for index in range(1, 7)]
seed_prompt_memory = rewritten_prompts
curated_prompts = list(seed_prompt_memory)
preset_id = rollout_id
preset_label = rollout_label
if initial_rollout_waiting_for_rewrite:
pending_seed_reset = True
pending_seed_reset_reason = "initial_rewrite"
generation_paused = False
initial_rollout_waiting_for_rewrite = False
await ws_send_json({
"type": "generation_paused_updated",
"paused": generation_paused,
})
if rollout_waiting_for_rewrite:
pending_seed_reset = True
pending_seed_reset_reason = "rewrite_rollout"
generation_paused = False
elif rewrite_restart_pending:
pending_seed_reset = True
pending_seed_reset_reason = "rewrite_during_generation"
generation_paused = False
rewrite_restart_pending = False
await ws_send_json({
"type":
"seed_prompts_updated",
"prompts":
seed_prompt_memory,
"preset_id":
preset_id,
"preset_label":
preset_label,
"reason":
"rewrite",
"fallback_used":
False,
"error":
None,
"model":
"mock-rewrite-model",
"latency_ms":
float(min(LATENCY_MS, 150)),
"raw_llm_output":
json.dumps({
"id": preset_id,
"label": preset_label,
"segment_prompts": seed_prompt_memory,
}),
})
await ws_send_json({
"type": "rewrite_seed_prompts_complete",
"fallback_used": False,
"error": None,
"preset_id": preset_id,
"preset_label": preset_label,
"model": "mock-rewrite-model",
"latency_ms": float(min(LATENCY_MS, 150)),
})
continue
if msg_type == "reset_to_seed_prompts":
pending_seed_reset = True
pending_seed_reset_reason = "manual_reset"
continue
if msg_type == "restart_generation":
pending_seed_reset = True
pending_seed_reset_reason = "cap_restart"
rollout_waiting_for_rewrite = False
await ws_send_json({
"type": "generation_restarted",
"reason": "cap_restart",
"segment_cap": 0,
})
continue
if msg_type == "set_enhancement":
enhancement_enabled = bool(data.get("enabled", enhancement_enabled))
await ws_send_json({
"type": "enhancement_updated",
"enabled": enhancement_enabled,
})
continue
if msg_type == "set_auto_extension":
auto_extension_enabled = bool(data.get("enabled", auto_extension_enabled))
await ws_send_json({
"type": "auto_extension_updated",
"enabled": auto_extension_enabled,
})
continue
if msg_type == "set_loop_generation":
loop_generation_enabled = bool(data.get("enabled", loop_generation_enabled))
await ws_send_json({
"type": "loop_generation_updated",
"enabled": loop_generation_enabled,
})
continue
if msg_type == "set_generation_paused":
generation_paused = bool(data.get("paused", generation_paused))
await ws_send_json({
"type": "generation_paused_updated",
"paused": generation_paused,
})
continue
async def generation_loop() -> None:
nonlocal curated_idx
nonlocal curated_prompts
nonlocal segment_idx
nonlocal loop_iteration
nonlocal pending_seed_reset
nonlocal pending_seed_reset_reason
nonlocal pending_simple_submission
nonlocal prompt_sources_blocked
nonlocal single_clip_waiting_for_request
nonlocal rollout_waiting_for_rewrite
nonlocal initial_rollout_waiting_for_rewrite
nonlocal segment_generation_active
nonlocal rewrite_restart_pending
nonlocal project_active
nonlocal project_stream_started
nonlocal pending_project_end
while not stop_event.is_set():
if pending_project_end:
await enter_project_idle()
await asyncio.sleep(0)
continue
if not project_active:
await asyncio.sleep(0.05)
continue
if pending_seed_reset:
curated_prompts = list(seed_prompt_memory)
curated_idx = 0
segment_idx = 0
pending_seed_reset = False
loop_iteration += 1
prompt_sources_blocked = False
single_clip_waiting_for_request = False
rollout_waiting_for_rewrite = False
project_stream_started = True
await send_stream_start(pending_seed_reset_reason)
pending_seed_reset_reason = ""
if pending_simple_submission is not None:
submission = pending_simple_submission
pending_simple_submission = None
await promote_submission_to_ready(submission)
if generation_paused:
await asyncio.sleep(0.05)
continue
if rewrite_restart_pending:
await asyncio.sleep(0.05)
continue
if (not single_clip_mode and not rollout_waiting_for_rewrite and GENERATION_SEGMENT_CAP > 0
and segment_idx >= GENERATION_SEGMENT_CAP):
rollout_waiting_for_rewrite = True
loop_generation_enabled = False
project_stream_started = False
await ws_send_json({
"type": "ltx2_stream_complete",
})
await asyncio.sleep(0.05)
continue
if rollout_waiting_for_rewrite:
await asyncio.sleep(0.05)
continue
if initial_rollout_waiting_for_rewrite:
await asyncio.sleep(0.05)
continue
selected = None
try:
selected = ready_prompt_queue.get_nowait()
except asyncio.QueueEmpty:
if curated_idx < len(curated_prompts):
prompt_idx = curated_idx
selected = ReadyPrompt(
prompt=curated_prompts[prompt_idx],
source="curated",
seed_prompt_index=prompt_idx,
loop_iteration=loop_iteration,
)
curated_idx += 1
if selected is None:
if single_clip_mode:
await asyncio.sleep(0.05)
continue
if not prompt_sources_blocked:
prompt_sources_blocked = True
await ws_send_json({
"type": "prompt_sources_blocked",
"segment_idx": segment_idx + 1,
})
await asyncio.sleep(0.05)
continue
if prompt_sources_blocked:
prompt_sources_blocked = False
await ws_send_json({
"type": "prompt_sources_resumed",
"segment_idx": segment_idx + 1,
"source": selected.source,
"prompt_id": selected.prompt_id,
})
segment_idx += 1
single_clip_waiting_for_request = False
total_segments_hint = max(segment_idx, len(curated_prompts))
await ws_send_json({
"type": "segment_prompt_source",
"segment_idx": segment_idx,
"source": selected.source,
"prompt_id": selected.prompt_id,
"fallback_used": False,
"seed_prompt_index": selected.seed_prompt_index,
"loop_iteration": selected.loop_iteration or loop_iteration,
})
await ws_send_json({
"type": "ltx2_segment_start",
"segment_idx": segment_idx,
"total_segments": total_segments_hint,
"prompt": selected.prompt,
"source": selected.source,
"seed_prompt_index": selected.seed_prompt_index,
"loop_iteration": selected.loop_iteration or loop_iteration,
})
segment_generation_active = True
await asyncio.sleep(LATENCY_MS / 1000.0)
await _stream_mock_segment(
ws_send_json,
ws_send_bytes,
segment_idx=segment_idx,
)
await ws_send_json({
"type": "step_complete",
"latency_ms": {
"total": round(LATENCY_MS + 120.0, 2),
"worker_e2e": round(float(LATENCY_MS), 2),
"main_user_step": round(LATENCY_MS + 120.0, 2),
"overhead": 120.0,
},
})
await ws_send_json({
"type": "ltx2_segment_complete",
"segment_idx": segment_idx,
"total_segments": total_segments_hint,
})
segment_generation_active = False
if single_clip_mode and not single_clip_waiting_for_request:
single_clip_waiting_for_request = True
project_stream_started = False
await ws_send_json({
"type": "ltx2_stream_complete",
})
reader_task = asyncio.create_task(reader_loop())
generation_task = asyncio.create_task(generation_loop())
await asyncio.sleep(0)
await ws_send_json({
"type": "loop_generation_updated",
"enabled": loop_generation_enabled,
})
await ws_send_json({
"type": "generation_paused_updated",
"paused": generation_paused,
})
if pending_project_end:
await enter_project_idle()
elif initial_rollout_waiting_for_rewrite:
await ws_send_json({
"type": "rewrite_seed_prompts_started",
"model": "mock-rewrite-model",
})
seed_prompt_memory = [f"{initial_rollout_prompt} [segment {index}]" for index in range(1, 7)]
curated_prompts = list(seed_prompt_memory)
await ws_send_json({
"type":
"seed_prompts_updated",
"prompts":
seed_prompt_memory,
"preset_id":
preset_id,
"preset_label":
preset_label,
"reason":
"rewrite",
"fallback_used":
False,
"error":
None,
"model":
"mock-rewrite-model",
"latency_ms":
float(min(LATENCY_MS, 150)),
"raw_llm_output":
json.dumps({
"id": preset_id or "current_rollout",
"label": preset_label or "Current rollout",
"segment_prompts": seed_prompt_memory,
}),
})
await ws_send_json({
"type": "rewrite_seed_prompts_complete",
"fallback_used": False,
"error": None,
"preset_id": preset_id,
"preset_label": preset_label,
"model": "mock-rewrite-model",
"latency_ms": float(min(LATENCY_MS, 150)),
})
pending_seed_reset = True
pending_seed_reset_reason = "initial_rewrite"
generation_paused = False
initial_rollout_waiting_for_rewrite = False
await ws_send_json({
"type": "generation_paused_updated",
"paused": generation_paused,
})
else:
project_stream_started = True
await send_stream_start("init")
done, pending = await asyncio.wait(
{reader_task, generation_task, timeout_task},
return_when=asyncio.FIRST_COMPLETED,
)
for task in pending:
task.cancel()
for task in done:
try:
await task
except asyncio.CancelledError:
pass
except WebSocketDisconnect:
print(f"Client {client_id[:8]} disconnected")
except Exception as exc:
print(f"Client {client_id[:8]} error: {exc}")
try:
await websocket.send_json({
"type": "error",
"message": f"Mock streaming failed: {exc}",
})
except Exception:
pass
import traceback
traceback.print_exc()
finally:
stop_event.set()
cleanup_session_init_image(session_init_image)
for static_dir in FRONTEND_STATIC_DIR_CANDIDATES:
if os.path.isdir(static_dir):
app.mount("/", StaticFiles(directory=static_dir, html=True), name="static")
break
def cli() -> None:
global LATENCY_MS
import argparse
import uvicorn
parser = argparse.ArgumentParser(description="Protocol-compatible mock server for the LTX2 UI")
parser.add_argument(
"--latency",
type=int,
default=200,
help="Simulated model latency in milliseconds",
)
parser.add_argument(
"--port",
type=int,
default=8009,
help="Server port",
)
args = parser.parse_args()
LATENCY_MS = args.latency
print(f"Starting mock server with {LATENCY_MS}ms latency on port {args.port}")
_install_heartbeat_log_filter()
uvicorn.run(app, host="0.0.0.0", port=args.port)
if __name__ == "__main__":
cli()