Video-Text-to-Text
Transformers
Safetensors
English
gemma4
image-text-to-text
video-captioning
multimodal
gemma
parakeet
Instructions to use SulphurAI/sulphur-caption with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use SulphurAI/sulphur-caption with Transformers:
# Load model directly from transformers import AutoProcessor, AutoModelForMultimodalLM processor = AutoProcessor.from_pretrained("SulphurAI/sulphur-caption") model = AutoModelForMultimodalLM.from_pretrained("SulphurAI/sulphur-caption", device_map="auto") - Notebooks
- Google Colab
- Kaggle
| from __future__ import annotations | |
| import sys | |
| from pathlib import Path | |
| SCRIPT_DIR = Path(__file__).resolve().parent | |
| sys.path.insert(0, str(SCRIPT_DIR / "vllm")) | |
| from vllm_caption_runtime import build_vllm_request, load_llm, load_processor, sampling_params | |
| # ============================================================================= | |
| # Paths | |
| # ============================================================================= | |
| INPUT_FOLDER = "/workspace/videos_to_caption" | |
| OUTPUT_FOLDER = "/workspace/captions_out" | |
| MODEL_DIR = str(SCRIPT_DIR / "model") | |
| RECURSIVE = True | |
| OVERWRITE_EXISTING = False | |
| OUTPUT_EXTENSION = ".txt" | |
| ERROR_EXTENSION = ".error.txt" | |
| VIDEO_EXTENSIONS = (".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v") | |
| # ============================================================================= | |
| # Prompt Settings | |
| # ============================================================================= | |
| PROMPT_OVERRIDE = "" | |
| CAPTION_LENGTH = "very large" | |
| INCLUDE_WATERMARK_INFO = False | |
| HAS_THINKING = True | |
| VULGARITY = "low" | |
| UNCERTAINTY = "low" | |
| CHARACTER_NAMES = "none" | |
| FLUFF = "none" | |
| HAS_REPETITION = False | |
| SPECULATION = "low" | |
| TEMPORAL_DETAIL = "medium" | |
| VISUAL_SPECIFICITY = "moderate" | |
| CAMERA_DETAIL = "medium" | |
| CAPTION_STYLE = "plain" | |
| # ============================================================================= | |
| # vLLM / Generation Hyperparameters | |
| # ============================================================================= | |
| NUM_FRAMES = 12 | |
| SAMPLING_RATE = 16_000 | |
| MAX_MODEL_LEN = 4096 | |
| MAX_NUM_SEQS = 20 | |
| BATCH_SIZE = 20 | |
| GPU_MEMORY_UTILIZATION = 0.88 | |
| DTYPE = "bfloat16" | |
| # Online vLLM FP8 for Gemma linear weights. Parakeet is left unchanged. | |
| USE_FP8 = True | |
| FP8_QUANTIZATION = "fp8_per_tensor" | |
| ENFORCE_EAGER = False | |
| ENABLE_PREFIX_CACHING = False | |
| TRUST_REMOTE_CODE = False | |
| MAX_TOKENS = 1200 | |
| TEMPERATURE = 0.0 | |
| TOP_P = 0.9 | |
| REPETITION_PENALTY = 1.1 | |
| def prompt_settings() -> dict[str, object]: | |
| return { | |
| "caption_length": CAPTION_LENGTH, | |
| "include_watermark_info": INCLUDE_WATERMARK_INFO, | |
| "has_thinking": HAS_THINKING, | |
| "vulgarity": VULGARITY, | |
| "uncertainty": UNCERTAINTY, | |
| "character_names": CHARACTER_NAMES, | |
| "fluff": FLUFF, | |
| "has_repetition": HAS_REPETITION, | |
| "speculation": SPECULATION, | |
| "temporal_detail": TEMPORAL_DETAIL, | |
| "visual_specificity": VISUAL_SPECIFICITY, | |
| "camera_detail": CAMERA_DETAIL, | |
| "caption_style": CAPTION_STYLE, | |
| } | |
| def find_videos(input_folder: Path) -> list[Path]: | |
| iterator = input_folder.rglob("*") if RECURSIVE else input_folder.glob("*") | |
| videos = [ | |
| path | |
| for path in iterator | |
| if path.is_file() and path.suffix.lower() in VIDEO_EXTENSIONS | |
| ] | |
| return sorted(videos) | |
| def output_path_for(video_path: Path, input_folder: Path, output_folder: Path) -> Path: | |
| relative = video_path.relative_to(input_folder) | |
| return (output_folder / relative).with_suffix(OUTPUT_EXTENSION) | |
| def write_text(path: Path, text: str) -> None: | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text(text.rstrip() + "\n", encoding="utf-8") | |
| def chunks(items: list[Path], size: int) -> list[list[Path]]: | |
| return [items[index : index + size] for index in range(0, len(items), size)] | |
| def find_json_end(text: str) -> int | None: | |
| if not text.startswith("{"): | |
| return None | |
| depth = 0 | |
| in_string = False | |
| escape = False | |
| for index, char in enumerate(text): | |
| if in_string: | |
| if escape: | |
| escape = False | |
| elif char == "\\": | |
| escape = True | |
| elif char == '"': | |
| in_string = False | |
| continue | |
| if char == '"': | |
| in_string = True | |
| elif char == "{": | |
| depth += 1 | |
| elif char == "}": | |
| depth -= 1 | |
| if depth == 0: | |
| return index + 1 | |
| return None | |
| def strip_caption_label(text: str) -> str: | |
| caption = text.strip() | |
| lowered = caption.lower() | |
| for label in ("final caption", "final_caption", "caption", "final"): | |
| if lowered == label: | |
| return "" | |
| if lowered.startswith(label): | |
| rest = caption[len(label) :] | |
| stripped = rest.lstrip() | |
| if stripped.startswith(":"): | |
| return stripped[1:].lstrip() | |
| if stripped.startswith("- "): | |
| return stripped[1:].lstrip() | |
| if rest[:1].isspace(): | |
| return rest.lstrip() | |
| return caption | |
| def final_caption_from_raw_output(text: str) -> str: | |
| raw = text.strip() | |
| lowered = raw.lower() | |
| for marker in ("thought", "thinking", "reasoning"): | |
| if lowered == marker: | |
| return "" | |
| if lowered.startswith(marker): | |
| rest = raw[len(marker) :].lstrip() | |
| if rest.startswith(":"): | |
| rest = rest[1:].lstrip() | |
| raw = rest | |
| break | |
| json_end = find_json_end(raw) | |
| if json_end is not None: | |
| return strip_caption_label(raw[json_end:]) | |
| return strip_caption_label(raw) | |
| def main() -> None: | |
| quantization = FP8_QUANTIZATION if USE_FP8 else None | |
| input_folder = Path(INPUT_FOLDER) | |
| output_folder = Path(OUTPUT_FOLDER) | |
| if not input_folder.is_dir(): | |
| raise RuntimeError(f"INPUT_FOLDER does not exist or is not a directory: {input_folder}") | |
| videos = find_videos(input_folder) | |
| if not videos: | |
| print(f"No videos found in {input_folder}") | |
| return | |
| processor = load_processor(MODEL_DIR) | |
| llm = load_llm( | |
| model_dir=MODEL_DIR, | |
| max_model_len=MAX_MODEL_LEN, | |
| max_num_seqs=MAX_NUM_SEQS, | |
| gpu_memory_utilization=GPU_MEMORY_UTILIZATION, | |
| dtype=DTYPE, | |
| quantization=quantization, | |
| enforce_eager=ENFORCE_EAGER, | |
| enable_prefix_caching=ENABLE_PREFIX_CACHING, | |
| trust_remote_code=TRUST_REMOTE_CODE, | |
| ) | |
| params = sampling_params( | |
| temperature=TEMPERATURE, | |
| max_tokens=MAX_TOKENS, | |
| top_p=TOP_P, | |
| repetition_penalty=REPETITION_PENALTY, | |
| ) | |
| done = 0 | |
| skipped = 0 | |
| failed = 0 | |
| settings = prompt_settings() | |
| for batch in chunks(videos, BATCH_SIZE): | |
| requests = [] | |
| request_videos = [] | |
| for video_path in batch: | |
| output_path = output_path_for(video_path, input_folder, output_folder) | |
| if output_path.exists() and not OVERWRITE_EXISTING: | |
| skipped += 1 | |
| continue | |
| try: | |
| requests.append( | |
| build_vllm_request( | |
| processor=processor, | |
| model_dir=MODEL_DIR, | |
| video_path=video_path, | |
| num_frames=NUM_FRAMES, | |
| sampling_rate=SAMPLING_RATE, | |
| prompt_override=PROMPT_OVERRIDE, | |
| prompt_settings=settings, | |
| ) | |
| ) | |
| request_videos.append(video_path) | |
| except Exception as exc: | |
| failed += 1 | |
| error_path = output_path.with_suffix(ERROR_EXTENSION) | |
| write_text(error_path, f"{type(exc).__name__}: {exc}") | |
| print(f"FAILED preprocess {video_path}: {exc}") | |
| if not requests: | |
| continue | |
| try: | |
| outputs = llm.generate(requests, sampling_params=params) | |
| except Exception as exc: | |
| failed += len(request_videos) | |
| for video_path in request_videos: | |
| output_path = output_path_for(video_path, input_folder, output_folder) | |
| error_path = output_path.with_suffix(ERROR_EXTENSION) | |
| write_text(error_path, f"{type(exc).__name__}: {exc}") | |
| print(f"FAILED batch starting {request_videos[0]}: {exc}") | |
| continue | |
| for video_path, output in zip(request_videos, outputs, strict=True): | |
| output_path = output_path_for(video_path, input_folder, output_folder) | |
| raw_text = output.outputs[0].text if output.outputs else "" | |
| text = final_caption_from_raw_output(raw_text) | |
| write_text(output_path, text) | |
| done += 1 | |
| print(f"WROTE {output_path}") | |
| print(f"Done. wrote={done} skipped={skipped} failed={failed}") | |
| if __name__ == "__main__": | |
| main() | |