@larkup/tool-video-intelligence 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.env.example +150 -0
  2. package/LICENSE +176 -0
  3. package/README.md +281 -0
  4. package/compose.gpu.yaml +14 -0
  5. package/compose.yaml +71 -0
  6. package/dist/agent.d.ts +131 -0
  7. package/dist/agent.js +2087 -0
  8. package/dist/brief.d.ts +2 -0
  9. package/dist/brief.js +37 -0
  10. package/dist/client.d.ts +46 -0
  11. package/dist/client.js +139 -0
  12. package/dist/contracts.d.ts +331 -0
  13. package/dist/contracts.js +1 -0
  14. package/dist/index.d.ts +87 -0
  15. package/dist/index.js +391 -0
  16. package/dist/runtime.d.ts +96 -0
  17. package/dist/runtime.js +592 -0
  18. package/dist/ui.d.ts +82 -0
  19. package/dist/ui.js +87 -0
  20. package/package.json +84 -0
  21. package/runtime/Dockerfile +119 -0
  22. package/runtime/app/__init__.py +3 -0
  23. package/runtime/app/__main__.py +19 -0
  24. package/runtime/app/api/__init__.py +0 -0
  25. package/runtime/app/api/deps.py +69 -0
  26. package/runtime/app/api/v1.py +166 -0
  27. package/runtime/app/config.py +78 -0
  28. package/runtime/app/db/__init__.py +0 -0
  29. package/runtime/app/db/schemas.py +162 -0
  30. package/runtime/app/db/store.py +466 -0
  31. package/runtime/app/main.py +27 -0
  32. package/runtime/app/model_configuration.py +157 -0
  33. package/runtime/app/services/__init__.py +0 -0
  34. package/runtime/app/services/brain.py +2221 -0
  35. package/runtime/app/services/embedding.py +473 -0
  36. package/runtime/app/services/jobs.py +237 -0
  37. package/runtime/app/services/motion.py +66 -0
  38. package/runtime/app/services/pipeline.py +1911 -0
  39. package/runtime/app/services/scene.py +161 -0
  40. package/runtime/app/services/storage.py +44 -0
  41. package/runtime/app/services/transcription.py +667 -0
  42. package/runtime/app/services/vision.py +1441 -0
  43. package/runtime/app/utils/__init__.py +0 -0
  44. package/runtime/app/utils/timing.py +99 -0
  45. package/runtime/app/worker.py +20 -0
  46. package/runtime/pyproject.toml +56 -0
  47. package/runtime/requirements-cpu.txt +15 -0
  48. package/runtime/requirements-smoke.txt +7 -0
  49. package/runtime/requirements.txt +14 -0
  50. package/runtime/uv.lock +3637 -0
  51. package/scripts/grant-cloud-credits.sh +43 -0
  52. package/scripts/runtime.mjs +156 -0
  53. package/scripts/validate-indexing.mjs +168 -0
  54. package/tool.manifest.json +617 -0
@@ -0,0 +1,473 @@
1
+ """Video-clip embeddings for cross-modal search -- "find the clip where this
2
+ happens" from a free-text query. Distinct from vision.py's captions: a
3
+ caption describes a clip in words, which can miss an action a viewer would
4
+ recognize but a VLM never put into text. An embedding catches that by
5
+ comparing meaning directly, not words.
6
+
7
+ Qwen3-VL-Embedding can use DashScope's multimodal API or dedicated RunPod
8
+ and Hugging Face Inference Endpoint deployments.
9
+ `disabled` (the default) turns this off with zero cost;
10
+ `get_video_embedding_provider()` swaps providers by env var alone.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import base64
16
+ import os
17
+ import threading
18
+ from abc import ABC, abstractmethod
19
+ from concurrent.futures import ThreadPoolExecutor, as_completed
20
+ from dataclasses import dataclass
21
+ from typing import Callable, ClassVar
22
+
23
+ import cv2
24
+ import numpy as np
25
+ import requests
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class VideoClipInput:
30
+ clip_id: str
31
+ start_ms: int
32
+ end_ms: int
33
+ frames: list[tuple[int, np.ndarray]]
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class VideoClipEmbedding:
38
+ clip_id: str
39
+ start_ms: int
40
+ end_ms: int
41
+ vector: list[float]
42
+
43
+
44
+ class VideoEmbeddingProviderError(RuntimeError):
45
+ pass
46
+
47
+
48
+ class VideoEmbeddingProvider(ABC):
49
+ name: ClassVar[str]
50
+ dimensions: ClassVar[int]
51
+
52
+ @abstractmethod
53
+ def embed_clips(
54
+ self,
55
+ clips: list[VideoClipInput],
56
+ on_progress: Callable[[int, int], None] | None = None,
57
+ ) -> list[VideoClipEmbedding]:
58
+ """One embedding per clip that had at least one frame."""
59
+
60
+ @abstractmethod
61
+ def embed_query(self, text: str) -> list[float]:
62
+ """Embeds free text into the same vector space as embed_clips, for cross-modal search."""
63
+
64
+
65
+ class DisabledVideoEmbeddingProvider(VideoEmbeddingProvider):
66
+ name = "disabled"
67
+ dimensions = 0
68
+
69
+ def embed_clips(
70
+ self,
71
+ clips: list[VideoClipInput],
72
+ on_progress: Callable[[int, int], None] | None = None,
73
+ ) -> list[VideoClipEmbedding]:
74
+ if on_progress:
75
+ on_progress(0, 0)
76
+ return []
77
+
78
+ def embed_query(self, text: str) -> list[float]:
79
+ raise VideoEmbeddingProviderError(
80
+ "Video embedding is disabled (set LARKUP_VIDEO_EMBEDDING_PROVIDER=qwen3-vl-embedding to enable it)."
81
+ )
82
+
83
+
84
+ class QwenVLEmbeddingProvider(VideoEmbeddingProvider):
85
+ """DashScope's multimodal-embedding API.
86
+
87
+ qwen3-vl-embedding is not served on DashScope's shared endpoint
88
+ (dashscope[-intl].aliyuncs.com) -- it only responds on a workspace-
89
+ dedicated domain, `https://{workspace_id}.{region}.maas.aliyuncs.com`,
90
+ with the workspace ID and region both taken from the Model Studio
91
+ console (Workspace Details page). `LARKUP_VIDEO_DASHSCOPE_BASE_URL` can
92
+ override the computed URL entirely, e.g. to point at a different API
93
+ path or a non-production workspace.
94
+ """
95
+
96
+ name = "qwen3-vl-embedding"
97
+ dimensions = 1024
98
+
99
+ API_PATH = "/api/v1/services/embeddings/multimodal-embedding/multimodal-embedding"
100
+ DEFAULT_MODEL = "qwen3-vl-embedding"
101
+
102
+ def __init__(self) -> None:
103
+ self.api_key = os.getenv("DASHSCOPE_API_KEY", "")
104
+ self.workspace_id = os.getenv("DASHSCOPE_WORKSPACE_ID", "")
105
+ self.region = os.getenv("DASHSCOPE_REGION", "")
106
+ override_url = os.getenv("LARKUP_VIDEO_DASHSCOPE_BASE_URL", "")
107
+ if override_url:
108
+ self.base_url = override_url
109
+ elif self.workspace_id and self.region:
110
+ self.base_url = f"https://{self.workspace_id}.{self.region}.maas.aliyuncs.com{self.API_PATH}"
111
+ else:
112
+ self.base_url = ""
113
+ self.model = os.getenv("LARKUP_VIDEO_EMBEDDING_MODEL", self.DEFAULT_MODEL)
114
+ self.dimensions = int(os.getenv("LARKUP_VIDEO_EMBEDDING_DIMENSION", str(self.dimensions)))
115
+ self.frames_per_clip = int(os.getenv("LARKUP_VIDEO_EMBEDDING_FRAMES_PER_CLIP", "4"))
116
+ self._session = requests.Session()
117
+
118
+ def _frame_content(self, frame: np.ndarray) -> dict:
119
+ ok, buffer = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
120
+ if not ok:
121
+ raise ValueError("could not encode frame as JPEG")
122
+ return {"image": "data:image/jpeg;base64," + base64.b64encode(buffer.tobytes()).decode("ascii")}
123
+
124
+ def _embed(self, contents: list[dict]) -> list[float]:
125
+ if not self.api_key:
126
+ raise VideoEmbeddingProviderError("DASHSCOPE_API_KEY is not configured")
127
+ if not self.base_url:
128
+ raise VideoEmbeddingProviderError(
129
+ "DASHSCOPE_WORKSPACE_ID and DASHSCOPE_REGION are not configured "
130
+ "(or set LARKUP_VIDEO_DASHSCOPE_BASE_URL directly)"
131
+ )
132
+ payload = {
133
+ "model": self.model,
134
+ "input": {"contents": contents},
135
+ "parameters": {"enable_fusion": len(contents) > 1, "dimension": self.dimensions},
136
+ }
137
+ headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
138
+ response = self._session.post(self.base_url, json=payload, headers=headers, timeout=60)
139
+ if not response.ok:
140
+ raise VideoEmbeddingProviderError(
141
+ f"DashScope embedding request failed {response.status_code}: {response.text[:300]}"
142
+ )
143
+ body = response.json()
144
+ try:
145
+ return list(body["output"]["embeddings"][0]["embedding"])
146
+ except (KeyError, IndexError, TypeError) as error:
147
+ raise VideoEmbeddingProviderError(
148
+ f"unexpected DashScope response shape: {str(body)[:500]}"
149
+ ) from error
150
+
151
+ def embed_clips(
152
+ self,
153
+ clips: list[VideoClipInput],
154
+ on_progress: Callable[[int, int], None] | None = None,
155
+ ) -> list[VideoClipEmbedding]:
156
+ embeddings: list[VideoClipEmbedding] = []
157
+ usable = [clip for clip in clips if clip.frames]
158
+ for completed, clip in enumerate(usable, start=1):
159
+ frames = clip.frames[: self.frames_per_clip]
160
+ contents = [self._frame_content(frame) for _, frame in frames]
161
+ vector = self._embed(contents)
162
+ embeddings.append(
163
+ VideoClipEmbedding(clip_id=clip.clip_id, start_ms=clip.start_ms, end_ms=clip.end_ms, vector=vector)
164
+ )
165
+ if on_progress:
166
+ on_progress(completed, len(usable))
167
+ return embeddings
168
+
169
+ def embed_query(self, text: str) -> list[float]:
170
+ return self._embed([{"text": text}])
171
+
172
+
173
+ class RunpodQwenVLEmbeddingProvider(VideoEmbeddingProvider):
174
+ """Dedicated Qwen/Qwen3-VL-Embedding-8B RunPod Serverless worker."""
175
+
176
+ name = "runpod-qwen3-vl-embedding"
177
+ dimensions = 1024
178
+
179
+ def __init__(self) -> None:
180
+ self.api_key = os.getenv("LARKUP_VIDEO_RUNPOD_EMBEDDING_API_KEY", os.getenv("RUNPOD_API_KEY", ""))
181
+ self.endpoint_id = os.getenv("LARKUP_VIDEO_RUNPOD_EMBEDDING_ENDPOINT_ID", "")
182
+ self.base_url = os.getenv("LARKUP_VIDEO_RUNPOD_EMBEDDING_BASE_URL", "")
183
+ if not self.base_url and self.endpoint_id:
184
+ self.base_url = f"https://api.runpod.ai/v2/{self.endpoint_id}/runsync"
185
+ self.dimensions = int(os.getenv("LARKUP_VIDEO_EMBEDDING_DIMENSION", str(self.dimensions)))
186
+ self.instruction = os.getenv("LARKUP_VIDEO_EMBEDDING_INSTRUCTION", "")
187
+ self._session = requests.Session()
188
+
189
+ def _embed(self, inputs: list[dict]) -> list[list[float]]:
190
+ if not self.api_key:
191
+ raise VideoEmbeddingProviderError("RUNPOD_API_KEY is not configured")
192
+ if not self.base_url:
193
+ raise VideoEmbeddingProviderError("LARKUP_VIDEO_RUNPOD_EMBEDDING_ENDPOINT_ID is not configured")
194
+ payload = {"input": {"inputs": inputs, "dimensions": self.dimensions}}
195
+ if self.instruction:
196
+ payload["input"]["instruction"] = self.instruction
197
+ response = self._session.post(
198
+ self.base_url,
199
+ json=payload,
200
+ headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"},
201
+ timeout=300,
202
+ )
203
+ if not response.ok:
204
+ raise VideoEmbeddingProviderError(
205
+ f"RunPod embedding request failed {response.status_code}: {response.text[:300]}"
206
+ )
207
+ body = response.json()
208
+ output = body.get("output", body)
209
+ try:
210
+ embeddings = output["embeddings"]
211
+ if not isinstance(embeddings, list) or not all(isinstance(vector, list) for vector in embeddings):
212
+ raise TypeError("embeddings is not a list of vectors")
213
+ return [[float(value) for value in vector] for vector in embeddings]
214
+ except (KeyError, TypeError, ValueError) as error:
215
+ raise VideoEmbeddingProviderError(
216
+ f"unexpected RunPod embedding response shape: {str(body)[:500]}"
217
+ ) from error
218
+
219
+ def _frame_content(self, frame: np.ndarray) -> str:
220
+ ok, buffer = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
221
+ if not ok:
222
+ raise ValueError("could not encode frame as JPEG")
223
+ return "data:image/jpeg;base64," + base64.b64encode(buffer.tobytes()).decode("ascii")
224
+
225
+ def embed_clips(
226
+ self,
227
+ clips: list[VideoClipInput],
228
+ on_progress: Callable[[int, int], None] | None = None,
229
+ ) -> list[VideoClipEmbedding]:
230
+ usable = [clip for clip in clips if clip.frames]
231
+ if not usable:
232
+ return []
233
+ # The deployment API represents one embedding input as text and/or one
234
+ # image. A midpoint frame keeps clip embeddings and query embeddings in
235
+ # the same model space without multiplying requests by frame count.
236
+ vectors = self._embed(
237
+ [{"image": self._frame_content(clip.frames[len(clip.frames) // 2][1])} for clip in usable]
238
+ )
239
+ if len(vectors) != len(usable):
240
+ raise VideoEmbeddingProviderError("RunPod returned a different number of embeddings than inputs")
241
+ if on_progress:
242
+ on_progress(len(usable), len(usable))
243
+ return [
244
+ VideoClipEmbedding(clip_id=clip.clip_id, start_ms=clip.start_ms, end_ms=clip.end_ms, vector=vector)
245
+ for clip, vector in zip(usable, vectors, strict=True)
246
+ ]
247
+
248
+ def embed_query(self, text: str) -> list[float]:
249
+ return self._embed([{"text": text}])[0]
250
+
251
+
252
+ class HuggingFaceQwenVLEmbeddingProvider(VideoEmbeddingProvider):
253
+ """Dedicated Qwen3-VL custom Hugging Face Inference Endpoint."""
254
+
255
+ name = "huggingface-qwen3-vl-embedding"
256
+ dimensions = 1024
257
+
258
+ def __init__(self) -> None:
259
+ self.base_url = os.getenv("LARKUP_VIDEO_HF_EMBEDDING_URL", "").rstrip("/")
260
+ self.api_key = os.getenv("LARKUP_VIDEO_HF_EMBEDDING_API_KEY", os.getenv("HF_TOKEN", ""))
261
+ self.dimensions = int(os.getenv("LARKUP_VIDEO_EMBEDDING_DIMENSION", str(self.dimensions)))
262
+ self.instruction = os.getenv("LARKUP_VIDEO_EMBEDDING_INSTRUCTION", "")
263
+ self._session = requests.Session()
264
+
265
+ def _embed(self, inputs: list[dict]) -> list[list[float]]:
266
+ if not self.base_url:
267
+ raise VideoEmbeddingProviderError("LARKUP_VIDEO_HF_EMBEDDING_URL is not configured")
268
+ if not self.api_key:
269
+ raise VideoEmbeddingProviderError("HF_TOKEN is not configured")
270
+ payload = {"inputs": inputs, "dimensions": self.dimensions}
271
+ if self.instruction:
272
+ payload["instruction"] = self.instruction
273
+ response = self._session.post(
274
+ self.base_url,
275
+ json=payload,
276
+ headers={"Authorization": f"Bearer {self.api_key}"},
277
+ timeout=300,
278
+ )
279
+ if not response.ok:
280
+ raise VideoEmbeddingProviderError(
281
+ f"Hugging Face embedding request failed {response.status_code}: {response.text[:300]}"
282
+ )
283
+ body = response.json()
284
+ try:
285
+ embeddings = body["embeddings"]
286
+ if not isinstance(embeddings, list) or not all(isinstance(vector, list) for vector in embeddings):
287
+ raise TypeError("embeddings is not a list of vectors")
288
+ return [[float(value) for value in vector] for vector in embeddings]
289
+ except (KeyError, TypeError, ValueError) as error:
290
+ raise VideoEmbeddingProviderError(
291
+ f"unexpected Hugging Face embedding response shape: {str(body)[:500]}"
292
+ ) from error
293
+
294
+ def _frame_content(self, frame: np.ndarray) -> str:
295
+ ok, buffer = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
296
+ if not ok:
297
+ raise ValueError("could not encode frame as JPEG")
298
+ return "data:image/jpeg;base64," + base64.b64encode(buffer.tobytes()).decode("ascii")
299
+
300
+ def embed_clips(
301
+ self,
302
+ clips: list[VideoClipInput],
303
+ on_progress: Callable[[int, int], None] | None = None,
304
+ ) -> list[VideoClipEmbedding]:
305
+ usable = [clip for clip in clips if clip.frames]
306
+ if not usable:
307
+ return []
308
+ vectors = self._embed(
309
+ [{"image": self._frame_content(clip.frames[len(clip.frames) // 2][1])} for clip in usable]
310
+ )
311
+ if len(vectors) != len(usable):
312
+ raise VideoEmbeddingProviderError("Hugging Face returned a different number of embeddings than inputs")
313
+ if on_progress:
314
+ on_progress(len(usable), len(usable))
315
+ return [
316
+ VideoClipEmbedding(clip_id=clip.clip_id, start_ms=clip.start_ms, end_ms=clip.end_ms, vector=vector)
317
+ for clip, vector in zip(usable, vectors, strict=True)
318
+ ]
319
+
320
+ def embed_query(self, text: str) -> list[float]:
321
+ return self._embed([{"text": text}])[0]
322
+
323
+
324
+ class GatewayGeminiMultimodalEmbeddingProvider(VideoEmbeddingProvider):
325
+ """Gemini Embedding 2 through Vercel AI Gateway's model-native API.
326
+
327
+ Unlike text-only embedding endpoints, this keeps a text query and JPEG
328
+ frame in one multimodal vector space. It replaces the dedicated Qwen
329
+ embedding endpoint for source indexing without introducing a GPU cold
330
+ start. The Gateway API differs from its OpenAI-compatible `/v1/embeddings`
331
+ route: model-native embedding calls use `/v4/ai/embedding-model`.
332
+ """
333
+
334
+ name = "gateway-gemini-embedding-2"
335
+ dimensions = 3072
336
+
337
+ def __init__(self) -> None:
338
+ self.api_key = os.getenv("AI_GATEWAY_API_KEY") or ""
339
+ self.base_url = os.getenv(
340
+ "LARKUP_VIDEO_GATEWAY_EMBEDDING_BASE_URL", "https://ai-gateway.vercel.sh/v4/ai"
341
+ ).rstrip("/")
342
+ self.model = os.getenv("LARKUP_VIDEO_GATEWAY_EMBEDDING_MODEL", "google/gemini-embedding-2")
343
+ self.dimensions = int(os.getenv("LARKUP_VIDEO_EMBEDDING_DIMENSION", str(self.dimensions)))
344
+ self.batch_size = max(1, min(6, int(os.getenv("LARKUP_VIDEO_GATEWAY_EMBEDDING_BATCH_SIZE", "6"))))
345
+ self.max_concurrency = max(
346
+ 1, min(6, int(os.getenv("LARKUP_VIDEO_GATEWAY_EMBEDDING_CONCURRENCY", "6")))
347
+ )
348
+ self._sessions = threading.local()
349
+
350
+ def _session(self) -> requests.Session:
351
+ session = getattr(self._sessions, "session", None)
352
+ if session is None:
353
+ session = requests.Session()
354
+ self._sessions.session = session
355
+ return session
356
+
357
+ def _embed(
358
+ self,
359
+ values: list[str],
360
+ *,
361
+ content: list[list[dict] | None] | None = None,
362
+ task_type: str,
363
+ ) -> list[list[float]]:
364
+ if not self.api_key:
365
+ raise VideoEmbeddingProviderError("AI_GATEWAY_API_KEY is not configured")
366
+ payload: dict = {"values": values}
367
+ google_options: dict = {"taskType": task_type}
368
+ if content is not None:
369
+ google_options["content"] = content
370
+ payload["providerOptions"] = {"google": google_options}
371
+ response = self._session().post(
372
+ f"{self.base_url}/embedding-model",
373
+ json=payload,
374
+ headers={
375
+ "Authorization": f"Bearer {self.api_key}",
376
+ "Content-Type": "application/json",
377
+ "ai-gateway-protocol-version": "0.0.1",
378
+ "ai-gateway-auth-method": "api-key",
379
+ "ai-embedding-model-specification-version": "4",
380
+ "ai-model-id": self.model,
381
+ },
382
+ timeout=60,
383
+ )
384
+ if not response.ok:
385
+ raise VideoEmbeddingProviderError(
386
+ f"AI Gateway embedding request failed {response.status_code}: {response.text[:300]}"
387
+ )
388
+ body = response.json()
389
+ try:
390
+ vectors = body["embeddings"]
391
+ if not isinstance(vectors, list) or not all(isinstance(vector, list) for vector in vectors):
392
+ raise TypeError("embeddings is not a list of vectors")
393
+ return [[float(value) for value in vector] for vector in vectors]
394
+ except (KeyError, TypeError, ValueError) as error:
395
+ raise VideoEmbeddingProviderError(
396
+ f"unexpected AI Gateway embedding response shape: {str(body)[:500]}"
397
+ ) from error
398
+
399
+ @staticmethod
400
+ def _frame_content(frame: np.ndarray) -> dict:
401
+ ok, buffer = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
402
+ if not ok:
403
+ raise ValueError("could not encode frame as JPEG")
404
+ return {
405
+ "inlineData": {
406
+ "mimeType": "image/jpeg",
407
+ "data": base64.b64encode(buffer.tobytes()).decode("ascii"),
408
+ }
409
+ }
410
+
411
+ def embed_clips(
412
+ self,
413
+ clips: list[VideoClipInput],
414
+ on_progress: Callable[[int, int], None] | None = None,
415
+ ) -> list[VideoClipEmbedding]:
416
+ usable = [clip for clip in clips if clip.frames]
417
+ if not usable:
418
+ return []
419
+ batches = [usable[offset : offset + self.batch_size] for offset in range(0, len(usable), self.batch_size)]
420
+
421
+ def embed_batch(batch: list[VideoClipInput]) -> list[VideoClipEmbedding]:
422
+ vectors = self._embed(
423
+ ["timestamped video clip" for _ in batch],
424
+ content=[
425
+ [self._frame_content(clip.frames[len(clip.frames) // 2][1])]
426
+ for clip in batch
427
+ ],
428
+ task_type="RETRIEVAL_DOCUMENT",
429
+ )
430
+ if len(vectors) != len(batch):
431
+ raise VideoEmbeddingProviderError("AI Gateway returned a different number of embeddings than inputs")
432
+ return [
433
+ VideoClipEmbedding(clip_id=clip.clip_id, start_ms=clip.start_ms, end_ms=clip.end_ms, vector=vector)
434
+ for clip, vector in zip(batch, vectors, strict=True)
435
+ ]
436
+
437
+ by_clip_id: dict[str, VideoClipEmbedding] = {}
438
+ completed = 0
439
+ with ThreadPoolExecutor(max_workers=min(self.max_concurrency, len(batches))) as pool:
440
+ futures = {pool.submit(embed_batch, batch): len(batch) for batch in batches}
441
+ for future in as_completed(futures):
442
+ embedded_batch = future.result()
443
+ by_clip_id.update({embedding.clip_id: embedding for embedding in embedded_batch})
444
+ completed += futures[future]
445
+ if on_progress:
446
+ on_progress(completed, len(usable))
447
+ return [by_clip_id[clip.clip_id] for clip in usable]
448
+
449
+ def embed_query(self, text: str) -> list[float]:
450
+ return self._embed([text], task_type="RETRIEVAL_QUERY")[0]
451
+
452
+
453
+ _PROVIDERS: dict[str, type[VideoEmbeddingProvider]] = {
454
+ "disabled": DisabledVideoEmbeddingProvider,
455
+ "gateway-gemini-embedding-2": GatewayGeminiMultimodalEmbeddingProvider,
456
+ "huggingface-qwen3-vl-embedding": HuggingFaceQwenVLEmbeddingProvider,
457
+ "qwen3-vl-embedding": QwenVLEmbeddingProvider,
458
+ "runpod-qwen3-vl-embedding": RunpodQwenVLEmbeddingProvider,
459
+ }
460
+
461
+
462
+ def available_video_embedding_providers() -> tuple[str, ...]:
463
+ return tuple(sorted(_PROVIDERS))
464
+
465
+
466
+ def get_video_embedding_provider(name: str | None = None) -> VideoEmbeddingProvider:
467
+ key = (name or os.getenv("LARKUP_VIDEO_EMBEDDING_PROVIDER", "disabled")).strip().lower()
468
+ provider_cls = _PROVIDERS.get(key)
469
+ if provider_cls is None:
470
+ raise VideoEmbeddingProviderError(
471
+ f"Unknown video embedding provider {key!r}; available: {available_video_embedding_providers()}"
472
+ )
473
+ return provider_cls()