项目文件夹

文件
T
saleban olow 563c4b53fc clean up 1
2025-05-27 11:02:35 -05:00

287 行
9.4 KiB
Python

"""
MemvidRetriever - Fast semantic search, QR frame extraction, and context assembly
"""
import json
import logging
from pathlib import Path
from typing import List, Dict, Any, Tuple, Optional
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
from functools import lru_cache
import cv2
from .utils import (
extract_frame, decode_qr, batch_extract_and_decode,
extract_and_decode_cached
)
from .index import IndexManager
from .config import get_default_config
logger = logging.getLogger(__name__)
class MemvidRetriever:
"""Fast retrieval from QR code videos using semantic search"""
def __init__(self, video_file: str, index_file: str,
config: Optional[Dict[str, Any]] = None):
"""
Initialize MemvidRetriever
Args:
video_file: Path to QR code video
index_file: Path to index file
config: Optional configuration
"""
self.video_file = str(Path(video_file).absolute())
self.index_file = str(Path(index_file).absolute())
self.config = config or get_default_config()
# Load index
self.index_manager = IndexManager(self.config)
self.index_manager.load(str(Path(index_file).with_suffix('')))
# Cache for decoded frames
self._frame_cache = {}
self._cache_size = self.config["retrieval"]["cache_size"]
# Verify video file
self._verify_video()
logger.info(f"Initialized retriever with {self.index_manager.get_stats()['total_chunks']} chunks")
def _verify_video(self):
"""Verify video file is accessible"""
cap = cv2.VideoCapture(self.video_file)
if not cap.isOpened():
raise ValueError(f"Cannot open video file: {self.video_file}")
self.total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
self.fps = cap.get(cv2.CAP_PROP_FPS)
cap.release()
logger.info(f"Video has {self.total_frames} frames at {self.fps} fps")
def search(self, query: str, top_k: int = 5) -> List[str]:
"""
Search for relevant chunks using semantic search
Args:
query: Search query
top_k: Number of results to return
Returns:
List of relevant text chunks
"""
start_time = time.time()
# Semantic search in index
search_results = self.index_manager.search(query, top_k)
# Extract unique frame numbers
frame_numbers = list(set(result[2]["frame"] for result in search_results))
# Decode frames in parallel
decoded_frames = self._decode_frames_parallel(frame_numbers)
# Extract text from decoded data
results = []
for chunk_id, distance, metadata in search_results:
frame_num = metadata["frame"]
if frame_num in decoded_frames:
try:
chunk_data = json.loads(decoded_frames[frame_num])
results.append(chunk_data["text"])
except (json.JSONDecodeError, KeyError):
# Fallback to metadata text
results.append(metadata["text"])
else:
# Fallback to metadata text if decode failed
results.append(metadata["text"])
elapsed = time.time() - start_time
logger.info(f"Search completed in {elapsed:.3f}s for query: '{query[:50]}...'")
return results
def get_chunk_by_id(self, chunk_id: int) -> Optional[str]:
"""
Get specific chunk by ID
Args:
chunk_id: Chunk ID
Returns:
Chunk text or None
"""
metadata = self.index_manager.get_chunk_by_id(chunk_id)
if metadata:
frame_num = metadata["frame"]
decoded = self._decode_single_frame(frame_num)
if decoded:
try:
chunk_data = json.loads(decoded)
return chunk_data["text"]
except (json.JSONDecodeError, KeyError):
pass
return metadata["text"]
return None
def _decode_single_frame(self, frame_number: int) -> Optional[str]:
"""Decode single frame with caching"""
# Check cache
if frame_number in self._frame_cache:
return self._frame_cache[frame_number]
# Decode frame
result = extract_and_decode_cached(self.video_file, frame_number)
# Update cache
if result and len(self._frame_cache) < self._cache_size:
self._frame_cache[frame_number] = result
return result
def _decode_frames_parallel(self, frame_numbers: List[int]) -> Dict[int, str]:
"""
Decode multiple frames in parallel
Args:
frame_numbers: List of frame numbers to decode
Returns:
Dict mapping frame number to decoded data
"""
# Check cache first
results = {}
uncached_frames = []
for frame_num in frame_numbers:
if frame_num in self._frame_cache:
results[frame_num] = self._frame_cache[frame_num]
else:
uncached_frames.append(frame_num)
if not uncached_frames:
return results
# Decode uncached frames in parallel
max_workers = self.config["retrieval"]["max_workers"]
decoded = batch_extract_and_decode(
self.video_file,
uncached_frames,
max_workers=max_workers
)
# Update results and cache
for frame_num, data in decoded.items():
results[frame_num] = data
if len(self._frame_cache) < self._cache_size:
self._frame_cache[frame_num] = data
return results
def search_with_metadata(self, query: str, top_k: int = 5) -> List[Dict[str, Any]]:
"""
Search with full metadata
Args:
query: Search query
top_k: Number of results
Returns:
List of result dictionaries with text, score, and metadata
"""
start_time = time.time()
# Semantic search
search_results = self.index_manager.search(query, top_k)
# Extract frame numbers
frame_numbers = list(set(result[2]["frame"] for result in search_results))
# Decode frames
decoded_frames = self._decode_frames_parallel(frame_numbers)
# Build results with metadata
results = []
for chunk_id, distance, metadata in search_results:
frame_num = metadata["frame"]
# Get text from decoded frame or metadata
if frame_num in decoded_frames:
try:
chunk_data = json.loads(decoded_frames[frame_num])
text = chunk_data["text"]
except (json.JSONDecodeError, KeyError):
text = metadata["text"]
else:
text = metadata["text"]
results.append({
"text": text,
"score": 1.0 / (1.0 + distance), # Convert distance to similarity score
"chunk_id": chunk_id,
"frame": frame_num,
"metadata": metadata
})
elapsed = time.time() - start_time
logger.info(f"Search with metadata completed in {elapsed:.3f}s")
return results
def get_context_window(self, chunk_id: int, window_size: int = 2) -> List[str]:
"""
Get chunk with surrounding context
Args:
chunk_id: Central chunk ID
window_size: Number of chunks before/after to include
Returns:
List of chunks in context window
"""
chunks = []
# Get chunks in window
for offset in range(-window_size, window_size + 1):
target_id = chunk_id + offset
chunk = self.get_chunk_by_id(target_id)
if chunk:
chunks.append(chunk)
return chunks
def prefetch_frames(self, frame_numbers: List[int]):
"""
Prefetch frames into cache for faster retrieval
Args:
frame_numbers: List of frame numbers to prefetch
"""
# Only prefetch frames not in cache
to_prefetch = [f for f in frame_numbers if f not in self._frame_cache]
if to_prefetch:
logger.info(f"Prefetching {len(to_prefetch)} frames...")
decoded = self._decode_frames_parallel(to_prefetch)
logger.info(f"Prefetched {len(decoded)} frames")
def clear_cache(self):
"""Clear frame cache"""
self._frame_cache.clear()
extract_and_decode_cached.cache_clear()
logger.info("Cleared frame cache")
def get_stats(self) -> Dict[str, Any]:
"""Get retriever statistics"""
return {
"video_file": self.video_file,
"total_frames": self.total_frames,
"fps": self.fps,
"cache_size": len(self._frame_cache),
"max_cache_size": self._cache_size,
"index_stats": self.index_manager.get_stats()
}