H022329's picture
Upload folder using huggingface_hub
9cf08e9 verified
Raw
History Blame Contribute Delete
8.37 kB
import os
import numpy as np
import torch
from PIL import Image
from .._llm import Qwen3VLEmbedderC
from .._utils import _pil_to_base64
import os
import torch
import unicodedata
import numpy as np
from PIL import Image
from urllib.parse import urlparse
from typing import Optional, List, Union, Dict, Any
# Constants for configuration
IMAGE_BASE_FACTOR = 16
IMAGE_FACTOR = IMAGE_BASE_FACTOR * 2
MIN_PIXELS = 4 * IMAGE_FACTOR * IMAGE_FACTOR
MAX_PIXELS = 1800 * IMAGE_FACTOR * IMAGE_FACTOR
FPS = 1
MAX_FRAMES = 10
FRAME_MAX_PIXELS = 768 * IMAGE_FACTOR * IMAGE_FACTOR
MAX_TOTAL_PIXELS = 10 * FRAME_MAX_PIXELS
_MAX_FRAMES = 5
model = None
def _get_model():
global model
if model is None:
model = Qwen3VLEmbedderC()
return model
def sample_frames(frames: List[Union[str, Image.Image]], max_segments: int) -> List[Union[str, Image.Image]]:
duration = len(frames)
if duration <= max_segments:
return frames
frame_id_array = np.linspace(0, duration - 1, max_segments, dtype=int)
frame_id_list = frame_id_array.tolist()
sampled_frames = [ frames[frame_idx] for frame_idx in frame_id_list ]
return sampled_frames
def is_image_path(path: str) -> bool:
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.tiff', '.svg'}
if path.startswith(('http://', 'https://')):
# Parse URL to remove query parameters
parsed_url = urlparse(path)
clean_path = parsed_url.path
else:
clean_path = path
# Check file extension
_, ext = os.path.splitext(clean_path.lower())
return ext in image_extensions
def is_video_input(video) -> bool:
if isinstance(video, str):
return True
if isinstance(video, list) and len(video) > 0:
# Check first element to determine the type
first_elem = video[0]
if isinstance(first_elem, Image.Image):
return True
if isinstance(first_elem, str):
return is_image_path(first_elem)
return False
def format_model_input(
text: Optional[Union[List[str], str]] = None,
image: Optional[Union[List[Union[str, Image.Image]], str, Image.Image]] = None,
video: Optional[Union[List[Union[str, List[Union[str, Image.Image]]]], str, List[Union[str, Image.Image]]]] = None,
instruction: Optional[str] = None,
fps: Optional[float] = None,
max_frames: Optional[int] = None,
default_instruction: str = "Represent the user's input.",
min_pixels: int = MIN_PIXELS,
max_pixels: int = MAX_PIXELS,
total_pixels: int = MAX_TOTAL_PIXELS,
max_frame_num: int = MAX_FRAMES
) -> List[Dict]:
# Ensure instruction ends with punctuation
if instruction:
instruction = instruction.strip()
if instruction and not unicodedata.category(instruction[-1]).startswith('P'):
instruction = instruction + '.'
# Initialize conversation with system prompts
content = []
conversation = [
{"role": "system", "content": [{"type": "text", "text": instruction or default_instruction}]},
{"role": "user", "content": content}
]
# Normalize text input to list
if text is None:
texts = []
elif isinstance(text, str):
texts = [text]
else:
texts = text
# Normalize image input to list
if image is None:
images = []
elif not isinstance(image, list):
images = [image]
else:
images = image
# Normalize video input to list
if video is None:
videos = []
elif is_video_input(video):
videos = [video]
else:
# Assume it's a list of videos
videos = video
# Add text, image, or video content to conversation
if not texts and not images and not videos:
content.append({'type': 'text', 'text': "NULL"})
return conversation
# Process each video
for vid in videos:
video_content = None
video_kwargs = {'total_pixels': total_pixels}
if isinstance(vid, list):
# Video as frame sequence
video_content = vid
if max_frame_num is not None:
video_content = sample_frames(video_content, max_frame_num)
video_content = [
('file://' + ele if isinstance(ele, str) else ele)
for ele in video_content
]
print("video_content:", video_content)
elif isinstance(vid, str):
# Video as file path
video_content = vid if vid.startswith(('http://', 'https://')) else 'file://' + vid
video_kwargs = {'fps': fps or FPS, 'max_frames': max_frames or max_frame_num}
else:
raise TypeError(f"Unrecognized video type: {type(vid)}")
# Add video input to content
if video_content:
content.append({
'type': 'video',
'video': video_content,
**video_kwargs
})
# Process each image
for img in images:
image_content = None
if isinstance(img, Image.Image):
image_content = img
elif isinstance(img, str):
image_content = img if img.startswith(('http://', 'https://')) else 'file://' + img
else:
raise TypeError(f"Unrecognized image type: {type(img)}")
# Add image input to content
if image_content:
content.append({
'type': 'image',
'image': image_content,
"min_pixels": min_pixels,
"max_pixels": max_pixels
})
# Process each text
for txt in texts:
content.append({'type': 'text', 'text': txt})
return conversation
def _load_frames(video_path: str, max_frames: int = _MAX_FRAMES):
"""用 OpenCV 均匀采样至多 max_frames 帧,返回 PIL Image 列表。"""
import cv2
cap = cv2.VideoCapture(video_path)
try:
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
if total <= 0:
cap.release()
raise ValueError(f"Cannot read video: {video_path}")
indices = np.linspace(0, total - 1, min(total, max_frames), dtype=int)
frames = []
for i in indices:
cap.set(cv2.CAP_PROP_POS_FRAMES, i)
ret, frame = cap.read()
if ret:
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
frames.append(Image.fromarray(frame_rgb))
finally:
cap.release()
return frames
def encode_video_segments(video_path):
"""Load frames locally, send as base64 images to the remote embedding API."""
embedder = _get_model()
frames = _load_frames(video_path)
encoded_frames = [_pil_to_base64(frame) for frame in frames]
# Build conversation compatible with embedding_server.py → process_vision_info → fetch_image
# Use type:"image" with data:image base64 string directly (avoid image_url dict nesting)
image_items = []
for b64 in encoded_frames:
image_items.append({
"type": "image",
"image": f"data:image/jpeg;base64,{b64}",
})
conversation = [
{"role": "system", "content": [{"type": "text", "text": "Represent the user's input."}]},
{"role": "user", "content": image_items},
]
embeddings = embedder.embedding_gen(conversation)
tensor_embeddings = torch.tensor(embeddings, dtype=torch.float32).unsqueeze(0)
return tensor_embeddings
def encode_image(image_path: str):
"""编码文本查询为嵌入向量"""
embedder = _get_model()
image = Image.open(image_path)
encoded_frame = _pil_to_base64(image)
content = {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{encoded_frame}"}}
embeddings = embedder.video_embedding(content=content)
tensor_embeddings = torch.tensor(embeddings, dtype=torch.float32).unsqueeze(0)
return tensor_embeddings
def encode_string_query(query: str):
"""编码文本查询为嵌入向量"""
embedder = _get_model()
message_input = format_model_input(text=query)
embeddings = embedder.embedding_gen(message_input)
tensor_embeddings = torch.tensor(embeddings, dtype=torch.float32).unsqueeze(0)
return tensor_embeddings