FusionCow commited on
Commit
7011765
·
verified ·
1 Parent(s): 88a8eb1

Upload folder using huggingface_hub

Browse files
Files changed (47) hide show
  1. .gitattributes +2 -0
  2. .ipynb_checkpoints/caption_folder-checkpoint.py +206 -0
  3. .ipynb_checkpoints/caption_model_runtime-checkpoint.py +771 -0
  4. .ipynb_checkpoints/infer-checkpoint.py +519 -0
  5. .ipynb_checkpoints/requirements-checkpoint.txt +19 -0
  6. .ipynb_checkpoints/run_inference-checkpoint.sh +5 -0
  7. .ipynb_checkpoints/setup_vllm-checkpoint.sh +22 -0
  8. .ipynb_checkpoints/vllm_caption_runtime-checkpoint.py +254 -0
  9. README.md +162 -0
  10. assets/.ipynb_checkpoints/decode_throughput-checkpoint.png +0 -0
  11. assets/decode_throughput.png +0 -0
  12. caption_folder.py +206 -0
  13. caption_model_runtime.py +771 -0
  14. example_run.py +123 -0
  15. infer.py +519 -0
  16. model/audio_config.json +25 -0
  17. model/chat_template.jinja +363 -0
  18. model/config.json +216 -0
  19. model/embed_audio.safetensors +3 -0
  20. model/gemma31b_with_audio_config.json +216 -0
  21. model/generation_config.json +14 -0
  22. model/model-00001-of-00007.safetensors +3 -0
  23. model/model-00002-of-00007.safetensors +3 -0
  24. model/model-00003-of-00007.safetensors +3 -0
  25. model/model-00004-of-00007.safetensors +3 -0
  26. model/model-00005-of-00007.safetensors +3 -0
  27. model/model-00006-of-00007.safetensors +3 -0
  28. model/model-00007-of-00007.safetensors +3 -0
  29. model/model.safetensors.index.json +0 -0
  30. model/processor_config.json +75 -0
  31. model/tokenizer.json +3 -0
  32. model/tokenizer_config.json +99 -0
  33. parakeet/config.json +49 -0
  34. parakeet/generation_config.json +16 -0
  35. parakeet/model.safetensors +3 -0
  36. parakeet/processor_config.json +16 -0
  37. parakeet/tokenizer.json +0 -0
  38. parakeet/tokenizer_config.json +10 -0
  39. processor/chat_template.jinja +363 -0
  40. processor/processor_config.json +75 -0
  41. processor/tokenizer.json +3 -0
  42. processor/tokenizer_config.json +99 -0
  43. requirements.txt +19 -0
  44. run_inference.sh +5 -0
  45. setup_vllm.sh +22 -0
  46. vllm.zip +3 -0
  47. vllm_caption_runtime.py +254 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ model/tokenizer.json filter=lfs diff=lfs merge=lfs -text
37
+ processor/tokenizer.json filter=lfs diff=lfs merge=lfs -text
.ipynb_checkpoints/caption_folder-checkpoint.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,771 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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))
README.md CHANGED
@@ -1,3 +1,165 @@
1
  ---
2
  license: apache-2.0
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ language:
4
+ - en
5
+ library_name: transformers
6
+ pipeline_tag: image-to-text
7
+ tags:
8
+ - video-captioning
9
+ - multimodal
10
+ - gemma
11
+ - parakeet
12
  ---
13
+
14
+ # Gemma 4 31B Parakeet Video Caption Model
15
+
16
+ This repository packages a Gemma 4 31B video captioning model with a Parakeet-based audio bridge and a patched vLLM runtime. It is intended for local or server-side inference on CUDA GPUs.
17
+
18
+ The model produces raw caption responses. When `HAS_THINKING = True`, the response includes the model's thought JSON followed by the final caption. When `HAS_THINKING = False`, the prompt asks for the final caption only.
19
+
20
+ > This package uses custom runtime code. Stock vLLM does not know how to load this Parakeet audio bridge unless you unzip and use the bundled patched `vllm/` folder.
21
+
22
+ ## Repository Contents
23
+
24
+ - `model/`: Gemma 4 31B caption model weights. The caption LoRA is already merged.
25
+ - `model/embed_audio.safetensors`: trained Parakeet-to-Gemma audio projector.
26
+ - `model/processor_config.json`, `model/tokenizer.json`, `model/tokenizer_config.json`, `model/chat_template.jinja`: processor/tokenizer files copied into `model/` so vLLM can point directly at `model/`.
27
+ - `processor/`: original processor/tokenizer files, kept for compatibility.
28
+ - `parakeet/`: bundled NVIDIA Parakeet TDT 0.6B audio model.
29
+ - `vllm.zip`: zipped patched vLLM source tree with Gemma4 Parakeet bridge support. Unzip this before installing.
30
+ - `example_run.py`: edit variables at the top, point it at one video, print the raw model output.
31
+ - `caption_folder.py`: edit variables at the top, point it at an input folder and output folder, write one raw output per video.
32
+ - `vllm_caption_runtime.py`: shared helper code for the vLLM examples.
33
+ - `infer.py` and `caption_model_runtime.py`: Transformers inference path from v2, kept for compatibility.
34
+ - `requirements.txt`: inference dependencies plus editable install of the unzipped bundled `vllm/`.
35
+ - `setup_vllm.sh`: convenience setup script.
36
+ - `assets/decode_throughput.png`: vLLM decode-throughput stress test graph.
37
+
38
+ ## Installation
39
+
40
+ You need `ffmpeg`, `ffprobe`, and `unzip` available on `PATH`.
41
+
42
+ ```bash
43
+ cd /path/to/this/repo
44
+ unzip -q vllm.zip
45
+ python3 -m venv .venv
46
+ source .venv/bin/activate
47
+ pip install --upgrade pip setuptools wheel
48
+ pip install -r requirements.txt
49
+ ```
50
+
51
+ Or run the helper:
52
+
53
+ ```bash
54
+ cd /path/to/this/repo
55
+ ./setup_vllm.sh
56
+ source .venv/bin/activate
57
+ ```
58
+
59
+ ## Single-Video Inference
60
+
61
+ Edit `VIDEO_PATH` in `example_run.py`:
62
+
63
+ ```python
64
+ VIDEO_PATH = "/path/to/video.mp4"
65
+ ```
66
+
67
+ Then run:
68
+
69
+ ```bash
70
+ cd /path/to/this/repo
71
+ source .venv/bin/activate
72
+ python example_run.py
73
+ ```
74
+
75
+ The script prints the raw model output.
76
+
77
+ ## Folder Captioning
78
+
79
+ Edit these variables in `caption_folder.py`:
80
+
81
+ ```python
82
+ INPUT_FOLDER = "/path/to/videos"
83
+ OUTPUT_FOLDER = "/path/to/captions"
84
+ ```
85
+
86
+ Then run:
87
+
88
+ ```bash
89
+ cd /path/to/this/repo
90
+ source .venv/bin/activate
91
+ python caption_folder.py
92
+ ```
93
+
94
+ The script mirrors the input folder structure into `OUTPUT_FOLDER` and writes one `.txt` file per video. It loads the model once and batches videos using `BATCH_SIZE`.
95
+
96
+ ## Prompt Settings
97
+
98
+ Both `example_run.py` and `caption_folder.py` are variable-based. They do not use command-line args.
99
+
100
+ Prompt settings are grouped under `Prompt Settings`, including:
101
+
102
+ - `HAS_THINKING`
103
+ - `CAPTION_LENGTH`
104
+ - `INCLUDE_WATERMARK_INFO`
105
+ - `VULGARITY`
106
+ - `UNCERTAINTY`
107
+ - `CHARACTER_NAMES`
108
+ - `FLUFF`
109
+ - `HAS_REPETITION`
110
+ - `SPECULATION`
111
+ - `TEMPORAL_DETAIL`
112
+ - `VISUAL_SPECIFICITY`
113
+ - `CAMERA_DETAIL`
114
+ - `CAPTION_STYLE`
115
+
116
+ Runtime and generation settings are grouped under `vLLM / Generation Hyperparameters`, including:
117
+
118
+ - `NUM_FRAMES`
119
+ - `MAX_MODEL_LEN`
120
+ - `MAX_NUM_SEQS`
121
+ - `BATCH_SIZE` in `caption_folder.py`
122
+ - `GPU_MEMORY_UTILIZATION`
123
+ - `MAX_TOKENS`
124
+ - `TEMPERATURE`
125
+ - `TOP_P`
126
+ - `REPETITION_PENALTY`
127
+
128
+ ## Performance
129
+
130
+ This graph shows measured vLLM decode throughput on an RTX PRO 6000 using 12 video frames, `max_tokens = 96`, `max_model_len = 4096`, prefix cache disabled, and unique requests:
131
+
132
+ ![Decode Throughput](assets/decode_throughput.png)
133
+
134
+ For short caption requests on the RTX PRO 6000 test machine, `MAX_NUM_SEQS = 20` was the best throughput point. vLLM reported about 24k KV-cache tokens, roughly 5.9 full 4096-token requests, so long-context traffic should use lower concurrency.
135
+
136
+ ## Architecture
137
+
138
+ The base language and vision model is Gemma 4 31B. The caption LoRA has already been merged into the Gemma weights in `model/`, so there is no separate LoRA adapter to load.
139
+
140
+ Audio is handled by a Parakeet bridge:
141
+
142
+ 1. The prompt uses Gemma's multimodal chat format with video and audio placeholders.
143
+ 2. vLLM's Gemma4 multimodal processor builds the text/video/audio request structure.
144
+ 3. The patched runtime detects this package layout by finding `model/embed_audio.safetensors` and sibling folder `parakeet/`.
145
+ 4. Instead of using Gemma's original audio tower, the runtime extracts waveform audio and converts it with the bundled Parakeet processor.
146
+ 5. The frozen Parakeet TDT model produces audio representations. The packaged bridge mode is `tdt_token_embeddings_with_encoder_context`, combining Parakeet TDT token embeddings with encoder context.
147
+ 6. The trained projector in `model/embed_audio.safetensors` maps the Parakeet-side audio stream into Gemma's 5376-dimensional hidden space.
148
+ 7. Those projected audio embeddings are inserted into the Gemma prompt where the audio soft tokens live.
149
+ 8. Gemma generates the raw thought/caption output using video frames, text prompt settings, and projected Parakeet audio features.
150
+
151
+ In short: Parakeet is frozen and used as the audio recognizer/encoder, the learned bridge maps Parakeet audio features into Gemma's embedding space, and the merged Gemma caption model writes the final output.
152
+
153
+ ## Limitations
154
+
155
+ - This is a custom multimodal package, not a stock Transformers pipeline.
156
+ - It requires the bundled patched vLLM runtime for the vLLM examples.
157
+ - Very long videos or long-context requests should use lower concurrency than short caption jobs.
158
+ - Audio captioning depends on the Parakeet bridge and may still miss unclear, quiet, overlapping, or non-speech sounds.
159
+ - The scripts intentionally print or save the raw model output rather than parsing it.
160
+
161
+ ## Notes
162
+
163
+ - Videos with no audio are handled by inserting a silent audio array so the model still receives the expected audio input shape.
164
+ - Keep `model/`, `parakeet/`, `processor/`, `vllm.zip`, the Python scripts, and `assets/` together when moving or uploading this repository.
165
+ - If uploading to Hugging Face, upload this README as `README.md` at the repository root.
assets/.ipynb_checkpoints/decode_throughput-checkpoint.png ADDED
assets/decode_throughput.png ADDED
caption_folder.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
caption_model_runtime.py ADDED
@@ -0,0 +1,771 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ )
example_run.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run the packaged vLLM caption model on one video.
2
+
3
+ Edit the variables below, then run:
4
+
5
+ cd /workspace/polished_model_v3
6
+ source .venv/bin/activate
7
+ python example_run.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
+ VIDEO_PATH = "/workspace/test7.mp4"
26
+ MODEL_DIR = str(SCRIPT_DIR / "model")
27
+
28
+
29
+ # =============================================================================
30
+ # Prompt Settings
31
+ # =============================================================================
32
+
33
+ PROMPT_OVERRIDE = ""
34
+ CAPTION_LENGTH = "very large"
35
+ INCLUDE_WATERMARK_INFO = False
36
+ HAS_THINKING = True
37
+
38
+ VULGARITY = "low"
39
+ UNCERTAINTY = "low"
40
+ CHARACTER_NAMES = "none"
41
+ FLUFF = "none"
42
+ HAS_REPETITION = False
43
+ SPECULATION = "low"
44
+ TEMPORAL_DETAIL = "medium"
45
+ VISUAL_SPECIFICITY = "moderate"
46
+ CAMERA_DETAIL = "medium"
47
+ CAPTION_STYLE = "plain"
48
+
49
+
50
+ # =============================================================================
51
+ # vLLM / Generation Hyperparameters
52
+ # =============================================================================
53
+
54
+ NUM_FRAMES = 12
55
+ SAMPLING_RATE = 16_000
56
+ MAX_MODEL_LEN = 4096
57
+ MAX_NUM_SEQS = 1
58
+ GPU_MEMORY_UTILIZATION = 0.88
59
+ DTYPE = "bfloat16"
60
+ ENFORCE_EAGER = False
61
+ ENABLE_PREFIX_CACHING = False
62
+ TRUST_REMOTE_CODE = False
63
+
64
+ MAX_TOKENS = 1200
65
+ TEMPERATURE = 0.0
66
+ TOP_P = 0.9
67
+ REPETITION_PENALTY = 1.1
68
+
69
+
70
+ def prompt_settings() -> dict[str, object]:
71
+ return {
72
+ "caption_length": CAPTION_LENGTH,
73
+ "include_watermark_info": INCLUDE_WATERMARK_INFO,
74
+ "has_thinking": HAS_THINKING,
75
+ "vulgarity": VULGARITY,
76
+ "uncertainty": UNCERTAINTY,
77
+ "character_names": CHARACTER_NAMES,
78
+ "fluff": FLUFF,
79
+ "has_repetition": HAS_REPETITION,
80
+ "speculation": SPECULATION,
81
+ "temporal_detail": TEMPORAL_DETAIL,
82
+ "visual_specificity": VISUAL_SPECIFICITY,
83
+ "camera_detail": CAMERA_DETAIL,
84
+ "caption_style": CAPTION_STYLE,
85
+ }
86
+
87
+
88
+ def main() -> None:
89
+ processor = load_processor(MODEL_DIR)
90
+ request = build_vllm_request(
91
+ processor=processor,
92
+ model_dir=MODEL_DIR,
93
+ video_path=VIDEO_PATH,
94
+ num_frames=NUM_FRAMES,
95
+ sampling_rate=SAMPLING_RATE,
96
+ prompt_override=PROMPT_OVERRIDE,
97
+ prompt_settings=prompt_settings(),
98
+ )
99
+
100
+ llm = load_llm(
101
+ model_dir=MODEL_DIR,
102
+ max_model_len=MAX_MODEL_LEN,
103
+ max_num_seqs=MAX_NUM_SEQS,
104
+ gpu_memory_utilization=GPU_MEMORY_UTILIZATION,
105
+ dtype=DTYPE,
106
+ enforce_eager=ENFORCE_EAGER,
107
+ enable_prefix_caching=ENABLE_PREFIX_CACHING,
108
+ trust_remote_code=TRUST_REMOTE_CODE,
109
+ )
110
+ outputs = llm.generate(
111
+ [request],
112
+ sampling_params=sampling_params(
113
+ temperature=TEMPERATURE,
114
+ max_tokens=MAX_TOKENS,
115
+ top_p=TOP_P,
116
+ repetition_penalty=REPETITION_PENALTY,
117
+ ),
118
+ )
119
+ print(outputs[0].outputs[0].text)
120
+
121
+
122
+ if __name__ == "__main__":
123
+ main()
infer.py ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
model/audio_config.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "attention_chunk_size": 12,
3
+ "attention_context_left": 13,
4
+ "attention_context_right": 0,
5
+ "attention_invalid_logits_value": -1000000000.0,
6
+ "attention_logit_cap": 50.0,
7
+ "conv_kernel_size": 5,
8
+ "dtype": "bfloat16",
9
+ "gradient_clipping": 10000000000.0,
10
+ "hidden_act": "silu",
11
+ "hidden_size": 1024,
12
+ "initializer_range": 0.02,
13
+ "model_type": "gemma4_audio",
14
+ "num_attention_heads": 8,
15
+ "num_hidden_layers": 12,
16
+ "output_proj_dims": 1536,
17
+ "residual_weight": 0.5,
18
+ "rms_norm_eps": 1e-06,
19
+ "subsampling_conv_channels": [
20
+ 128,
21
+ 32
22
+ ],
23
+ "transformers_version": "5.12.1",
24
+ "use_clipped_linears": true
25
+ }
model/chat_template.jinja ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- macro format_parameters(properties, required, filter_keys=false) -%}
2
+ {%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
3
+ {%- set ns = namespace(found_first=false) -%}
4
+ {%- for key, value in properties | dictsort -%}
5
+ {%- set add_comma = false -%}
6
+ {%- if not filter_keys or key not in standard_keys -%}
7
+ {%- if ns.found_first %},{% endif -%}
8
+ {%- set ns.found_first = true -%}
9
+ {{ key }}:{
10
+ {%- if value['description'] -%}
11
+ description:<|"|>{{ value['description'] }}<|"|>
12
+ {%- set add_comma = true -%}
13
+ {%- endif -%}
14
+ {%- if value['type'] | upper == 'STRING' -%}
15
+ {%- if value['enum'] -%}
16
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
17
+ enum:{{ format_argument(value['enum']) }}
18
+ {%- endif -%}
19
+ {%- elif value['type'] | upper == 'ARRAY' -%}
20
+ {%- if value['items'] is mapping and value['items'] -%}
21
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
22
+ items:{
23
+ {%- set ns_items = namespace(found_first=false) -%}
24
+ {%- for item_key, item_value in value['items'] | dictsort -%}
25
+ {%- if item_value is not none -%}
26
+ {%- if ns_items.found_first %},{% endif -%}
27
+ {%- set ns_items.found_first = true -%}
28
+ {%- if item_key == 'properties' -%}
29
+ properties:{
30
+ {%- if item_value is mapping -%}
31
+ {{- format_parameters(item_value, value['items']['required'] | default([])) -}}
32
+ {%- endif -%}
33
+ }
34
+ {%- elif item_key == 'required' -%}
35
+ required:[
36
+ {%- for req_item in item_value -%}
37
+ <|"|>{{- req_item -}}<|"|>
38
+ {%- if not loop.last %},{% endif -%}
39
+ {%- endfor -%}
40
+ ]
41
+ {%- elif item_key == 'type' -%}
42
+ {%- if item_value is string -%}
43
+ type:{{ format_argument(item_value | upper) }}
44
+ {%- else -%}
45
+ type:{{ format_argument(item_value | map('upper') | list) }}
46
+ {%- endif -%}
47
+ {%- else -%}
48
+ {{ item_key }}:{{ format_argument(item_value) }}
49
+ {%- endif -%}
50
+ {%- endif -%}
51
+ {%- endfor -%}
52
+ }
53
+ {%- endif -%}
54
+ {%- endif -%}
55
+ {%- if value['nullable'] %}
56
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
57
+ nullable:true
58
+ {%- endif -%}
59
+ {%- if value['type'] | upper == 'OBJECT' -%}
60
+ {%- if value['properties'] is defined and value['properties'] is mapping -%}
61
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
62
+ properties:{
63
+ {{- format_parameters(value['properties'], value['required'] | default([])) -}}
64
+ }
65
+ {%- elif value is mapping -%}
66
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
67
+ properties:{
68
+ {{- format_parameters(value, value['required'] | default([]), filter_keys=true) -}}
69
+ }
70
+ {%- endif -%}
71
+ {%- if value['required'] -%}
72
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
73
+ required:[
74
+ {%- for item in value['required'] | default([]) -%}
75
+ <|"|>{{- item -}}<|"|>
76
+ {%- if not loop.last %},{% endif -%}
77
+ {%- endfor -%}
78
+ ]
79
+ {%- endif -%}
80
+ {%- endif -%}
81
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
82
+ type:<|"|>{{ value['type'] | upper }}<|"|>}
83
+ {%- endif -%}
84
+ {%- endfor -%}
85
+ {%- endmacro -%}
86
+ {%- macro format_function_declaration(tool_data) -%}
87
+ declaration:{{- tool_data['function']['name'] -}}{description:<|"|>{{- tool_data['function']['description'] -}}<|"|>
88
+ {%- set params = tool_data['function']['parameters'] -%}
89
+ {%- if params -%}
90
+ ,parameters:{
91
+ {%- if params['properties'] -%}
92
+ properties:{ {{- format_parameters(params['properties'], params['required']) -}} },
93
+ {%- endif -%}
94
+ {%- if params['required'] -%}
95
+ required:[
96
+ {%- for item in params['required'] -%}
97
+ <|"|>{{- item -}}<|"|>
98
+ {{- ',' if not loop.last -}}
99
+ {%- endfor -%}
100
+ ],
101
+ {%- endif -%}
102
+ {%- if params['type'] -%}
103
+ type:<|"|>{{- params['type'] | upper -}}<|"|>}
104
+ {%- endif -%}
105
+ {%- endif -%}
106
+ {%- if 'response' in tool_data['function'] -%}
107
+ {%- set response_declaration = tool_data['function']['response'] -%}
108
+ ,response:{
109
+ {%- if response_declaration['description'] -%}
110
+ description:<|"|>{{- response_declaration['description'] -}}<|"|>,
111
+ {%- endif -%}
112
+ {%- if response_declaration['type'] | upper == 'OBJECT' -%}
113
+ type:<|"|>{{- response_declaration['type'] | upper -}}<|"|>}
114
+ {%- endif -%}
115
+ {%- endif -%}
116
+ }
117
+ {%- endmacro -%}
118
+ {%- macro format_argument(argument, escape_keys=True) -%}
119
+ {%- if argument is string -%}
120
+ {{- '<|"|>' + argument + '<|"|>' -}}
121
+ {%- elif argument is boolean -%}
122
+ {{- 'true' if argument else 'false' -}}
123
+ {%- elif argument is mapping -%}
124
+ {{- '{' -}}
125
+ {%- set ns = namespace(found_first=false) -%}
126
+ {%- for key, value in argument | dictsort -%}
127
+ {%- if ns.found_first %},{% endif -%}
128
+ {%- set ns.found_first = true -%}
129
+ {%- if escape_keys -%}
130
+ {{- '<|"|>' + key + '<|"|>' -}}
131
+ {%- else -%}
132
+ {{- key -}}
133
+ {%- endif -%}
134
+ :{{- format_argument(value, escape_keys=escape_keys) -}}
135
+ {%- endfor -%}
136
+ {{- '}' -}}
137
+ {%- elif argument is sequence -%}
138
+ {{- '[' -}}
139
+ {%- for item in argument -%}
140
+ {{- format_argument(item, escape_keys=escape_keys) -}}
141
+ {%- if not loop.last %},{% endif -%}
142
+ {%- endfor -%}
143
+ {{- ']' -}}
144
+ {%- else -%}
145
+ {{- argument -}}
146
+ {%- endif -%}
147
+ {%- endmacro -%}
148
+ {%- macro strip_thinking(text) -%}
149
+ {%- set ns = namespace(result='') -%}
150
+ {%- for part in text.split('<channel|>') -%}
151
+ {%- if '<|channel>' in part -%}
152
+ {%- set ns.result = ns.result + part.split('<|channel>')[0] -%}
153
+ {%- else -%}
154
+ {%- set ns.result = ns.result + part -%}
155
+ {%- endif -%}
156
+ {%- endfor -%}
157
+ {{- ns.result | trim -}}
158
+ {%- endmacro -%}
159
+
160
+ {%- macro format_tool_response_block(tool_name, response) -%}
161
+ {{- '<|tool_response>' -}}
162
+ {%- if response is mapping -%}
163
+ {{- 'response:' + tool_name + '{' -}}
164
+ {%- for key, value in response | dictsort -%}
165
+ {{- key -}}:{{- format_argument(value, escape_keys=False) -}}
166
+ {%- if not loop.last %},{% endif -%}
167
+ {%- endfor -%}
168
+ {{- '}' -}}
169
+ {%- else -%}
170
+ {{- 'response:' + tool_name + '{value:' + format_argument(response, escape_keys=False) + '}' -}}
171
+ {%- endif -%}
172
+ {{- '<tool_response|>' -}}
173
+ {%- endmacro -%}
174
+
175
+ {%- set ns = namespace(prev_message_type=None) -%}
176
+ {%- set loop_messages = messages -%}
177
+ {{- bos_token -}}
178
+ {#- Handle System/Tool Definitions Block -#}
179
+ {%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
180
+ {{- '<|turn>system\n' -}}
181
+ {#- Inject Thinking token at the very top of the FIRST system turn -#}
182
+ {%- if enable_thinking is defined and enable_thinking -%}
183
+ {{- '<|think|>\n' -}}
184
+ {%- set ns.prev_message_type = 'think' -%}
185
+ {%- endif -%}
186
+ {%- if messages[0]['role'] in ['system', 'developer'] -%}
187
+ {%- if messages[0]['content'] is string -%}
188
+ {{- messages[0]['content'] | trim -}}
189
+ {%- elif messages[0]['content'] is sequence -%}
190
+ {%- for item in messages[0]['content'] -%}
191
+ {{- item['text'] | trim + ' '-}}
192
+ {%- endfor -%}
193
+ {%- endif -%}
194
+ {%- set loop_messages = messages[1:] -%}
195
+ {%- endif -%}
196
+ {%- if tools -%}
197
+ {%- for tool in tools %}
198
+ {{- '<|tool>' -}}
199
+ {{- format_function_declaration(tool) | trim -}}
200
+ {{- '<tool|>' -}}
201
+ {%- endfor %}
202
+ {%- set ns.prev_message_type = 'tool' -%}
203
+ {%- endif -%}
204
+ {{- '<turn|>\n' -}}
205
+ {%- endif %}
206
+
207
+ {#- Pre-scan: find last user message index for reasoning guard -#}
208
+ {%- set ns_turn = namespace(last_user_idx=-1) -%}
209
+ {%- for i in range(loop_messages | length) -%}
210
+ {%- if loop_messages[i]['role'] == 'user' -%}
211
+ {%- set ns_turn.last_user_idx = i -%}
212
+ {%- endif -%}
213
+ {%- endfor -%}
214
+
215
+ {#- Loop through messages -#}
216
+ {%- for message in loop_messages -%}
217
+ {%- if message['role'] != 'tool' -%}
218
+ {%- set ns.prev_message_type = None -%}
219
+ {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
220
+ {#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}
221
+ {%- set prev_nt = namespace(role=None, found=false) -%}
222
+ {%- if loop.index0 > 0 -%}
223
+ {%- for j in range(loop.index0 - 1, -1, -1) -%}
224
+ {%- if not prev_nt.found -%}
225
+ {%- if loop_messages[j]['role'] != 'tool' -%}
226
+ {%- set prev_nt.role = loop_messages[j]['role'] -%}
227
+ {%- set prev_nt.found = true -%}
228
+ {%- endif -%}
229
+ {%- endif -%}
230
+ {%- endfor -%}
231
+ {%- endif -%}
232
+ {%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%}
233
+ {%- if not continue_same_model_turn -%}
234
+ {{- '<|turn>' + role + '\n' }}
235
+ {%- endif -%}
236
+
237
+ {#- Render reasoning/reasoning_content as thinking channel -#}
238
+ {%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
239
+ {%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}
240
+ {{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
241
+ {%- endif -%}
242
+
243
+ {%- if message['tool_calls'] -%}
244
+ {%- for tool_call in message['tool_calls'] -%}
245
+ {%- set function = tool_call['function'] -%}
246
+ {{- '<|tool_call>call:' + function['name'] + '{' -}}
247
+ {%- if function['arguments'] is mapping -%}
248
+ {%- set ns_args = namespace(found_first=false) -%}
249
+ {%- for key, value in function['arguments'] | dictsort -%}
250
+ {%- if ns_args.found_first %},{% endif -%}
251
+ {%- set ns_args.found_first = true -%}
252
+ {{- key -}}:{{- format_argument(value, escape_keys=False) -}}
253
+ {%- endfor -%}
254
+ {%- elif function['arguments'] is string -%}
255
+ {{- function['arguments'] -}}
256
+ {%- endif -%}
257
+ {{- '}<tool_call|>' -}}
258
+ {%- endfor -%}
259
+ {%- set ns.prev_message_type = 'tool_call' -%}
260
+ {%- endif -%}
261
+
262
+ {%- set ns_tr_out = namespace(flag=false) -%}
263
+ {%- if message.get('tool_responses') -%}
264
+ {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
265
+ {%- for tool_response in message['tool_responses'] -%}
266
+ {{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}
267
+ {%- set ns_tr_out.flag = true -%}
268
+ {%- set ns.prev_message_type = 'tool_response' -%}
269
+ {%- endfor -%}
270
+ {%- elif message.get('tool_calls') -%}
271
+ {#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#}
272
+ {%- set ns_tool_scan = namespace(stopped=false) -%}
273
+ {%- for k in range(loop.index0 + 1, loop_messages | length) -%}
274
+ {%- if ns_tool_scan.stopped -%}
275
+ {%- elif loop_messages[k]['role'] != 'tool' -%}
276
+ {%- set ns_tool_scan.stopped = true -%}
277
+ {%- else -%}
278
+ {%- set follow = loop_messages[k] -%}
279
+ {#- Resolve tool_call_id to function name -#}
280
+ {%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}
281
+ {%- for tc in message['tool_calls'] -%}
282
+ {%- if tc.get('id') == follow.get('tool_call_id') -%}
283
+ {%- set ns_tname.name = tc['function']['name'] -%}
284
+ {%- endif -%}
285
+ {%- endfor -%}
286
+ {#- Handle content as string or content-parts array -#}
287
+ {%- set tool_body = follow.get('content') -%}
288
+ {%- if tool_body is string -%}
289
+ {{- format_tool_response_block(ns_tname.name, tool_body) -}}
290
+ {%- elif tool_body is sequence and tool_body is not string -%}
291
+ {%- set ns_txt = namespace(s='') -%}
292
+ {%- for part in tool_body -%}
293
+ {%- if part.get('type') == 'text' -%}
294
+ {%- set ns_txt.s = ns_txt.s + (part.get('text') | default('')) -%}
295
+ {%- endif -%}
296
+ {%- endfor -%}
297
+ {{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
298
+ {%- for part in tool_body -%}
299
+ {%- if part.get('type') == 'image' -%}
300
+ {{- '<|image|>' -}}
301
+ {%- elif part.get('type') == 'audio' -%}
302
+ {{- '<|audio|>' -}}
303
+ {%- elif part.get('type') == 'video' -%}
304
+ {{- '<|video|>' -}}
305
+ {%- endif -%}
306
+ {%- endfor -%}
307
+ {%- else -%}
308
+ {{- format_tool_response_block(ns_tname.name, tool_body) -}}
309
+ {%- endif -%}
310
+ {%- set ns_tr_out.flag = true -%}
311
+ {%- set ns.prev_message_type = 'tool_response' -%}
312
+ {%- endif -%}
313
+ {%- endfor -%}
314
+ {%- endif -%}
315
+
316
+ {%- set captured_content -%}
317
+ {%- if message['content'] is string -%}
318
+ {%- if role == 'model' -%}
319
+ {{- strip_thinking(message['content']) -}}
320
+ {%- else -%}
321
+ {{- message['content'] | trim -}}
322
+ {%- endif -%}
323
+ {%- elif message['content'] is sequence -%}
324
+ {%- for item in message['content'] -%}
325
+ {%- if item['type'] == 'text' -%}
326
+ {%- if role == 'model' -%}
327
+ {{- strip_thinking(item['text']) -}}
328
+ {%- else -%}
329
+ {{- item['text'] | trim -}}
330
+ {%- endif -%}
331
+ {%- elif item['type'] == 'image' -%}
332
+ {{- '<|image|>' -}}
333
+ {%- set ns.prev_message_type = 'image' -%}
334
+ {%- elif item['type'] == 'audio' -%}
335
+ {{- '<|audio|>' -}}
336
+ {%- set ns.prev_message_type = 'audio' -%}
337
+ {%- elif item['type'] == 'video' -%}
338
+ {{- '<|video|>' -}}
339
+ {%- set ns.prev_message_type = 'video' -%}
340
+ {%- endif -%}
341
+ {%- endfor -%}
342
+ {%- endif -%}
343
+ {%- endset -%}
344
+
345
+ {{- captured_content -}}
346
+ {%- set has_content = captured_content | trim | length > 0 -%}
347
+
348
+ {%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
349
+ {{- '<|tool_response>' -}}
350
+ {%- elif not (ns_tr_out.flag and not has_content) -%}
351
+ {{- '<turn|>\n' -}}
352
+ {%- endif -%}
353
+ {%- endif -%}
354
+ {%- endfor -%}
355
+
356
+ {%- if add_generation_prompt -%}
357
+ {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
358
+ {{- '<|turn>model\n' -}}
359
+ {%- if not enable_thinking | default(false) -%}
360
+ {{- '<|channel>thought\n<channel|>' -}}
361
+ {%- endif -%}
362
+ {%- endif -%}
363
+ {%- endif -%}
model/config.json ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Gemma4ForConditionalGeneration"
4
+ ],
5
+ "audio_config": {
6
+ "_name_or_path": "",
7
+ "architectures": null,
8
+ "attention_chunk_size": 12,
9
+ "attention_context_left": 13,
10
+ "attention_context_right": 0,
11
+ "attention_invalid_logits_value": -1000000000.0,
12
+ "attention_logit_cap": 50.0,
13
+ "chunk_size_feed_forward": 0,
14
+ "conv_kernel_size": 5,
15
+ "dtype": "bfloat16",
16
+ "gradient_clipping": 10000000000.0,
17
+ "hidden_act": "silu",
18
+ "hidden_size": 1024,
19
+ "id2label": {
20
+ "0": "LABEL_0",
21
+ "1": "LABEL_1"
22
+ },
23
+ "initializer_range": 0.02,
24
+ "is_encoder_decoder": false,
25
+ "label2id": {
26
+ "LABEL_0": 0,
27
+ "LABEL_1": 1
28
+ },
29
+ "model_type": "gemma4_audio",
30
+ "num_attention_heads": 8,
31
+ "num_hidden_layers": 12,
32
+ "output_attentions": false,
33
+ "output_hidden_states": false,
34
+ "output_proj_dims": 1536,
35
+ "problem_type": null,
36
+ "residual_weight": 0.5,
37
+ "return_dict": true,
38
+ "rms_norm_eps": 1e-06,
39
+ "subsampling_conv_channels": [
40
+ 128,
41
+ 32
42
+ ],
43
+ "use_clipped_linears": true
44
+ },
45
+ "audio_token_id": 258881,
46
+ "boa_token_id": 256000,
47
+ "boi_token_id": 255999,
48
+ "dtype": "bfloat16",
49
+ "eoa_token_id": 258883,
50
+ "eoa_token_index": 258883,
51
+ "eoi_token_id": 258882,
52
+ "eos_token_id": [
53
+ 1,
54
+ 106
55
+ ],
56
+ "image_token_id": 258880,
57
+ "initializer_range": 0.02,
58
+ "model_type": "gemma4",
59
+ "text_config": {
60
+ "attention_bias": false,
61
+ "attention_dropout": 0.0,
62
+ "attention_k_eq_v": true,
63
+ "bos_token_id": 2,
64
+ "dtype": "bfloat16",
65
+ "enable_moe_block": false,
66
+ "eos_token_id": 1,
67
+ "expert_intermediate_size": null,
68
+ "final_logit_softcapping": 30.0,
69
+ "global_head_dim": 512,
70
+ "head_dim": 256,
71
+ "hidden_activation": "gelu_pytorch_tanh",
72
+ "hidden_size": 5376,
73
+ "hidden_size_per_layer_input": 0,
74
+ "initializer_range": 0.02,
75
+ "intermediate_size": 21504,
76
+ "layer_types": [
77
+ "sliding_attention",
78
+ "sliding_attention",
79
+ "sliding_attention",
80
+ "sliding_attention",
81
+ "sliding_attention",
82
+ "full_attention",
83
+ "sliding_attention",
84
+ "sliding_attention",
85
+ "sliding_attention",
86
+ "sliding_attention",
87
+ "sliding_attention",
88
+ "full_attention",
89
+ "sliding_attention",
90
+ "sliding_attention",
91
+ "sliding_attention",
92
+ "sliding_attention",
93
+ "sliding_attention",
94
+ "full_attention",
95
+ "sliding_attention",
96
+ "sliding_attention",
97
+ "sliding_attention",
98
+ "sliding_attention",
99
+ "sliding_attention",
100
+ "full_attention",
101
+ "sliding_attention",
102
+ "sliding_attention",
103
+ "sliding_attention",
104
+ "sliding_attention",
105
+ "sliding_attention",
106
+ "full_attention",
107
+ "sliding_attention",
108
+ "sliding_attention",
109
+ "sliding_attention",
110
+ "sliding_attention",
111
+ "sliding_attention",
112
+ "full_attention",
113
+ "sliding_attention",
114
+ "sliding_attention",
115
+ "sliding_attention",
116
+ "sliding_attention",
117
+ "sliding_attention",
118
+ "full_attention",
119
+ "sliding_attention",
120
+ "sliding_attention",
121
+ "sliding_attention",
122
+ "sliding_attention",
123
+ "sliding_attention",
124
+ "full_attention",
125
+ "sliding_attention",
126
+ "sliding_attention",
127
+ "sliding_attention",
128
+ "sliding_attention",
129
+ "sliding_attention",
130
+ "full_attention",
131
+ "sliding_attention",
132
+ "sliding_attention",
133
+ "sliding_attention",
134
+ "sliding_attention",
135
+ "sliding_attention",
136
+ "full_attention"
137
+ ],
138
+ "max_position_embeddings": 262144,
139
+ "model_type": "gemma4_text",
140
+ "moe_intermediate_size": null,
141
+ "num_attention_heads": 32,
142
+ "num_experts": null,
143
+ "num_global_key_value_heads": 4,
144
+ "num_hidden_layers": 60,
145
+ "num_key_value_heads": 16,
146
+ "num_kv_shared_layers": 0,
147
+ "pad_token_id": 0,
148
+ "rms_norm_eps": 1e-06,
149
+ "rope_parameters": {
150
+ "full_attention": {
151
+ "partial_rotary_factor": 0.25,
152
+ "rope_theta": 1000000.0,
153
+ "rope_type": "proportional"
154
+ },
155
+ "sliding_attention": {
156
+ "rope_theta": 10000.0,
157
+ "rope_type": "default"
158
+ }
159
+ },
160
+ "sliding_window": 1024,
161
+ "tie_word_embeddings": true,
162
+ "top_k_experts": null,
163
+ "use_bidirectional_attention": "vision",
164
+ "use_cache": true,
165
+ "use_double_wide_mlp": false,
166
+ "vocab_size": 262144,
167
+ "vocab_size_per_layer_input": 262144
168
+ },
169
+ "tie_word_embeddings": true,
170
+ "transformers_version": "5.12.1",
171
+ "video_token_id": 258884,
172
+ "vision_config": {
173
+ "_name_or_path": "",
174
+ "architectures": null,
175
+ "attention_bias": false,
176
+ "attention_dropout": 0.0,
177
+ "chunk_size_feed_forward": 0,
178
+ "default_output_length": 280,
179
+ "dtype": "bfloat16",
180
+ "global_head_dim": 72,
181
+ "head_dim": 72,
182
+ "hidden_activation": "gelu_pytorch_tanh",
183
+ "hidden_size": 1152,
184
+ "id2label": {
185
+ "0": "LABEL_0",
186
+ "1": "LABEL_1"
187
+ },
188
+ "initializer_range": 0.02,
189
+ "intermediate_size": 4304,
190
+ "is_encoder_decoder": false,
191
+ "label2id": {
192
+ "LABEL_0": 0,
193
+ "LABEL_1": 1
194
+ },
195
+ "max_position_embeddings": 131072,
196
+ "model_type": "gemma4_vision",
197
+ "num_attention_heads": 16,
198
+ "num_hidden_layers": 27,
199
+ "num_key_value_heads": 16,
200
+ "output_attentions": false,
201
+ "output_hidden_states": false,
202
+ "patch_size": 16,
203
+ "pooling_kernel_size": 3,
204
+ "position_embedding_size": 10240,
205
+ "problem_type": null,
206
+ "return_dict": true,
207
+ "rms_norm_eps": 1e-06,
208
+ "rope_parameters": {
209
+ "rope_theta": 100.0,
210
+ "rope_type": "default"
211
+ },
212
+ "standardize": true,
213
+ "use_clipped_linears": false
214
+ },
215
+ "vision_soft_tokens_per_image": 280
216
+ }
model/embed_audio.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1f961b58fe76eb58c936f7b48cfb45d87ae431a0d820b2a0f995bd22a936b19f
3
+ size 101801106
model/gemma31b_with_audio_config.json ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Gemma4ForConditionalGeneration"
4
+ ],
5
+ "audio_config": {
6
+ "_name_or_path": "",
7
+ "architectures": null,
8
+ "attention_chunk_size": 12,
9
+ "attention_context_left": 13,
10
+ "attention_context_right": 0,
11
+ "attention_invalid_logits_value": -1000000000.0,
12
+ "attention_logit_cap": 50.0,
13
+ "chunk_size_feed_forward": 0,
14
+ "conv_kernel_size": 5,
15
+ "dtype": "bfloat16",
16
+ "gradient_clipping": 10000000000.0,
17
+ "hidden_act": "silu",
18
+ "hidden_size": 1024,
19
+ "id2label": {
20
+ "0": "LABEL_0",
21
+ "1": "LABEL_1"
22
+ },
23
+ "initializer_range": 0.02,
24
+ "is_encoder_decoder": false,
25
+ "label2id": {
26
+ "LABEL_0": 0,
27
+ "LABEL_1": 1
28
+ },
29
+ "model_type": "gemma4_audio",
30
+ "num_attention_heads": 8,
31
+ "num_hidden_layers": 12,
32
+ "output_attentions": false,
33
+ "output_hidden_states": false,
34
+ "output_proj_dims": 1536,
35
+ "problem_type": null,
36
+ "residual_weight": 0.5,
37
+ "return_dict": true,
38
+ "rms_norm_eps": 1e-06,
39
+ "subsampling_conv_channels": [
40
+ 128,
41
+ 32
42
+ ],
43
+ "use_clipped_linears": true
44
+ },
45
+ "audio_token_id": 258881,
46
+ "boa_token_id": 256000,
47
+ "boi_token_id": 255999,
48
+ "bos_token_id": 2,
49
+ "dtype": "bfloat16",
50
+ "eoa_token_id": 258883,
51
+ "eoa_token_index": 258883,
52
+ "eoi_token_id": 258882,
53
+ "eos_token_id": 1,
54
+ "image_token_id": 258880,
55
+ "initializer_range": 0.02,
56
+ "model_type": "gemma4",
57
+ "pad_token_id": 0,
58
+ "text_config": {
59
+ "attention_bias": false,
60
+ "attention_dropout": 0.0,
61
+ "attention_k_eq_v": true,
62
+ "bos_token_id": 2,
63
+ "dtype": "bfloat16",
64
+ "enable_moe_block": false,
65
+ "eos_token_id": 1,
66
+ "expert_intermediate_size": null,
67
+ "final_logit_softcapping": 30.0,
68
+ "global_head_dim": 512,
69
+ "head_dim": 256,
70
+ "hidden_activation": "gelu_pytorch_tanh",
71
+ "hidden_size": 5376,
72
+ "hidden_size_per_layer_input": 0,
73
+ "initializer_range": 0.02,
74
+ "intermediate_size": 21504,
75
+ "layer_types": [
76
+ "sliding_attention",
77
+ "sliding_attention",
78
+ "sliding_attention",
79
+ "sliding_attention",
80
+ "sliding_attention",
81
+ "full_attention",
82
+ "sliding_attention",
83
+ "sliding_attention",
84
+ "sliding_attention",
85
+ "sliding_attention",
86
+ "sliding_attention",
87
+ "full_attention",
88
+ "sliding_attention",
89
+ "sliding_attention",
90
+ "sliding_attention",
91
+ "sliding_attention",
92
+ "sliding_attention",
93
+ "full_attention",
94
+ "sliding_attention",
95
+ "sliding_attention",
96
+ "sliding_attention",
97
+ "sliding_attention",
98
+ "sliding_attention",
99
+ "full_attention",
100
+ "sliding_attention",
101
+ "sliding_attention",
102
+ "sliding_attention",
103
+ "sliding_attention",
104
+ "sliding_attention",
105
+ "full_attention",
106
+ "sliding_attention",
107
+ "sliding_attention",
108
+ "sliding_attention",
109
+ "sliding_attention",
110
+ "sliding_attention",
111
+ "full_attention",
112
+ "sliding_attention",
113
+ "sliding_attention",
114
+ "sliding_attention",
115
+ "sliding_attention",
116
+ "sliding_attention",
117
+ "full_attention",
118
+ "sliding_attention",
119
+ "sliding_attention",
120
+ "sliding_attention",
121
+ "sliding_attention",
122
+ "sliding_attention",
123
+ "full_attention",
124
+ "sliding_attention",
125
+ "sliding_attention",
126
+ "sliding_attention",
127
+ "sliding_attention",
128
+ "sliding_attention",
129
+ "full_attention",
130
+ "sliding_attention",
131
+ "sliding_attention",
132
+ "sliding_attention",
133
+ "sliding_attention",
134
+ "sliding_attention",
135
+ "full_attention"
136
+ ],
137
+ "max_position_embeddings": 262144,
138
+ "model_type": "gemma4_text",
139
+ "moe_intermediate_size": null,
140
+ "num_attention_heads": 32,
141
+ "num_experts": null,
142
+ "num_global_key_value_heads": 4,
143
+ "num_hidden_layers": 60,
144
+ "num_key_value_heads": 16,
145
+ "num_kv_shared_layers": 0,
146
+ "pad_token_id": 0,
147
+ "rms_norm_eps": 1e-06,
148
+ "rope_parameters": {
149
+ "full_attention": {
150
+ "partial_rotary_factor": 0.25,
151
+ "rope_theta": 1000000.0,
152
+ "rope_type": "proportional"
153
+ },
154
+ "sliding_attention": {
155
+ "rope_theta": 10000.0,
156
+ "rope_type": "default"
157
+ }
158
+ },
159
+ "sliding_window": 1024,
160
+ "tie_word_embeddings": true,
161
+ "top_k_experts": null,
162
+ "use_bidirectional_attention": "vision",
163
+ "use_cache": true,
164
+ "use_double_wide_mlp": false,
165
+ "vocab_size": 262144,
166
+ "vocab_size_per_layer_input": 262144
167
+ },
168
+ "tie_word_embeddings": true,
169
+ "transformers_version": "5.12.1",
170
+ "use_cache": false,
171
+ "video_token_id": 258884,
172
+ "vision_config": {
173
+ "_name_or_path": "",
174
+ "architectures": null,
175
+ "attention_bias": false,
176
+ "attention_dropout": 0.0,
177
+ "chunk_size_feed_forward": 0,
178
+ "default_output_length": 280,
179
+ "dtype": "bfloat16",
180
+ "global_head_dim": 72,
181
+ "head_dim": 72,
182
+ "hidden_activation": "gelu_pytorch_tanh",
183
+ "hidden_size": 1152,
184
+ "id2label": {
185
+ "0": "LABEL_0",
186
+ "1": "LABEL_1"
187
+ },
188
+ "initializer_range": 0.02,
189
+ "intermediate_size": 4304,
190
+ "is_encoder_decoder": false,
191
+ "label2id": {
192
+ "LABEL_0": 0,
193
+ "LABEL_1": 1
194
+ },
195
+ "max_position_embeddings": 131072,
196
+ "model_type": "gemma4_vision",
197
+ "num_attention_heads": 16,
198
+ "num_hidden_layers": 27,
199
+ "num_key_value_heads": 16,
200
+ "output_attentions": false,
201
+ "output_hidden_states": false,
202
+ "patch_size": 16,
203
+ "pooling_kernel_size": 3,
204
+ "position_embedding_size": 10240,
205
+ "problem_type": null,
206
+ "return_dict": true,
207
+ "rms_norm_eps": 1e-06,
208
+ "rope_parameters": {
209
+ "rope_theta": 100.0,
210
+ "rope_type": "default"
211
+ },
212
+ "standardize": true,
213
+ "use_clipped_linears": false
214
+ },
215
+ "vision_soft_tokens_per_image": 280
216
+ }
model/generation_config.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token_id": 2,
3
+ "do_sample": true,
4
+ "eos_token_id": [
5
+ 1,
6
+ 106,
7
+ 50
8
+ ],
9
+ "pad_token_id": 0,
10
+ "temperature": 1.0,
11
+ "top_k": 64,
12
+ "top_p": 0.95,
13
+ "transformers_version": "5.12.1"
14
+ }
model/model-00001-of-00007.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d6b0656ed09013386c59ad75c974d4ece51e3e8cd6fce0b05a49405b723be47a
3
+ size 9777274982
model/model-00002-of-00007.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ad6ee3a08ec001a62cec0af7fe5ca8b62a9edaa2079211792f359cf3c3caa536
3
+ size 9964586468
model/model-00003-of-00007.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d10cff174725b889e3a8197119c01076bad608030a853ddf359c46ad8189a0f3
3
+ size 9975564812
model/model-00004-of-00007.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:732ce9afb9c62c56af850674a2139681f58a1cf314599810bea06897b329079a
3
+ size 9843454454
model/model-00005-of-00007.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:25a6ce60aaf1b2ea49562d1c98d07c9d5b9ea7dce6d7cf987b82207046534d6d
3
+ size 9920512908
model/model-00006-of-00007.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b25ed49d8e9c8c5208f9ecd1250172fc274b1a9c9ef4b5fef301c4bb81c465e2
3
+ size 9799403148
model/model-00007-of-00007.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9fa309158e5fbc7626c7080c0e86b62bb875b2405d2615c63fd9df247f212b29
3
+ size 3891800932
model/model.safetensors.index.json ADDED
The diff for this file is too large to render. See raw diff
 
model/processor_config.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_ms_per_token": 40,
3
+ "audio_seq_length": 750,
4
+ "feature_extractor": {
5
+ "dither": 0.0,
6
+ "feature_extractor_type": "Gemma4AudioFeatureExtractor",
7
+ "feature_size": 128,
8
+ "fft_length": 512,
9
+ "fft_overdrive": false,
10
+ "frame_length": 320,
11
+ "hop_length": 160,
12
+ "input_scale_factor": 1.0,
13
+ "max_frequency": 8000.0,
14
+ "mel_floor": 0.001,
15
+ "min_frequency": 0.0,
16
+ "padding_side": "right",
17
+ "padding_value": 0.0,
18
+ "per_bin_mean": null,
19
+ "per_bin_stddev": null,
20
+ "preemphasis": 0.0,
21
+ "preemphasis_htk_flavor": true,
22
+ "return_attention_mask": true,
23
+ "sampling_rate": 16000
24
+ },
25
+ "image_processor": {
26
+ "do_convert_rgb": true,
27
+ "do_normalize": false,
28
+ "do_rescale": true,
29
+ "do_resize": true,
30
+ "image_mean": [
31
+ 0.0,
32
+ 0.0,
33
+ 0.0
34
+ ],
35
+ "image_processor_type": "Gemma4ImageProcessor",
36
+ "image_seq_length": 280,
37
+ "image_std": [
38
+ 1.0,
39
+ 1.0,
40
+ 1.0
41
+ ],
42
+ "max_soft_tokens": 280,
43
+ "patch_size": 16,
44
+ "pooling_kernel_size": 3,
45
+ "resample": 3,
46
+ "rescale_factor": 0.00392156862745098
47
+ },
48
+ "image_seq_length": 280,
49
+ "processor_class": "Gemma4Processor",
50
+ "video_processor": {
51
+ "do_convert_rgb": true,
52
+ "do_normalize": true,
53
+ "do_rescale": true,
54
+ "do_resize": true,
55
+ "do_sample_frames": true,
56
+ "image_mean": [
57
+ 0.0,
58
+ 0.0,
59
+ 0.0
60
+ ],
61
+ "image_std": [
62
+ 1.0,
63
+ 1.0,
64
+ 1.0
65
+ ],
66
+ "max_soft_tokens": 70,
67
+ "num_frames": 32,
68
+ "patch_size": 16,
69
+ "pooling_kernel_size": 3,
70
+ "resample": 3,
71
+ "rescale_factor": 0.00392156862745098,
72
+ "return_metadata": false,
73
+ "video_processor_type": "Gemma4VideoProcessor"
74
+ }
75
+ }
model/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a2619fe11b50dbed06ac443c51d757b354d0b62d64baa514404d4e84e6713519
3
+ size 32169780
model/tokenizer_config.json ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_token": "<|audio|>",
3
+ "backend": "tokenizers",
4
+ "boa_token": "<|audio>",
5
+ "boi_token": "<|image>",
6
+ "bos_token": "<bos>",
7
+ "eoa_token": "<audio|>",
8
+ "eoc_token": "<channel|>",
9
+ "eoi_token": "<image|>",
10
+ "eos_token": "<eos>",
11
+ "eot_token": "<turn|>",
12
+ "escape_token": "<|\"|>",
13
+ "etc_token": "<tool_call|>",
14
+ "etd_token": "<tool|>",
15
+ "etr_token": "<tool_response|>",
16
+ "extra_special_tokens": [
17
+ "<|video|>"
18
+ ],
19
+ "image_token": "<|image|>",
20
+ "is_local": true,
21
+ "local_files_only": true,
22
+ "mask_token": "<mask>",
23
+ "max_length": null,
24
+ "model_max_length": 1000000000000000019884624838656,
25
+ "model_specific_special_tokens": {
26
+ "audio_token": "<|audio|>",
27
+ "boa_token": "<|audio>",
28
+ "boi_token": "<|image>",
29
+ "eoa_token": "<audio|>",
30
+ "eoc_token": "<channel|>",
31
+ "eoi_token": "<image|>",
32
+ "eot_token": "<turn|>",
33
+ "escape_token": "<|\"|>",
34
+ "etc_token": "<tool_call|>",
35
+ "etd_token": "<tool|>",
36
+ "etr_token": "<tool_response|>",
37
+ "image_token": "<|image|>",
38
+ "soc_token": "<|channel>",
39
+ "sot_token": "<|turn>",
40
+ "stc_token": "<|tool_call>",
41
+ "std_token": "<|tool>",
42
+ "str_token": "<|tool_response>",
43
+ "think_token": "<|think|>"
44
+ },
45
+ "pad_to_multiple_of": null,
46
+ "pad_token": "<pad>",
47
+ "pad_token_type_id": 0,
48
+ "padding_side": "left",
49
+ "processor_class": "Gemma4Processor",
50
+ "response_schema": {
51
+ "properties": {
52
+ "content": {
53
+ "type": "string"
54
+ },
55
+ "role": {
56
+ "const": "assistant"
57
+ },
58
+ "thinking": {
59
+ "type": "string"
60
+ },
61
+ "tool_calls": {
62
+ "items": {
63
+ "properties": {
64
+ "function": {
65
+ "properties": {
66
+ "arguments": {
67
+ "additionalProperties": {},
68
+ "type": "object",
69
+ "x-parser": "gemma4-tool-call"
70
+ },
71
+ "name": {
72
+ "type": "string"
73
+ }
74
+ },
75
+ "type": "object",
76
+ "x-regex": "call\\:(?P<name>\\w+)(?P<arguments>\\{.*\\})"
77
+ },
78
+ "type": {
79
+ "const": "function"
80
+ }
81
+ },
82
+ "type": "object"
83
+ },
84
+ "type": "array",
85
+ "x-regex-iterator": "<\\|tool_call>(.*?)<tool_call\\|>"
86
+ }
87
+ },
88
+ "type": "object",
89
+ "x-regex": "(\\<\\|channel\\>thought\\n(?P<thinking>.*?)\\<channel\\|\\>)?(?P<tool_calls>\\<\\|tool_call\\>.*\\<tool_call\\|\\>)?(?P<content>(?:(?!\\<turn\\|\\>)(?!\\<\\|tool_response\\>).)+)?(?:\\<turn\\|\\>|\\<\\|tool_response\\>)?"
90
+ },
91
+ "soc_token": "<|channel>",
92
+ "sot_token": "<|turn>",
93
+ "stc_token": "<|tool_call>",
94
+ "std_token": "<|tool>",
95
+ "str_token": "<|tool_response>",
96
+ "think_token": "<|think|>",
97
+ "tokenizer_class": "GemmaTokenizer",
98
+ "unk_token": "<unk>"
99
+ }
parakeet/config.json ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "ParakeetForTDT"
4
+ ],
5
+ "blank_token_id": 8192,
6
+ "decoder_hidden_size": 640,
7
+ "dtype": "float32",
8
+ "durations": [
9
+ 0,
10
+ 1,
11
+ 2,
12
+ 3,
13
+ 4
14
+ ],
15
+ "encoder_config": {
16
+ "activation_dropout": 0.1,
17
+ "attention_bias": false,
18
+ "attention_dropout": 0.1,
19
+ "conv_kernel_size": 9,
20
+ "convolution_bias": false,
21
+ "dropout": 0.1,
22
+ "dropout_positions": 0.0,
23
+ "hidden_act": "silu",
24
+ "hidden_size": 1024,
25
+ "initializer_range": 0.02,
26
+ "intermediate_size": 4096,
27
+ "layerdrop": 0.1,
28
+ "max_position_embeddings": 5000,
29
+ "model_type": "parakeet_encoder",
30
+ "num_attention_heads": 8,
31
+ "num_hidden_layers": 24,
32
+ "num_key_value_heads": 8,
33
+ "num_mel_bins": 128,
34
+ "scale_input": false,
35
+ "subsampling_conv_channels": 256,
36
+ "subsampling_conv_kernel_size": 3,
37
+ "subsampling_conv_stride": 2,
38
+ "subsampling_factor": 8
39
+ },
40
+ "hidden_act": "relu",
41
+ "initializer_range": 0.02,
42
+ "is_encoder_decoder": true,
43
+ "max_symbols_per_step": 10,
44
+ "model_type": "parakeet_tdt",
45
+ "num_decoder_layers": 2,
46
+ "pad_token_id": 2,
47
+ "transformers_version": "5.6.0.dev0",
48
+ "vocab_size": 8193
49
+ }
parakeet/generation_config.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "decoder_start_token_id": 8192,
4
+ "eos_token_id": 3,
5
+ "output_attentions": false,
6
+ "output_hidden_states": false,
7
+ "pad_token_id": 2,
8
+ "suppress_tokens": [
9
+ 8193,
10
+ 8194,
11
+ 8195,
12
+ 8196,
13
+ 8197
14
+ ],
15
+ "transformers_version": "5.6.0.dev0"
16
+ }
parakeet/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3a2026366188c8c68598edbbff92f8d11590a08e0ae2e6775544e7b07d6a5e11
3
+ size 2508311120
parakeet/processor_config.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "blank_token": "<blank>",
3
+ "feature_extractor": {
4
+ "feature_extractor_type": "ParakeetFeatureExtractor",
5
+ "feature_size": 128,
6
+ "hop_length": 160,
7
+ "n_fft": 512,
8
+ "padding_side": "right",
9
+ "padding_value": 0.0,
10
+ "preemphasis": 0.97,
11
+ "return_attention_mask": true,
12
+ "sampling_rate": 16000,
13
+ "win_length": 400
14
+ },
15
+ "processor_class": "ParakeetProcessor"
16
+ }
parakeet/tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
parakeet/tokenizer_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "clean_up_tokenization_spaces": false,
4
+ "eos_token": "<|endoftext|>",
5
+ "model_max_length": 1000000000000000019884624838656,
6
+ "pad_token": "<pad>",
7
+ "processor_class": "ParakeetProcessor",
8
+ "tokenizer_class": "ParakeetTokenizer",
9
+ "unk_token": "<unk>"
10
+ }
processor/chat_template.jinja ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- macro format_parameters(properties, required, filter_keys=false) -%}
2
+ {%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%}
3
+ {%- set ns = namespace(found_first=false) -%}
4
+ {%- for key, value in properties | dictsort -%}
5
+ {%- set add_comma = false -%}
6
+ {%- if not filter_keys or key not in standard_keys -%}
7
+ {%- if ns.found_first %},{% endif -%}
8
+ {%- set ns.found_first = true -%}
9
+ {{ key }}:{
10
+ {%- if value['description'] -%}
11
+ description:<|"|>{{ value['description'] }}<|"|>
12
+ {%- set add_comma = true -%}
13
+ {%- endif -%}
14
+ {%- if value['type'] | upper == 'STRING' -%}
15
+ {%- if value['enum'] -%}
16
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
17
+ enum:{{ format_argument(value['enum']) }}
18
+ {%- endif -%}
19
+ {%- elif value['type'] | upper == 'ARRAY' -%}
20
+ {%- if value['items'] is mapping and value['items'] -%}
21
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
22
+ items:{
23
+ {%- set ns_items = namespace(found_first=false) -%}
24
+ {%- for item_key, item_value in value['items'] | dictsort -%}
25
+ {%- if item_value is not none -%}
26
+ {%- if ns_items.found_first %},{% endif -%}
27
+ {%- set ns_items.found_first = true -%}
28
+ {%- if item_key == 'properties' -%}
29
+ properties:{
30
+ {%- if item_value is mapping -%}
31
+ {{- format_parameters(item_value, value['items']['required'] | default([])) -}}
32
+ {%- endif -%}
33
+ }
34
+ {%- elif item_key == 'required' -%}
35
+ required:[
36
+ {%- for req_item in item_value -%}
37
+ <|"|>{{- req_item -}}<|"|>
38
+ {%- if not loop.last %},{% endif -%}
39
+ {%- endfor -%}
40
+ ]
41
+ {%- elif item_key == 'type' -%}
42
+ {%- if item_value is string -%}
43
+ type:{{ format_argument(item_value | upper) }}
44
+ {%- else -%}
45
+ type:{{ format_argument(item_value | map('upper') | list) }}
46
+ {%- endif -%}
47
+ {%- else -%}
48
+ {{ item_key }}:{{ format_argument(item_value) }}
49
+ {%- endif -%}
50
+ {%- endif -%}
51
+ {%- endfor -%}
52
+ }
53
+ {%- endif -%}
54
+ {%- endif -%}
55
+ {%- if value['nullable'] %}
56
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
57
+ nullable:true
58
+ {%- endif -%}
59
+ {%- if value['type'] | upper == 'OBJECT' -%}
60
+ {%- if value['properties'] is defined and value['properties'] is mapping -%}
61
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
62
+ properties:{
63
+ {{- format_parameters(value['properties'], value['required'] | default([])) -}}
64
+ }
65
+ {%- elif value is mapping -%}
66
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
67
+ properties:{
68
+ {{- format_parameters(value, value['required'] | default([]), filter_keys=true) -}}
69
+ }
70
+ {%- endif -%}
71
+ {%- if value['required'] -%}
72
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
73
+ required:[
74
+ {%- for item in value['required'] | default([]) -%}
75
+ <|"|>{{- item -}}<|"|>
76
+ {%- if not loop.last %},{% endif -%}
77
+ {%- endfor -%}
78
+ ]
79
+ {%- endif -%}
80
+ {%- endif -%}
81
+ {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%}
82
+ type:<|"|>{{ value['type'] | upper }}<|"|>}
83
+ {%- endif -%}
84
+ {%- endfor -%}
85
+ {%- endmacro -%}
86
+ {%- macro format_function_declaration(tool_data) -%}
87
+ declaration:{{- tool_data['function']['name'] -}}{description:<|"|>{{- tool_data['function']['description'] -}}<|"|>
88
+ {%- set params = tool_data['function']['parameters'] -%}
89
+ {%- if params -%}
90
+ ,parameters:{
91
+ {%- if params['properties'] -%}
92
+ properties:{ {{- format_parameters(params['properties'], params['required']) -}} },
93
+ {%- endif -%}
94
+ {%- if params['required'] -%}
95
+ required:[
96
+ {%- for item in params['required'] -%}
97
+ <|"|>{{- item -}}<|"|>
98
+ {{- ',' if not loop.last -}}
99
+ {%- endfor -%}
100
+ ],
101
+ {%- endif -%}
102
+ {%- if params['type'] -%}
103
+ type:<|"|>{{- params['type'] | upper -}}<|"|>}
104
+ {%- endif -%}
105
+ {%- endif -%}
106
+ {%- if 'response' in tool_data['function'] -%}
107
+ {%- set response_declaration = tool_data['function']['response'] -%}
108
+ ,response:{
109
+ {%- if response_declaration['description'] -%}
110
+ description:<|"|>{{- response_declaration['description'] -}}<|"|>,
111
+ {%- endif -%}
112
+ {%- if response_declaration['type'] | upper == 'OBJECT' -%}
113
+ type:<|"|>{{- response_declaration['type'] | upper -}}<|"|>}
114
+ {%- endif -%}
115
+ {%- endif -%}
116
+ }
117
+ {%- endmacro -%}
118
+ {%- macro format_argument(argument, escape_keys=True) -%}
119
+ {%- if argument is string -%}
120
+ {{- '<|"|>' + argument + '<|"|>' -}}
121
+ {%- elif argument is boolean -%}
122
+ {{- 'true' if argument else 'false' -}}
123
+ {%- elif argument is mapping -%}
124
+ {{- '{' -}}
125
+ {%- set ns = namespace(found_first=false) -%}
126
+ {%- for key, value in argument | dictsort -%}
127
+ {%- if ns.found_first %},{% endif -%}
128
+ {%- set ns.found_first = true -%}
129
+ {%- if escape_keys -%}
130
+ {{- '<|"|>' + key + '<|"|>' -}}
131
+ {%- else -%}
132
+ {{- key -}}
133
+ {%- endif -%}
134
+ :{{- format_argument(value, escape_keys=escape_keys) -}}
135
+ {%- endfor -%}
136
+ {{- '}' -}}
137
+ {%- elif argument is sequence -%}
138
+ {{- '[' -}}
139
+ {%- for item in argument -%}
140
+ {{- format_argument(item, escape_keys=escape_keys) -}}
141
+ {%- if not loop.last %},{% endif -%}
142
+ {%- endfor -%}
143
+ {{- ']' -}}
144
+ {%- else -%}
145
+ {{- argument -}}
146
+ {%- endif -%}
147
+ {%- endmacro -%}
148
+ {%- macro strip_thinking(text) -%}
149
+ {%- set ns = namespace(result='') -%}
150
+ {%- for part in text.split('<channel|>') -%}
151
+ {%- if '<|channel>' in part -%}
152
+ {%- set ns.result = ns.result + part.split('<|channel>')[0] -%}
153
+ {%- else -%}
154
+ {%- set ns.result = ns.result + part -%}
155
+ {%- endif -%}
156
+ {%- endfor -%}
157
+ {{- ns.result | trim -}}
158
+ {%- endmacro -%}
159
+
160
+ {%- macro format_tool_response_block(tool_name, response) -%}
161
+ {{- '<|tool_response>' -}}
162
+ {%- if response is mapping -%}
163
+ {{- 'response:' + tool_name + '{' -}}
164
+ {%- for key, value in response | dictsort -%}
165
+ {{- key -}}:{{- format_argument(value, escape_keys=False) -}}
166
+ {%- if not loop.last %},{% endif -%}
167
+ {%- endfor -%}
168
+ {{- '}' -}}
169
+ {%- else -%}
170
+ {{- 'response:' + tool_name + '{value:' + format_argument(response, escape_keys=False) + '}' -}}
171
+ {%- endif -%}
172
+ {{- '<tool_response|>' -}}
173
+ {%- endmacro -%}
174
+
175
+ {%- set ns = namespace(prev_message_type=None) -%}
176
+ {%- set loop_messages = messages -%}
177
+ {{- bos_token -}}
178
+ {#- Handle System/Tool Definitions Block -#}
179
+ {%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}
180
+ {{- '<|turn>system\n' -}}
181
+ {#- Inject Thinking token at the very top of the FIRST system turn -#}
182
+ {%- if enable_thinking is defined and enable_thinking -%}
183
+ {{- '<|think|>\n' -}}
184
+ {%- set ns.prev_message_type = 'think' -%}
185
+ {%- endif -%}
186
+ {%- if messages[0]['role'] in ['system', 'developer'] -%}
187
+ {%- if messages[0]['content'] is string -%}
188
+ {{- messages[0]['content'] | trim -}}
189
+ {%- elif messages[0]['content'] is sequence -%}
190
+ {%- for item in messages[0]['content'] -%}
191
+ {{- item['text'] | trim + ' '-}}
192
+ {%- endfor -%}
193
+ {%- endif -%}
194
+ {%- set loop_messages = messages[1:] -%}
195
+ {%- endif -%}
196
+ {%- if tools -%}
197
+ {%- for tool in tools %}
198
+ {{- '<|tool>' -}}
199
+ {{- format_function_declaration(tool) | trim -}}
200
+ {{- '<tool|>' -}}
201
+ {%- endfor %}
202
+ {%- set ns.prev_message_type = 'tool' -%}
203
+ {%- endif -%}
204
+ {{- '<turn|>\n' -}}
205
+ {%- endif %}
206
+
207
+ {#- Pre-scan: find last user message index for reasoning guard -#}
208
+ {%- set ns_turn = namespace(last_user_idx=-1) -%}
209
+ {%- for i in range(loop_messages | length) -%}
210
+ {%- if loop_messages[i]['role'] == 'user' -%}
211
+ {%- set ns_turn.last_user_idx = i -%}
212
+ {%- endif -%}
213
+ {%- endfor -%}
214
+
215
+ {#- Loop through messages -#}
216
+ {%- for message in loop_messages -%}
217
+ {%- if message['role'] != 'tool' -%}
218
+ {%- set ns.prev_message_type = None -%}
219
+ {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}
220
+ {#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}
221
+ {%- set prev_nt = namespace(role=None, found=false) -%}
222
+ {%- if loop.index0 > 0 -%}
223
+ {%- for j in range(loop.index0 - 1, -1, -1) -%}
224
+ {%- if not prev_nt.found -%}
225
+ {%- if loop_messages[j]['role'] != 'tool' -%}
226
+ {%- set prev_nt.role = loop_messages[j]['role'] -%}
227
+ {%- set prev_nt.found = true -%}
228
+ {%- endif -%}
229
+ {%- endif -%}
230
+ {%- endfor -%}
231
+ {%- endif -%}
232
+ {%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%}
233
+ {%- if not continue_same_model_turn -%}
234
+ {{- '<|turn>' + role + '\n' }}
235
+ {%- endif -%}
236
+
237
+ {#- Render reasoning/reasoning_content as thinking channel -#}
238
+ {%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}
239
+ {%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}
240
+ {{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}
241
+ {%- endif -%}
242
+
243
+ {%- if message['tool_calls'] -%}
244
+ {%- for tool_call in message['tool_calls'] -%}
245
+ {%- set function = tool_call['function'] -%}
246
+ {{- '<|tool_call>call:' + function['name'] + '{' -}}
247
+ {%- if function['arguments'] is mapping -%}
248
+ {%- set ns_args = namespace(found_first=false) -%}
249
+ {%- for key, value in function['arguments'] | dictsort -%}
250
+ {%- if ns_args.found_first %},{% endif -%}
251
+ {%- set ns_args.found_first = true -%}
252
+ {{- key -}}:{{- format_argument(value, escape_keys=False) -}}
253
+ {%- endfor -%}
254
+ {%- elif function['arguments'] is string -%}
255
+ {{- function['arguments'] -}}
256
+ {%- endif -%}
257
+ {{- '}<tool_call|>' -}}
258
+ {%- endfor -%}
259
+ {%- set ns.prev_message_type = 'tool_call' -%}
260
+ {%- endif -%}
261
+
262
+ {%- set ns_tr_out = namespace(flag=false) -%}
263
+ {%- if message.get('tool_responses') -%}
264
+ {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}
265
+ {%- for tool_response in message['tool_responses'] -%}
266
+ {{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}
267
+ {%- set ns_tr_out.flag = true -%}
268
+ {%- set ns.prev_message_type = 'tool_response' -%}
269
+ {%- endfor -%}
270
+ {%- elif message.get('tool_calls') -%}
271
+ {#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#}
272
+ {%- set ns_tool_scan = namespace(stopped=false) -%}
273
+ {%- for k in range(loop.index0 + 1, loop_messages | length) -%}
274
+ {%- if ns_tool_scan.stopped -%}
275
+ {%- elif loop_messages[k]['role'] != 'tool' -%}
276
+ {%- set ns_tool_scan.stopped = true -%}
277
+ {%- else -%}
278
+ {%- set follow = loop_messages[k] -%}
279
+ {#- Resolve tool_call_id to function name -#}
280
+ {%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}
281
+ {%- for tc in message['tool_calls'] -%}
282
+ {%- if tc.get('id') == follow.get('tool_call_id') -%}
283
+ {%- set ns_tname.name = tc['function']['name'] -%}
284
+ {%- endif -%}
285
+ {%- endfor -%}
286
+ {#- Handle content as string or content-parts array -#}
287
+ {%- set tool_body = follow.get('content') -%}
288
+ {%- if tool_body is string -%}
289
+ {{- format_tool_response_block(ns_tname.name, tool_body) -}}
290
+ {%- elif tool_body is sequence and tool_body is not string -%}
291
+ {%- set ns_txt = namespace(s='') -%}
292
+ {%- for part in tool_body -%}
293
+ {%- if part.get('type') == 'text' -%}
294
+ {%- set ns_txt.s = ns_txt.s + (part.get('text') | default('')) -%}
295
+ {%- endif -%}
296
+ {%- endfor -%}
297
+ {{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}
298
+ {%- for part in tool_body -%}
299
+ {%- if part.get('type') == 'image' -%}
300
+ {{- '<|image|>' -}}
301
+ {%- elif part.get('type') == 'audio' -%}
302
+ {{- '<|audio|>' -}}
303
+ {%- elif part.get('type') == 'video' -%}
304
+ {{- '<|video|>' -}}
305
+ {%- endif -%}
306
+ {%- endfor -%}
307
+ {%- else -%}
308
+ {{- format_tool_response_block(ns_tname.name, tool_body) -}}
309
+ {%- endif -%}
310
+ {%- set ns_tr_out.flag = true -%}
311
+ {%- set ns.prev_message_type = 'tool_response' -%}
312
+ {%- endif -%}
313
+ {%- endfor -%}
314
+ {%- endif -%}
315
+
316
+ {%- set captured_content -%}
317
+ {%- if message['content'] is string -%}
318
+ {%- if role == 'model' -%}
319
+ {{- strip_thinking(message['content']) -}}
320
+ {%- else -%}
321
+ {{- message['content'] | trim -}}
322
+ {%- endif -%}
323
+ {%- elif message['content'] is sequence -%}
324
+ {%- for item in message['content'] -%}
325
+ {%- if item['type'] == 'text' -%}
326
+ {%- if role == 'model' -%}
327
+ {{- strip_thinking(item['text']) -}}
328
+ {%- else -%}
329
+ {{- item['text'] | trim -}}
330
+ {%- endif -%}
331
+ {%- elif item['type'] == 'image' -%}
332
+ {{- '<|image|>' -}}
333
+ {%- set ns.prev_message_type = 'image' -%}
334
+ {%- elif item['type'] == 'audio' -%}
335
+ {{- '<|audio|>' -}}
336
+ {%- set ns.prev_message_type = 'audio' -%}
337
+ {%- elif item['type'] == 'video' -%}
338
+ {{- '<|video|>' -}}
339
+ {%- set ns.prev_message_type = 'video' -%}
340
+ {%- endif -%}
341
+ {%- endfor -%}
342
+ {%- endif -%}
343
+ {%- endset -%}
344
+
345
+ {{- captured_content -}}
346
+ {%- set has_content = captured_content | trim | length > 0 -%}
347
+
348
+ {%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}
349
+ {{- '<|tool_response>' -}}
350
+ {%- elif not (ns_tr_out.flag and not has_content) -%}
351
+ {{- '<turn|>\n' -}}
352
+ {%- endif -%}
353
+ {%- endif -%}
354
+ {%- endfor -%}
355
+
356
+ {%- if add_generation_prompt -%}
357
+ {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}
358
+ {{- '<|turn>model\n' -}}
359
+ {%- if not enable_thinking | default(false) -%}
360
+ {{- '<|channel>thought\n<channel|>' -}}
361
+ {%- endif -%}
362
+ {%- endif -%}
363
+ {%- endif -%}
processor/processor_config.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_ms_per_token": 40,
3
+ "audio_seq_length": 750,
4
+ "feature_extractor": {
5
+ "dither": 0.0,
6
+ "feature_extractor_type": "Gemma4AudioFeatureExtractor",
7
+ "feature_size": 128,
8
+ "fft_length": 512,
9
+ "fft_overdrive": false,
10
+ "frame_length": 320,
11
+ "hop_length": 160,
12
+ "input_scale_factor": 1.0,
13
+ "max_frequency": 8000.0,
14
+ "mel_floor": 0.001,
15
+ "min_frequency": 0.0,
16
+ "padding_side": "right",
17
+ "padding_value": 0.0,
18
+ "per_bin_mean": null,
19
+ "per_bin_stddev": null,
20
+ "preemphasis": 0.0,
21
+ "preemphasis_htk_flavor": true,
22
+ "return_attention_mask": true,
23
+ "sampling_rate": 16000
24
+ },
25
+ "image_processor": {
26
+ "do_convert_rgb": true,
27
+ "do_normalize": false,
28
+ "do_rescale": true,
29
+ "do_resize": true,
30
+ "image_mean": [
31
+ 0.0,
32
+ 0.0,
33
+ 0.0
34
+ ],
35
+ "image_processor_type": "Gemma4ImageProcessor",
36
+ "image_seq_length": 280,
37
+ "image_std": [
38
+ 1.0,
39
+ 1.0,
40
+ 1.0
41
+ ],
42
+ "max_soft_tokens": 280,
43
+ "patch_size": 16,
44
+ "pooling_kernel_size": 3,
45
+ "resample": 3,
46
+ "rescale_factor": 0.00392156862745098
47
+ },
48
+ "image_seq_length": 280,
49
+ "processor_class": "Gemma4Processor",
50
+ "video_processor": {
51
+ "do_convert_rgb": true,
52
+ "do_normalize": true,
53
+ "do_rescale": true,
54
+ "do_resize": true,
55
+ "do_sample_frames": true,
56
+ "image_mean": [
57
+ 0.0,
58
+ 0.0,
59
+ 0.0
60
+ ],
61
+ "image_std": [
62
+ 1.0,
63
+ 1.0,
64
+ 1.0
65
+ ],
66
+ "max_soft_tokens": 70,
67
+ "num_frames": 32,
68
+ "patch_size": 16,
69
+ "pooling_kernel_size": 3,
70
+ "resample": 3,
71
+ "rescale_factor": 0.00392156862745098,
72
+ "return_metadata": false,
73
+ "video_processor_type": "Gemma4VideoProcessor"
74
+ }
75
+ }
processor/tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a2619fe11b50dbed06ac443c51d757b354d0b62d64baa514404d4e84e6713519
3
+ size 32169780
processor/tokenizer_config.json ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_token": "<|audio|>",
3
+ "backend": "tokenizers",
4
+ "boa_token": "<|audio>",
5
+ "boi_token": "<|image>",
6
+ "bos_token": "<bos>",
7
+ "eoa_token": "<audio|>",
8
+ "eoc_token": "<channel|>",
9
+ "eoi_token": "<image|>",
10
+ "eos_token": "<eos>",
11
+ "eot_token": "<turn|>",
12
+ "escape_token": "<|\"|>",
13
+ "etc_token": "<tool_call|>",
14
+ "etd_token": "<tool|>",
15
+ "etr_token": "<tool_response|>",
16
+ "extra_special_tokens": [
17
+ "<|video|>"
18
+ ],
19
+ "image_token": "<|image|>",
20
+ "is_local": true,
21
+ "local_files_only": true,
22
+ "mask_token": "<mask>",
23
+ "max_length": null,
24
+ "model_max_length": 1000000000000000019884624838656,
25
+ "model_specific_special_tokens": {
26
+ "audio_token": "<|audio|>",
27
+ "boa_token": "<|audio>",
28
+ "boi_token": "<|image>",
29
+ "eoa_token": "<audio|>",
30
+ "eoc_token": "<channel|>",
31
+ "eoi_token": "<image|>",
32
+ "eot_token": "<turn|>",
33
+ "escape_token": "<|\"|>",
34
+ "etc_token": "<tool_call|>",
35
+ "etd_token": "<tool|>",
36
+ "etr_token": "<tool_response|>",
37
+ "image_token": "<|image|>",
38
+ "soc_token": "<|channel>",
39
+ "sot_token": "<|turn>",
40
+ "stc_token": "<|tool_call>",
41
+ "std_token": "<|tool>",
42
+ "str_token": "<|tool_response>",
43
+ "think_token": "<|think|>"
44
+ },
45
+ "pad_to_multiple_of": null,
46
+ "pad_token": "<pad>",
47
+ "pad_token_type_id": 0,
48
+ "padding_side": "left",
49
+ "processor_class": "Gemma4Processor",
50
+ "response_schema": {
51
+ "properties": {
52
+ "content": {
53
+ "type": "string"
54
+ },
55
+ "role": {
56
+ "const": "assistant"
57
+ },
58
+ "thinking": {
59
+ "type": "string"
60
+ },
61
+ "tool_calls": {
62
+ "items": {
63
+ "properties": {
64
+ "function": {
65
+ "properties": {
66
+ "arguments": {
67
+ "additionalProperties": {},
68
+ "type": "object",
69
+ "x-parser": "gemma4-tool-call"
70
+ },
71
+ "name": {
72
+ "type": "string"
73
+ }
74
+ },
75
+ "type": "object",
76
+ "x-regex": "call\\:(?P<name>\\w+)(?P<arguments>\\{.*\\})"
77
+ },
78
+ "type": {
79
+ "const": "function"
80
+ }
81
+ },
82
+ "type": "object"
83
+ },
84
+ "type": "array",
85
+ "x-regex-iterator": "<\\|tool_call>(.*?)<tool_call\\|>"
86
+ }
87
+ },
88
+ "type": "object",
89
+ "x-regex": "(\\<\\|channel\\>thought\\n(?P<thinking>.*?)\\<channel\\|\\>)?(?P<tool_calls>\\<\\|tool_call\\>.*\\<tool_call\\|\\>)?(?P<content>(?:(?!\\<turn\\|\\>)(?!\\<\\|tool_response\\>).)+)?(?:\\<turn\\|\\>|\\<\\|tool_response\\>)?"
90
+ },
91
+ "soc_token": "<|channel>",
92
+ "sot_token": "<|turn>",
93
+ "stc_token": "<|tool_call>",
94
+ "std_token": "<|tool>",
95
+ "str_token": "<|tool_response>",
96
+ "think_token": "<|think|>",
97
+ "tokenizer_class": "GemmaTokenizer",
98
+ "unk_token": "<unk>"
99
+ }
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
run_inference.sh ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ set -euo pipefail
2
+
3
+ cd "$(dirname "$0")"
4
+ source /venv/main/bin/activate
5
+ python infer.py
setup_vllm.sh ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
vllm.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:641359f12335daec713a57084ecdc9b222916660440cf07c8b795bfab5707673
3
+ size 328568395
vllm_caption_runtime.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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))