FusionCow commited on
Commit
e363f35
·
verified ·
1 Parent(s): 01b4df8

Delete .ipynb_checkpoints

Browse files
.ipynb_checkpoints/caption_folder-checkpoint.py DELETED
@@ -1,206 +0,0 @@
1
- """Caption every video in a folder with the packaged vLLM model.
2
-
3
- Edit the variables below, then run:
4
-
5
- cd /workspace/polished_model_v3
6
- source .venv/bin/activate
7
- python caption_folder.py
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import sys
13
- from pathlib import Path
14
-
15
- SCRIPT_DIR = Path(__file__).resolve().parent
16
- sys.path.insert(0, str(SCRIPT_DIR / "vllm"))
17
-
18
- from vllm_caption_runtime import build_vllm_request, load_llm, load_processor, sampling_params
19
-
20
-
21
- # =============================================================================
22
- # Paths
23
- # =============================================================================
24
-
25
- INPUT_FOLDER = "/workspace/videos_to_caption"
26
- OUTPUT_FOLDER = "/workspace/captions_out"
27
- MODEL_DIR = str(SCRIPT_DIR / "model")
28
-
29
- RECURSIVE = True
30
- OVERWRITE_EXISTING = False
31
- OUTPUT_EXTENSION = ".txt"
32
- ERROR_EXTENSION = ".error.txt"
33
- VIDEO_EXTENSIONS = (".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v")
34
-
35
-
36
- # =============================================================================
37
- # Prompt Settings
38
- # =============================================================================
39
-
40
- PROMPT_OVERRIDE = ""
41
- CAPTION_LENGTH = "very large"
42
- INCLUDE_WATERMARK_INFO = False
43
- HAS_THINKING = True
44
-
45
- VULGARITY = "low"
46
- UNCERTAINTY = "low"
47
- CHARACTER_NAMES = "none"
48
- FLUFF = "none"
49
- HAS_REPETITION = False
50
- SPECULATION = "low"
51
- TEMPORAL_DETAIL = "medium"
52
- VISUAL_SPECIFICITY = "moderate"
53
- CAMERA_DETAIL = "medium"
54
- CAPTION_STYLE = "plain"
55
-
56
-
57
- # =============================================================================
58
- # vLLM / Generation Hyperparameters
59
- # =============================================================================
60
-
61
- NUM_FRAMES = 12
62
- SAMPLING_RATE = 16_000
63
- MAX_MODEL_LEN = 4096
64
- MAX_NUM_SEQS = 20
65
- BATCH_SIZE = 20
66
- GPU_MEMORY_UTILIZATION = 0.88
67
- DTYPE = "bfloat16"
68
- ENFORCE_EAGER = False
69
- ENABLE_PREFIX_CACHING = False
70
- TRUST_REMOTE_CODE = False
71
-
72
- MAX_TOKENS = 1200
73
- TEMPERATURE = 0.0
74
- TOP_P = 0.9
75
- REPETITION_PENALTY = 1.1
76
-
77
-
78
- def prompt_settings() -> dict[str, object]:
79
- return {
80
- "caption_length": CAPTION_LENGTH,
81
- "include_watermark_info": INCLUDE_WATERMARK_INFO,
82
- "has_thinking": HAS_THINKING,
83
- "vulgarity": VULGARITY,
84
- "uncertainty": UNCERTAINTY,
85
- "character_names": CHARACTER_NAMES,
86
- "fluff": FLUFF,
87
- "has_repetition": HAS_REPETITION,
88
- "speculation": SPECULATION,
89
- "temporal_detail": TEMPORAL_DETAIL,
90
- "visual_specificity": VISUAL_SPECIFICITY,
91
- "camera_detail": CAMERA_DETAIL,
92
- "caption_style": CAPTION_STYLE,
93
- }
94
-
95
-
96
- def find_videos(input_folder: Path) -> list[Path]:
97
- iterator = input_folder.rglob("*") if RECURSIVE else input_folder.glob("*")
98
- videos = [
99
- path
100
- for path in iterator
101
- if path.is_file() and path.suffix.lower() in VIDEO_EXTENSIONS
102
- ]
103
- return sorted(videos)
104
-
105
-
106
- def output_path_for(video_path: Path, input_folder: Path, output_folder: Path) -> Path:
107
- relative = video_path.relative_to(input_folder)
108
- return (output_folder / relative).with_suffix(OUTPUT_EXTENSION)
109
-
110
-
111
- def write_text(path: Path, text: str) -> None:
112
- path.parent.mkdir(parents=True, exist_ok=True)
113
- path.write_text(text.rstrip() + "\n", encoding="utf-8")
114
-
115
-
116
- def chunks(items: list[Path], size: int) -> list[list[Path]]:
117
- return [items[index : index + size] for index in range(0, len(items), size)]
118
-
119
-
120
- def main() -> None:
121
- input_folder = Path(INPUT_FOLDER)
122
- output_folder = Path(OUTPUT_FOLDER)
123
- if not input_folder.is_dir():
124
- raise RuntimeError(f"INPUT_FOLDER does not exist or is not a directory: {input_folder}")
125
-
126
- videos = find_videos(input_folder)
127
- if not videos:
128
- print(f"No videos found in {input_folder}")
129
- return
130
-
131
- processor = load_processor(MODEL_DIR)
132
- llm = load_llm(
133
- model_dir=MODEL_DIR,
134
- max_model_len=MAX_MODEL_LEN,
135
- max_num_seqs=MAX_NUM_SEQS,
136
- gpu_memory_utilization=GPU_MEMORY_UTILIZATION,
137
- dtype=DTYPE,
138
- enforce_eager=ENFORCE_EAGER,
139
- enable_prefix_caching=ENABLE_PREFIX_CACHING,
140
- trust_remote_code=TRUST_REMOTE_CODE,
141
- )
142
- params = sampling_params(
143
- temperature=TEMPERATURE,
144
- max_tokens=MAX_TOKENS,
145
- top_p=TOP_P,
146
- repetition_penalty=REPETITION_PENALTY,
147
- )
148
-
149
- done = 0
150
- skipped = 0
151
- failed = 0
152
- settings = prompt_settings()
153
-
154
- for batch in chunks(videos, BATCH_SIZE):
155
- requests = []
156
- request_videos = []
157
- for video_path in batch:
158
- output_path = output_path_for(video_path, input_folder, output_folder)
159
- if output_path.exists() and not OVERWRITE_EXISTING:
160
- skipped += 1
161
- continue
162
- try:
163
- requests.append(
164
- build_vllm_request(
165
- processor=processor,
166
- model_dir=MODEL_DIR,
167
- video_path=video_path,
168
- num_frames=NUM_FRAMES,
169
- sampling_rate=SAMPLING_RATE,
170
- prompt_override=PROMPT_OVERRIDE,
171
- prompt_settings=settings,
172
- )
173
- )
174
- request_videos.append(video_path)
175
- except Exception as exc:
176
- failed += 1
177
- error_path = output_path.with_suffix(ERROR_EXTENSION)
178
- write_text(error_path, f"{type(exc).__name__}: {exc}")
179
- print(f"FAILED preprocess {video_path}: {exc}")
180
-
181
- if not requests:
182
- continue
183
-
184
- try:
185
- outputs = llm.generate(requests, sampling_params=params)
186
- except Exception as exc:
187
- failed += len(request_videos)
188
- for video_path in request_videos:
189
- output_path = output_path_for(video_path, input_folder, output_folder)
190
- error_path = output_path.with_suffix(ERROR_EXTENSION)
191
- write_text(error_path, f"{type(exc).__name__}: {exc}")
192
- print(f"FAILED batch starting {request_videos[0]}: {exc}")
193
- continue
194
-
195
- for video_path, output in zip(request_videos, outputs, strict=True):
196
- output_path = output_path_for(video_path, input_folder, output_folder)
197
- text = output.outputs[0].text if output.outputs else ""
198
- write_text(output_path, text)
199
- done += 1
200
- print(f"WROTE {output_path}")
201
-
202
- print(f"Done. wrote={done} skipped={skipped} failed={failed}")
203
-
204
-
205
- if __name__ == "__main__":
206
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.ipynb_checkpoints/caption_model_runtime-checkpoint.py DELETED
@@ -1,771 +0,0 @@
1
- """Runtime helpers for the condensed Gemma 4 Parakeet caption model."""
2
-
3
- from __future__ import annotations
4
-
5
- import math
6
- import types
7
- from collections import OrderedDict
8
- from pathlib import Path
9
- from typing import Any
10
-
11
- import numpy as np
12
- import soundfile as sf
13
- import torch
14
- from torch import nn
15
- from transformers import AutoModelForTDT
16
-
17
- try:
18
- from scipy.signal import resample_poly
19
- except Exception: # pragma: no cover - only needed for non-16k audio.
20
- resample_poly = None
21
-
22
-
23
- def unwrap_parallel(model: torch.nn.Module) -> torch.nn.Module:
24
- while hasattr(model, "module"):
25
- model = model.module
26
- return model
27
-
28
-
29
- def gemma_core(model: torch.nn.Module) -> torch.nn.Module:
30
- base = unwrap_parallel(model)
31
- core = getattr(base, "model", None)
32
- if core is not None and hasattr(core, "audio_tower") and hasattr(core, "embed_audio"):
33
- return core
34
- if hasattr(base, "audio_tower") and hasattr(base, "embed_audio"):
35
- return base
36
- raise AttributeError("Could not locate Gemma4Model core with audio_tower/embed_audio")
37
-
38
-
39
- def load_state_file(path: Path) -> dict[str, torch.Tensor]:
40
- if path.suffix == ".safetensors":
41
- from safetensors.torch import load_file
42
-
43
- return load_file(str(path))
44
- return torch.load(path, map_location="cpu")
45
-
46
-
47
- DEFAULT_PARAKEET_MODEL_ID = "nvidia/parakeet-tdt-0.6b-v3"
48
-
49
- class FrozenParakeetAudioTower(nn.Module):
50
- """Gemma audio tower replacement backed by a frozen Parakeet encoder.
51
-
52
- Gemma's processor creates one audio soft token per roughly four 10ms feature
53
- frames. Parakeet's encoder subsamples by eight, so the tower upsamples the
54
- Parakeet sequence back to the Gemma audio-token count before Gemma scatters
55
- the projected features into the prompt.
56
- """
57
-
58
- def __init__(
59
- self,
60
- model_id: str,
61
- *,
62
- local_files_only: bool,
63
- dtype: torch.dtype,
64
- expected_subsample_factor: int = 4,
65
- ) -> None:
66
- super().__init__()
67
- parakeet = AutoModelForTDT.from_pretrained(
68
- model_id,
69
- local_files_only=local_files_only,
70
- dtype=dtype,
71
- low_cpu_mem_usage=True,
72
- )
73
- self.encoder = parakeet.encoder
74
- self.hidden_size = int(parakeet.config.encoder_config.hidden_size)
75
- self.token_hidden_size = int(getattr(parakeet.config, "decoder_hidden_size", 640))
76
- self.model_id = model_id
77
- self.expected_subsample_factor = int(expected_subsample_factor)
78
- self.register_buffer("_parakeet_bridge_marker", torch.ones(1), persistent=True)
79
- for parameter in self.encoder.parameters():
80
- parameter.requires_grad = False
81
- self._disable_decode_expert_switching()
82
- self.encoder.eval()
83
- del parakeet
84
-
85
- def _disable_decode_expert_switching(self) -> None:
86
- def get_correct_experts_implementation(encoder: nn.Module, implementation: Any = None) -> Any:
87
- del encoder
88
- return implementation
89
-
90
- def set_experts_implementation(encoder: nn.Module, implementation: Any = None) -> None:
91
- del encoder, implementation
92
- return None
93
-
94
- self.encoder.get_correct_experts_implementation = types.MethodType(
95
- get_correct_experts_implementation,
96
- self.encoder,
97
- )
98
- self.encoder.set_experts_implementation = types.MethodType(
99
- set_experts_implementation,
100
- self.encoder,
101
- )
102
-
103
- def train(self, mode: bool = True) -> "FrozenParakeetAudioTower":
104
- super().train(mode)
105
- self.encoder.eval()
106
- return self
107
-
108
- def state_dict(self, *args: Any, **kwargs: Any) -> OrderedDict[str, torch.Tensor]:
109
- prefix = kwargs.get("prefix", "")
110
- destination = kwargs.get("destination")
111
- if destination is None:
112
- destination = OrderedDict()
113
- destination[prefix + "_parakeet_bridge_marker"] = self._parakeet_bridge_marker.detach().cpu()
114
- return destination
115
-
116
- def load_state_dict(self, state_dict: dict[str, torch.Tensor], strict: bool = True, assign: bool = False):
117
- del assign
118
- marker = state_dict.get("_parakeet_bridge_marker")
119
- if marker is not None:
120
- self._parakeet_bridge_marker.copy_(marker.to(self._parakeet_bridge_marker.device))
121
- missing = [] if marker is not None or not strict else ["_parakeet_bridge_marker"]
122
- unexpected = [key for key in state_dict if key != "_parakeet_bridge_marker"]
123
- if strict and (missing or unexpected):
124
- raise RuntimeError(f"Parakeet audio tower state mismatch: missing={missing} unexpected={unexpected}")
125
- return missing, unexpected
126
-
127
- @staticmethod
128
- def _gemma_audio_mask(input_features_mask: torch.Tensor, target_length: int) -> torch.Tensor:
129
- mask = input_features_mask
130
- while mask.shape[1] > target_length:
131
- mask = mask[:, ::2]
132
- if mask.shape[1] > target_length:
133
- mask = mask[:, :target_length]
134
- if mask.shape[1] < target_length:
135
- pad = torch.zeros(
136
- (mask.shape[0], target_length - mask.shape[1]),
137
- dtype=mask.dtype,
138
- device=mask.device,
139
- )
140
- mask = torch.cat([mask, pad], dim=1)
141
- return mask.bool()
142
-
143
- def forward(
144
- self,
145
- input_features: torch.Tensor,
146
- attention_mask: torch.Tensor | None = None,
147
- **kwargs: Any,
148
- ) -> Any:
149
- del kwargs
150
- if attention_mask is None:
151
- attention_mask = torch.ones(
152
- input_features.shape[:2],
153
- dtype=torch.long,
154
- device=input_features.device,
155
- )
156
- target_length = (input_features.shape[1] + self.expected_subsample_factor - 1) // self.expected_subsample_factor
157
- encoder_dtype = next(self.encoder.parameters()).dtype
158
- with torch.no_grad():
159
- encoded = self.encoder(
160
- input_features=input_features.to(dtype=encoder_dtype),
161
- attention_mask=attention_mask.long(),
162
- output_attention_mask=True,
163
- )
164
- hidden = encoded.last_hidden_state
165
-
166
- if hidden.shape[1] != target_length:
167
- hidden = torch.nn.functional.interpolate(
168
- hidden.transpose(1, 2).float(),
169
- size=target_length,
170
- mode="linear",
171
- align_corners=False,
172
- ).transpose(1, 2).to(dtype=encoder_dtype)
173
-
174
- output_mask = self._gemma_audio_mask(attention_mask, target_length)
175
- return type(
176
- "ParakeetAudioTowerOutput",
177
- (),
178
- {
179
- "last_hidden_state": hidden,
180
- "attention_mask": output_mask,
181
- "pooler_output": None,
182
- },
183
- )()
184
-
185
-
186
- class FrozenParakeetTDTTokenAudioTower(nn.Module):
187
- """Gemma audio tower that exposes Parakeet's audio-derived token stream.
188
-
189
- This still does not insert transcript text into the Gemma prompt. Parakeet
190
- runs from audio features to its own TDT token/duration sequence internally,
191
- then the decoder hidden states for that sequence become Gemma audio soft
192
- tokens after the trainable projector.
193
- """
194
-
195
- def __init__(
196
- self,
197
- model_id: str,
198
- *,
199
- local_files_only: bool,
200
- dtype: torch.dtype,
201
- expected_subsample_factor: int = 4,
202
- min_token_repeats: int = 1,
203
- token_feature_source: str = "decoder_states",
204
- filter_blank_tokens: bool = True,
205
- filter_special_token_ids: bool = True,
206
- ) -> None:
207
- super().__init__()
208
- self.tdt = AutoModelForTDT.from_pretrained(
209
- model_id,
210
- local_files_only=local_files_only,
211
- dtype=dtype,
212
- low_cpu_mem_usage=True,
213
- )
214
- self.hidden_size = int(self.tdt.config.decoder_hidden_size)
215
- self.blank_token_id = int(self.tdt.config.blank_token_id)
216
- self.model_id = model_id
217
- self.expected_subsample_factor = int(expected_subsample_factor)
218
- self.min_token_repeats = max(1, int(min_token_repeats))
219
- self.filter_blank_tokens = bool(filter_blank_tokens)
220
- self.filter_special_token_ids = bool(filter_special_token_ids)
221
- self.special_token_ids = {0, 2, 3}
222
- if token_feature_source not in {"decoder_states", "token_embeddings"}:
223
- raise ValueError(f"Unsupported token_feature_source={token_feature_source}")
224
- self.token_feature_source = token_feature_source
225
- self.register_buffer("_parakeet_tdt_token_bridge_marker", torch.ones(1), persistent=True)
226
- for parameter in self.tdt.parameters():
227
- parameter.requires_grad = False
228
- self._disable_decode_expert_switching()
229
- self.tdt.eval()
230
-
231
- def _disable_decode_expert_switching(self) -> None:
232
- def get_correct_experts_implementation(encoder: nn.Module, implementation: Any = None) -> Any:
233
- del encoder
234
- return implementation
235
-
236
- def set_experts_implementation(encoder: nn.Module, implementation: Any = None) -> None:
237
- del encoder, implementation
238
- return None
239
-
240
- self.tdt.encoder.get_correct_experts_implementation = types.MethodType(
241
- get_correct_experts_implementation,
242
- self.tdt.encoder,
243
- )
244
- self.tdt.encoder.set_experts_implementation = types.MethodType(
245
- set_experts_implementation,
246
- self.tdt.encoder,
247
- )
248
- self.tdt.get_correct_experts_implementation = types.MethodType(
249
- get_correct_experts_implementation,
250
- self.tdt,
251
- )
252
- self.tdt.set_experts_implementation = types.MethodType(
253
- set_experts_implementation,
254
- self.tdt,
255
- )
256
-
257
- def train(self, mode: bool = True) -> "FrozenParakeetTDTTokenAudioTower":
258
- super().train(mode)
259
- self.tdt.eval()
260
- return self
261
-
262
- def state_dict(self, *args: Any, **kwargs: Any) -> OrderedDict[str, torch.Tensor]:
263
- prefix = kwargs.get("prefix", "")
264
- destination = kwargs.get("destination")
265
- if destination is None:
266
- destination = OrderedDict()
267
- destination[prefix + "_parakeet_tdt_token_bridge_marker"] = (
268
- self._parakeet_tdt_token_bridge_marker.detach().cpu()
269
- )
270
- return destination
271
-
272
- def load_state_dict(self, state_dict: dict[str, torch.Tensor], strict: bool = True, assign: bool = False):
273
- del assign
274
- marker = state_dict.get("_parakeet_tdt_token_bridge_marker")
275
- if marker is not None:
276
- self._parakeet_tdt_token_bridge_marker.copy_(marker.to(self._parakeet_tdt_token_bridge_marker.device))
277
- missing = [] if marker is not None or not strict else ["_parakeet_tdt_token_bridge_marker"]
278
- unexpected = [key for key in state_dict if key != "_parakeet_tdt_token_bridge_marker"]
279
- if strict and (missing or unexpected):
280
- raise RuntimeError(f"Parakeet TDT token audio tower state mismatch: missing={missing} unexpected={unexpected}")
281
- return missing, unexpected
282
-
283
- def _target_length(self, input_features: torch.Tensor) -> int:
284
- return (input_features.shape[1] + self.expected_subsample_factor - 1) // self.expected_subsample_factor
285
-
286
- def _sequence_to_target_length(
287
- self,
288
- token_ids: torch.Tensor,
289
- decoder_states: torch.Tensor,
290
- durations: torch.Tensor,
291
- target_length: int,
292
- ) -> torch.Tensor:
293
- keep_mask = torch.ones_like(token_ids, dtype=torch.bool)
294
- if self.filter_blank_tokens:
295
- keep_mask &= token_ids.ne(self.blank_token_id)
296
- if self.filter_special_token_ids:
297
- for special_id in self.special_token_ids:
298
- keep_mask &= token_ids.ne(special_id)
299
- if keep_mask.any():
300
- decoder_states = decoder_states[keep_mask]
301
- durations = durations[keep_mask]
302
- repeats = durations.long().clamp_min(self.min_token_repeats)
303
- expanded = decoder_states.repeat_interleave(repeats, dim=0)
304
- if expanded.numel() == 0:
305
- expanded = decoder_states[:1]
306
- if expanded.shape[0] != target_length:
307
- expanded = torch.nn.functional.interpolate(
308
- expanded.transpose(0, 1).unsqueeze(0).float(),
309
- size=target_length,
310
- mode="linear",
311
- align_corners=False,
312
- ).squeeze(0).transpose(0, 1).to(dtype=decoder_states.dtype)
313
- return expanded
314
-
315
- def forward(
316
- self,
317
- input_features: torch.Tensor,
318
- attention_mask: torch.Tensor | None = None,
319
- **kwargs: Any,
320
- ) -> Any:
321
- del kwargs
322
- if attention_mask is None:
323
- attention_mask = torch.ones(
324
- input_features.shape[:2],
325
- dtype=torch.long,
326
- device=input_features.device,
327
- )
328
- target_length = self._target_length(input_features)
329
- model_dtype = next(self.tdt.parameters()).dtype
330
- with torch.no_grad():
331
- generated = self.tdt.generate(
332
- input_features=input_features.to(dtype=model_dtype),
333
- attention_mask=attention_mask.long(),
334
- max_new_tokens=target_length,
335
- )
336
- token_ids = generated.sequences.to(device=input_features.device)
337
- durations = generated.durations.to(device=input_features.device)
338
- if self.token_feature_source == "token_embeddings":
339
- decoder_states = self.tdt.decoder.embedding(token_ids)
340
- else:
341
- decoder_states = self.tdt.decoder(token_ids)
342
-
343
- projected_inputs: list[torch.Tensor] = []
344
- output_masks: list[torch.Tensor] = []
345
- for batch_index in range(decoder_states.shape[0]):
346
- expanded = self._sequence_to_target_length(
347
- token_ids[batch_index],
348
- decoder_states[batch_index],
349
- durations[batch_index],
350
- target_length,
351
- )
352
- projected_inputs.append(expanded)
353
- output_masks.append(torch.ones(target_length, dtype=torch.bool, device=input_features.device))
354
-
355
- hidden = torch.stack(projected_inputs, dim=0)
356
- output_mask = torch.stack(output_masks, dim=0)
357
- return type(
358
- "ParakeetTDTTokenAudioTowerOutput",
359
- (),
360
- {
361
- "last_hidden_state": hidden,
362
- "attention_mask": output_mask,
363
- "pooler_output": None,
364
- },
365
- )()
366
-
367
-
368
- class FrozenParakeetTDTTokenEncoderHybridAudioTower(FrozenParakeetTDTTokenAudioTower):
369
- """Expose both Parakeet TDT token embeddings and continuous encoder states.
370
-
371
- The token embedding stream carries the audio-derived ASR signal that already
372
- works. The continuous encoder stream preserves native acoustic information
373
- that does not survive the hard TDT token path.
374
- """
375
-
376
- def __init__(self, *args: Any, **kwargs: Any) -> None:
377
- super().__init__(*args, **kwargs)
378
- self.token_hidden_size = int(self.hidden_size)
379
- self.encoder_hidden_size = int(self.tdt.config.encoder_config.hidden_size)
380
- self.hidden_size = self.token_hidden_size + self.encoder_hidden_size
381
-
382
- @staticmethod
383
- def _match_length(hidden: torch.Tensor, target_length: int) -> torch.Tensor:
384
- if hidden.shape[1] == target_length:
385
- return hidden
386
- return torch.nn.functional.interpolate(
387
- hidden.transpose(1, 2).float(),
388
- size=target_length,
389
- mode="linear",
390
- align_corners=False,
391
- ).transpose(1, 2).to(dtype=hidden.dtype)
392
-
393
- def forward(
394
- self,
395
- input_features: torch.Tensor,
396
- attention_mask: torch.Tensor | None = None,
397
- **kwargs: Any,
398
- ) -> Any:
399
- del kwargs
400
- if attention_mask is None:
401
- attention_mask = torch.ones(
402
- input_features.shape[:2],
403
- dtype=torch.long,
404
- device=input_features.device,
405
- )
406
- target_length = self._target_length(input_features)
407
- model_dtype = next(self.tdt.parameters()).dtype
408
- model_features = input_features.to(dtype=model_dtype)
409
- with torch.no_grad():
410
- encoded = self.tdt.encoder(
411
- input_features=model_features,
412
- attention_mask=attention_mask.long(),
413
- output_attention_mask=True,
414
- )
415
- encoder_hidden = self._match_length(encoded.last_hidden_state, target_length)
416
- generated = self.tdt.generate(
417
- input_features=model_features,
418
- attention_mask=attention_mask.long(),
419
- max_new_tokens=target_length,
420
- )
421
- token_ids = generated.sequences.to(device=input_features.device)
422
- durations = generated.durations.to(device=input_features.device)
423
- if self.token_feature_source == "token_embeddings":
424
- decoder_states = self.tdt.decoder.embedding(token_ids)
425
- else:
426
- decoder_states = self.tdt.decoder(token_ids)
427
-
428
- token_inputs: list[torch.Tensor] = []
429
- for batch_index in range(decoder_states.shape[0]):
430
- token_inputs.append(
431
- self._sequence_to_target_length(
432
- token_ids[batch_index],
433
- decoder_states[batch_index],
434
- durations[batch_index],
435
- target_length,
436
- )
437
- )
438
-
439
- token_hidden = torch.stack(token_inputs, dim=0).to(dtype=model_dtype)
440
- encoder_hidden = encoder_hidden.to(dtype=model_dtype)
441
- hidden = torch.cat([token_hidden, encoder_hidden], dim=-1).to(dtype=model_dtype)
442
- output_mask = FrozenParakeetAudioTower._gemma_audio_mask(attention_mask, target_length)
443
- return type(
444
- "ParakeetTDTTokenEncoderHybridAudioTowerOutput",
445
- (),
446
- {
447
- "last_hidden_state": hidden,
448
- "attention_mask": output_mask,
449
- "pooler_output": None,
450
- },
451
- )()
452
-
453
-
454
- class ParakeetToGemmaAudioProjector(nn.Module):
455
- def __init__(
456
- self,
457
- input_hidden_size: int,
458
- output_hidden_size: int,
459
- intermediate_size: int = 4096,
460
- dropout: float = 0.0,
461
- ) -> None:
462
- super().__init__()
463
- self.input_norm = nn.LayerNorm(input_hidden_size)
464
- self.up = nn.Linear(input_hidden_size, intermediate_size)
465
- self.act = nn.GELU()
466
- self.dropout = nn.Dropout(dropout)
467
- self.down = nn.Linear(intermediate_size, output_hidden_size)
468
- self.output_norm = nn.LayerNorm(output_hidden_size)
469
-
470
- def forward(self, inputs_embeds: torch.Tensor) -> torch.Tensor:
471
- output_dtype = inputs_embeds.dtype
472
- hidden = self.input_norm(inputs_embeds)
473
- hidden = self.up(hidden)
474
- hidden = self.act(hidden)
475
- hidden = self.dropout(hidden)
476
- hidden = self.down(hidden)
477
- return self.output_norm(hidden).to(dtype=output_dtype)
478
-
479
-
480
- class ParakeetEncoderToTokenEmbeddingProjector(nn.Module):
481
- """Map continuous Parakeet encoder states through a speech-token-like space.
482
-
483
- The working TDT-token bridge proved that Gemma can use Parakeet's 640-dim
484
- token embedding space once it is projected into Gemma hidden size. This
485
- projector keeps the continuous encoder path, but gives it a trainable
486
- 1024->640 bottleneck before the known-good 640->Gemma projector.
487
- """
488
-
489
- def __init__(
490
- self,
491
- input_hidden_size: int,
492
- token_hidden_size: int,
493
- output_hidden_size: int,
494
- intermediate_size: int = 4096,
495
- dropout: float = 0.0,
496
- ) -> None:
497
- super().__init__()
498
- self.encoder_to_token = nn.Sequential(
499
- nn.LayerNorm(input_hidden_size),
500
- nn.Linear(input_hidden_size, intermediate_size),
501
- nn.GELU(),
502
- nn.Dropout(dropout),
503
- nn.Linear(intermediate_size, token_hidden_size),
504
- nn.LayerNorm(token_hidden_size),
505
- )
506
- self.token_projector = ParakeetToGemmaAudioProjector(
507
- input_hidden_size=token_hidden_size,
508
- output_hidden_size=output_hidden_size,
509
- intermediate_size=intermediate_size,
510
- dropout=dropout,
511
- )
512
-
513
- def forward(self, inputs_embeds: torch.Tensor) -> torch.Tensor:
514
- output_dtype = inputs_embeds.dtype
515
- token_like = self.encoder_to_token(inputs_embeds).to(dtype=output_dtype)
516
- return self.token_projector(token_like).to(dtype=output_dtype)
517
-
518
-
519
- class ParakeetTDTTokenEncoderHybridProjector(nn.Module):
520
- """Project token embeddings plus continuous encoder states into Gemma space."""
521
-
522
- def __init__(
523
- self,
524
- token_hidden_size: int,
525
- encoder_hidden_size: int,
526
- output_hidden_size: int,
527
- intermediate_size: int = 4096,
528
- dropout: float = 0.0,
529
- encoder_gate_init: float = 0.0,
530
- ) -> None:
531
- super().__init__()
532
- self.token_hidden_size = int(token_hidden_size)
533
- self.encoder_hidden_size = int(encoder_hidden_size)
534
- self.token_projector = ParakeetToGemmaAudioProjector(
535
- input_hidden_size=token_hidden_size,
536
- output_hidden_size=output_hidden_size,
537
- intermediate_size=intermediate_size,
538
- dropout=dropout,
539
- )
540
- self.encoder_projector = ParakeetToGemmaAudioProjector(
541
- input_hidden_size=encoder_hidden_size,
542
- output_hidden_size=output_hidden_size,
543
- intermediate_size=intermediate_size,
544
- dropout=dropout,
545
- )
546
- self.encoder_gate = nn.Parameter(torch.tensor(float(encoder_gate_init), dtype=torch.float32))
547
-
548
- def forward(self, inputs_embeds: torch.Tensor) -> torch.Tensor:
549
- output_dtype = inputs_embeds.dtype
550
- token_hidden, encoder_hidden = torch.split(
551
- inputs_embeds,
552
- [self.token_hidden_size, self.encoder_hidden_size],
553
- dim=-1,
554
- )
555
- token_output = self.token_projector(token_hidden)
556
- encoder_output = self.encoder_projector(encoder_hidden)
557
- gate = torch.tanh(self.encoder_gate).to(dtype=output_dtype)
558
- return (token_output + gate * encoder_output).to(dtype=output_dtype)
559
-
560
-
561
- def load_audio_array(path: str | Path, sampling_rate: int, max_length_samples: int) -> np.ndarray:
562
- audio, source_rate = sf.read(str(path), dtype="float32", always_2d=False)
563
- if audio.ndim > 1:
564
- audio = audio.mean(axis=1)
565
- if source_rate != sampling_rate:
566
- if resample_poly is None:
567
- raise RuntimeError(
568
- f"Audio {path} has sample rate {source_rate}, but scipy is unavailable for resampling"
569
- )
570
- divisor = math.gcd(int(source_rate), int(sampling_rate))
571
- audio = resample_poly(audio, sampling_rate // divisor, source_rate // divisor).astype("float32")
572
- if max_length_samples > 0 and audio.shape[0] > max_length_samples:
573
- audio = audio[:max_length_samples]
574
- return np.asarray(audio, dtype=np.float32)
575
-
576
-
577
- def pad_or_trim_parakeet_features(
578
- input_features: torch.Tensor,
579
- attention_mask: torch.Tensor,
580
- target_length: int,
581
- ) -> tuple[torch.Tensor, torch.Tensor]:
582
- if input_features.shape[1] > target_length:
583
- input_features = input_features[:, :target_length]
584
- attention_mask = attention_mask[:, :target_length]
585
- if input_features.shape[1] < target_length:
586
- pad_length = target_length - input_features.shape[1]
587
- input_features = torch.nn.functional.pad(input_features, (0, 0, 0, pad_length), value=0.0)
588
- attention_mask = torch.nn.functional.pad(attention_mask, (0, pad_length), value=0)
589
- return input_features, attention_mask
590
-
591
-
592
- def parakeet_feature_tensors(
593
- *,
594
- audio_paths: list[str],
595
- parakeet_processor: Any,
596
- sampling_rate: int,
597
- max_length_samples: int,
598
- target_length: int,
599
- ) -> tuple[torch.Tensor, torch.Tensor]:
600
- waveforms = [load_audio_array(path, sampling_rate, max_length_samples) for path in audio_paths]
601
- parakeet_batch = parakeet_processor(
602
- waveforms,
603
- sampling_rate=sampling_rate,
604
- return_tensors="pt",
605
- padding=True,
606
- )
607
- features = parakeet_batch["input_features"].float()
608
- mask = parakeet_batch.get("attention_mask")
609
- if mask is None:
610
- mask = torch.ones(features.shape[:2], dtype=torch.bool)
611
- features, mask = pad_or_trim_parakeet_features(features, mask.bool(), target_length)
612
- return features, mask
613
-
614
-
615
- def replace_batch_audio_features(
616
- batch: dict[str, torch.Tensor],
617
- *,
618
- audio_paths: list[str],
619
- parakeet_processor: Any,
620
- sampling_rate: int,
621
- max_length_samples: int,
622
- prefix: str = "",
623
- ) -> None:
624
- feature_key = f"{prefix}input_features"
625
- mask_key = f"{prefix}input_features_mask"
626
- if feature_key not in batch:
627
- return
628
- original_features = batch[feature_key]
629
- original_mask = batch[mask_key]
630
- features, mask = parakeet_feature_tensors(
631
- audio_paths=audio_paths,
632
- parakeet_processor=parakeet_processor,
633
- sampling_rate=sampling_rate,
634
- max_length_samples=max_length_samples,
635
- target_length=int(original_features.shape[1]),
636
- )
637
- batch[feature_key] = features.to(dtype=original_features.dtype)
638
- batch[mask_key] = mask.to(dtype=original_mask.dtype)
639
-
640
-
641
- def install_parakeet_audio_bridge(model: torch.nn.Module, args: argparse.Namespace) -> None:
642
- core = gemma_core(model)
643
- text_hidden_size = int(model.config.get_text_config().hidden_size)
644
- if args.parakeet_bridge_mode == "tdt_tokens":
645
- audio_tower = FrozenParakeetTDTTokenAudioTower(
646
- args.parakeet_model_id,
647
- local_files_only=args.local_files_only,
648
- dtype=torch.bfloat16,
649
- token_feature_source="decoder_states",
650
- filter_blank_tokens=getattr(args, "parakeet_tdt_filter_blank_tokens", True),
651
- filter_special_token_ids=getattr(args, "parakeet_tdt_filter_special_token_ids", True),
652
- )
653
- elif args.parakeet_bridge_mode in {"tdt_token_embeddings", "tdt_token_embeddings_with_encoder_context"}:
654
- tower_class = (
655
- FrozenParakeetTDTTokenEncoderHybridAudioTower
656
- if args.parakeet_bridge_mode == "tdt_token_embeddings_with_encoder_context"
657
- else FrozenParakeetTDTTokenAudioTower
658
- )
659
- audio_tower = tower_class(
660
- args.parakeet_model_id,
661
- local_files_only=args.local_files_only,
662
- dtype=torch.bfloat16,
663
- token_feature_source="token_embeddings",
664
- filter_blank_tokens=getattr(args, "parakeet_tdt_filter_blank_tokens", True),
665
- filter_special_token_ids=getattr(args, "parakeet_tdt_filter_special_token_ids", True),
666
- )
667
- else:
668
- audio_tower = FrozenParakeetAudioTower(
669
- args.parakeet_model_id,
670
- local_files_only=args.local_files_only,
671
- dtype=torch.bfloat16,
672
- )
673
- if args.parakeet_bridge_mode == "encoder_soft_tdt_token_embeddings":
674
- projector = ParakeetEncoderToTokenEmbeddingProjector(
675
- input_hidden_size=audio_tower.hidden_size,
676
- token_hidden_size=getattr(audio_tower, "token_hidden_size", 640),
677
- output_hidden_size=text_hidden_size,
678
- intermediate_size=args.projector_intermediate_size,
679
- dropout=args.projector_dropout,
680
- ).to(dtype=torch.bfloat16)
681
- elif args.parakeet_bridge_mode == "tdt_token_embeddings_with_encoder_context":
682
- projector = ParakeetTDTTokenEncoderHybridProjector(
683
- token_hidden_size=getattr(audio_tower, "token_hidden_size", 640),
684
- encoder_hidden_size=getattr(audio_tower, "encoder_hidden_size", 1024),
685
- output_hidden_size=text_hidden_size,
686
- intermediate_size=args.projector_intermediate_size,
687
- dropout=args.projector_dropout,
688
- encoder_gate_init=getattr(args, "hybrid_encoder_gate_init", 0.0),
689
- ).to(dtype=torch.bfloat16)
690
- else:
691
- projector = ParakeetToGemmaAudioProjector(
692
- input_hidden_size=audio_tower.hidden_size,
693
- output_hidden_size=text_hidden_size,
694
- intermediate_size=args.projector_intermediate_size,
695
- dropout=args.projector_dropout,
696
- ).to(dtype=torch.bfloat16)
697
- core.audio_tower = audio_tower
698
- core.embed_audio = projector
699
- target_device = getattr(model, "device", None)
700
- if isinstance(target_device, torch.device) and target_device.type != "cpu":
701
- core.audio_tower.to(device=target_device)
702
- core.embed_audio.to(device=target_device)
703
- print(
704
- "parakeet_audio_bridge_installed=true "
705
- f"bridge_mode={args.parakeet_bridge_mode} "
706
- f"parakeet_model_id={args.parakeet_model_id} "
707
- f"parakeet_hidden_size={audio_tower.hidden_size} "
708
- f"token_hidden_size={getattr(audio_tower, 'token_hidden_size', 'n/a')} "
709
- f"gemma_hidden_size={text_hidden_size} "
710
- f"projector_intermediate_size={args.projector_intermediate_size}",
711
- flush=True,
712
- )
713
-
714
-
715
- CAPTION_LENGTH_LABELS = ("very small", "small", "medium", "large", "very large")
716
- TAG_KEYS = ("tags", "tag_list", "tag_string", "danbooru_tags", "booru_tags")
717
- CAPTION_SETTING_FIELD_CHOICES = {
718
- "vulgarity": ("none", "low", "medium", "high"),
719
- "uncertainty": ("none", "low", "medium", "high"),
720
- "character_names": ("none", "ambiguous", "single", "multiple"),
721
- "fluff": ("none", "low", "medium", "high"),
722
- "speculation": ("none", "low", "medium", "high"),
723
- "temporal_detail": ("static", "low", "medium", "high"),
724
- "visual_specificity": ("generic", "moderate", "detailed", "excessive"),
725
- "camera_detail": ("none", "low", "medium", "high"),
726
- "caption_style": ("plain", "verbose", "ornate", "robotic"),
727
- }
728
- CAPTION_SETTING_FIELDS = tuple(CAPTION_SETTING_FIELD_CHOICES)
729
- DEFAULT_CAPTION_SETTING_VALUES = {
730
- "vulgarity": "none",
731
- "uncertainty": "none",
732
- "character_names": "none",
733
- "fluff": "none",
734
- "has_repetition": False,
735
- "has_thinking": True,
736
- "speculation": "none",
737
- "temporal_detail": "medium",
738
- "visual_specificity": "moderate",
739
- "camera_detail": "low",
740
- "caption_style": "plain",
741
- }
742
-
743
-
744
- def format_caption_settings_prompt(settings: dict[str, Any]) -> str:
745
- watermark_instruction = (
746
- "Include watermark info." if settings["include_watermark_info"] else "Do not include watermark info."
747
- )
748
- repetition_value = str(bool(settings["has_repetition"])).lower()
749
- thinking_value = str(bool(settings.get("has_thinking", True))).lower()
750
- thinking_instruction = (
751
- "Output thought JSON before the final caption."
752
- if settings.get("has_thinking", True)
753
- else "Do not output thought JSON; output only the caption."
754
- )
755
- setting_text = (
756
- f"vulgarity={settings['vulgarity']}; "
757
- f"uncertainty={settings['uncertainty']}; "
758
- f"character_names={settings['character_names']}; "
759
- f"fluff={settings['fluff']}; "
760
- f"has_repetition={repetition_value}; "
761
- f"has_thinking={thinking_value}; "
762
- f"speculation={settings['speculation']}; "
763
- f"temporal_detail={settings['temporal_detail']}; "
764
- f"visual_specificity={settings['visual_specificity']}; "
765
- f"camera_detail={settings['camera_detail']}; "
766
- f"caption_style={settings['caption_style']}"
767
- )
768
- return (
769
- f"Write a {settings['caption_length']} caption for this clip using both the visuals and the audio. "
770
- f"{watermark_instruction} {thinking_instruction} Match these caption settings: {setting_text}."
771
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.ipynb_checkpoints/infer-checkpoint.py DELETED
@@ -1,519 +0,0 @@
1
- """Run Gemma 4 31B Parakeet-hybrid caption inference on one video.
2
-
3
- Edit the variables below, then run on the Vast instance:
4
-
5
- source /venv/main/bin/activate
6
- cd /workspace/polished_model_v3
7
- python infer.py
8
-
9
- This uses the condensed inference package:
10
- - video + text prompt are passed through the Gemma processor
11
- - audio is supplied as a sidecar WAV
12
- - processor-created audio features are replaced with Parakeet-native features
13
- - the language LoRA is already merged into model/
14
- - the trained audio projector is loaded from model/embed_audio.safetensors
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- import contextlib
20
- import io
21
- import json
22
- import logging
23
- import subprocess
24
- import tempfile
25
- import warnings
26
- from pathlib import Path
27
- from typing import Any
28
-
29
- import torch
30
- from transformers import AutoModelForMultimodalLM, AutoProcessor
31
-
32
- SCRIPT_DIR = Path(__file__).resolve().parent
33
- MODEL_ROOT = SCRIPT_DIR
34
-
35
- from caption_model_runtime import (
36
- DEFAULT_PARAKEET_MODEL_ID,
37
- CAPTION_LENGTH_LABELS,
38
- CAPTION_SETTING_FIELD_CHOICES,
39
- DEFAULT_CAPTION_SETTING_VALUES,
40
- format_caption_settings_prompt,
41
- gemma_core,
42
- install_parakeet_audio_bridge,
43
- load_state_file,
44
- replace_batch_audio_features,
45
- )
46
-
47
-
48
- # Edit these.
49
- VIDEO_PATH = "/workspace/test7.mp4" # Options: path to the video you want to caption.
50
- MODEL_PATH = str(MODEL_ROOT / "model") # Options: merged model path.
51
- PROCESSOR_PATH = str(MODEL_ROOT / "processor") # Options: processor path.
52
- AUDIO_PROJECTOR_PATH = str(MODEL_ROOT / "model" / "embed_audio.safetensors") # Options: trained audio projector path.
53
-
54
- # Parakeet hybrid audio bridge settings. These should normally match the packaged model.
55
- PARAKEET_MODEL_ID = str(MODEL_ROOT / "parakeet") # Options: "nvidia/parakeet-tdt-0.6b-v3" or compatible local/HF path.
56
- PARAKEET_BRIDGE_MODE = "tdt_token_embeddings_with_encoder_context" # Options: "encoder", "tdt_tokens", "tdt_token_embeddings", "encoder_soft_tdt_token_embeddings", "tdt_token_embeddings_with_encoder_context".
57
- PARAKEET_NATIVE_FEATURES = True # Options: True to replace Gemma audio features with Parakeet features, False for debugging only.
58
- PARAKEET_TDT_FILTER_BLANK_TOKENS = True # Options: True or False.
59
- PARAKEET_TDT_FILTER_SPECIAL_TOKEN_IDS = True # Options: True or False.
60
- PROJECTOR_INTERMEDIATE_SIZE = 4096 # Options: integer; the packaged model uses 4096.
61
- PROJECTOR_DROPOUT = 0.0 # Options: float; inference should normally be 0.0.
62
- HYBRID_ENCODER_GATE_INIT = 0.0 # Options: float; saved audio projector weights override the initial gate.
63
-
64
- # Prompt settings. Empty PROMPT_OVERRIDE builds the standard dynamic prompt.
65
- PROMPT_OVERRIDE = "" # Options: "" or any full custom prompt string.
66
- CAPTION_SETTINGS_JSON_PATH = "" # Options: "" or a JSON path under /workspace/dataset_jsons to override the settings below.
67
- CAPTION_LENGTH = "very large" # Options: "very small", "small", "medium", "large", "very large".
68
- INCLUDE_WATERMARK_INFO = False # Options: True or False.
69
- VULGARITY = "low" # Options: "none", "low", "medium", "high".
70
- UNCERTAINTY = "low" # Options: "none", "low", "medium", "high".
71
- CHARACTER_NAMES = "none" # Options: "none", "ambiguous", "single", "multiple".
72
- FLUFF = "none" # Options: "none", "low", "medium", "high".
73
- HAS_REPETITION = False # Options: True or False.
74
- SPECULATION = "low" # Options: "none", "low", "medium", "high".
75
- TEMPORAL_DETAIL = "medium" # Options: "static", "low", "medium", "high".
76
- VISUAL_SPECIFICITY = "moderate" # Options: "generic", "moderate", "detailed", "excessive".
77
- CAMERA_DETAIL = "medium" # Options: "none", "low", "medium", "high".
78
- CAPTION_STYLE = "plain" # Options: "plain", "verbose", "ornate", "robotic".
79
- HAS_THINKING = True # Options: True to request thought JSON plus final caption, False to request only the final caption.
80
-
81
- # Media settings. Training used separate sidecar audio and random frame counts.
82
- NUM_FRAMES = 12 # Options: None for processor default, or an integer frame count.
83
- FPS = None # Options: None for processor default, or a float such as 1.0.
84
- SAMPLING_RATE = 16_000 # Options: normally 16000.
85
- AUDIO_MAX_LENGTH_SAMPLES = 0 # Options: 0 keeps full audio; positive integer truncates Parakeet audio.
86
- MAX_AUDIO_SECONDS = 0.0 # Options: 0.0 keeps full audio; positive float caps extracted sidecar audio.
87
-
88
- # Generation settings.
89
- MAX_NEW_TOKENS = 1200 # Options: positive integer token cap.
90
- TEMPERATURE = 0.0 # Options: 0.0 for greedy decoding, >0.0 for sampling.
91
- TOP_P = 0.9 # Options: float in (0, 1], used only when TEMPERATURE > 0.
92
- REPETITION_PENALTY = 1.1 # Options: 1.0 disables the penalty, >1.0 penalizes repetition.
93
- PRINT_INPUT_STATS = False # Options: True or False.
94
- QUIET_MODEL_LOAD = True # Options: True hides noisy missing-key load reports; False prints full loader output.
95
-
96
- # Usually leave these alone.
97
- LOCAL_FILES_ONLY = True # Options: True to use cached files only, False to allow downloads.
98
- DTYPE = torch.bfloat16 # Options: torch.bfloat16, torch.float16, torch.float32.
99
- DEVICE_MAP = "auto" # Options: "auto", "cuda", or another Transformers device_map value.
100
- ATTN_IMPLEMENTATION = "sdpa" # Options: "sdpa", "flash_attention_2", None.
101
-
102
- warnings.filterwarnings(
103
- "ignore",
104
- message=r"RNN module weights are not part of single contiguous chunk of memory.*",
105
- category=UserWarning,
106
- )
107
-
108
-
109
- @contextlib.contextmanager
110
- def quiet_model_load() -> Any:
111
- if not QUIET_MODEL_LOAD:
112
- yield
113
- return
114
- load_report_logger = logging.getLogger("transformers.utils.loading_report")
115
- old_level = load_report_logger.level
116
- load_report_logger.setLevel(logging.ERROR)
117
- patched_modules: list[tuple[Any, Any]] = []
118
- try:
119
- import transformers.modeling_utils as modeling_utils
120
- import transformers.utils.loading_report as loading_report
121
-
122
- original_report = modeling_utils.log_state_dict_report
123
-
124
- def quiet_report(
125
- model: Any,
126
- pretrained_model_name_or_path: str,
127
- ignore_mismatched_sizes: bool,
128
- loading_info: Any,
129
- logger: logging.Logger | None = None,
130
- ) -> None:
131
- has_fatal_issue = bool(getattr(loading_info, "error_msgs", None)) or bool(
132
- getattr(loading_info, "conversion_errors", None)
133
- )
134
- if not ignore_mismatched_sizes and bool(getattr(loading_info, "mismatched_keys", None)):
135
- has_fatal_issue = True
136
- if has_fatal_issue:
137
- original_report(
138
- model,
139
- pretrained_model_name_or_path,
140
- ignore_mismatched_sizes,
141
- loading_info,
142
- logger=logger,
143
- )
144
-
145
- for module in (loading_report, modeling_utils):
146
- patched_modules.append((module, module.log_state_dict_report))
147
- module.log_state_dict_report = quiet_report
148
- except Exception:
149
- patched_modules = []
150
- try:
151
- with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
152
- yield
153
- finally:
154
- for module, original in patched_modules:
155
- module.log_state_dict_report = original
156
- load_report_logger.setLevel(old_level)
157
-
158
-
159
- def load_parakeet_projector(model: torch.nn.Module) -> None:
160
- state_path = Path(AUDIO_PROJECTOR_PATH)
161
- state = load_state_file(state_path)
162
- module = gemma_core(model).embed_audio
163
- module.load_state_dict(state, strict=True)
164
-
165
-
166
- def video_has_audio_stream(video_path: Path) -> bool:
167
- cmd = [
168
- "ffprobe",
169
- "-v",
170
- "error",
171
- "-select_streams",
172
- "a:0",
173
- "-show_entries",
174
- "stream=index",
175
- "-of",
176
- "csv=p=0",
177
- str(video_path),
178
- ]
179
- result = subprocess.run(cmd, check=True, capture_output=True, text=True)
180
- return bool(result.stdout.strip())
181
-
182
-
183
- def probe_video_duration_seconds(video_path: Path) -> float:
184
- cmd = [
185
- "ffprobe",
186
- "-v",
187
- "error",
188
- "-show_entries",
189
- "format=duration",
190
- "-of",
191
- "default=noprint_wrappers=1:nokey=1",
192
- str(video_path),
193
- ]
194
- result = subprocess.run(cmd, check=True, capture_output=True, text=True)
195
- duration = float(result.stdout.strip())
196
- if duration <= 0:
197
- raise RuntimeError(f"Video duration must be positive: {video_path}")
198
- return duration
199
-
200
-
201
- def extract_audio(video_path: Path, audio_path: Path) -> None:
202
- cmd = [
203
- "ffmpeg",
204
- "-hide_banner",
205
- "-loglevel",
206
- "error",
207
- "-y",
208
- "-i",
209
- str(video_path),
210
- "-vn",
211
- "-ac",
212
- "1",
213
- "-ar",
214
- str(SAMPLING_RATE),
215
- ]
216
- if MAX_AUDIO_SECONDS > 0:
217
- cmd.extend(["-t", f"{MAX_AUDIO_SECONDS:.6f}"])
218
- cmd.extend(["-c:a", "pcm_s16le", str(audio_path)])
219
- subprocess.run(cmd, check=True)
220
-
221
-
222
- def create_silent_audio(audio_path: Path, duration_seconds: float) -> None:
223
- if MAX_AUDIO_SECONDS > 0:
224
- duration_seconds = min(duration_seconds, MAX_AUDIO_SECONDS)
225
- cmd = [
226
- "ffmpeg",
227
- "-hide_banner",
228
- "-loglevel",
229
- "error",
230
- "-y",
231
- "-f",
232
- "lavfi",
233
- "-i",
234
- f"anullsrc=channel_layout=mono:sample_rate={SAMPLING_RATE}",
235
- "-t",
236
- f"{duration_seconds:.6f}",
237
- "-ac",
238
- "1",
239
- "-ar",
240
- str(SAMPLING_RATE),
241
- "-c:a",
242
- "pcm_s16le",
243
- str(audio_path),
244
- ]
245
- subprocess.run(cmd, check=True)
246
-
247
-
248
- def prepare_sidecar_audio(video_path: Path, tmpdir: Path) -> tuple[Path, bool]:
249
- audio_path = tmpdir / "sidecar_audio.wav"
250
- if video_has_audio_stream(video_path):
251
- extract_audio(video_path, audio_path)
252
- return audio_path, True
253
-
254
- duration_seconds = probe_video_duration_seconds(video_path)
255
- print(f"input_video_has_audio=false; creating_silent_sidecar_audio duration_seconds={duration_seconds:.3f}", flush=True)
256
- create_silent_audio(audio_path, duration_seconds)
257
- return audio_path, False
258
-
259
-
260
- def bool_from_json(value: Any, field_name: str) -> bool:
261
- if isinstance(value, bool):
262
- return value
263
- if isinstance(value, str):
264
- lowered = value.strip().lower()
265
- if lowered in {"1", "true", "yes", "y", "on"}:
266
- return True
267
- if lowered in {"0", "false", "no", "n", "off"}:
268
- return False
269
- raise ValueError(f"{field_name} must be boolean-like, got {value!r}")
270
-
271
-
272
- def load_prompt_settings_json() -> dict[str, Any]:
273
- if not CAPTION_SETTINGS_JSON_PATH.strip():
274
- return {}
275
- path = Path(CAPTION_SETTINGS_JSON_PATH)
276
- data = json.loads(path.read_text(encoding="utf-8"))
277
- if not isinstance(data, dict):
278
- raise ValueError(f"CAPTION_SETTINGS_JSON_PATH must point to a JSON object: {path}")
279
- return data
280
-
281
-
282
- def build_prompt() -> str:
283
- if PROMPT_OVERRIDE.strip():
284
- return PROMPT_OVERRIDE.strip()
285
-
286
- settings: dict[str, Any] = {
287
- "caption_length": CAPTION_LENGTH,
288
- "include_watermark_info": INCLUDE_WATERMARK_INFO,
289
- **DEFAULT_CAPTION_SETTING_VALUES,
290
- "vulgarity": VULGARITY,
291
- "uncertainty": UNCERTAINTY,
292
- "character_names": CHARACTER_NAMES,
293
- "fluff": FLUFF,
294
- "has_repetition": HAS_REPETITION,
295
- "speculation": SPECULATION,
296
- "temporal_detail": TEMPORAL_DETAIL,
297
- "visual_specificity": VISUAL_SPECIFICITY,
298
- "camera_detail": CAMERA_DETAIL,
299
- "caption_style": CAPTION_STYLE,
300
- "has_thinking": HAS_THINKING,
301
- }
302
- settings.update(load_prompt_settings_json())
303
- settings["has_thinking"] = HAS_THINKING
304
-
305
- settings["caption_length"] = str(settings["caption_length"]).strip().lower()
306
- if settings["caption_length"] not in CAPTION_LENGTH_LABELS:
307
- raise ValueError(f"caption_length must be one of {CAPTION_LENGTH_LABELS}, got {settings['caption_length']!r}")
308
- settings["include_watermark_info"] = bool_from_json(settings["include_watermark_info"], "include_watermark_info")
309
- settings["has_repetition"] = bool_from_json(settings["has_repetition"], "has_repetition")
310
- settings["has_thinking"] = bool_from_json(settings["has_thinking"], "has_thinking")
311
- for field_name, allowed in CAPTION_SETTING_FIELD_CHOICES.items():
312
- value = str(settings[field_name]).strip().lower()
313
- if value not in allowed:
314
- raise ValueError(f"{field_name} must be one of {allowed}, got {value!r}")
315
- settings[field_name] = value
316
-
317
- return format_caption_settings_prompt(settings)
318
-
319
-
320
- def build_messages(video_path: Path, audio_path: Path, prompt: str) -> list[dict[str, Any]]:
321
- return [
322
- {
323
- "role": "user",
324
- "content": [
325
- {"type": "video", "path": str(video_path)},
326
- {"type": "text", "text": prompt},
327
- {"type": "audio", "path": str(audio_path)},
328
- ],
329
- },
330
- {"role": "assistant", "content": [{"type": "text", "text": ""}]},
331
- ]
332
-
333
-
334
- def trim_empty_assistant_terminator(inputs: dict[str, torch.Tensor], processor: Any) -> dict[str, torch.Tensor]:
335
- eos_tail = processor.tokenizer.encode("<turn|>\n", add_special_tokens=False)
336
- if not eos_tail:
337
- return inputs
338
-
339
- tail_len = len(eos_tail)
340
- input_ids = inputs["input_ids"][0]
341
- if input_ids[-tail_len:].tolist() != eos_tail:
342
- return inputs
343
-
344
- trimmed = {}
345
- for key, value in inputs.items():
346
- if isinstance(value, torch.Tensor) and value.ndim >= 2 and value.shape[1] == input_ids.shape[0]:
347
- trimmed[key] = value[:, :-tail_len]
348
- else:
349
- trimmed[key] = value
350
- return trimmed
351
-
352
-
353
- def tensor_stats(tensor: torch.Tensor | None, mask: torch.Tensor | None = None) -> dict[str, Any]:
354
- if tensor is None:
355
- return {"present": False}
356
- stats_tensor = tensor.detach().float().cpu()
357
- result: dict[str, Any] = {
358
- "present": True,
359
- "shape": list(tensor.shape),
360
- "mean": round(float(stats_tensor.mean().item()), 8),
361
- "std": round(float(stats_tensor.std().item()), 8),
362
- "abs_mean": round(float(stats_tensor.abs().mean().item()), 8),
363
- }
364
- if mask is not None:
365
- result["mask_shape"] = list(mask.shape)
366
- result["mask_sum"] = int(mask.detach().cpu().sum().item())
367
- return result
368
-
369
-
370
- def print_input_stats(inputs: dict[str, torch.Tensor], label: str) -> None:
371
- stats = {
372
- "input_ids_shape": list(inputs["input_ids"].shape),
373
- "input_features": tensor_stats(inputs.get("input_features"), inputs.get("input_features_mask")),
374
- "keys": sorted(inputs.keys()),
375
- }
376
- print(f"{label}=" + json.dumps(stats, sort_keys=True), flush=True)
377
-
378
-
379
- def move_inputs_to_model_device(inputs: dict[str, Any], model: torch.nn.Module) -> dict[str, Any]:
380
- device = getattr(model, "device", None)
381
- if device is None:
382
- try:
383
- device = next(model.parameters()).device
384
- except StopIteration:
385
- device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
386
-
387
- moved = {}
388
- for key, value in inputs.items():
389
- moved[key] = value.to(device) if isinstance(value, torch.Tensor) else value
390
- return moved
391
-
392
-
393
- def generation_kwargs() -> dict[str, Any]:
394
- kwargs: dict[str, Any] = {
395
- "max_new_tokens": MAX_NEW_TOKENS,
396
- "do_sample": TEMPERATURE > 0,
397
- "repetition_penalty": REPETITION_PENALTY,
398
- "use_cache": True,
399
- }
400
- if TEMPERATURE > 0:
401
- kwargs["temperature"] = TEMPERATURE
402
- kwargs["top_p"] = TOP_P
403
- return kwargs
404
-
405
-
406
- def prepare_inputs(
407
- processor: Any,
408
- parakeet_processor: Any,
409
- video_path: Path,
410
- audio_path: Path,
411
- prompt: str,
412
- ) -> dict[str, torch.Tensor]:
413
- processor_kwargs: dict[str, Any] = {
414
- "padding": True,
415
- "truncation": False,
416
- "sampling_rate": SAMPLING_RATE,
417
- }
418
- if NUM_FRAMES is not None:
419
- processor_kwargs["num_frames"] = NUM_FRAMES
420
- if FPS is not None:
421
- processor_kwargs["fps"] = FPS
422
-
423
- inputs = processor.apply_chat_template(
424
- build_messages(video_path, audio_path, prompt),
425
- tokenize=True,
426
- return_dict=True,
427
- return_tensors="pt",
428
- load_audio_from_video=False,
429
- processor_kwargs=processor_kwargs,
430
- )
431
- inputs = trim_empty_assistant_terminator(inputs, processor)
432
- if PRINT_INPUT_STATS:
433
- print_input_stats(inputs, "input_stats_before_parakeet_swap")
434
- if PARAKEET_NATIVE_FEATURES:
435
- replace_batch_audio_features(
436
- inputs,
437
- audio_paths=[str(audio_path)],
438
- parakeet_processor=parakeet_processor,
439
- sampling_rate=SAMPLING_RATE,
440
- max_length_samples=AUDIO_MAX_LENGTH_SAMPLES,
441
- )
442
- if PRINT_INPUT_STATS:
443
- print_input_stats(inputs, "input_stats_after_parakeet_swap")
444
- return inputs
445
-
446
-
447
- def load_model(processor_path: Path) -> tuple[Any, torch.nn.Module, Any]:
448
- processor = AutoProcessor.from_pretrained(str(processor_path), local_files_only=LOCAL_FILES_ONLY)
449
- parakeet_processor = AutoProcessor.from_pretrained(PARAKEET_MODEL_ID, local_files_only=LOCAL_FILES_ONLY)
450
-
451
- model_kwargs: dict[str, Any] = {
452
- "local_files_only": LOCAL_FILES_ONLY,
453
- "dtype": DTYPE,
454
- "low_cpu_mem_usage": True,
455
- "device_map": DEVICE_MAP,
456
- }
457
- if ATTN_IMPLEMENTATION:
458
- model_kwargs["attn_implementation"] = ATTN_IMPLEMENTATION
459
-
460
- with quiet_model_load():
461
- model = AutoModelForMultimodalLM.from_pretrained(MODEL_PATH, **model_kwargs)
462
- bridge_args = type(
463
- "BridgeArgs",
464
- (),
465
- {
466
- "parakeet_model_id": PARAKEET_MODEL_ID,
467
- "parakeet_bridge_mode": PARAKEET_BRIDGE_MODE,
468
- "local_files_only": LOCAL_FILES_ONLY,
469
- "projector_intermediate_size": PROJECTOR_INTERMEDIATE_SIZE,
470
- "projector_dropout": PROJECTOR_DROPOUT,
471
- "hybrid_encoder_gate_init": HYBRID_ENCODER_GATE_INIT,
472
- "parakeet_tdt_filter_blank_tokens": PARAKEET_TDT_FILTER_BLANK_TOKENS,
473
- "parakeet_tdt_filter_special_token_ids": PARAKEET_TDT_FILTER_SPECIAL_TOKEN_IDS,
474
- },
475
- )()
476
- with quiet_model_load():
477
- install_parakeet_audio_bridge(model, bridge_args)
478
- gemma_core(model)
479
- load_parakeet_projector(model)
480
-
481
- model.eval()
482
- return processor, model, parakeet_processor
483
-
484
-
485
- def main() -> None:
486
- video_path = Path(VIDEO_PATH)
487
- model_path = Path(MODEL_PATH)
488
- processor_path = Path(PROCESSOR_PATH)
489
- audio_projector_path = Path(AUDIO_PROJECTOR_PATH)
490
- parakeet_path = Path(PARAKEET_MODEL_ID)
491
-
492
- for label, path in (
493
- ("VIDEO_PATH", video_path),
494
- ("MODEL_PATH", model_path),
495
- ("processor", processor_path),
496
- ("audio_projector", audio_projector_path),
497
- ("parakeet", parakeet_path),
498
- ):
499
- if not path.exists():
500
- raise FileNotFoundError(f"{label} does not exist: {path}")
501
-
502
- prompt = build_prompt()
503
- processor, model, parakeet_processor = load_model(processor_path)
504
- with tempfile.TemporaryDirectory(prefix="gemma4_caption_inference_") as tmpdir_raw:
505
- tmpdir = Path(tmpdir_raw)
506
- audio_path, _had_audio = prepare_sidecar_audio(video_path, tmpdir)
507
- inputs = prepare_inputs(processor, parakeet_processor, video_path, audio_path, prompt)
508
- moved_inputs = move_inputs_to_model_device(inputs, model)
509
- input_len = moved_inputs["input_ids"].shape[-1]
510
- with torch.inference_mode():
511
- output_ids = model.generate(**moved_inputs, **generation_kwargs())
512
-
513
- new_tokens = output_ids[0, input_len:]
514
- response = processor.decode(new_tokens, skip_special_tokens=True).strip()
515
- print(response, flush=True)
516
-
517
-
518
- if __name__ == "__main__":
519
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.ipynb_checkpoints/requirements-checkpoint.txt DELETED
@@ -1,19 +0,0 @@
1
- # Inference dependencies for polished_model_v3.
2
- # ffmpeg/ffprobe are required as system binaries for video/audio extraction.
3
-
4
- --extra-index-url https://download.pytorch.org/whl/cu130
5
-
6
- accelerate==1.14.0
7
- av==17.1.0
8
- librosa==0.11.0
9
- numpy==2.4.4
10
- opencv-python-headless>=4.10.0
11
- safetensors==0.8.0
12
- scipy==1.18.0
13
- soundfile==0.14.0
14
- torch==2.12.1+cu130
15
- torchcodec==0.14.0
16
- torchvision==0.27.1+cu130
17
- transformers==5.12.1
18
-
19
- -e ./vllm
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.ipynb_checkpoints/run_inference-checkpoint.sh DELETED
@@ -1,5 +0,0 @@
1
- set -euo pipefail
2
-
3
- cd "$(dirname "$0")"
4
- source /venv/main/bin/activate
5
- python infer.py
 
 
 
 
 
 
.ipynb_checkpoints/setup_vllm-checkpoint.sh DELETED
@@ -1,22 +0,0 @@
1
- set -euo pipefail
2
-
3
- cd "$(dirname "$0")"
4
-
5
- if [ ! -d vllm ]; then
6
- if [ ! -f vllm.zip ]; then
7
- echo "Missing vllm/ and vllm.zip" >&2
8
- exit 1
9
- fi
10
- unzip -q vllm.zip
11
- fi
12
-
13
- python3 -m venv .venv
14
- source .venv/bin/activate
15
-
16
- pip install --upgrade pip setuptools wheel
17
- pip install -r requirements.txt
18
-
19
- python - <<'PY'
20
- import vllm
21
- print("vLLM import OK:", getattr(vllm, "__version__", "unknown"))
22
- PY
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.ipynb_checkpoints/vllm_caption_runtime-checkpoint.py DELETED
@@ -1,254 +0,0 @@
1
- """Shared helpers for the polished vLLM caption examples."""
2
-
3
- from __future__ import annotations
4
-
5
- import subprocess
6
- import sys
7
- from pathlib import Path
8
- from typing import Any
9
-
10
- import cv2
11
- import numpy as np
12
- from transformers import AutoProcessor
13
- from vllm import LLM, SamplingParams
14
-
15
- CAPTION_LENGTH_LABELS = ("very small", "small", "medium", "large", "very large")
16
- CAPTION_SETTING_FIELD_CHOICES = {
17
- "vulgarity": ("none", "low", "medium", "high"),
18
- "uncertainty": ("none", "low", "medium", "high"),
19
- "character_names": ("none", "ambiguous", "single", "multiple"),
20
- "fluff": ("none", "low", "medium", "high"),
21
- "speculation": ("none", "low", "medium", "high"),
22
- "temporal_detail": ("static", "low", "medium", "high"),
23
- "visual_specificity": ("generic", "moderate", "detailed", "excessive"),
24
- "camera_detail": ("none", "low", "medium", "high"),
25
- "caption_style": ("plain", "verbose", "ornate", "robotic"),
26
- }
27
-
28
-
29
- def bool_from_value(value: Any, field_name: str) -> bool:
30
- if isinstance(value, bool):
31
- return value
32
- if isinstance(value, str):
33
- lowered = value.strip().lower()
34
- if lowered in {"1", "true", "yes", "y", "on"}:
35
- return True
36
- if lowered in {"0", "false", "no", "n", "off"}:
37
- return False
38
- raise ValueError(f"{field_name} must be boolean-like, got {value!r}")
39
-
40
-
41
- def format_caption_settings_prompt(settings: dict[str, Any]) -> str:
42
- caption_length = str(settings["caption_length"]).strip().lower()
43
- if caption_length not in CAPTION_LENGTH_LABELS:
44
- raise ValueError(f"caption_length must be one of {CAPTION_LENGTH_LABELS}, got {caption_length!r}")
45
-
46
- include_watermark_info = bool_from_value(
47
- settings["include_watermark_info"], "include_watermark_info"
48
- )
49
- has_repetition = bool_from_value(settings["has_repetition"], "has_repetition")
50
- has_thinking = bool_from_value(settings["has_thinking"], "has_thinking")
51
-
52
- normalized: dict[str, str] = {}
53
- for field_name, choices in CAPTION_SETTING_FIELD_CHOICES.items():
54
- value = str(settings[field_name]).strip().lower()
55
- if value not in choices:
56
- raise ValueError(f"{field_name} must be one of {choices}, got {value!r}")
57
- normalized[field_name] = value
58
-
59
- watermark_instruction = (
60
- "Include watermark info." if include_watermark_info else "Do not include watermark info."
61
- )
62
- thinking_instruction = (
63
- "Output thought JSON before the final caption."
64
- if has_thinking
65
- else "Do not output thought JSON; output only the caption."
66
- )
67
- setting_text = (
68
- f"vulgarity={normalized['vulgarity']}; "
69
- f"uncertainty={normalized['uncertainty']}; "
70
- f"character_names={normalized['character_names']}; "
71
- f"fluff={normalized['fluff']}; "
72
- f"has_repetition={str(has_repetition).lower()}; "
73
- f"has_thinking={str(has_thinking).lower()}; "
74
- f"speculation={normalized['speculation']}; "
75
- f"temporal_detail={normalized['temporal_detail']}; "
76
- f"visual_specificity={normalized['visual_specificity']}; "
77
- f"camera_detail={normalized['camera_detail']}; "
78
- f"caption_style={normalized['caption_style']}"
79
- )
80
- return (
81
- f"Write a {caption_length} caption for this clip using both the visuals and the audio. "
82
- f"{watermark_instruction} {thinking_instruction} Match these caption settings: {setting_text}."
83
- )
84
-
85
-
86
- def load_video_frames(path: str | Path, num_frames: int) -> tuple[np.ndarray, dict[str, Any]]:
87
- video_path = str(path)
88
- cap = cv2.VideoCapture(video_path)
89
- if not cap.isOpened():
90
- raise RuntimeError(f"Could not open video: {video_path}")
91
-
92
- total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
93
- fps = float(cap.get(cv2.CAP_PROP_FPS) or 24.0)
94
- if total_frames <= 0:
95
- cap.release()
96
- raise RuntimeError(f"Could not determine frame count: {video_path}")
97
-
98
- indices = np.linspace(0, max(0, total_frames - 1), num_frames).round().astype(int)
99
- frames = []
100
- for idx in indices:
101
- cap.set(cv2.CAP_PROP_POS_FRAMES, int(idx))
102
- ok, frame_bgr = cap.read()
103
- if ok:
104
- frames.append(cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB))
105
- cap.release()
106
-
107
- if not frames:
108
- raise RuntimeError(f"Could not read frames: {video_path}")
109
-
110
- metadata = {
111
- "fps": fps,
112
- "duration": total_frames / fps if fps > 0 else 0.0,
113
- "total_num_frames": total_frames,
114
- "frames_indices": [int(x) for x in indices[: len(frames)]],
115
- "video_backend": "opencv",
116
- "do_sample_frames": False,
117
- }
118
- return np.stack(frames, axis=0), metadata
119
-
120
-
121
- def load_audio(path: str | Path, sampling_rate: int) -> tuple[np.ndarray, int]:
122
- video_path = str(path)
123
- cmd = [
124
- "ffmpeg",
125
- "-hide_banner",
126
- "-loglevel",
127
- "error",
128
- "-i",
129
- video_path,
130
- "-vn",
131
- "-ac",
132
- "1",
133
- "-ar",
134
- str(sampling_rate),
135
- "-f",
136
- "f32le",
137
- "-",
138
- ]
139
- result = subprocess.run(cmd, check=False, capture_output=True)
140
- if result.returncode == 0 and result.stdout:
141
- return np.frombuffer(result.stdout, dtype=np.float32), sampling_rate
142
-
143
- frames, metadata = load_video_frames(video_path, 2)
144
- del frames
145
- duration = max(1.0, float(metadata.get("duration") or 1.0))
146
- return np.zeros(max(1, int(duration * sampling_rate)), dtype=np.float32), sampling_rate
147
-
148
-
149
- def build_prompt(
150
- *,
151
- processor: AutoProcessor,
152
- model_dir: str | Path,
153
- video_path: str | Path,
154
- prompt_override: str,
155
- prompt_settings: dict[str, Any],
156
- ) -> str:
157
- del model_dir
158
- prompt_text = prompt_override.strip() or format_caption_settings_prompt(prompt_settings)
159
- messages = [
160
- {
161
- "role": "user",
162
- "content": [
163
- {"type": "video", "path": str(video_path)},
164
- {"type": "text", "text": prompt_text},
165
- {"type": "audio", "path": "/tmp/polished_model_v3_audio.wav"},
166
- ],
167
- },
168
- {"role": "assistant", "content": [{"type": "text", "text": ""}]},
169
- ]
170
- prompt = processor.apply_chat_template(messages, tokenize=False)
171
- assistant_tail = "<turn|>\n"
172
- if prompt.endswith(assistant_tail):
173
- prompt = prompt[: -len(assistant_tail)]
174
- return prompt
175
-
176
-
177
- def build_vllm_request(
178
- *,
179
- processor: AutoProcessor,
180
- model_dir: str | Path,
181
- video_path: str | Path,
182
- num_frames: int,
183
- sampling_rate: int,
184
- prompt_override: str,
185
- prompt_settings: dict[str, Any],
186
- ) -> dict[str, Any]:
187
- video, video_metadata = load_video_frames(video_path, num_frames)
188
- audio, sr = load_audio(video_path, sampling_rate)
189
- prompt = build_prompt(
190
- processor=processor,
191
- model_dir=model_dir,
192
- video_path=video_path,
193
- prompt_override=prompt_override,
194
- prompt_settings=prompt_settings,
195
- )
196
- return {
197
- "prompt": prompt,
198
- "multi_modal_data": {
199
- "video": [(video, video_metadata)],
200
- "audio": (audio, sr),
201
- },
202
- }
203
-
204
-
205
- def load_processor(model_dir: str | Path) -> AutoProcessor:
206
- return AutoProcessor.from_pretrained(str(model_dir), local_files_only=True)
207
-
208
-
209
- def load_llm(
210
- *,
211
- model_dir: str | Path,
212
- max_model_len: int,
213
- max_num_seqs: int,
214
- gpu_memory_utilization: float,
215
- dtype: str,
216
- enforce_eager: bool,
217
- enable_prefix_caching: bool,
218
- trust_remote_code: bool,
219
- ) -> LLM:
220
- return LLM(
221
- model=str(model_dir),
222
- tokenizer=str(model_dir),
223
- max_model_len=max_model_len,
224
- max_num_seqs=max_num_seqs,
225
- gpu_memory_utilization=gpu_memory_utilization,
226
- limit_mm_per_prompt={"video": 1, "audio": 1, "image": 0},
227
- enforce_eager=enforce_eager,
228
- enable_prefix_caching=enable_prefix_caching,
229
- trust_remote_code=trust_remote_code,
230
- dtype=dtype,
231
- )
232
-
233
-
234
- def sampling_params(
235
- *,
236
- temperature: float,
237
- max_tokens: int,
238
- top_p: float,
239
- repetition_penalty: float,
240
- ) -> SamplingParams:
241
- kwargs: dict[str, Any] = {
242
- "temperature": temperature,
243
- "max_tokens": max_tokens,
244
- "repetition_penalty": repetition_penalty,
245
- }
246
- if temperature > 0:
247
- kwargs["top_p"] = top_p
248
- return SamplingParams(**kwargs)
249
-
250
-
251
- def ensure_local_vllm_source(script_dir: Path) -> None:
252
- local_vllm = script_dir / "vllm"
253
- if local_vllm.is_dir():
254
- sys.path.insert(0, str(local_vllm))