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
46 changes: 28 additions & 18 deletions scripts/generate_fastvideo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@
from _runner_common import emit_runtime_fingerprint, establish_process_group # noqa: E402


_DENOISE_STEP_PATTERN = re.compile(
r'\bdenois(?:e|ing)\s+step\s*(\d+)\s*/\s*(\d+)\b', re.IGNORECASE,
)
_PERCENT_PATTERN = re.compile(r'\d+%')


def translate_line(line: str) -> str:
"""Translate one upstream output line into PortOS's progress protocol."""
step_match = _DENOISE_STEP_PATTERN.search(line)
if step_match:
cur, total = int(step_match.group(1)), int(step_match.group(2))
return f"STAGE:fastvideo:step:{cur}:{total}:denoising step {cur}/{total}"
if _PERCENT_PATTERN.search(line):
return f"STATUS:FastVideo: {line}"
if "loading" in line.lower() or "encoding" in line.lower():
return f"STATUS:{line}"
return line


def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="PortOS FastVideo MLX helper")
p.add_argument("--repo-dir", default=None, help="Path to cloned FastVideo repo")
Expand Down Expand Up @@ -88,6 +107,9 @@ def main() -> int:
"--width", str(args.width),
"--height", str(args.height),
"--num-frames", str(args.num_frames),
"--num-inference-steps", str(args.steps),
"--fps", str(args.fps),
"--seed", str(args.seed),
"--output-path", str(args.output),
]
if args.fast:
Expand Down Expand Up @@ -115,28 +137,16 @@ def main() -> int:
)

assert proc.stdout is not None
# Parse output lines and map to STAGE: / STATUS: protocols
step_pattern = re.compile(r'(?:step|Step)\s*(\d+)[/:](\d+)', re.IGNORECASE)
percent_pattern = re.compile(r'(\d+)%')

# Parse output lines and map to STAGE: / STATUS: protocols. Only the
# upstream denoising-step message represents render progress. Startup
# model-loading bars also contain percentages (often ending at 100%) and
# must remain status output or the generic server parser will report them
# as completed rendering.
for raw in proc.stdout:
line = raw.rstrip()
if not line:
continue

step_match = step_pattern.search(line)
if step_match:
cur, total = int(step_match.group(1)), int(step_match.group(2))
print(f"STAGE:fastvideo:step:{cur}:{total}:denoising step {cur}/{total}", file=sys.stderr, flush=True)
else:
pct_match = percent_pattern.search(line)
if pct_match:
pct = int(pct_match.group(1))
print(f"STAGE:fastvideo:step:{pct}:100:generating {pct}%", file=sys.stderr, flush=True)
elif "loading" in line.lower() or "encoding" in line.lower():
print(f"STATUS:{line}", file=sys.stderr, flush=True)
else:
print(line, file=sys.stderr, flush=True)
print(translate_line(line), file=sys.stderr, flush=True)

return_code = proc.wait()
if return_code != 0:
Expand Down
47 changes: 47 additions & 0 deletions scripts/generate_fastvideo.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { resolveTestPython } from '../server/lib/testHelper.js';

const script = join(dirname(fileURLToPath(import.meta.url)), 'generate_fastvideo.py');
const pyBin = resolveTestPython();
const runPython = (source) => execFileSync(pyBin, ['-c', source, script], { encoding: 'utf8' });
const lines = (output) => output.trim().split('\n').map((line) => line.trimEnd());

const importRunner = [
'import importlib.util, sys',
'from pathlib import Path',
'script = Path(sys.argv[1])',
'spec = importlib.util.spec_from_file_location("generate_fastvideo", script)',
'runner = importlib.util.module_from_spec(spec)',
'spec.loader.exec_module(runner)',
].join('\n');

describe.skipIf(!pyBin)('generate_fastvideo.py', () => {
it('reports only denoising steps as render progress', () => {
const output = runPython(`${importRunner}\n${[
'print(runner.translate_line("Loading checkpoint: 100%|##########| 10/10"))',
'print(runner.translate_line("denoise step 1/3 complete"))',
'print(runner.translate_line("denoising step 3 / 3 complete"))',
].join('\n')}`);

expect(lines(output)).toEqual([
'STATUS:FastVideo: Loading checkpoint: 100%|##########| 10/10',
'STAGE:fastvideo:step:1:3:denoising step 1/3',
'STAGE:fastvideo:step:3:3:denoising step 3/3',
]);
});

it('does not treat an unrelated step or percentage as render completion', () => {
const output = runPython(`${importRunner}\n${[
'print(runner.translate_line("Loading pipeline step 3/3"))',
'print(runner.translate_line("100%|##########| 1/1"))',
].join('\n')}`);

expect(lines(output)).toEqual([
'STATUS:Loading pipeline step 3/3',
'STATUS:FastVideo: 100%|##########| 1/1',
]);
});
});