multimodalart HF Staff commited on
Commit
f40d97f
Β·
verified Β·
1 Parent(s): 844bc93

Molmo2Fish interactive sonar fish tracking demo

Browse files
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/elwha_2018-07-29.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ examples/kenai_leftfar_2018-06-03.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ examples/nushagak_rb_f15-52.mp4 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,55 @@
1
  ---
2
- title: Molmo2fish Tracking
3
- emoji: πŸ¦€
4
- colorFrom: red
5
- colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.25.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Molmo2Fish Tracking
3
+ emoji: 🐟
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.25.0
 
8
  app_file: app.py
9
+ short_description: Track fish in sonar video, fix it with plain English
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 1h
12
  ---
13
 
14
+ # 🐟 Molmo2Fish β€” interactive fish tracking with natural language guidance
15
+
16
+ Demo of [**tidalove/Molmo2Fish**](https://huggingface.co/tidalove/Molmo2Fish), the model from
17
+ *"Teach a Molmo2Fish: Towards interactive fish tracking with natural language guidance"*
18
+ ([paper](https://huggingface.co/papers/2608.18602) Β·
19
+ [code](https://github.com/tidalove/molmo2fish)).
20
+
21
+ Molmo2Fish is a LoRA-finetuned [Molmo2](https://huggingface.co/allenai) VLM that tracks
22
+ salmon in ARIS **sonar** video and β€” crucially β€” accepts a plain-English critique of its
23
+ own output and re-emits corrected tracks. The paper reports tracking accuracy going from
24
+ ~5% to ~79% over a handful of conversational correction turns.
25
+
26
+ ## How the demo works
27
+
28
+ 1. **β‘  Track all fish** sends the clip with the prompt `track all fish`. The model replies
29
+ with the html-v2 pointing format used in training:
30
+ `<tracks coords="0.0 1 409 852;0.5 1 436 890 2 300 120;…">fish</tracks>`
31
+ (timestamps in seconds at 2 FPS, ids, and x/y normalised to 0–1000).
32
+ 2. **β‘‘ Apply correction** rebuilds the conversation β€” video on the first user turn, the
33
+ model's previous `<tracks …>` answer as the assistant turn, your critique as the new
34
+ user turn β€” exactly as in `olmo/eval/vllm_runner.py::build_multi_turn_chat`, and the
35
+ model regenerates the track set.
36
+
37
+ Points are parsed with the same regexes as
38
+ `olmo/preprocessing/point_formatter.py` and drawn back onto the 6 FPS source video.
39
+
40
+ ## Example clips
41
+
42
+ The three sonar clips shipped with this Space are re-encoded (G channel, 6 FPS, libx264,
43
+ matching the repo's `encode_frames_to_video` recipe) from the Caltech Fish Counting
44
+ release [**perona-lab/cfc26**](https://huggingface.co/datasets/perona-lab/cfc26), which is
45
+ distributed under **CC-BY-4.0** β€” credit to the Caltech Fish Counting / CFC26 authors.
46
+ The correction prompts pre-filled with each example are verbatim from the validation split
47
+ of [tidalove/cfc-track-instruction](https://huggingface.co/datasets/tidalove/cfc-track-instruction).
48
+
49
+ ## Notes
50
+
51
+ - Runs on ZeroGPU; the model is loaded in bfloat16 (~16 GB VRAM).
52
+ - Video is sampled at 2 FPS, max 128 frames, per the model's
53
+ `video_preprocessor_config.json`.
54
+ - A truncated answer (no closing `</tracks>`) means you hit the *Max new tokens* cap β€”
55
+ raise it in **Advanced**.
app.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Molmo2Fish β€” interactive fish tracking in ARIS sonar video with natural-language guidance.
2
+
3
+ Paper: "Teach a Molmo2Fish: Towards interactive fish tracking with natural language
4
+ guidance" (arXiv 2608.18602). Model: tidalove/Molmo2Fish.
5
+
6
+ The demo mirrors the paper's two-stage correction loop:
7
+ 1. an initial pass ("track all fish") produces `<tracks coords="...">fish</tracks>`
8
+ 2. the user types a plain-English critique and the model re-emits corrected tracks,
9
+ conditioned on the video, its own previous answer, and the critique.
10
+ """
11
+
12
+ import os
13
+
14
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
15
+
16
+ import spaces # noqa: E402 β€” must precede torch / CUDA-touching imports
17
+
18
+ import re # noqa: E402
19
+ import tempfile # noqa: E402
20
+ import time # noqa: E402
21
+ from collections import defaultdict # noqa: E402
22
+
23
+ import cv2 # noqa: E402
24
+ import gradio as gr # noqa: E402
25
+ import imageio.v2 as imageio # noqa: E402
26
+ import numpy as np # noqa: E402
27
+ import torch # noqa: E402
28
+ from molmo_utils import process_vision_info # noqa: E402
29
+ from transformers import AutoModelForImageTextToText, AutoProcessor # noqa: E402
30
+
31
+ MODEL_ID = "tidalove/Molmo2Fish"
32
+
33
+ # Matches the released video_preprocessor_config.json of tidalove/Molmo2Fish.
34
+ NUM_FRAMES = 128
35
+ FRAME_SAMPLE_MODE = "uniform_last_frame"
36
+ MAX_FPS = 2
37
+ SAMPLING_FPS = 2
38
+
39
+ TRACK_STYLE = "video_point_track_per_frame"
40
+ DEFAULT_PROMPT = "track all fish"
41
+
42
+ # html-v2 pointing format, exactly as in olmo/preprocessing/point_formatter.py
43
+ COORD_RE = re.compile(r"<(?:points|tracks).*? coords=\"([0-9\t:;, .]+)\"/?>")
44
+ FRAME_RE = re.compile(r"(?:^|\t|:|,|;)([0-9\.]+) ([0-9\. ]+)")
45
+ POINTS_RE = re.compile(r"([0-9]+) ([0-9]{3,4}) ([0-9]{3,4})")
46
+
47
+ PALETTE = [
48
+ (240, 82, 156), # the authors' pink (scripts/unified_demo.py)
49
+ (66, 214, 255),
50
+ (124, 252, 118),
51
+ (255, 196, 61),
52
+ (186, 132, 255),
53
+ (255, 122, 92),
54
+ (0, 255, 214),
55
+ (255, 255, 120),
56
+ ]
57
+
58
+ print(f"Loading {MODEL_ID} …", flush=True)
59
+ processor = AutoProcessor.from_pretrained(
60
+ MODEL_ID, trust_remote_code=True, padding_side="left"
61
+ )
62
+ model = AutoModelForImageTextToText.from_pretrained(
63
+ MODEL_ID, trust_remote_code=True, dtype=torch.bfloat16
64
+ ).to("cuda")
65
+ model.eval()
66
+ print("Model ready.", flush=True)
67
+
68
+
69
+ # --------------------------------------------------------------------------- #
70
+ # Track parsing / rendering
71
+ # --------------------------------------------------------------------------- #
72
+ def parse_tracks(text: str, width: int, height: int) -> dict:
73
+ """Parse `<tracks coords="t id x y …">fish</tracks>` into {time: {id: (x, y)}}.
74
+
75
+ Coordinates in the model output are normalised to 0-1000; they are scaled
76
+ back to pixels here.
77
+ """
78
+ out: dict = {}
79
+ for coord in COORD_RE.finditer(text):
80
+ for frame in FRAME_RE.finditer(coord.group(1)):
81
+ t = float(frame.group(1))
82
+ per_frame = out.setdefault(t, {})
83
+ for pt in POINTS_RE.finditer(frame.group(2)):
84
+ idx, xs, ys = pt.group(1), pt.group(2), pt.group(3)
85
+ x = float(xs) / 1000.0 * width
86
+ y = float(ys) / 1000.0 * height
87
+ if 0 <= x <= width and 0 <= y <= height:
88
+ per_frame.setdefault(idx, (x, y))
89
+ return out
90
+
91
+
92
+ def render_overlay(video_path: str, tracks: dict, out_path: str) -> None:
93
+ """Draw the parsed tracks (points + fading trails + ids) onto the source video."""
94
+ cap = cv2.VideoCapture(video_path)
95
+ fps = cap.get(cv2.CAP_PROP_FPS) or 6.0
96
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
97
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
98
+
99
+ times = sorted(tracks)
100
+ times_arr = np.asarray(times) if times else None
101
+
102
+ # id -> ordered list of (time, x, y), used to draw the trail behind each fish
103
+ history = defaultdict(list)
104
+ for t in times:
105
+ for idx, (x, y) in tracks[t].items():
106
+ history[idx].append((t, x, y))
107
+
108
+ ids = sorted(history, key=lambda s: (len(s), s))
109
+ color_of = {idx: PALETTE[i % len(PALETTE)] for i, idx in enumerate(ids)}
110
+
111
+ radius = max(4, int(max(width, height) * 0.008))
112
+ thickness = max(2, radius // 2)
113
+ font_scale = max(0.5, max(width, height) / 1400.0)
114
+
115
+ writer = imageio.get_writer(
116
+ out_path, fps=fps, codec="libx264", quality=7,
117
+ macro_block_size=1, pixelformat="yuv420p", ffmpeg_log_level="error",
118
+ )
119
+ try:
120
+ frame_ix = 0
121
+ while True:
122
+ ok, frame = cap.read()
123
+ if not ok:
124
+ break
125
+ rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
126
+ if times_arr is not None:
127
+ t_now = frame_ix / fps
128
+ k = int(np.argmin(np.abs(times_arr - t_now)))
129
+ t_key = times[k]
130
+ for idx, pts in history.items():
131
+ trail = [(x, y) for (t, x, y) in pts if t <= t_key]
132
+ if len(trail) > 1:
133
+ poly = np.asarray(trail[-24:], dtype=np.int32).reshape(-1, 1, 2)
134
+ cv2.polylines(rgb, [poly], False, color_of[idx],
135
+ max(1, thickness - 1), cv2.LINE_AA)
136
+ for idx, (x, y) in tracks[t_key].items():
137
+ c = color_of[idx]
138
+ cv2.circle(rgb, (int(x), int(y)), radius, c, thickness, cv2.LINE_AA)
139
+ cv2.putText(rgb, str(idx), (int(x) + radius + 3, int(y) - radius - 3),
140
+ cv2.FONT_HERSHEY_SIMPLEX, font_scale, c,
141
+ max(1, thickness - 1), cv2.LINE_AA)
142
+ writer.append_data(rgb)
143
+ frame_ix += 1
144
+ finally:
145
+ writer.close()
146
+ cap.release()
147
+
148
+
149
+ def summarise(tracks: dict) -> str:
150
+ if not tracks:
151
+ return "No fish tracks were returned for this clip."
152
+ ids = {i for frame in tracks.values() for i in frame}
153
+ return (f"**{len(ids)} track(s)** across **{len(tracks)}** sampled timesteps "
154
+ f"(2 FPS). Track ids: {', '.join(sorted(ids, key=int))}.")
155
+
156
+
157
+ # --------------------------------------------------------------------------- #
158
+ # Model plumbing
159
+ # --------------------------------------------------------------------------- #
160
+ def build_messages(video_path: str, turns: list) -> list:
161
+ """Chat list for Molmo2Fish. The video is attached to the *first* user turn only.
162
+
163
+ `turns` is a list of (user_text, assistant_text_or_None), matching
164
+ olmo/eval/vllm_runner.py::build_multi_turn_chat.
165
+ """
166
+ messages = []
167
+ for i, (user_text, assistant_text) in enumerate(turns):
168
+ content = [dict(type="text", text=user_text, style=TRACK_STYLE)]
169
+ if i == 0:
170
+ content.append(dict(
171
+ type="video",
172
+ video=video_path,
173
+ num_frames=NUM_FRAMES,
174
+ frame_sample_mode=FRAME_SAMPLE_MODE,
175
+ max_fps=MAX_FPS,
176
+ sampling_fps=SAMPLING_FPS,
177
+ ))
178
+ messages.append({"role": "user", "content": content})
179
+ if assistant_text is not None:
180
+ messages.append({"role": "assistant",
181
+ "content": [dict(type="text", text=assistant_text)]})
182
+ return messages
183
+
184
+
185
+ def run_model(video_path: str, turns: list, max_new_tokens: int) -> str:
186
+ messages = build_messages(video_path, turns)
187
+ _, videos, video_kwargs = process_vision_info(messages)
188
+ frames, metadatas = zip(*videos)
189
+ text = processor.apply_chat_template(
190
+ messages, tokenize=False, add_generation_prompt=True
191
+ )
192
+ inputs = processor(
193
+ videos=list(frames),
194
+ video_metadata=list(metadatas),
195
+ text=text,
196
+ padding=True,
197
+ return_tensors="pt",
198
+ **video_kwargs,
199
+ )
200
+ inputs = {k: (v.to(model.device) if hasattr(v, "to") else v)
201
+ for k, v in inputs.items()}
202
+ with torch.inference_mode():
203
+ with torch.autocast("cuda", enabled=True, dtype=torch.bfloat16):
204
+ generated = model.generate(
205
+ **inputs, max_new_tokens=max_new_tokens, do_sample=False
206
+ )
207
+ prompt_len = inputs["input_ids"].size(1)
208
+ return processor.post_process_image_text_to_text(
209
+ generated[:, prompt_len:],
210
+ skip_special_tokens=True,
211
+ clean_up_tokenization_spaces=False,
212
+ )[0].strip()
213
+
214
+
215
+ def _video_size(video_path: str):
216
+ cap = cv2.VideoCapture(video_path)
217
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
218
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
219
+ cap.release()
220
+ return w, h
221
+
222
+
223
+ def _infer(video_path: str, turns: list, max_new_tokens: int):
224
+ t0 = time.perf_counter()
225
+ raw = run_model(video_path, turns, max_new_tokens)
226
+ elapsed = time.perf_counter() - t0
227
+ width, height = _video_size(video_path)
228
+ tracks = parse_tracks(raw, width, height)
229
+ if not tracks:
230
+ return video_path, raw, f"{summarise(tracks)} \n_Inference: {elapsed:.1f}s_"
231
+ out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
232
+ render_overlay(video_path, tracks, out_path)
233
+ return out_path, raw, f"{summarise(tracks)} \n_Inference: {elapsed:.1f}s_"
234
+
235
+
236
+ # --------------------------------------------------------------------------- #
237
+ # Gradio handlers
238
+ # --------------------------------------------------------------------------- #
239
+ @spaces.GPU(duration=140)
240
+ def track_fish(
241
+ video_path: str,
242
+ correction_hint: str = "",
243
+ max_new_tokens: int = 1600,
244
+ progress=gr.Progress(track_tqdm=True),
245
+ ):
246
+ """Run the first tracking pass over a sonar clip ("track all fish").
247
+
248
+ Args:
249
+ video_path: path to an ARIS sonar clip (mp4).
250
+ correction_hint: ignored here β€” it only exists so an example row can
251
+ pre-fill the correction box alongside the video.
252
+ max_new_tokens: generation budget for the `<tracks …>` string.
253
+
254
+ Returns:
255
+ (overlay video, raw model output, markdown summary)
256
+ """
257
+ if not video_path:
258
+ raise gr.Error("Please provide a sonar video first.")
259
+ return _infer(video_path, [(DEFAULT_PROMPT, None)], int(max_new_tokens))
260
+
261
+
262
+ @spaces.GPU(duration=140)
263
+ def refine_tracks(
264
+ video_path: str,
265
+ previous_tracks: str,
266
+ correction: str,
267
+ max_new_tokens: int = 1600,
268
+ progress=gr.Progress(track_tqdm=True),
269
+ ):
270
+ """Correct the current tracks using a natural-language instruction.
271
+
272
+ The model sees the video, its own previous `<tracks …>` answer, and the
273
+ critique, then re-emits a corrected track set.
274
+
275
+ Args:
276
+ video_path: the same sonar clip used for the first pass.
277
+ previous_tracks: the model's previous `<tracks …>` output.
278
+ correction: plain-English critique, e.g. "Track 1 is shifted downward".
279
+ max_new_tokens: generation budget for the corrected `<tracks …>` string.
280
+
281
+ Returns:
282
+ (overlay video, raw model output, markdown summary)
283
+ """
284
+ if not video_path:
285
+ raise gr.Error("Please provide a sonar video first.")
286
+ if not previous_tracks or not previous_tracks.strip():
287
+ raise gr.Error("Run 'Track all fish' first β€” there is nothing to correct yet.")
288
+ if not correction or not correction.strip():
289
+ raise gr.Error("Type a correction instruction, e.g. 'Track 1 is shifted downward'.")
290
+ turns = [(DEFAULT_PROMPT, previous_tracks.strip()), (correction.strip(), None)]
291
+ return _infer(video_path, turns, int(max_new_tokens))
292
+
293
+
294
+ # --------------------------------------------------------------------------- #
295
+ # UI
296
+ # --------------------------------------------------------------------------- #
297
+ CSS = """
298
+ #col-container { max-width: 1200px; margin: 0 auto; }
299
+ .dark .gradio-container { color: var(--body-text-color); }
300
+ """
301
+
302
+ EXAMPLES = [
303
+ [
304
+ "examples/elwha_2018-07-29.mp4",
305
+ "Track 1 looks good overall, just slightly shifted downward from the "
306
+ "actual fish position throughout.",
307
+ ],
308
+ [
309
+ "examples/kenai_leftfar_2018-06-03.mp4",
310
+ "Track 1 doesn't correspond to any real fish β€” you've got a false "
311
+ "detection moving left that should be removed. The actual fish starts "
312
+ "in the lower left around 8s and swims upward until the end of the clip, "
313
+ "and you missed it entirely.",
314
+ ],
315
+ [
316
+ "examples/nushagak_rb_f15-52.mp4",
317
+ "You missed a fish near the top of the frame β€” please add it.",
318
+ ],
319
+ ]
320
+
321
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
322
+ with gr.Column(elem_id="col-container"):
323
+ gr.Markdown(
324
+ "# 🐟 Molmo2Fish β€” interactive fish tracking\n"
325
+ "Track salmon in ARIS **sonar** video, then fix the mistakes by *talking to the model*.\n\n"
326
+ "Step 1 runs the model's `track all fish` pass. Step 2 feeds your plain-English "
327
+ "critique back in β€” the model re-emits a corrected track set instead of you "
328
+ "editing keypoints by hand.\n\n"
329
+ "[Paper](https://huggingface.co/papers/2608.18602) Β· "
330
+ "[Model](https://huggingface.co/tidalove/Molmo2Fish) Β· "
331
+ "[Code](https://github.com/tidalove/molmo2fish) Β· "
332
+ "[Data](https://huggingface.co/datasets/tidalove/cfc-track-instruction)"
333
+ )
334
+
335
+ with gr.Row():
336
+ with gr.Column():
337
+ video_in = gr.Video(label="Sonar clip", height=420)
338
+ track_btn = gr.Button("β‘  Track all fish", variant="primary")
339
+ correction = gr.Textbox(
340
+ label="β‘‘ Correction instruction",
341
+ placeholder="Track 2 drifts off the fish after about 6s β€” it should keep "
342
+ "following the fish swimming up the right side.",
343
+ lines=3,
344
+ )
345
+ refine_btn = gr.Button("β‘‘ Apply correction", variant="secondary")
346
+ with gr.Column():
347
+ video_out = gr.Video(label="Tracks", height=420, autoplay=True)
348
+ summary = gr.Markdown()
349
+ tracks_box = gr.Textbox(
350
+ label="Model output (html-v2 tracks) β€” edited in place by step β‘‘",
351
+ lines=6,
352
+ max_lines=12,
353
+ show_copy_button=True,
354
+ )
355
+
356
+ with gr.Accordion("Advanced", open=False):
357
+ max_new_tokens = gr.Slider(
358
+ 256, 3072, value=1600, step=64,
359
+ label="Max new tokens",
360
+ info="Long clips with many fish need a bigger budget; an unclosed "
361
+ "</tracks> means you hit the cap.",
362
+ )
363
+
364
+ gr.Markdown(
365
+ "### Examples\n"
366
+ "Clicking a row loads the clip **and** pre-fills a real correction from the "
367
+ "paper's CFC validation split, and runs step β‘  for you."
368
+ )
369
+ gr.Examples(
370
+ examples=EXAMPLES,
371
+ inputs=[video_in, correction],
372
+ outputs=[video_out, tracks_box, summary],
373
+ fn=track_fish,
374
+ cache_examples=True,
375
+ cache_mode="lazy",
376
+ label="Sonar clips (CFC26, CC-BY-4.0)",
377
+ )
378
+
379
+ gr.Markdown(
380
+ "Sonar clips are re-encoded from the "
381
+ "[perona-lab/cfc26](https://huggingface.co/datasets/perona-lab/cfc26) "
382
+ "Caltech Fish Counting release (CC-BY-4.0); correction prompts come from "
383
+ "[tidalove/cfc-track-instruction](https://huggingface.co/datasets/tidalove/cfc-track-instruction). "
384
+ "Tracks are predicted at 2 FPS and interpolated onto the 6 FPS source for display."
385
+ )
386
+
387
+ track_btn.click(
388
+ track_fish,
389
+ inputs=[video_in, correction, max_new_tokens],
390
+ outputs=[video_out, tracks_box, summary],
391
+ api_name="track_fish",
392
+ )
393
+ refine_btn.click(
394
+ refine_tracks,
395
+ inputs=[video_in, tracks_box, correction, max_new_tokens],
396
+ outputs=[video_out, tracks_box, summary],
397
+ api_name="refine_tracks",
398
+ )
399
+
400
+ if __name__ == "__main__":
401
+ demo.launch(mcp_server=True)
examples/elwha_2018-07-29.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:86daba3bd947f30d983a7a2c9b2565b69b706bb4e49e861410274940bf94571a
3
+ size 25601886
examples/kenai_leftfar_2018-06-03.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec527b78fbcdf2762e4956cbf028822f5379197c5950ffb5d124709ceb378124
3
+ size 17401090
examples/nushagak_rb_f15-52.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:567a6f771cff7858cdcb9df77884c94d0af429def51b525a62e4edf5b057b303
3
+ size 6778063
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ transformers==4.57.6
2
+ accelerate
3
+ molmo_utils
4
+ torchvision
5
+ av
6
+ einops
7
+ timm
8
+ opencv-python-headless
9
+ imageio
10
+ imageio-ffmpeg
11
+ numpy
12
+ pillow