VideoAgent-AX650N / VideoAgent /vidrag_pipeline.py
H022329's picture
Upload folder using huggingface_hub
9cf08e9 verified
Raw
History Blame Contribute Delete
12.8 kB
import os
import sys
import json
import shutil
import asyncio
import multiprocessing
from dataclasses import asdict, dataclass, field
from datetime import datetime
from functools import partial
from typing import Callable, Dict, List, Optional, Type, Union, cast
from transformers import AutoModel, AutoTokenizer
import torch
import queue
from ._storage import (
JsonKVStorage,
NanoVectorDBStorage,
NanoVectorDBVideoSegmentStorage,
# NetworkXStorage,
)
from ._utils import (
always_get_an_event_loop,
logger,
)
from .base import (
# BaseGraphStorage,
BaseKVStorage,
BaseVectorStorage,
StorageNameSpace,
QueryParam,
)
from ._videoutil import(
split_video,
speech_to_text,
segment_caption,
merge_segment_information,
saving_video_segments,
preprocess_video,
retrieved_segment_caption_kw,
)
from .chunk import (
get_chunks,
chunking_by_video_segments,
)
from .query import (
videorag_query,
_result_query_stream,
_extract_keywords_query,
truncate_list_by_token_size,
list_of_list_to_csv,
)
from .prompt import PROMPTS
@dataclass
class VideoRAG:
working_dir: str = field(
default_factory=lambda: f"./videorag_cache_{datetime.now().strftime('%Y-%m-%d-%H:%M:%S')}"
)
# video
threads_for_split: int = 10
video_segment_length: int = 10 # seconds
rough_num_frames_per_segment: int = 5 # frames
video_output_format: str = "mp4"
audio_output_format: str = "wav"
# preprocessing: resize and resample input video before indexing
preprocess_target_width: int = 384
preprocess_target_height: int = 384
preprocess_target_fps: int = 5
video_embedding_batch_num: int = 2
segment_retrieval_top_k: int = 1
video_embedding_dim: int = 2048 # qwen3-vl-embedding θΎ“ε‡Ίη»΄εΊ¦
embedding_batch_num : int = 2
# query
retrieval_topk_chunks: int = 1
query_better_than_threshold: float = 0.2
enable_local: bool = True
enable_naive_rag: bool = True
chunk_token_size: int = 1000
entity_extract_max_gleaning: int = 1
entity_summary_to_max_tokens: int = 500
key_string_value_json_storage_cls: Type[BaseKVStorage] = JsonKVStorage
vector_db_storage_cls: Type[BaseVectorStorage] = NanoVectorDBStorage
vs_vector_db_storage_cls: Type[BaseVectorStorage] = NanoVectorDBVideoSegmentStorage
vector_db_storage_cls_kwargs: dict = field(default_factory=dict)
enable_llm_cache: bool = True
always_create_working_dir: bool = True
def __post_init__(self):
if not os.path.exists(self.working_dir) and self.always_create_working_dir:
logger.info(f"Creating working directory {self.working_dir}")
os.makedirs(self.working_dir)
self.video_path_db = self.key_string_value_json_storage_cls(
namespace="video_path", global_config=asdict(self)
)
self.video_segments = self.key_string_value_json_storage_cls(
namespace="video_segments", global_config=asdict(self)
)
self.text_chunks = self.key_string_value_json_storage_cls(
namespace="text_chunks", global_config=asdict(self)
)
self.chunks_vdb = self.vector_db_storage_cls(
namespace="chunks_vdb", global_config=asdict(self)
)
self.video_segment_feature_vdb = (
self.vs_vector_db_storage_cls(
namespace="video_segment_feature",
global_config=asdict(self),
)
)
def insert_video(self, video_path_list=None):
loop = always_get_an_event_loop()
total = len(video_path_list)
for i, video_path in enumerate(video_path_list, start=1):
# Step0: check the existence
video_name = os.path.basename(video_path).split('.')[0]
logger.info(f"[{i}/{total}] 🎬 Processing: {video_name}")
if video_name in self.video_segments._data:
logger.info(f"Find the video named {os.path.basename(video_path)} in storage and skip it.")
continue
logger.info(f"[{i}/{total}] β”œ Step 1/7: Preprocessing video (resize/resample)...")
video_output_path = preprocess_video(
video_path,
self.preprocess_target_width,
self.preprocess_target_height,
self.preprocess_target_fps,
self.video_output_format,
)
logger.info(f"[{i}/{total}] β”œ Step 2/7: Saving video path metadata...")
loop.run_until_complete(self.video_path_db.upsert(
{video_name: video_output_path}
))
loop.run_until_complete(self.video_path_db.index_done_callback())
logger.info(f"[{i}/{total}] β”œ Step 3/7: Splitting video into segments + extracting audio...")
segment_index2name, segment_times_info = split_video(
video_output_path,
self.working_dir,
self.video_segment_length,
self.rough_num_frames_per_segment,
self.audio_output_format,
)
logger.info(f"[{i}/{total}] β”œ Step 4/7: Speech recognition (ASR)...")
transcripts = speech_to_text(
video_name,
self.working_dir,
segment_index2name,
self.audio_output_format
)
captions = dict()
error_queue = queue.Queue()
logger.info(f"[{i}/{total}] β”œ Step 5/7: Saving video segments to cache...")
saving_video_segments(video_name,
video_output_path,
self.working_dir,
segment_index2name,
segment_times_info,
error_queue,
self.video_output_format,)
logger.info(f"[{i}/{total}] β”œ Step 6/7: Generating captions with VLM...")
segment_caption(
video_name,
video_output_path,
segment_index2name,
transcripts,
segment_times_info,
captions,
error_queue,
)
while not error_queue.empty():
error_message = error_queue.get()
with open('error_log_videorag.txt', 'a', encoding='utf-8') as log_file:
log_file.write(f"Video Name:{video_name} Error processing:\n{error_message}\n\n")
raise RuntimeError(error_message)
logger.info(f"[{i}/{total}] β”œ Step 7/7: Merging segment info & encoding features...")
segments_information = merge_segment_information(
segment_index2name,
segment_times_info,
transcripts,
captions,
)
loop.run_until_complete(self.video_segments.upsert(
{video_name: segments_information}
))
loop.run_until_complete(self.video_segments.index_done_callback())
loop.run_until_complete(self.video_segment_feature_vdb.upsert(
video_name,
segment_index2name,
self.video_output_format,
))
loop.run_until_complete(self.video_segment_feature_vdb.index_done_callback())
video_segment_cache_path = os.path.join(self.working_dir, '_cache', video_name)
if os.path.exists(video_segment_cache_path):
shutil.rmtree(video_segment_cache_path)
# ζ·»εŠ εžƒεœΎε›žζ”Άε’Œε†…ε­˜ζΈ…η†
import gc
if torch.cuda.is_available():
torch.cuda.empty_cache()
gc.collect()
logger.info(f"[{i}/{total}] βœ… Completed: {video_name}")
logger.info("πŸ“ Chunking and building text index for all videos...")
loop.run_until_complete(self.ainsert(self.video_segments._data, asdict(self)))
logger.info("πŸŽ‰ All indexing completed.")
async def ainsert(self, new_video_segment, global_configs):
logger.info(" β†’ Tokenizing and chunking video segments...")
inserting_chunks = get_chunks(
new_videos=new_video_segment,
chunk_func=chunking_by_video_segments,
max_token_size=self.chunk_token_size,
)
logger.info(" β†’ Filtering already-indexed chunks...")
_add_chunk_keys = await self.text_chunks.filter_keys(
list(inserting_chunks.keys())
)
inserting_chunks = {
k: v for k, v in inserting_chunks.items() if k in _add_chunk_keys
}
if not len(inserting_chunks):
logger.warning(f"All chunks are already in the storage")
return
logger.info(f" β†’ Inserting {len(inserting_chunks)} new chunks into vector DB...")
logger.info(" β†’ Encoding and upserting chunks to vector DB...")
await self.chunks_vdb.upsert(inserting_chunks)
await self.chunks_vdb.index_done_callback()
logger.info(" β†’ Saving text chunks to KV store...")
await self.text_chunks.upsert(inserting_chunks)
await self.text_chunks.index_done_callback()
def query(self, query: str, param: QueryParam = QueryParam()):
loop = always_get_an_event_loop()
return loop.run_until_complete(self.aquery(query, param))
async def aquery(self, query: str, param: QueryParam = QueryParam()):
response = await videorag_query(
query,
self.text_chunks,
self.chunks_vdb,
self.video_path_db,
self.video_segments,
self.video_segment_feature_vdb,
param
)
return response
def query_stream(self, query: str, param: QueryParam = QueryParam()):
sys_prompt = self._prepare_query_context(query, param)
for chunk in _result_query_stream(query, sys_prompt):
yield chunk
def _prepare_query_context(self, query: str, param: QueryParam) -> str:
loop = always_get_an_event_loop()
results = loop.run_until_complete(self.chunks_vdb.query(query))
if not len(results):
return PROMPTS["fail_response"]
chunks_ids = [r["id"] for r in results]
chunks = loop.run_until_complete(self.text_chunks.get_by_ids(chunks_ids))
maybe_trun_chunks = truncate_list_by_token_size(
chunks,
key=lambda x: x["content"],
max_token_size=param.naive_max_token_for_text_unit,
)
section = "-----New Chunk-----\n".join([c["content"] for c in maybe_trun_chunks])
retreived_chunk_context = section
segment_results = loop.run_until_complete(
self.video_segment_feature_vdb.query(query)
)
visual_retrieved_segments = set()
if len(segment_results):
for n in segment_results:
visual_retrieved_segments.add(n['__id__'])
retrieved_segments = sorted(
visual_retrieved_segments,
key=lambda x: (
'_'.join(x.split('_')[:-1]),
eval(x.split('_')[-1])
)
)
remain_segments = retrieved_segments
keywords_for_caption = _extract_keywords_query(query)
caption_results = retrieved_segment_caption_kw(
remain_segments,
self.video_path_db,
self.video_segments,
keywords_for_caption,
num_sampled_frames=param.retrieved_num_sampled_frames
)
text_units_section_list = [["video_name", "start_time", "end_time", "content"]]
for s_id in caption_results:
video_name = '_'.join(s_id.split('_')[:-1])
index = s_id.split('_')[-1]
start_time = eval(self.video_segments._data[video_name][index]["time"].split('-')[0])
end_time = eval(self.video_segments._data[video_name][index]["time"].split('-')[1])
start_time = f"{start_time // 3600}:{(start_time % 3600) // 60}:{start_time % 60}"
end_time = f"{end_time // 3600}:{(end_time % 3600) // 60}:{end_time % 60}"
text_units_section_list.append([video_name, start_time, end_time, caption_results[s_id]])
text_units_context = list_of_list_to_csv(text_units_section_list)
retreived_video_context = f"\n-----Retrieved Knowledge From Videos-----\n```csv\n{text_units_context}\n```\n"
sys_prompt_temp = PROMPTS["videorag_response"]
return sys_prompt_temp.format(
video_data=retreived_video_context,
chunk_data=retreived_chunk_context,
)