mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-19 11:04:43 +00:00
Trains a real fal.ai LoRA fine-tune (fal-ai/flux-lora-fast-training) on frames extracted from the real fal.ai-generated room-visualization MP4s already captured this session, so display-layer generation has a specific trained style target instead of prompt engineering against a stock model. Architecture boundary: this model only ever produces supplementary synthetic visual/pixel output. It has no path into measured/derived RF, pose, vitals, identity, or confidence values, which remain local and authoritative. Real run: request 01a060b2-c940-7972-abf9-ea48847eb2ae, 1000 steps, 27 training images, ~$2.04 spend, validated with a real inference call against the trained LoRA (assets/fal-visual-teacher-sample.png).
99 lines
3.9 KiB
Python
99 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract training stills from the real fal.ai room-visualization MP4s.
|
|
|
|
Reads the source videos (real fal.ai-generated room visualizations, one per
|
|
style preset) and writes JPEG frames + a caption .txt per frame into a local
|
|
(gitignored) training-data directory, ready to be zipped for fal.ai LoRA
|
|
training. Frame interval is time-based (not frame-count-based) so each
|
|
source contributes visual diversity proportional to its length.
|
|
"""
|
|
|
|
import argparse
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
# (source mp4 path, style tag, caption) -- sources are real fal.ai output,
|
|
# captured earlier this session by the fal-room-viz / room-viz-h3-upgrade
|
|
# agents. Caption describes the RuView visual style so the LoRA learns the
|
|
# recurring motifs (glowing pose overlay, WiFi signal arcs, vitals readout,
|
|
# dark tech aesthetic) rather than any single room layout.
|
|
SOURCES = [
|
|
("/tmp/room-viz-architectural/room-viz.mp4", "architectural",
|
|
"ruview style room visualization, architectural wireframe overlay, "
|
|
"glowing cyan pose skeleton, wifi signal arcs, dark tech aesthetic"),
|
|
("/tmp/room-viz-abstract/room-viz.mp4", "abstract",
|
|
"ruview style room visualization, abstract particle field, "
|
|
"glowing pose overlay, wifi signal arcs, dark tech aesthetic"),
|
|
("/tmp/room-viz-branded/room-viz.mp4", "branded",
|
|
"ruview style room visualization, branded hud overlay, "
|
|
"glowing cyan and emerald pose skeleton, vitals readout, dark tech aesthetic"),
|
|
("/tmp/room-viz-h3-architectural/room-viz.mp4", "h3-architectural",
|
|
"ruview style room visualization, architectural wireframe overlay, "
|
|
"glowing pose skeleton, wifi signal arcs, vitals readout, dark tech aesthetic"),
|
|
("/tmp/room-viz-h3-cinematic/room-viz.mp4", "h3-cinematic",
|
|
"ruview style room visualization, cinematic lighting, "
|
|
"glowing pose overlay, wifi signal arcs, vitals readout, dark tech aesthetic"),
|
|
("/tmp/room-viz-h3-minimal/room-viz.mp4", "h3-minimal",
|
|
"ruview style room visualization, minimal clean overlay, "
|
|
"glowing pose skeleton, subtle wifi signal arcs, dark tech aesthetic"),
|
|
("/tmp/room-viz-h3-abstract/room-viz.mp4", "h3-abstract",
|
|
"ruview style room visualization, abstract particle field, "
|
|
"glowing pose overlay, wifi signal arcs, dark tech aesthetic"),
|
|
]
|
|
|
|
FRAME_INTERVAL_SEC = 1.5
|
|
|
|
|
|
def extract(src: Path, style: str, caption: str, out_dir: Path) -> int:
|
|
pattern = out_dir / f"{style}_%03d.jpg"
|
|
cmd = [
|
|
"ffmpeg", "-y", "-loglevel", "error",
|
|
"-i", str(src),
|
|
"-vf", f"fps=1/{FRAME_INTERVAL_SEC}",
|
|
"-q:v", "2",
|
|
str(pattern),
|
|
]
|
|
subprocess.run(cmd, check=True)
|
|
frames = sorted(out_dir.glob(f"{style}_*.jpg"))
|
|
for frame in frames:
|
|
frame.with_suffix(".txt").write_text(caption, encoding="utf-8")
|
|
return len(frames)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"--out", default=str(REPO_ROOT / "scripts" / "fal-visual-teacher" / "training-data"),
|
|
help="Output directory for extracted frames + captions (gitignored)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
out_dir = Path(args.out)
|
|
if out_dir.exists():
|
|
shutil.rmtree(out_dir)
|
|
out_dir.mkdir(parents=True)
|
|
|
|
total = 0
|
|
for src_str, style, caption in SOURCES:
|
|
src = Path(src_str)
|
|
if not src.exists():
|
|
print(f"SKIP (missing): {src}", file=sys.stderr)
|
|
continue
|
|
n = extract(src, style, caption, out_dir)
|
|
print(f"{style}: {n} frames from {src}")
|
|
total += n
|
|
|
|
print(f"\nTotal frames extracted: {total}")
|
|
print(f"Training data dir: {out_dir}")
|
|
if total < 4:
|
|
print("ERROR: fal.ai training needs at least 4 images", file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|