diff --git a/apps/fastvideo_studio/mock_server.py b/apps/fastvideo_studio/mock_server.py index 2af805b91..98e241cf7 100644 --- a/apps/fastvideo_studio/mock_server.py +++ b/apps/fastvideo_studio/mock_server.py @@ -4,8 +4,8 @@ In-memory mock of the FastVideo Studio API (``server.py``). This mock implements the same ``/api`` routes and response shapes as the real FastAPI server but keeps everything in memory and never touches the FastVideo -library, a GPU, or a database. It is meant to back the Playwright e2e suite so -the Next.js frontend can be exercised end-to-end without a real backend. +library or a GPU. It is meant to back the Playwright e2e suite so the Next.js +frontend can be exercised end-to-end without a real backend. Job lifecycle is simulated by recording a start timestamp and *computing* the status on read: a started job reports ``running`` for a few seconds and then @@ -13,14 +13,24 @@ flips to ``completed`` with an ``output_path``, so polling the job list / logs shows progression. Generated media is a tiny 1s ``testsrc`` MP4 built lazily with ffmpeg and cached. +Pass ``--data-dir`` to seed jobs/datasets/settings from a real server's +persisted SQLite database instead of the built-in fake demo data, so you can +browse real job history through the mock without a GPU. This is a read-only +snapshot taken once at startup: nothing created or changed while the mock +runs is ever written back to that database, so it's safe to point at the +same ``--data-dir`` a real ``server.py`` is using. + Usage (from the ``apps/`` directory):: PYTHONPATH=.. python -m fastvideo_studio.mock_server --port 8190 + PYTHONPATH=.. python -m fastvideo_studio.mock_server --port 8190 --data-dir ../outputs/ui_data """ from __future__ import annotations import contextlib +import json +import logging import os import random import shutil @@ -29,22 +39,31 @@ import tempfile import threading import time import uuid +from pathlib import Path from typing import Annotated, Any from fastapi import FastAPI, File, HTTPException, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, PlainTextResponse -from fastvideo_studio.database import default_settings_dict +from fastvideo_studio.database import Database, default_settings_dict from fastvideo_studio.models import (CreateDatasetRequest, CreateJobRequest, SettingsUpdate, UpdateCaptionRequest, model_label) +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s") +logger = logging.getLogger("fastvideo.studio.mock") + # --- Config ----------------------------------------------------------------- # How long a started job stays "running" before it flips to "completed". COMPLETE_AFTER_SECONDS = 3.0 FFMPEG_BIN = shutil.which(os.getenv("FASTVIDEO_FFMPEG_BIN", "ffmpeg")) +# Marks a job dict as a frozen row loaded from a real --data-dir snapshot +# (see _load_snapshot_from_db) rather than something this process is +# simulating. Stripped from API responses by _public_job. +_FROM_SNAPSHOT_KEY = "_from_db_snapshot" + # A small catalogue of fake models keyed by workload type. Mirrors the real # server's {id, label} shape (label derived from the HF-style path). _MODELS_BY_WORKLOAD: dict[str, list[str]] = { @@ -54,6 +73,10 @@ _MODELS_BY_WORKLOAD: dict[str, list[str]] = { ], "i2v": [ "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers", + # Real registered id (fastvideo/registry.py) -- needed so the frontend's + # `modelId.toLowerCase().includes('minimax-h3')` gate (speech-tag + # console, references, last-frame upload) actually lights up in the mock. + "MiniMaxAI/MiniMax-H3", ], "t2i": [ "black-forest-labs/FLUX.1-schnell", @@ -98,16 +121,76 @@ _mock_media_cache: dict[str, str] = {} _mock_media_lock = threading.Lock() +def _testsrc_frame(width: int, height: int, index: int): + """A cheap, dependency-light stand-in for ffmpeg's `testsrc` pattern: + a gradient that shifts over `index` so an encoded clip visibly animates.""" + import numpy as np + + frame = np.zeros((height, width, 3), dtype=np.uint8) + frame[:, :, 0] = np.linspace(0, 255, width, dtype=np.uint8)[None, :] + frame[:, :, 1] = np.linspace(0, 255, height, dtype=np.uint8)[:, None] + frame[:, :, 2] = (index * 10) % 256 + return frame + + +def _build_mock_media_with_pyav(kind: str, path: str) -> None: + """ffmpeg isn't installed on every dev box; PyAV (a real FastVideo + dependency already, used for reference-video decoding) can encode the + same tiny clip directly via its bundled libav, no external binary.""" + from PIL import Image + + if kind == "png": + Image.fromarray(_testsrc_frame(320, 240, 0)).save(path) + return + + import av as pyav + + # h264 is what every browser's