""" clip_score.py — CLIP-based image-text alignment scoring, giving an objective numeric signal for the same question "faithfulness" currently asks the VLM judge to answer subjectively: does this image match the caption? CAVEAT, stated up front rather than glossed over: CLIP was trained on natural photographs, not line-art sketches. Its embeddings may be meaningfully less reliable on hand-drawn strokes than on real images — this is untested against this pipeline's actual sketches. Treat the numbers as informative context for the judge, not as ground truth. """ import numpy as np def load_clip_model(model_name="openai/clip-vit-base-patch32"): from transformers import CLIPModel, CLIPProcessor print(f"loading CLIP from {model_name} ...") # CONFIRMED on real hardware: without use_safetensors=True, transformers # tries the legacy pytorch_model.bin format first, which newer # transformers refuses to load via torch.load unless torch>=2.6 # (CVE-2025-32434) — forcing safetensors bypasses that check entirely, # per the error message itself ("does not apply when loading files # with safetensors"). model = CLIPModel.from_pretrained(model_name, use_safetensors=True) processor = CLIPProcessor.from_pretrained(model_name) model.eval() print("CLIP loaded.") return model, processor def compute_clip_score(model, processor, image, text): """ Returns raw cosine similarity between one image and one text string, typically in roughly [-1, 1] but in practice CLIP similarities for plausible pairs usually land in ~0.2-0.35. The literature's "CLIPScore" convention multiplies this by 2.5 and clips at 0 — left as raw cosine here so the caller decides how to present it. """ import torch inputs = processor(text=[text], images=image, return_tensors="pt", padding=True) with torch.no_grad(): outputs = model(**inputs) image_embeds = outputs.image_embeds.squeeze(0) text_embeds = outputs.text_embeds.squeeze(0) image_embeds = image_embeds / image_embeds.norm() text_embeds = text_embeds / text_embeds.norm() return float(torch.dot(image_embeds, text_embeds).item()) def compute_sequence_clip_scores(model, processor, images, caption): """ images: list of PIL Images (one per keyframe). Returns: dict with per-keyframe scores and the mean — same shape as dino_similarity's stagnation_score, for consistency. """ per_keyframe = [] for i, img in enumerate(images): score = compute_clip_score(model, processor, img, caption) per_keyframe.append(score) print(f" kf{i}: CLIP score vs caption = {score:.4f}") mean_score = float(np.mean(per_keyframe)) return {"per_keyframe_clip_score": per_keyframe, "mean_clip_score": mean_score}