|
| 1 | +# nuScenes Fixture Setup (Fresh Clone, Local Testing Only) |
| 2 | + |
| 3 | +## Important: this is a fixture workflow |
| 4 | + |
| 5 | +This guide creates a **local testing fixture** for HyperView. |
| 6 | + |
| 7 | +- It is for **UI/API smoke testing and iteration**. |
| 8 | +- It is **not** a real Cosmos-Curate production run. |
| 9 | +- Captions/embeddings in the fixture are synthetic (generated by `scripts/create_nuscenes_fixture.py`). |
| 10 | +- Videos are built from local nuScenes camera frames to avoid solid-color test clips. |
| 11 | + |
| 12 | +## Prerequisites |
| 13 | + |
| 14 | +- macOS/Linux shell |
| 15 | +- `uv` installed |
| 16 | +- `node` + `npm` installed |
| 17 | +- `ffmpeg` installed (for frame->mp4 clip generation) |
| 18 | +- nuScenes mini data available under `~/nuscenes` |
| 19 | + - Must include: |
| 20 | + - `~/nuscenes/v1.0-mini` |
| 21 | + - `~/nuscenes/samples` |
| 22 | + - `~/nuscenes/sweeps` |
| 23 | + |
| 24 | +Check quickly: |
| 25 | + |
| 26 | +```bash |
| 27 | +ls -d ~/nuscenes/v1.0-mini ~/nuscenes/samples ~/nuscenes/sweeps |
| 28 | +``` |
| 29 | + |
| 30 | +If `ffmpeg` is missing on macOS: |
| 31 | + |
| 32 | +```bash |
| 33 | +brew install ffmpeg |
| 34 | +``` |
| 35 | + |
| 36 | +## 1) Fresh clone + dependencies |
| 37 | + |
| 38 | +```bash |
| 39 | +git clone <YOUR_FORK_OR_REPO_URL> nvidia-hyper |
| 40 | +cd nvidia-hyper |
| 41 | + |
| 42 | +uv sync |
| 43 | +cd frontend |
| 44 | +npm install |
| 45 | +cd .. |
| 46 | +``` |
| 47 | + |
| 48 | +## 2) Build nuScenes source clips (real image frames -> mp4) |
| 49 | + |
| 50 | +This creates `48` short clips from `~/nuscenes/sweeps/CAM_FRONT`: |
| 51 | + |
| 52 | +```bash |
| 53 | +uv run python - <<'PY' |
| 54 | +import glob |
| 55 | +import os |
| 56 | +import shutil |
| 57 | +import subprocess |
| 58 | +from pathlib import Path |
| 59 | +
|
| 60 | +src_dir = Path.home() / 'nuscenes' / 'sweeps' / 'CAM_FRONT' |
| 61 | +out_dir = Path('/private/tmp/nuscenes_source_clips') |
| 62 | +work_root = Path('/private/tmp/nuscenes_frames_work') |
| 63 | +
|
| 64 | +num_clips = 48 |
| 65 | +frames_per_clip = 20 |
| 66 | +stride = 12 |
| 67 | +fps = 10 |
| 68 | +
|
| 69 | +if not src_dir.exists(): |
| 70 | + raise SystemExit(f'Missing source dir: {src_dir}') |
| 71 | +
|
| 72 | +frames = sorted(glob.glob(str(src_dir / '*.jpg'))) |
| 73 | +if len(frames) < frames_per_clip: |
| 74 | + raise SystemExit(f'Not enough frames: {len(frames)}') |
| 75 | +
|
| 76 | +out_dir.mkdir(parents=True, exist_ok=True) |
| 77 | +work_root.mkdir(parents=True, exist_ok=True) |
| 78 | +
|
| 79 | +for old in out_dir.glob('*.mp4'): |
| 80 | + old.unlink() |
| 81 | +
|
| 82 | +max_possible = 1 + (len(frames) - frames_per_clip) // stride |
| 83 | +clips_to_make = min(num_clips, max_possible) |
| 84 | +
|
| 85 | +for clip_idx in range(clips_to_make): |
| 86 | + start = clip_idx * stride |
| 87 | + chunk = frames[start:start + frames_per_clip] |
| 88 | + if len(chunk) < frames_per_clip: |
| 89 | + break |
| 90 | +
|
| 91 | + clip_work = work_root / f'clip_{clip_idx + 1:03d}' |
| 92 | + if clip_work.exists(): |
| 93 | + shutil.rmtree(clip_work) |
| 94 | + clip_work.mkdir(parents=True, exist_ok=True) |
| 95 | +
|
| 96 | + for i, src in enumerate(chunk, start=1): |
| 97 | + dst = clip_work / f'{i:04d}.jpg' |
| 98 | + os.symlink(src, dst) |
| 99 | +
|
| 100 | + out_mp4 = out_dir / f'clip_{clip_idx + 1:03d}.mp4' |
| 101 | + subprocess.run( |
| 102 | + [ |
| 103 | + 'ffmpeg', '-y', '-v', 'error', |
| 104 | + '-framerate', str(fps), |
| 105 | + '-i', str(clip_work / '%04d.jpg'), |
| 106 | + '-vf', 'scale=640:360:force_original_aspect_ratio=decrease,pad=640:360:(ow-iw)/2:(oh-ih)/2', |
| 107 | + '-c:v', 'libx264', '-pix_fmt', 'yuv420p', |
| 108 | + str(out_mp4), |
| 109 | + ], |
| 110 | + check=True, |
| 111 | + ) |
| 112 | +
|
| 113 | +print(f'Created {len(list(out_dir.glob("*.mp4")))} clips in {out_dir}') |
| 114 | +PY |
| 115 | +``` |
| 116 | + |
| 117 | +## 3) Create the full fixture artifacts |
| 118 | + |
| 119 | +This creates: |
| 120 | + |
| 121 | +- `/private/tmp/nuscenes_fixture_real/split_output` |
| 122 | +- `/private/tmp/nuscenes_fixture_real/dedup_output` |
| 123 | + |
| 124 | +```bash |
| 125 | +uv run python scripts/create_nuscenes_fixture.py \ |
| 126 | + --source-clips-path /private/tmp/nuscenes_source_clips \ |
| 127 | + --output-root /private/tmp/nuscenes_fixture_real \ |
| 128 | + --num-clips 48 \ |
| 129 | + --embedding-dim 256 |
| 130 | +``` |
| 131 | + |
| 132 | +## 4) Run backend (fixed port `6263`) |
| 133 | + |
| 134 | +```bash |
| 135 | +lsof -ti tcp:6263 | xargs -r kill -9 |
| 136 | + |
| 137 | +uv run python scripts/load_cosmos_curate.py \ |
| 138 | + --split-output-path /private/tmp/nuscenes_fixture_real/split_output \ |
| 139 | + --dataset-name nuscenes_fixture_real_live \ |
| 140 | + --no-persist \ |
| 141 | + --no-browser \ |
| 142 | + --port 6263 |
| 143 | +``` |
| 144 | + |
| 145 | +## 5) Run frontend (fixed port `3001`) |
| 146 | + |
| 147 | +Open a second terminal: |
| 148 | + |
| 149 | +```bash |
| 150 | +cd frontend |
| 151 | +PORT=3001 npm run dev -- --webpack |
| 152 | +``` |
| 153 | + |
| 154 | +Open: |
| 155 | + |
| 156 | +- `http://127.0.0.1:3001` |
| 157 | + |
| 158 | +## 6) Smoke-check that video serving works |
| 159 | + |
| 160 | +In another terminal: |
| 161 | + |
| 162 | +```bash |
| 163 | +uv run python - <<'PY' |
| 164 | +import json |
| 165 | +import os |
| 166 | +import urllib.request |
| 167 | +
|
| 168 | +base = 'http://127.0.0.1:6263' |
| 169 | +ds = json.load(urllib.request.urlopen(base + '/api/dataset')) |
| 170 | +print('dataset=', ds.get('name')) |
| 171 | +
|
| 172 | +samples = json.load(urllib.request.urlopen(base + '/api/samples?offset=0&limit=1')).get('samples', []) |
| 173 | +if not samples: |
| 174 | + raise SystemExit('No samples loaded') |
| 175 | +
|
| 176 | +sid = samples[0]['id'] |
| 177 | +video = urllib.request.urlopen(base + f'/api/video/{sid}') |
| 178 | +print('video_status=', video.status, 'content_type=', video.headers.get('Content-Type')) |
| 179 | +
|
| 180 | +all_samples = json.load(urllib.request.urlopen(base + '/api/samples?offset=0&limit=500')).get('samples', []) |
| 181 | +missing = 0 |
| 182 | +for sample in all_samples: |
| 183 | + metadata = sample.get('metadata') or {} |
| 184 | + path = metadata.get('video_path') or sample.get('filepath') or '' |
| 185 | + ok = isinstance(path, str) and path.endswith('.mp4') and os.path.exists(path) |
| 186 | + if not ok: |
| 187 | + missing += 1 |
| 188 | +print('total=', len(all_samples), 'missing_video_paths=', missing) |
| 189 | +PY |
| 190 | +``` |
| 191 | + |
| 192 | +Expected: |
| 193 | + |
| 194 | +- `dataset= nuscenes_fixture_real_live` |
| 195 | +- `video_status= 200` |
| 196 | +- `content_type= video/mp4` |
| 197 | +- `missing_video_paths= 0` |
| 198 | + |
| 199 | +## Troubleshooting |
| 200 | + |
| 201 | +### I still see solid-color videos |
| 202 | + |
| 203 | +Most common causes: |
| 204 | + |
| 205 | +1. Backend still points to old synthetic fixture (example: `/private/tmp/cookoff_fixture_256`). |
| 206 | +2. Browser has stale frontend state/cache. |
| 207 | +3. Another backend process is running on `6263`. |
| 208 | + |
| 209 | +Fix: |
| 210 | + |
| 211 | +```bash |
| 212 | +lsof -ti tcp:6263 | xargs -r kill -9 |
| 213 | +``` |
| 214 | + |
| 215 | +Then restart backend with: |
| 216 | + |
| 217 | +```bash |
| 218 | +uv run python scripts/load_cosmos_curate.py \ |
| 219 | + --split-output-path /private/tmp/nuscenes_fixture_real/split_output \ |
| 220 | + --dataset-name nuscenes_fixture_real_live \ |
| 221 | + --no-persist \ |
| 222 | + --no-browser \ |
| 223 | + --port 6263 |
| 224 | +``` |
| 225 | + |
| 226 | +Then hard refresh frontend (`Cmd+Shift+R`). |
| 227 | + |
| 228 | +### I only have `v1.0-mini` JSON metadata, no images |
| 229 | + |
| 230 | +You need the nuScenes media folders (`samples/`, `sweeps/`) under `~/nuscenes`. |
| 231 | +Without those, clip generation cannot run. |
0 commit comments