파트너십
멀티모달 RAG: Twelve Labs와 Pinecone을 활용한 비디오 대화 구현
제임스 러, 매니쉬 마헤슈와리, 알렉스 오웬
개발자는 Twelve Labs의 Embed API와 Pinecone을 활용해 비디오 임베딩을 생성하고, 시맨틱 검색으로 관련 클립을 찾아내며, Pegasus를 통해 비디오 콘텐츠와 대화하는 멀티모달 RAG 시스템을 구축할 수 있습니다. 오픈소스 LLaVA-NeXT-Video 모델과의 직접적인 비교 분석도 함께 제공됩니다.
개발자는 Twelve Labs의 Embed API와 Pinecone을 활용해 비디오 임베딩을 생성하고, 시맨틱 검색으로 관련 클립을 찾아내며, Pegasus를 통해 비디오 콘텐츠와 대화하는 멀티모달 RAG 시스템을 구축할 수 있습니다. 오픈소스 LLaVA-NeXT-Video 모델과의 직접적인 비교 분석도 함께 제공됩니다.

In this article
뉴스레터 구독하기
뉴스레터 구독하기
영상 이해 분야의 최신 기술 업데이트, 튜토리얼 및 인사이트를 받아보세요.
영상 이해 분야의 최신 기술 업데이트, 튜토리얼 및 인사이트를 받아보세요.
AI로 영상을 검색하고, 분석하고, 탐색하세요.
2024. 10. 3.
12분
링크 복사하기
이 튜토리얼을 위해 함께 협업해 준 Pinecone 팀(Adam Heerwagen 및 Cory Waddingham)에 진심으로 감사드립니다.
Introduction (소개)
비디오 대상 RAG 기반 Q&A를 구현하기 위해 Twelve Labs의 Embed API와 Pinecone의 호스팅 벡터 데이터베이스를 통합하는 이번 튜토리얼에 오신 것을 환영합니다. 본 가이드에서는 생성형 모델을 사용해 비정형 비디오 데이터베이스에서 텍스트 답변을 추출하는 방법을 알아봅니다.
Twelve Labs의 풍부한 문맥 이해 임베딩과 Pinecone의 벡터 데이터베이스를 결합하여 비디오 임베딩을 저장, 인덱싱 및 쿼리하는 대화형 애플리케이션을 구축합니다. 이 노트북은 단 몇 줄의 코드만으로 이러한 기술이 구현할 수 있는 현재의 놀라운 가능성들을 명확히 보여줍니다.
또한 비교를 위해, 텍스트 응답을 생성하는 데 Twelve Labs의 Generate API를 사용할 때와 대표적인 오픈소스 모델인 LLaVA-NeXT-Video를 사용할 때의 개발자 경험(DX) 차이도 함께 소개합니다.
설정 및 설치
핵심 기능을 살펴보기 전에, 먼저 환경을 구성하고 필요한 라이브러리를 설치해 보겠습니다.
필수 라이브러리 설치
가장 먼저 Twelve Labs와 Pinecone용 라이브러리를 설치합니다. 노트북 셀에서 다음 명령어를 실행하세요.
# Install required libraries !pip install twelvelabs pinecone-client
그 다음, 비디오 포맷 변환을 위한 PyAV, 그리고 Hugging Face에서 오픈소스 모델을 사용하기 위해 bitsandbytes 및 transformers를 설치합니다.
!pip install -q av !pip install --upgrade -q accelerate bitsandbytes !pip install transformers
인증
Twelve Labs API와 Pinecone 사용을 위한 키를 설정해야 합니다. 이 키들을 안전하게 보관하기 위해 Google Colab의 내장 `userdata` 라이브러리를 활용하겠습니다. 가입 후 Pinecone 콘솔에서 필요한 정보를 바로 확인할 수 있으며, 이번 데모를 진행하기에 충분한 무료 Starter 티어를 제공합니다.
Twelve Labs API 키는 https://playground.twelvelabs.io 가입 후 계정 내에서 확인하실 수 있습니다.
from google.colab import userdata TL_API_KEY=userdata.get('TL_API_KEY') PINECONE_API_KEY=userdata.get('PINECONE_API_KEY')
비디오 데이터 설정
이제 임베딩할 비디오 데이터를 준비해야 합니다. 이 링크를 통해 구글 드라이브 폴더에서 비디오 데이터를 확인할 수 있습니다. 이 데이터를 구글 드라이브 루트 폴더의 "TwelveLabs-Pinecone" 폴더에 복사해 주세요. 다음 셀을 실행해 드라이브를 마운트하고 노트북에서 비디오 파일에 접근할 수 있도록 설정하겠습니다.
from google.colab import drive drive.mount('/content/drive') base_folder_path = "/content/drive/MyDrive/TwelveLabs-Pinecone" single_video = base_folder_path + "/ad_vids/Rare Beauty By Selena Gomez - Makeup Made To Feel Good In.mp4" split_video_dir = base_folder_path + "/split_ad_videos"
클라이언트 초기화
이제 Pinecone과 Twelve Labs 설정을 구성할 차례입니다. 두 서비스의 라이브러리를 임포트하고 각각의 API 키를 사용해 클라이언트를 초기화합니다.
# Configure Pinecone from pinecone import Pinecone, ServerlessSpec pc = Pinecone(api_key=PINECONE_API_KEY) from twelvelabs import TwelveLabs from twelvelabs.models.embed import EmbeddingsTask # Initialize the Twelve Labs client twelvelabs_client = TwelveLabs(api_key=TL_API_KEY)
임베딩 생성 및 Pinecone에 데이터 수집(Ingestion)
아래 코드 블록은 Twelve Labs API와 Pinecone 벡터 데이터베이스를 활용하여 비디오 임베딩을 생성하고 저장하는 프로세스를 보여줍니다. 여기서는 두 가지 핵심 함수를 정의합니다.
generate_embedding함수는 임베딩 태스크의 생성 및 관리를 담당합니다.지정된 비디오 파일과 엔진 정보를 토대로 Twelve Labs API를 사용하여 임베딩 태스크를 생성합니다.
태스크 진행 상황을 모니터링하기 위한 콜백 함수를 정의합니다.
작업이 완료될 때까지 대기한 후 결과 데이터를 수집합니다.
최종적으로 태스크 결과물로부터 메타데이터(시간 범위 및 스코프)가 포함된 임베딩 값을 추출합니다.
ingest_data함수는 데이터 수집을 수행하는 메인 함수입니다.generate_embedding을 호출하여 지정한 비디오 파일의 임베딩을 가져옵니다.수집 타겟인 Pinecone 인덱스(이 예시에서는
twelve-labs)에 연결합니다.임베딩 데이터와 메타데이터 형식을 맞추어 업서트(Upsert)할 벡터를 준비합니다.
준비된 벡터들을 Pinecone 인덱스에 최종 업서트합니다.
코드를 실행하는 동안 임베딩 태스크 처리에 따른 진행 상황 업데이트를 확인할 수 있으며, 마지막에 몇 개의 임베딩이 Pinecone에 수집되었는지 성공 메시지가 출력됩니다. 이를 통해 추후 해당 임베딩을 사용하여 비디오 콘텐츠를 검색하고 분석할 수 있는 기반이 마련됩니다.
# Define a callback function to monitor task progress def on_task_update(task: EmbeddingsTask): print(f" Status={task.status}") def generate_embedding(video_file): # Create an embedding task task = twelvelabs_client.embed.task.create( engine_name="Marengo-retrieval-2.6", video_file=video_file ) print(f"Created task: id={task.id} engine_name={task.engine_name} status={task.status}") # Wait for the task to complete status = task.wait_for_done( sleep_interval=2, callback=on_task_update ) print(f"Embedding done: {status}") # Retrieve the task result task_result = twelvelabs_client.embed.task.retrieve(task.id) # Extract and return the embeddings embeddings = [] for v in task_result.video_embeddings: embeddings.append({ 'embedding': v.embedding.float, 'start_offset_sec': v.start_offset_sec, 'end_offset_sec': v.end_offset_sec, 'embedding_scope': v.embedding_scope }) return embeddings, task_result def ingest_data(video_file_path, index_name = "twelve-labs"): """ Generate embeddings for video and store in Pinecone """ #Strip the extension and the folders from the video_file_path video_name = os.path.splitext(os.path.basename(video_file_path))[0] print(video_name) # Connect to Pinecone index if index_name not in pc.list_indexes().names(): pc.create_index( name=index_name, dimension=1024, # The dimensions of Twelve Lab's Embedding Model metric="cosine", spec=ServerlessSpec( cloud="aws", region="us-east-1" ) ) index = pc.Index(index_name) # Generate embeddings using Twelve Labs Embed API embeddings, task_result = generate_embedding(video_file_path) # Prepare vectors for upsert vectors_to_upsert = [] for i, emb in enumerate(embeddings): vector_id = f"{video_name}_{i}" vectors_to_upsert.append((vector_id, emb['embedding'], { 'video_file': video_name, 'video_segment': i, 'start_time': emb['start_offset_sec'], 'end_time': emb['end_offset_sec'], 'scope': emb['embedding_scope'] })) # Upsert embeddings to Pinecone index.upsert(vectors=vectors_to_upsert) return f"Ingested {len(embeddings)} embeddings for {video_file_path}"
이제 이 두 함수를 사용해 비디오 임베딩을 Pinecone에 로드해 보겠습니다.
# Example usage result = ingest_data(single_video) print(result)
이 코드를 통해 Twelve Labs의 Embed API를 사용하여 영상의 멀티모달 임베딩을 생성하고, 나중에 필요할 때 검색할 수 있도록 Pinecone 벡터 데이터베이스에 저장할 수 있습니다. 이 임베딩은 시각, 청각 및 텍스트 정보를 포함하여 비디오 콘텐츠의 다양한 측면을 완벽히 포착하므로 광범위한 AI 애플리케이션에 매우 유용합니다.
텍스트 쿼리로 검색하기
다음으로 Twelve Labs의 Marengo 모델을 통해 텍스트를 임베딩하고, Pinecone 데이터베이스에서 유사한 콘텐츠를 찾는 함수를 정의하겠습니다.
get_text_embedding함수는 Twelve Labs Embed API를 사용해 텍스트 쿼리를 임베딩 벡터로 변환합니다.twelvelabs_client.embed.create메서드를 사용하여 텍스트에 대한 임베딩을 생성합니다.engine_name파라미터는 사용할 임베딩 모델("Marengo-retrieval-2.6")을 지정합니다.text_truncate파라미터는 "start"로 설정되며, 이는 텍스트 길이가 너무 길 경우 시작 부분부터 자르게 됨을 의미합니다.
retrieve_similar_content함수는 실질적인 검색을 수행하는 메인 함수입니다.입력 파라미터로 텍스트 쿼리와 반환할 결과 수(
top_k)를 전달받습니다.get_text_embedding을 호출하여 텍스트 쿼리를 임베딩 데이터로 변환합니다.twelve-labs라는 명칭의 Pinecone 인덱스에 연결합니다.쿼리 임베딩과 가장 유사한 벡터를 인덱스에서 검색하여, 메타데이터와 함께 반환할 결과 개수만큼 탐색합니다.
콘텐츠 검색은 텍스트 쿼리의 임베딩과 Pinecone에 미리 저장되어 있는 비디오 세그먼트의 임베딩 간의 유사도를 비교하는 방식으로 동작합니다. 이를 통해 대규모 비디오 데이터셋 전체를 빠르고 효율적으로 검색할 수 있습니다.
def get_text_embedding(text_query): # Twelve Labs Embed API supports text-to-embedding text_embedding = twelvelabs_client.embed.create( engine_name="Marengo-retrieval-2.6", text=text_query, text_truncate="start" ) return text_embedding.text_embedding.float def retrieve_similar_content(query, index_name="twelve-labs", top_k=5): """ Retrieve similar content based on query embedding """ # Generate query embedding query_embedding = get_text_embedding(query) # Connect to Pinecone index index = pc.Index(index_name) # Query Pinecone for similar vectors results = index.query(vector=query_embedding, top_k=top_k, include_metadata=True) return results
이제 샘플 텍스트 쿼리를 인자로 하여 retrieve_similar_content 함수를 호출하고, 검색해 낸 가장 유사한 콘텐츠의 자세한 정보들을 화면에 출력해 보겠습니다.
# Example usage text_query = "Lipstick" similar_content = retrieve_similar_content(text_query) print(f"Query: '{text_query}'") print(f"Top {len(similar_content['matches'])} similar content:") for i, match in enumerate(similar_content['matches']): print(f"{i+1}. Score: {match['score']:.4f}") print(f" Video File: {match['metadata']['video_file']}") print(f" Video ID: {match['metadata']['video_segment']}") print(f" Time range: {match['metadata']['start_time']} - {match['metadata']['end_time']} seconds") print(f" Scope: {match['metadata']['scope']}") print()
이 코드를 통해 비디오 콘텐츠상에서 텍스트 쿼리를 기반으로 의미 분석을 수행하는 시맨틱 검색(Semantic Search)이 가능해집니다. Twelve Labs의 강력한 멀티모달 임베딩 성능 덕분에 사용자가 검색한 정확한 단어가 비디오 내에 포함되어 있지 않더라도, 의미상 밀접하게 매칭되는 비디오 구간을 정확하게 찾아낼 수 있습니다.
이 코드를 실행하면 유사도 점수, 비디오 파일명, 비디오 ID, 해당 타임스탬프 구간, 임베딩 스코프를 포함한 검색 매칭 순위가 함께 노출됩니다. 이를 통해 고도화된 비디오 검색, 개인화 추천 시스템 등 다양한 형태의 서비스로 응용 및 확장할 수 있습니다.
비디오 포맷 제어
벡터 데이터베이스에 임베딩을 저장하고 검색 준비를 마친 단계에서, 우리의 첫 번째 실험은 전체 긴 비디오 단위가 아닌 세부 구간 클립 단위로 임베딩을 구성하여 쿼리와 분석 모델을 연동하는 것입니다. 임베딩 모델의 타임스탬프 처리 방식에 맞춰 비디오 파일을 여러 조각의 세그먼트로 나누어 보겠습니다.
아래 정의된 split_video 함수는 `av` 라이브러리를 이용하여 하나의 비디오를 지정된 시간 단위로 쪼개는 역할을 담당합니다. 처리 원리는 다음과 같습니다.
함수의 인자로 입력 비디오 경로, 저장 폴더, 세그먼트 생성 단위 시간(기본값: 6초)을 받습니다.
입력 영상을 연 다음, 해당 영상의 초당 프레임 수(FPS)를 기준으로 한 세그먼트에 들어갈 프레임 개수를 계측하고 영상 프레임을 순회합니다.
각 세그먼트 주기에 도달할 때마다 새로운 출력 파일(MP4) 컨테이너를 생성한 후 프레임 데이터를 복사하고 타임스탬프 값을 보정합니다.
조각난 결과 파일들은 일련번호가 포함된 별도의 MP4 형식으로 지정된 서브 폴더 안에 저장됩니다.
import av def split_video(input_path, output_dir, segment_duration=6): # Ensure output directory exists os.makedirs(output_dir, exist_ok=True) input_file_name = os.path.splitext(os.path.basename(input_path))[0] print(input_file_name) with av.open(input_path) as input_container: # Get video stream input_stream = input_container.streams.video[0] fps = input_stream.average_rate # Calculate how many frames are in each segment frames_per_segment = int(segment_duration * fps) segment_count = 0 frame_count = 0 output_container = None output_stream = None first_frame_timestamp = None for frame in input_container.decode(video=0): if frame_count % frames_per_segment == 0: # Close previous output container if it exists if output_container: output_container.close() # Create a new output container output_path = os.path.join(output_dir, f'{input_file_name}_segment_{segment_count:03d}.mp4') segment_count += 1 output_container = av.open(output_path, mode='w') output_stream = output_container.add_stream('h264', rate=fps) output_stream.width = frame.width output_stream.height = frame.height output_stream.pix_fmt = 'yuv420p' # Reset the first frame timestamp for the new segment first_frame_timestamp = frame.pts # Adjust the frame timestamp frame.pts -= first_frame_timestamp # Encode frame packet = output_stream.encode(frame) output_container.mux(packet) frame_count += 1 # Flush the encoder packet = output_stream.encode(None) output_container.mux(packet) # Close the last output container if output_container: output_container.close() split_video(input_path=single_video, output_dir=split_video_dir)
쿼리 준비하기
이제 생성형 모델과 상호작용하기 위한 모든 준비가 끝났습니다. 질문(Query)을 정의하고 데이터베이스로부터 관련된 콘텐츠를 검색해 오겠습니다.
query = "What is this advertisement selling?" similar_content = retrieve_similar_content(query)
Pegasus를 활용하여 비디오 영상 클립과 대화하기
Pegasus-1 모델을 성공적으로 구동하는 과정은 크게 세 기둥으로 구분할 수 있습니다.
Twelve Labs상에 업로드 비디오를 관리 및 매핑하기 위한 고유 인덱스 설정 — 이 단계는 서비스당 최초 한 번만 수행하면 충분합니다.
Twelve Labs 플랫폼에 비디오 파일 자체 업로드하기 — 비디오 파일당 한 번만 필요합니다.
최종 확인하고픈 질문(프롬프트)과 대상 비디오를 기반으로 Pegasus에 직접 쿼리하기
먼저 아래 코드로 플랫폼 내에 인덱스를 생성해 줍니다.
engines = [ { "name": "pegasus1.1", "options": ["visual", "conversation"] } ] index_name = "ads_index" indices_list = twelvelabs_client.index.list(name=index_name) if len(indices_list) == 0: index = twelvelabs_client.index.create( name=index_name, engines=engines, ) print(f"A new index has been created: id={index.id} name={index.name} engines={index.engines}") else: index = indices_list[0] print(f"Index already exists: id={index.id} name={index.name} engines={index.engines}")
이어서 원활한 연동을 위한 비디오 파일 업로드 유틸리티 로직을 구성합니다.
def upload_video_to_twelve_labs(video_path): task = twelvelabs_client.task.create( index_id=index.id, file = video_path ) print(f"Task created: id={task.id} status={task.status}") task.wait_for_done(sleep_interval=5, callback=on_task_update) if task.status != "ready": raise RuntimeError(f"Indexing failed with status {task.status}") print(f"The unique identifier of your video is {task.video_id}.") return task.video_id
이제 잘게 쪼갠 세그먼트 영상 디렉터리를 탐색하며, 루프를 통해 파일을 하나씩 Twelve Labs 상의 생성된 인덱스로 업로드합니다.
video_ids = {} for split_video_filename in os.listdir(split_video_dir): split_video_path = os.path.join(split_video_dir, split_video_filename) print(split_video_path) split_video_name = split_video_filename.split('.')[0] print(split_video_name) video_id = upload_video_to_twelve_labs(split_video_path) video_ids[split_video_name] = video_id print(video_ids)
Pegasus API 호출
이제 사전에 구현한 검색 결과값을 토대로 도출한 실제 동영상 클립 정보를 바인딩하고 간단히 쿼리를 전송하기만 하면 됩니다.
# retrieve the correct video_id for the relevant video video_segment = (int) (similar_content['matches'][0]['metadata']['video_segment']) print(f"Retrieved video segment: {video_segment}") base_filename = os.path.splitext(os.path.basename(single_video))[0] video_key = f"{base_filename}_segment_{video_segment:03d}" video_id = video_ids[video_key] res = twelvelabs_client.generate.text( video_id=video_id, prompt=query ) print(f"{res.data}")
오픈소스 LLaVA-NeXT-Video 모델 적용
대조군으로 오픈소스 모델을 활용하기 위해서는 다음과 같은 작업을 추가로 처리해야 합니다.
동영상을 NumPy 다차원 배열 형식으로 파싱하고 가공하기
모델 추론에 전송할 핵심 프레임 부분집합(Subset)을 균등 샘플링하기
사용자 GPU 자원에 필요한 오픈소스 라이브러리 및 가중치를 직접 다운로드하고 호스팅하기
사용 모델 고유의 전처리 포맷 처리 및 개별 질의 관리 수행하기
동영상을 NumPy 포맷으로 파싱
이 아래 기술된 read_video_pyav() 유틸리티는 PyAV 디코더를 활용하여 비디오의 특정 지정된 프레임셋만 선별 디코딩하는 함수입니다.
import av import numpy as np def read_video_pyav(container, indices): ''' Decode the video with PyAV decoder. Args: container (av.container.input.InputContainer): PyAV container. indices (List[int]): List of frame indices to decode. Returns: np.ndarray: np array of decoded frames of shape (num_frames, height, width, 3). ''' frames = [] container.seek(0) start_index = indices[0] end_index = indices[-1] for i, frame in enumerate(container.decode(video=0)): if i > end_index: break if i >= start_index and i in indices: frames.append(frame)
비디오 프레임 샘플링
아래 코드 구조는 다음과 같이 작동합니다.
get_total_frames(): 비디오 파일에 포함된 전체 원본 물리적 프레임의 총 개수를 반환합니다. 일부 균등 분할 계산 예외로 결과가 0으로 도출되는 것을 방지합니다.sample_video(): 전체 비디오 범위에 걸쳐 균등한 간격으로 특정된 가시 샘플 개수만큼 대표 프레임을 추출합니다.process_videos_in_folder(): 타겟 디렉터리 내의 동영상을 돌며 이 샘플링 로직을 통과시켜 연관 캐시 배열 정보를 빌드합니다.
def get_total_frames(video_path): """ Manually count the total number of frames in a video. Used in case uniformly sampling comes up as 0 frames. """ container = av.open(video_path) video_stream = container.streams.video[0] total_frames = 0 for frame in container.decode(video_stream): total_frames += 1 return total_frames def sample_video(video_path, num_samples=8): container = av.open(video_path) video_stream = container.streams.video[0] # sample uniformly num_samples frames from the video total_frames = container.streams.video[0].frames if total_frames == 0: total_frames = get_total_frames(video_path) indices = np.arange(0, total_frames, total_frames / num_samples).astype(int) sampled_frames = read_video_pyav(container, indices) return sampled_frames def process_videos_in_folder(folder_path): sample_info = {} # Supported video file extensions video_extensions = ('.mp4', '.avi', '.mov', '.mkv') for filename in os.listdir(folder_path): simple_video_name = os.path.splitext(os.path.basename(filename))[0] if filename.lower().endswith(video_extensions): video_path = os.path.join(folder_path, filename) try: print("Sampling " + video_path) sampled_clip = sample_video(video_path) sample_info[simple_video_name] = {"sampled_video": sampled_clip, "video_path" : video_path} except Exception as e: print(f"Error processing {filename}: {str(e)}") return sample_info sampled_video_info = process_videos_in_folder(split_video_dir)
모델 로딩
여기서는 Hugging Face의 Transformers 라이브러리를 통해 오픈소스 모델인 LLaVA-NeXT-Video와 프로세서를 불러옵니다. 불필요한 GPU 메모리 소모를 극적으로 줄이기 위해 4비트 양자화 방식을 활용해 적재합니다.
from transformers import BitsAndBytesConfig, LlavaNextVideoForConditionalGeneration, LlavaNextVideoProcessor import torch quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16 ) processor = LlavaNextVideoProcessor.from_pretrained("llava-hf/LLaVA-NeXT-Video-7B-hf") model = LlavaNextVideoForConditionalGeneration.from_pretrained( "llava-hf/LLaVA-NeXT-Video-7B-hf", quantization_config=quantization_config, device_map='auto' )
모델 대상 질의 전송
Pinecone 데이터베이스의 유사 이미지 클립 반환 결과에 포함된 물리 오프셋 값을 역계산해 조각난 소스 세그먼트 파일명을 파싱해낼 유틸리티 기능입니다. video_segment_name_from_offset() 함수는 비디오 경로와 시작 시각 값을 받아 규칙화된 파일 포맷 명칭을 반환합니다.
def video_segment_name_from_offset(video_path, start_time, segment_length = 6): segment_number = int (start_time // segment_length) simple_video_name = os.path.splitext(os.path.basename(video_path))[0] return f"{simple_video_name}_segment_{segment_number:03d}"
이제 파싱된 클립을 가져온 뒤 모델에 던질 형식을 생성합니다. 순서에 맞춘 사용자와 텍스트-비디오 모달리티 대화 템플릿(Conversation structure) 형태로 입력을 구성하고, 전용 텐서 디큐잉 및 빌딩 단계를 거쳐 지시 추론을 수행하게 됩니다.
video_segment = similar_content['matches'][0]['metadata']['video_file'] print(video_segment) video_offset = similar_content['matches'][0]['metadata']['start_time'] video_segment_name = video_segment_name_from_offset(video_segment, video_offset) video_segment = sampled_video_info[video_segment_name]['sampled_video'] # Each "content" is a list of dicts and you can add image/video/text modalities conversation = [ { "role": "user", "content": [ {"type": "text", "text": query}, {"type": "video"}, ], }, ] prompt = processor.apply_chat_template(conversation, add_generation_prompt=True) prompt_len = len(prompt) inputs = processor([prompt], videos=[video_segment], padding=True, return_tensors="pt").to(model.device) generate_kwargs = {"max_new_tokens": 100, "do_sample": True, "top_p": 0.9} output = model.generate(**inputs, **generate_kwargs) generated_text = processor.batch_decode(output, skip_special_tokens=True) print(generated_text[0])
비교 평가
두 모델에서 뽑아낸 결과를 직접 비교해 보면, LLaVA-NeXT-Video와 비교해 Pegasus가 제공하는 답변이 타임 범위 및 상황 맥락적으로 훨씬 우수하고 정보 밀도가 높음을 확인할 수 있습니다.
다만, 데이터가 쪼개진 탓에 두 모델 모두 단편 조각 클립만 입력으로 받아서는 전체 비디오 내 종합 맥락을 관찰하는 데 다소 제약이 있습니다. 이제 두 방식에 완전한 크기의 비디오 전량을 제공하는 경우 어떻게 다른지 비교해 보겠습니다.
여러 비디오 다루기
구조화되지 않은 상태의 완전한 영상들이 디렉터리에 놓여 있는 환경에서 이 아카이브 전체에 결친 고차원 질의 연동 예시입니다. 이전 시점에는 세그먼테이션된 조각 데이터셋으로 질의 유사 클립을 정렬했다면, 이번에는 해당 클립의 원천이 되는 완전한 무삭제 원본 비디오 파일 자체를 모델에 피딩할 예정입니다. Pinecone 벡터 내 세그먼트 메타데이터에 파일 상위 계층 구조가 저장되어 있기에 손쉽게 매핑할 수 있습니다.
일단 타겟 영상을 Pinecone에 일괄 분석·인덱싱 처리하겠습니다.
ads_dir = os.path.join(base_folder_path,"ad_vids") video_list = [] #Make sure that we' don't waste time re-embedding the original single video: single_video_filename = os.path.splitext(os.path.basename(single_video))[0] for filename in os.listdir(ads_dir): if filename.endswith(".mp4") and single_video_filename not in filename: video_list.append(ads_dir + "/" + filename) print(video_list) for video in video_list: ingest_data(video)
이어서 데이터베이스에 전질의하여, 의미상 가장 밀접도가 높은 영상 후보군을 가려내기 위한 샘플 질문을 몇 개 작성해 보겠습니다.
full_database_questions = ["Who is the actor in the Miss Dior video?", "What ad is Selena Gomez in?", "What is the ad for Rare Beauty about?", "Why should people buy the Rare Beauty product according to their ad?"] question = full_database_questions[0] similar_content_from_question = retrieve_similar_content(question) video_name = similar_content_from_question['matches'][0]['metadata']['video_file']
Pegasus 구현
파이썬 SDK 연동 가이드를 따라 Pegasus 모델상에서 질의를 이어나가기 위한 절차를 살펴보겠습니다. 이미 인덱스는 앞에서 한 차례 가설했기에, 이번에는 쿼리에 매핑할 전체 영상만을 태스크 업로드하면 준비 작업이 완성됩니다.
Twelve Labs 측으로 원본 영상 일괄 업로드
디렉터리에 존재하는 여러 동영상 파일을 반복 순회하여 태스크를 요청하고, 식별 식별자인 비디오 ID들을 고유 변수에 매핑합니다.
for vid in os.listdir(ads_dir): vid_path = os.path.join(ads_dir, vid) vid_name = os.path.splitext(os.path.basename(vid_path))[0] print(vid_path) video_id = upload_video_to_twelve_labs(vid_path) video_ids[vid_name] = video_id
전체 비디오 ID 구조 기반으로 Pegasus 질의 실행
결과적으로 검색된 메타데이터 비디오 ID 고유 식별 명칭에 직접 질의 지시문(Prompt)을 투사하여 텍스트 응답을 정밀 획득해 내는 구문입니다.
video_id = video_ids[video_name] res = twelvelabs_client.generate.text( video_id=video_id, prompt=question ) print(f"{res.data}")
오픈소스 LLaVA-NeXT-Video 기반 질의 수행
통합 비디오 샘플링
가용한 원본 비디오 파일들 전부를 순차 샘플링 배열로 포맷 처리한 후, 사전에 추려 진 고유 명칭과 일치하는 임베딩 배열을 식별해 맵핑해야 합니다.
sampled_database_video_info = process_videos_in_folder(ads_dir) video_segment = sampled_database_video_info[video_name]['sampled_video']
추론 런타임 가동
이제 로컬 또는 탑재한 추론 엔진으로 해당 데이터 샘플상에 모델을 순회합니다.
사용자 정보 역할군 정의 및 질의(Question)와 대표 비디오 배열 정보를 대화 포맷으로 매핑합니다.
해당 모델 인터페이스 템플릿으로 변환 후, 입출력 디코드 시 조건 인자(`max_new_tokens`, `do_sample`, `top_p`) 세트를 토대로 연계를 시도합니다.
최종 디코딩 단계를 거쳐 추출한 프롬프트 완성문을 로깅 모듈로 터미널 화면에 노출합니다.
conversation = [ { "role": "user", "content": [ {"type": "text", "text": question}, {"type": "video"}, ], }, ] prompt = processor.apply_chat_template(conversation, add_generation_prompt=True) prompt_len = len(prompt) inputs = processor([prompt], videos=[video_segment], padding=True, return_tensors="pt").to(model.device) generate_kwargs = {"max_new_tokens": 100, "do_sample": True, "top_p": 0.9} output = model.generate(**inputs, **generate_kwargs) generated_text = processor.batch_decode(output, skip_special_tokens=True) print(generated_text[0])
비교 평가
두 가지 결과 파일을 직접 수급해 확인해 보면, Pegasus는 디올(Miss Dior) 광고 영상 맥락 속에 등장한 배우 "나탈리 포트만"의 존재를 명확히 판별하고 정확하게 대답합니다. 대조적으로 로컬 LLaVA-NeXT-Video 구조에서는 나탈리 포트만의 등장 유무 자체를 이해하지 못하거나 프레임 샘플 해상도 단계에서 정보를 놓쳐 식별에 실패합니다. 또한 질문에 명확히 응답하지 못하고 엉뚱한 설명을 장황하게 늘어놓는 경향(Hallucination)이 있으며, 응답 지연 시간(Latency)이 크게 길어져 실제 프로덕션 서비스에 바로 투입하기에는 한계가 있음을 알 수 있습니다.
결론 (Conclusion)
본 튜토리얼 아티클에서는 개별 영상 파일부터 가용할 수 있는 완전한 영상 데이터셋 군집 도메인 전체에 이르기까지 비디오와 유기적으로 대화하는 방법을 다루었습니다. 데이터셋 인덱싱 정렬 및 유사 클립 수급 통제 장치로서 Twelve Labs Embed API와 Pinecone 벡터 엔진을 융합하는 방법을 완벽히 이해하는 데 기여했습니다.
동시에 Twelve Labs 고유의 Pegasus 모델과 범용 비디오 멀티모달 오픈소스 진영인 LLaVA-NeXT-Video 간의 필수 인프라 비용 부담, 개발 엔지니어링 활용성(DX), 세부 정밀 탐색 신뢰도를 다각도로 비교해 보았습니다. Pegasus는 복잡한 하부 인프라 운영 부담 없이 지시 프롬프트 수행 부문에서 확실하게 정밀하고 깔끔한 사용 환경 메커니즘을 제공해 줌을 보여줍니다.
주요 모범 사례 (Best Practices)
안정적인 엔터프라이즈 환경 구축이 주요 목적인 경우, 전용 호스트 활용 측면에서 Pinecone의 Pod 기반 서비스 구조를 채택하는 것을 권장합니다.
오픈소스 탑재 시 샘플링 프레임의 크기를 무조건 확대하기보다 추론 연산 처리 정밀성 및 서비스 지연 속도 사이의 효율적 절충안(Trade-off) 지점을 세심히 고려해야 합니다.
로컬 운영이 필수적이라면 양자화 기법을 적극 접목해 속도 부하를 완화하는 것이 좋습니다. 사내 램 소모 사양 및 대답 완성 완성도를 모니링하면서 비즈니스 상황에 최적화된 선택을 하시길 바랍니다.
다음 도전 과제 (Next Steps)
수십만 건 이상의 방대한 영상 데이터 아카이브를 핸들링할 때 탐색 품질이 떨어질 수 있습니다. 이를 우회하고 정합성을 정화할 수 있는 솔루션은 다음과 같습니다.
임베딩 결과 레이어 최상단 영역에 단순 선형 보정기(Linear adapter)를 수립 보완하여 자사 고유 형태 도메인 데이터셋 규격에 정합합니다.
다양한 동영상에서 반환된 후보군이 있을 경우, 2차 단에서 라이트하게 Pegasus 추론을 돌려 후보 영상들의 타당성 우선순위를 재정렬(Re-ranking)합니다.
Pinecone 항목의 메타데이터 측면에 비디오 전 구간의 자동 생성된 요약문 데이터셋을 복합 구축하여 하이브리드 서치 방식을 통해 의미적 탐색 범주를 비약적으로 개선합니다. 자세한 사항은 Pinecone 메타데이터 연동 문서를 참조해주시기 바랍니다.
부록 (Appendix)
심화 학습과 직접 실습을 위한 유용한 참고 자료 모음입니다.
구현 소스 코드가 포함된 Colab Notebook 주소
세부 활용 Twelve Labs 가이드 문서
Pinecone 공식 통합 라이브러리 클라이언트 SDK 문서 및 클라우드 백서
이 튜토리얼을 위해 함께 협업해 준 Pinecone 팀(Adam Heerwagen 및 Cory Waddingham)에 진심으로 감사드립니다.
Introduction (소개)
비디오 대상 RAG 기반 Q&A를 구현하기 위해 Twelve Labs의 Embed API와 Pinecone의 호스팅 벡터 데이터베이스를 통합하는 이번 튜토리얼에 오신 것을 환영합니다. 본 가이드에서는 생성형 모델을 사용해 비정형 비디오 데이터베이스에서 텍스트 답변을 추출하는 방법을 알아봅니다.
Twelve Labs의 풍부한 문맥 이해 임베딩과 Pinecone의 벡터 데이터베이스를 결합하여 비디오 임베딩을 저장, 인덱싱 및 쿼리하는 대화형 애플리케이션을 구축합니다. 이 노트북은 단 몇 줄의 코드만으로 이러한 기술이 구현할 수 있는 현재의 놀라운 가능성들을 명확히 보여줍니다.
또한 비교를 위해, 텍스트 응답을 생성하는 데 Twelve Labs의 Generate API를 사용할 때와 대표적인 오픈소스 모델인 LLaVA-NeXT-Video를 사용할 때의 개발자 경험(DX) 차이도 함께 소개합니다.
설정 및 설치
핵심 기능을 살펴보기 전에, 먼저 환경을 구성하고 필요한 라이브러리를 설치해 보겠습니다.
필수 라이브러리 설치
가장 먼저 Twelve Labs와 Pinecone용 라이브러리를 설치합니다. 노트북 셀에서 다음 명령어를 실행하세요.
# Install required libraries !pip install twelvelabs pinecone-client
그 다음, 비디오 포맷 변환을 위한 PyAV, 그리고 Hugging Face에서 오픈소스 모델을 사용하기 위해 bitsandbytes 및 transformers를 설치합니다.
!pip install -q av !pip install --upgrade -q accelerate bitsandbytes !pip install transformers
인증
Twelve Labs API와 Pinecone 사용을 위한 키를 설정해야 합니다. 이 키들을 안전하게 보관하기 위해 Google Colab의 내장 `userdata` 라이브러리를 활용하겠습니다. 가입 후 Pinecone 콘솔에서 필요한 정보를 바로 확인할 수 있으며, 이번 데모를 진행하기에 충분한 무료 Starter 티어를 제공합니다.
Twelve Labs API 키는 https://playground.twelvelabs.io 가입 후 계정 내에서 확인하실 수 있습니다.
from google.colab import userdata TL_API_KEY=userdata.get('TL_API_KEY') PINECONE_API_KEY=userdata.get('PINECONE_API_KEY')
비디오 데이터 설정
이제 임베딩할 비디오 데이터를 준비해야 합니다. 이 링크를 통해 구글 드라이브 폴더에서 비디오 데이터를 확인할 수 있습니다. 이 데이터를 구글 드라이브 루트 폴더의 "TwelveLabs-Pinecone" 폴더에 복사해 주세요. 다음 셀을 실행해 드라이브를 마운트하고 노트북에서 비디오 파일에 접근할 수 있도록 설정하겠습니다.
from google.colab import drive drive.mount('/content/drive') base_folder_path = "/content/drive/MyDrive/TwelveLabs-Pinecone" single_video = base_folder_path + "/ad_vids/Rare Beauty By Selena Gomez - Makeup Made To Feel Good In.mp4" split_video_dir = base_folder_path + "/split_ad_videos"
클라이언트 초기화
이제 Pinecone과 Twelve Labs 설정을 구성할 차례입니다. 두 서비스의 라이브러리를 임포트하고 각각의 API 키를 사용해 클라이언트를 초기화합니다.
# Configure Pinecone from pinecone import Pinecone, ServerlessSpec pc = Pinecone(api_key=PINECONE_API_KEY) from twelvelabs import TwelveLabs from twelvelabs.models.embed import EmbeddingsTask # Initialize the Twelve Labs client twelvelabs_client = TwelveLabs(api_key=TL_API_KEY)
임베딩 생성 및 Pinecone에 데이터 수집(Ingestion)
아래 코드 블록은 Twelve Labs API와 Pinecone 벡터 데이터베이스를 활용하여 비디오 임베딩을 생성하고 저장하는 프로세스를 보여줍니다. 여기서는 두 가지 핵심 함수를 정의합니다.
generate_embedding함수는 임베딩 태스크의 생성 및 관리를 담당합니다.지정된 비디오 파일과 엔진 정보를 토대로 Twelve Labs API를 사용하여 임베딩 태스크를 생성합니다.
태스크 진행 상황을 모니터링하기 위한 콜백 함수를 정의합니다.
작업이 완료될 때까지 대기한 후 결과 데이터를 수집합니다.
최종적으로 태스크 결과물로부터 메타데이터(시간 범위 및 스코프)가 포함된 임베딩 값을 추출합니다.
ingest_data함수는 데이터 수집을 수행하는 메인 함수입니다.generate_embedding을 호출하여 지정한 비디오 파일의 임베딩을 가져옵니다.수집 타겟인 Pinecone 인덱스(이 예시에서는
twelve-labs)에 연결합니다.임베딩 데이터와 메타데이터 형식을 맞추어 업서트(Upsert)할 벡터를 준비합니다.
준비된 벡터들을 Pinecone 인덱스에 최종 업서트합니다.
코드를 실행하는 동안 임베딩 태스크 처리에 따른 진행 상황 업데이트를 확인할 수 있으며, 마지막에 몇 개의 임베딩이 Pinecone에 수집되었는지 성공 메시지가 출력됩니다. 이를 통해 추후 해당 임베딩을 사용하여 비디오 콘텐츠를 검색하고 분석할 수 있는 기반이 마련됩니다.
# Define a callback function to monitor task progress def on_task_update(task: EmbeddingsTask): print(f" Status={task.status}") def generate_embedding(video_file): # Create an embedding task task = twelvelabs_client.embed.task.create( engine_name="Marengo-retrieval-2.6", video_file=video_file ) print(f"Created task: id={task.id} engine_name={task.engine_name} status={task.status}") # Wait for the task to complete status = task.wait_for_done( sleep_interval=2, callback=on_task_update ) print(f"Embedding done: {status}") # Retrieve the task result task_result = twelvelabs_client.embed.task.retrieve(task.id) # Extract and return the embeddings embeddings = [] for v in task_result.video_embeddings: embeddings.append({ 'embedding': v.embedding.float, 'start_offset_sec': v.start_offset_sec, 'end_offset_sec': v.end_offset_sec, 'embedding_scope': v.embedding_scope }) return embeddings, task_result def ingest_data(video_file_path, index_name = "twelve-labs"): """ Generate embeddings for video and store in Pinecone """ #Strip the extension and the folders from the video_file_path video_name = os.path.splitext(os.path.basename(video_file_path))[0] print(video_name) # Connect to Pinecone index if index_name not in pc.list_indexes().names(): pc.create_index( name=index_name, dimension=1024, # The dimensions of Twelve Lab's Embedding Model metric="cosine", spec=ServerlessSpec( cloud="aws", region="us-east-1" ) ) index = pc.Index(index_name) # Generate embeddings using Twelve Labs Embed API embeddings, task_result = generate_embedding(video_file_path) # Prepare vectors for upsert vectors_to_upsert = [] for i, emb in enumerate(embeddings): vector_id = f"{video_name}_{i}" vectors_to_upsert.append((vector_id, emb['embedding'], { 'video_file': video_name, 'video_segment': i, 'start_time': emb['start_offset_sec'], 'end_time': emb['end_offset_sec'], 'scope': emb['embedding_scope'] })) # Upsert embeddings to Pinecone index.upsert(vectors=vectors_to_upsert) return f"Ingested {len(embeddings)} embeddings for {video_file_path}"
이제 이 두 함수를 사용해 비디오 임베딩을 Pinecone에 로드해 보겠습니다.
# Example usage result = ingest_data(single_video) print(result)
이 코드를 통해 Twelve Labs의 Embed API를 사용하여 영상의 멀티모달 임베딩을 생성하고, 나중에 필요할 때 검색할 수 있도록 Pinecone 벡터 데이터베이스에 저장할 수 있습니다. 이 임베딩은 시각, 청각 및 텍스트 정보를 포함하여 비디오 콘텐츠의 다양한 측면을 완벽히 포착하므로 광범위한 AI 애플리케이션에 매우 유용합니다.
텍스트 쿼리로 검색하기
다음으로 Twelve Labs의 Marengo 모델을 통해 텍스트를 임베딩하고, Pinecone 데이터베이스에서 유사한 콘텐츠를 찾는 함수를 정의하겠습니다.
get_text_embedding함수는 Twelve Labs Embed API를 사용해 텍스트 쿼리를 임베딩 벡터로 변환합니다.twelvelabs_client.embed.create메서드를 사용하여 텍스트에 대한 임베딩을 생성합니다.engine_name파라미터는 사용할 임베딩 모델("Marengo-retrieval-2.6")을 지정합니다.text_truncate파라미터는 "start"로 설정되며, 이는 텍스트 길이가 너무 길 경우 시작 부분부터 자르게 됨을 의미합니다.
retrieve_similar_content함수는 실질적인 검색을 수행하는 메인 함수입니다.입력 파라미터로 텍스트 쿼리와 반환할 결과 수(
top_k)를 전달받습니다.get_text_embedding을 호출하여 텍스트 쿼리를 임베딩 데이터로 변환합니다.twelve-labs라는 명칭의 Pinecone 인덱스에 연결합니다.쿼리 임베딩과 가장 유사한 벡터를 인덱스에서 검색하여, 메타데이터와 함께 반환할 결과 개수만큼 탐색합니다.
콘텐츠 검색은 텍스트 쿼리의 임베딩과 Pinecone에 미리 저장되어 있는 비디오 세그먼트의 임베딩 간의 유사도를 비교하는 방식으로 동작합니다. 이를 통해 대규모 비디오 데이터셋 전체를 빠르고 효율적으로 검색할 수 있습니다.
def get_text_embedding(text_query): # Twelve Labs Embed API supports text-to-embedding text_embedding = twelvelabs_client.embed.create( engine_name="Marengo-retrieval-2.6", text=text_query, text_truncate="start" ) return text_embedding.text_embedding.float def retrieve_similar_content(query, index_name="twelve-labs", top_k=5): """ Retrieve similar content based on query embedding """ # Generate query embedding query_embedding = get_text_embedding(query) # Connect to Pinecone index index = pc.Index(index_name) # Query Pinecone for similar vectors results = index.query(vector=query_embedding, top_k=top_k, include_metadata=True) return results
이제 샘플 텍스트 쿼리를 인자로 하여 retrieve_similar_content 함수를 호출하고, 검색해 낸 가장 유사한 콘텐츠의 자세한 정보들을 화면에 출력해 보겠습니다.
# Example usage text_query = "Lipstick" similar_content = retrieve_similar_content(text_query) print(f"Query: '{text_query}'") print(f"Top {len(similar_content['matches'])} similar content:") for i, match in enumerate(similar_content['matches']): print(f"{i+1}. Score: {match['score']:.4f}") print(f" Video File: {match['metadata']['video_file']}") print(f" Video ID: {match['metadata']['video_segment']}") print(f" Time range: {match['metadata']['start_time']} - {match['metadata']['end_time']} seconds") print(f" Scope: {match['metadata']['scope']}") print()
이 코드를 통해 비디오 콘텐츠상에서 텍스트 쿼리를 기반으로 의미 분석을 수행하는 시맨틱 검색(Semantic Search)이 가능해집니다. Twelve Labs의 강력한 멀티모달 임베딩 성능 덕분에 사용자가 검색한 정확한 단어가 비디오 내에 포함되어 있지 않더라도, 의미상 밀접하게 매칭되는 비디오 구간을 정확하게 찾아낼 수 있습니다.
이 코드를 실행하면 유사도 점수, 비디오 파일명, 비디오 ID, 해당 타임스탬프 구간, 임베딩 스코프를 포함한 검색 매칭 순위가 함께 노출됩니다. 이를 통해 고도화된 비디오 검색, 개인화 추천 시스템 등 다양한 형태의 서비스로 응용 및 확장할 수 있습니다.
비디오 포맷 제어
벡터 데이터베이스에 임베딩을 저장하고 검색 준비를 마친 단계에서, 우리의 첫 번째 실험은 전체 긴 비디오 단위가 아닌 세부 구간 클립 단위로 임베딩을 구성하여 쿼리와 분석 모델을 연동하는 것입니다. 임베딩 모델의 타임스탬프 처리 방식에 맞춰 비디오 파일을 여러 조각의 세그먼트로 나누어 보겠습니다.
아래 정의된 split_video 함수는 `av` 라이브러리를 이용하여 하나의 비디오를 지정된 시간 단위로 쪼개는 역할을 담당합니다. 처리 원리는 다음과 같습니다.
함수의 인자로 입력 비디오 경로, 저장 폴더, 세그먼트 생성 단위 시간(기본값: 6초)을 받습니다.
입력 영상을 연 다음, 해당 영상의 초당 프레임 수(FPS)를 기준으로 한 세그먼트에 들어갈 프레임 개수를 계측하고 영상 프레임을 순회합니다.
각 세그먼트 주기에 도달할 때마다 새로운 출력 파일(MP4) 컨테이너를 생성한 후 프레임 데이터를 복사하고 타임스탬프 값을 보정합니다.
조각난 결과 파일들은 일련번호가 포함된 별도의 MP4 형식으로 지정된 서브 폴더 안에 저장됩니다.
import av def split_video(input_path, output_dir, segment_duration=6): # Ensure output directory exists os.makedirs(output_dir, exist_ok=True) input_file_name = os.path.splitext(os.path.basename(input_path))[0] print(input_file_name) with av.open(input_path) as input_container: # Get video stream input_stream = input_container.streams.video[0] fps = input_stream.average_rate # Calculate how many frames are in each segment frames_per_segment = int(segment_duration * fps) segment_count = 0 frame_count = 0 output_container = None output_stream = None first_frame_timestamp = None for frame in input_container.decode(video=0): if frame_count % frames_per_segment == 0: # Close previous output container if it exists if output_container: output_container.close() # Create a new output container output_path = os.path.join(output_dir, f'{input_file_name}_segment_{segment_count:03d}.mp4') segment_count += 1 output_container = av.open(output_path, mode='w') output_stream = output_container.add_stream('h264', rate=fps) output_stream.width = frame.width output_stream.height = frame.height output_stream.pix_fmt = 'yuv420p' # Reset the first frame timestamp for the new segment first_frame_timestamp = frame.pts # Adjust the frame timestamp frame.pts -= first_frame_timestamp # Encode frame packet = output_stream.encode(frame) output_container.mux(packet) frame_count += 1 # Flush the encoder packet = output_stream.encode(None) output_container.mux(packet) # Close the last output container if output_container: output_container.close() split_video(input_path=single_video, output_dir=split_video_dir)
쿼리 준비하기
이제 생성형 모델과 상호작용하기 위한 모든 준비가 끝났습니다. 질문(Query)을 정의하고 데이터베이스로부터 관련된 콘텐츠를 검색해 오겠습니다.
query = "What is this advertisement selling?" similar_content = retrieve_similar_content(query)
Pegasus를 활용하여 비디오 영상 클립과 대화하기
Pegasus-1 모델을 성공적으로 구동하는 과정은 크게 세 기둥으로 구분할 수 있습니다.
Twelve Labs상에 업로드 비디오를 관리 및 매핑하기 위한 고유 인덱스 설정 — 이 단계는 서비스당 최초 한 번만 수행하면 충분합니다.
Twelve Labs 플랫폼에 비디오 파일 자체 업로드하기 — 비디오 파일당 한 번만 필요합니다.
최종 확인하고픈 질문(프롬프트)과 대상 비디오를 기반으로 Pegasus에 직접 쿼리하기
먼저 아래 코드로 플랫폼 내에 인덱스를 생성해 줍니다.
engines = [ { "name": "pegasus1.1", "options": ["visual", "conversation"] } ] index_name = "ads_index" indices_list = twelvelabs_client.index.list(name=index_name) if len(indices_list) == 0: index = twelvelabs_client.index.create( name=index_name, engines=engines, ) print(f"A new index has been created: id={index.id} name={index.name} engines={index.engines}") else: index = indices_list[0] print(f"Index already exists: id={index.id} name={index.name} engines={index.engines}")
이어서 원활한 연동을 위한 비디오 파일 업로드 유틸리티 로직을 구성합니다.
def upload_video_to_twelve_labs(video_path): task = twelvelabs_client.task.create( index_id=index.id, file = video_path ) print(f"Task created: id={task.id} status={task.status}") task.wait_for_done(sleep_interval=5, callback=on_task_update) if task.status != "ready": raise RuntimeError(f"Indexing failed with status {task.status}") print(f"The unique identifier of your video is {task.video_id}.") return task.video_id
이제 잘게 쪼갠 세그먼트 영상 디렉터리를 탐색하며, 루프를 통해 파일을 하나씩 Twelve Labs 상의 생성된 인덱스로 업로드합니다.
video_ids = {} for split_video_filename in os.listdir(split_video_dir): split_video_path = os.path.join(split_video_dir, split_video_filename) print(split_video_path) split_video_name = split_video_filename.split('.')[0] print(split_video_name) video_id = upload_video_to_twelve_labs(split_video_path) video_ids[split_video_name] = video_id print(video_ids)
Pegasus API 호출
이제 사전에 구현한 검색 결과값을 토대로 도출한 실제 동영상 클립 정보를 바인딩하고 간단히 쿼리를 전송하기만 하면 됩니다.
# retrieve the correct video_id for the relevant video video_segment = (int) (similar_content['matches'][0]['metadata']['video_segment']) print(f"Retrieved video segment: {video_segment}") base_filename = os.path.splitext(os.path.basename(single_video))[0] video_key = f"{base_filename}_segment_{video_segment:03d}" video_id = video_ids[video_key] res = twelvelabs_client.generate.text( video_id=video_id, prompt=query ) print(f"{res.data}")
오픈소스 LLaVA-NeXT-Video 모델 적용
대조군으로 오픈소스 모델을 활용하기 위해서는 다음과 같은 작업을 추가로 처리해야 합니다.
동영상을 NumPy 다차원 배열 형식으로 파싱하고 가공하기
모델 추론에 전송할 핵심 프레임 부분집합(Subset)을 균등 샘플링하기
사용자 GPU 자원에 필요한 오픈소스 라이브러리 및 가중치를 직접 다운로드하고 호스팅하기
사용 모델 고유의 전처리 포맷 처리 및 개별 질의 관리 수행하기
동영상을 NumPy 포맷으로 파싱
이 아래 기술된 read_video_pyav() 유틸리티는 PyAV 디코더를 활용하여 비디오의 특정 지정된 프레임셋만 선별 디코딩하는 함수입니다.
import av import numpy as np def read_video_pyav(container, indices): ''' Decode the video with PyAV decoder. Args: container (av.container.input.InputContainer): PyAV container. indices (List[int]): List of frame indices to decode. Returns: np.ndarray: np array of decoded frames of shape (num_frames, height, width, 3). ''' frames = [] container.seek(0) start_index = indices[0] end_index = indices[-1] for i, frame in enumerate(container.decode(video=0)): if i > end_index: break if i >= start_index and i in indices: frames.append(frame)
비디오 프레임 샘플링
아래 코드 구조는 다음과 같이 작동합니다.
get_total_frames(): 비디오 파일에 포함된 전체 원본 물리적 프레임의 총 개수를 반환합니다. 일부 균등 분할 계산 예외로 결과가 0으로 도출되는 것을 방지합니다.sample_video(): 전체 비디오 범위에 걸쳐 균등한 간격으로 특정된 가시 샘플 개수만큼 대표 프레임을 추출합니다.process_videos_in_folder(): 타겟 디렉터리 내의 동영상을 돌며 이 샘플링 로직을 통과시켜 연관 캐시 배열 정보를 빌드합니다.
def get_total_frames(video_path): """ Manually count the total number of frames in a video. Used in case uniformly sampling comes up as 0 frames. """ container = av.open(video_path) video_stream = container.streams.video[0] total_frames = 0 for frame in container.decode(video_stream): total_frames += 1 return total_frames def sample_video(video_path, num_samples=8): container = av.open(video_path) video_stream = container.streams.video[0] # sample uniformly num_samples frames from the video total_frames = container.streams.video[0].frames if total_frames == 0: total_frames = get_total_frames(video_path) indices = np.arange(0, total_frames, total_frames / num_samples).astype(int) sampled_frames = read_video_pyav(container, indices) return sampled_frames def process_videos_in_folder(folder_path): sample_info = {} # Supported video file extensions video_extensions = ('.mp4', '.avi', '.mov', '.mkv') for filename in os.listdir(folder_path): simple_video_name = os.path.splitext(os.path.basename(filename))[0] if filename.lower().endswith(video_extensions): video_path = os.path.join(folder_path, filename) try: print("Sampling " + video_path) sampled_clip = sample_video(video_path) sample_info[simple_video_name] = {"sampled_video": sampled_clip, "video_path" : video_path} except Exception as e: print(f"Error processing {filename}: {str(e)}") return sample_info sampled_video_info = process_videos_in_folder(split_video_dir)
모델 로딩
여기서는 Hugging Face의 Transformers 라이브러리를 통해 오픈소스 모델인 LLaVA-NeXT-Video와 프로세서를 불러옵니다. 불필요한 GPU 메모리 소모를 극적으로 줄이기 위해 4비트 양자화 방식을 활용해 적재합니다.
from transformers import BitsAndBytesConfig, LlavaNextVideoForConditionalGeneration, LlavaNextVideoProcessor import torch quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16 ) processor = LlavaNextVideoProcessor.from_pretrained("llava-hf/LLaVA-NeXT-Video-7B-hf") model = LlavaNextVideoForConditionalGeneration.from_pretrained( "llava-hf/LLaVA-NeXT-Video-7B-hf", quantization_config=quantization_config, device_map='auto' )
모델 대상 질의 전송
Pinecone 데이터베이스의 유사 이미지 클립 반환 결과에 포함된 물리 오프셋 값을 역계산해 조각난 소스 세그먼트 파일명을 파싱해낼 유틸리티 기능입니다. video_segment_name_from_offset() 함수는 비디오 경로와 시작 시각 값을 받아 규칙화된 파일 포맷 명칭을 반환합니다.
def video_segment_name_from_offset(video_path, start_time, segment_length = 6): segment_number = int (start_time // segment_length) simple_video_name = os.path.splitext(os.path.basename(video_path))[0] return f"{simple_video_name}_segment_{segment_number:03d}"
이제 파싱된 클립을 가져온 뒤 모델에 던질 형식을 생성합니다. 순서에 맞춘 사용자와 텍스트-비디오 모달리티 대화 템플릿(Conversation structure) 형태로 입력을 구성하고, 전용 텐서 디큐잉 및 빌딩 단계를 거쳐 지시 추론을 수행하게 됩니다.
video_segment = similar_content['matches'][0]['metadata']['video_file'] print(video_segment) video_offset = similar_content['matches'][0]['metadata']['start_time'] video_segment_name = video_segment_name_from_offset(video_segment, video_offset) video_segment = sampled_video_info[video_segment_name]['sampled_video'] # Each "content" is a list of dicts and you can add image/video/text modalities conversation = [ { "role": "user", "content": [ {"type": "text", "text": query}, {"type": "video"}, ], }, ] prompt = processor.apply_chat_template(conversation, add_generation_prompt=True) prompt_len = len(prompt) inputs = processor([prompt], videos=[video_segment], padding=True, return_tensors="pt").to(model.device) generate_kwargs = {"max_new_tokens": 100, "do_sample": True, "top_p": 0.9} output = model.generate(**inputs, **generate_kwargs) generated_text = processor.batch_decode(output, skip_special_tokens=True) print(generated_text[0])
비교 평가
두 모델에서 뽑아낸 결과를 직접 비교해 보면, LLaVA-NeXT-Video와 비교해 Pegasus가 제공하는 답변이 타임 범위 및 상황 맥락적으로 훨씬 우수하고 정보 밀도가 높음을 확인할 수 있습니다.
다만, 데이터가 쪼개진 탓에 두 모델 모두 단편 조각 클립만 입력으로 받아서는 전체 비디오 내 종합 맥락을 관찰하는 데 다소 제약이 있습니다. 이제 두 방식에 완전한 크기의 비디오 전량을 제공하는 경우 어떻게 다른지 비교해 보겠습니다.
여러 비디오 다루기
구조화되지 않은 상태의 완전한 영상들이 디렉터리에 놓여 있는 환경에서 이 아카이브 전체에 결친 고차원 질의 연동 예시입니다. 이전 시점에는 세그먼테이션된 조각 데이터셋으로 질의 유사 클립을 정렬했다면, 이번에는 해당 클립의 원천이 되는 완전한 무삭제 원본 비디오 파일 자체를 모델에 피딩할 예정입니다. Pinecone 벡터 내 세그먼트 메타데이터에 파일 상위 계층 구조가 저장되어 있기에 손쉽게 매핑할 수 있습니다.
일단 타겟 영상을 Pinecone에 일괄 분석·인덱싱 처리하겠습니다.
ads_dir = os.path.join(base_folder_path,"ad_vids") video_list = [] #Make sure that we' don't waste time re-embedding the original single video: single_video_filename = os.path.splitext(os.path.basename(single_video))[0] for filename in os.listdir(ads_dir): if filename.endswith(".mp4") and single_video_filename not in filename: video_list.append(ads_dir + "/" + filename) print(video_list) for video in video_list: ingest_data(video)
이어서 데이터베이스에 전질의하여, 의미상 가장 밀접도가 높은 영상 후보군을 가려내기 위한 샘플 질문을 몇 개 작성해 보겠습니다.
full_database_questions = ["Who is the actor in the Miss Dior video?", "What ad is Selena Gomez in?", "What is the ad for Rare Beauty about?", "Why should people buy the Rare Beauty product according to their ad?"] question = full_database_questions[0] similar_content_from_question = retrieve_similar_content(question) video_name = similar_content_from_question['matches'][0]['metadata']['video_file']
Pegasus 구현
파이썬 SDK 연동 가이드를 따라 Pegasus 모델상에서 질의를 이어나가기 위한 절차를 살펴보겠습니다. 이미 인덱스는 앞에서 한 차례 가설했기에, 이번에는 쿼리에 매핑할 전체 영상만을 태스크 업로드하면 준비 작업이 완성됩니다.
Twelve Labs 측으로 원본 영상 일괄 업로드
디렉터리에 존재하는 여러 동영상 파일을 반복 순회하여 태스크를 요청하고, 식별 식별자인 비디오 ID들을 고유 변수에 매핑합니다.
for vid in os.listdir(ads_dir): vid_path = os.path.join(ads_dir, vid) vid_name = os.path.splitext(os.path.basename(vid_path))[0] print(vid_path) video_id = upload_video_to_twelve_labs(vid_path) video_ids[vid_name] = video_id
전체 비디오 ID 구조 기반으로 Pegasus 질의 실행
결과적으로 검색된 메타데이터 비디오 ID 고유 식별 명칭에 직접 질의 지시문(Prompt)을 투사하여 텍스트 응답을 정밀 획득해 내는 구문입니다.
video_id = video_ids[video_name] res = twelvelabs_client.generate.text( video_id=video_id, prompt=question ) print(f"{res.data}")
오픈소스 LLaVA-NeXT-Video 기반 질의 수행
통합 비디오 샘플링
가용한 원본 비디오 파일들 전부를 순차 샘플링 배열로 포맷 처리한 후, 사전에 추려 진 고유 명칭과 일치하는 임베딩 배열을 식별해 맵핑해야 합니다.
sampled_database_video_info = process_videos_in_folder(ads_dir) video_segment = sampled_database_video_info[video_name]['sampled_video']
추론 런타임 가동
이제 로컬 또는 탑재한 추론 엔진으로 해당 데이터 샘플상에 모델을 순회합니다.
사용자 정보 역할군 정의 및 질의(Question)와 대표 비디오 배열 정보를 대화 포맷으로 매핑합니다.
해당 모델 인터페이스 템플릿으로 변환 후, 입출력 디코드 시 조건 인자(`max_new_tokens`, `do_sample`, `top_p`) 세트를 토대로 연계를 시도합니다.
최종 디코딩 단계를 거쳐 추출한 프롬프트 완성문을 로깅 모듈로 터미널 화면에 노출합니다.
conversation = [ { "role": "user", "content": [ {"type": "text", "text": question}, {"type": "video"}, ], }, ] prompt = processor.apply_chat_template(conversation, add_generation_prompt=True) prompt_len = len(prompt) inputs = processor([prompt], videos=[video_segment], padding=True, return_tensors="pt").to(model.device) generate_kwargs = {"max_new_tokens": 100, "do_sample": True, "top_p": 0.9} output = model.generate(**inputs, **generate_kwargs) generated_text = processor.batch_decode(output, skip_special_tokens=True) print(generated_text[0])
비교 평가
두 가지 결과 파일을 직접 수급해 확인해 보면, Pegasus는 디올(Miss Dior) 광고 영상 맥락 속에 등장한 배우 "나탈리 포트만"의 존재를 명확히 판별하고 정확하게 대답합니다. 대조적으로 로컬 LLaVA-NeXT-Video 구조에서는 나탈리 포트만의 등장 유무 자체를 이해하지 못하거나 프레임 샘플 해상도 단계에서 정보를 놓쳐 식별에 실패합니다. 또한 질문에 명확히 응답하지 못하고 엉뚱한 설명을 장황하게 늘어놓는 경향(Hallucination)이 있으며, 응답 지연 시간(Latency)이 크게 길어져 실제 프로덕션 서비스에 바로 투입하기에는 한계가 있음을 알 수 있습니다.
결론 (Conclusion)
본 튜토리얼 아티클에서는 개별 영상 파일부터 가용할 수 있는 완전한 영상 데이터셋 군집 도메인 전체에 이르기까지 비디오와 유기적으로 대화하는 방법을 다루었습니다. 데이터셋 인덱싱 정렬 및 유사 클립 수급 통제 장치로서 Twelve Labs Embed API와 Pinecone 벡터 엔진을 융합하는 방법을 완벽히 이해하는 데 기여했습니다.
동시에 Twelve Labs 고유의 Pegasus 모델과 범용 비디오 멀티모달 오픈소스 진영인 LLaVA-NeXT-Video 간의 필수 인프라 비용 부담, 개발 엔지니어링 활용성(DX), 세부 정밀 탐색 신뢰도를 다각도로 비교해 보았습니다. Pegasus는 복잡한 하부 인프라 운영 부담 없이 지시 프롬프트 수행 부문에서 확실하게 정밀하고 깔끔한 사용 환경 메커니즘을 제공해 줌을 보여줍니다.
주요 모범 사례 (Best Practices)
안정적인 엔터프라이즈 환경 구축이 주요 목적인 경우, 전용 호스트 활용 측면에서 Pinecone의 Pod 기반 서비스 구조를 채택하는 것을 권장합니다.
오픈소스 탑재 시 샘플링 프레임의 크기를 무조건 확대하기보다 추론 연산 처리 정밀성 및 서비스 지연 속도 사이의 효율적 절충안(Trade-off) 지점을 세심히 고려해야 합니다.
로컬 운영이 필수적이라면 양자화 기법을 적극 접목해 속도 부하를 완화하는 것이 좋습니다. 사내 램 소모 사양 및 대답 완성 완성도를 모니링하면서 비즈니스 상황에 최적화된 선택을 하시길 바랍니다.
다음 도전 과제 (Next Steps)
수십만 건 이상의 방대한 영상 데이터 아카이브를 핸들링할 때 탐색 품질이 떨어질 수 있습니다. 이를 우회하고 정합성을 정화할 수 있는 솔루션은 다음과 같습니다.
임베딩 결과 레이어 최상단 영역에 단순 선형 보정기(Linear adapter)를 수립 보완하여 자사 고유 형태 도메인 데이터셋 규격에 정합합니다.
다양한 동영상에서 반환된 후보군이 있을 경우, 2차 단에서 라이트하게 Pegasus 추론을 돌려 후보 영상들의 타당성 우선순위를 재정렬(Re-ranking)합니다.
Pinecone 항목의 메타데이터 측면에 비디오 전 구간의 자동 생성된 요약문 데이터셋을 복합 구축하여 하이브리드 서치 방식을 통해 의미적 탐색 범주를 비약적으로 개선합니다. 자세한 사항은 Pinecone 메타데이터 연동 문서를 참조해주시기 바랍니다.
부록 (Appendix)
심화 학습과 직접 실습을 위한 유용한 참고 자료 모음입니다.
구현 소스 코드가 포함된 Colab Notebook 주소
세부 활용 Twelve Labs 가이드 문서
Pinecone 공식 통합 라이브러리 클라이언트 SDK 문서 및 클라우드 백서
이 튜토리얼을 위해 함께 협업해 준 Pinecone 팀(Adam Heerwagen 및 Cory Waddingham)에 진심으로 감사드립니다.
Introduction (소개)
비디오 대상 RAG 기반 Q&A를 구현하기 위해 Twelve Labs의 Embed API와 Pinecone의 호스팅 벡터 데이터베이스를 통합하는 이번 튜토리얼에 오신 것을 환영합니다. 본 가이드에서는 생성형 모델을 사용해 비정형 비디오 데이터베이스에서 텍스트 답변을 추출하는 방법을 알아봅니다.
Twelve Labs의 풍부한 문맥 이해 임베딩과 Pinecone의 벡터 데이터베이스를 결합하여 비디오 임베딩을 저장, 인덱싱 및 쿼리하는 대화형 애플리케이션을 구축합니다. 이 노트북은 단 몇 줄의 코드만으로 이러한 기술이 구현할 수 있는 현재의 놀라운 가능성들을 명확히 보여줍니다.
또한 비교를 위해, 텍스트 응답을 생성하는 데 Twelve Labs의 Generate API를 사용할 때와 대표적인 오픈소스 모델인 LLaVA-NeXT-Video를 사용할 때의 개발자 경험(DX) 차이도 함께 소개합니다.
설정 및 설치
핵심 기능을 살펴보기 전에, 먼저 환경을 구성하고 필요한 라이브러리를 설치해 보겠습니다.
필수 라이브러리 설치
가장 먼저 Twelve Labs와 Pinecone용 라이브러리를 설치합니다. 노트북 셀에서 다음 명령어를 실행하세요.
# Install required libraries !pip install twelvelabs pinecone-client
그 다음, 비디오 포맷 변환을 위한 PyAV, 그리고 Hugging Face에서 오픈소스 모델을 사용하기 위해 bitsandbytes 및 transformers를 설치합니다.
!pip install -q av !pip install --upgrade -q accelerate bitsandbytes !pip install transformers
인증
Twelve Labs API와 Pinecone 사용을 위한 키를 설정해야 합니다. 이 키들을 안전하게 보관하기 위해 Google Colab의 내장 `userdata` 라이브러리를 활용하겠습니다. 가입 후 Pinecone 콘솔에서 필요한 정보를 바로 확인할 수 있으며, 이번 데모를 진행하기에 충분한 무료 Starter 티어를 제공합니다.
Twelve Labs API 키는 https://playground.twelvelabs.io 가입 후 계정 내에서 확인하실 수 있습니다.
from google.colab import userdata TL_API_KEY=userdata.get('TL_API_KEY') PINECONE_API_KEY=userdata.get('PINECONE_API_KEY')
비디오 데이터 설정
이제 임베딩할 비디오 데이터를 준비해야 합니다. 이 링크를 통해 구글 드라이브 폴더에서 비디오 데이터를 확인할 수 있습니다. 이 데이터를 구글 드라이브 루트 폴더의 "TwelveLabs-Pinecone" 폴더에 복사해 주세요. 다음 셀을 실행해 드라이브를 마운트하고 노트북에서 비디오 파일에 접근할 수 있도록 설정하겠습니다.
from google.colab import drive drive.mount('/content/drive') base_folder_path = "/content/drive/MyDrive/TwelveLabs-Pinecone" single_video = base_folder_path + "/ad_vids/Rare Beauty By Selena Gomez - Makeup Made To Feel Good In.mp4" split_video_dir = base_folder_path + "/split_ad_videos"
클라이언트 초기화
이제 Pinecone과 Twelve Labs 설정을 구성할 차례입니다. 두 서비스의 라이브러리를 임포트하고 각각의 API 키를 사용해 클라이언트를 초기화합니다.
# Configure Pinecone from pinecone import Pinecone, ServerlessSpec pc = Pinecone(api_key=PINECONE_API_KEY) from twelvelabs import TwelveLabs from twelvelabs.models.embed import EmbeddingsTask # Initialize the Twelve Labs client twelvelabs_client = TwelveLabs(api_key=TL_API_KEY)
임베딩 생성 및 Pinecone에 데이터 수집(Ingestion)
아래 코드 블록은 Twelve Labs API와 Pinecone 벡터 데이터베이스를 활용하여 비디오 임베딩을 생성하고 저장하는 프로세스를 보여줍니다. 여기서는 두 가지 핵심 함수를 정의합니다.
generate_embedding함수는 임베딩 태스크의 생성 및 관리를 담당합니다.지정된 비디오 파일과 엔진 정보를 토대로 Twelve Labs API를 사용하여 임베딩 태스크를 생성합니다.
태스크 진행 상황을 모니터링하기 위한 콜백 함수를 정의합니다.
작업이 완료될 때까지 대기한 후 결과 데이터를 수집합니다.
최종적으로 태스크 결과물로부터 메타데이터(시간 범위 및 스코프)가 포함된 임베딩 값을 추출합니다.
ingest_data함수는 데이터 수집을 수행하는 메인 함수입니다.generate_embedding을 호출하여 지정한 비디오 파일의 임베딩을 가져옵니다.수집 타겟인 Pinecone 인덱스(이 예시에서는
twelve-labs)에 연결합니다.임베딩 데이터와 메타데이터 형식을 맞추어 업서트(Upsert)할 벡터를 준비합니다.
준비된 벡터들을 Pinecone 인덱스에 최종 업서트합니다.
코드를 실행하는 동안 임베딩 태스크 처리에 따른 진행 상황 업데이트를 확인할 수 있으며, 마지막에 몇 개의 임베딩이 Pinecone에 수집되었는지 성공 메시지가 출력됩니다. 이를 통해 추후 해당 임베딩을 사용하여 비디오 콘텐츠를 검색하고 분석할 수 있는 기반이 마련됩니다.
# Define a callback function to monitor task progress def on_task_update(task: EmbeddingsTask): print(f" Status={task.status}") def generate_embedding(video_file): # Create an embedding task task = twelvelabs_client.embed.task.create( engine_name="Marengo-retrieval-2.6", video_file=video_file ) print(f"Created task: id={task.id} engine_name={task.engine_name} status={task.status}") # Wait for the task to complete status = task.wait_for_done( sleep_interval=2, callback=on_task_update ) print(f"Embedding done: {status}") # Retrieve the task result task_result = twelvelabs_client.embed.task.retrieve(task.id) # Extract and return the embeddings embeddings = [] for v in task_result.video_embeddings: embeddings.append({ 'embedding': v.embedding.float, 'start_offset_sec': v.start_offset_sec, 'end_offset_sec': v.end_offset_sec, 'embedding_scope': v.embedding_scope }) return embeddings, task_result def ingest_data(video_file_path, index_name = "twelve-labs"): """ Generate embeddings for video and store in Pinecone """ #Strip the extension and the folders from the video_file_path video_name = os.path.splitext(os.path.basename(video_file_path))[0] print(video_name) # Connect to Pinecone index if index_name not in pc.list_indexes().names(): pc.create_index( name=index_name, dimension=1024, # The dimensions of Twelve Lab's Embedding Model metric="cosine", spec=ServerlessSpec( cloud="aws", region="us-east-1" ) ) index = pc.Index(index_name) # Generate embeddings using Twelve Labs Embed API embeddings, task_result = generate_embedding(video_file_path) # Prepare vectors for upsert vectors_to_upsert = [] for i, emb in enumerate(embeddings): vector_id = f"{video_name}_{i}" vectors_to_upsert.append((vector_id, emb['embedding'], { 'video_file': video_name, 'video_segment': i, 'start_time': emb['start_offset_sec'], 'end_time': emb['end_offset_sec'], 'scope': emb['embedding_scope'] })) # Upsert embeddings to Pinecone index.upsert(vectors=vectors_to_upsert) return f"Ingested {len(embeddings)} embeddings for {video_file_path}"
이제 이 두 함수를 사용해 비디오 임베딩을 Pinecone에 로드해 보겠습니다.
# Example usage result = ingest_data(single_video) print(result)
이 코드를 통해 Twelve Labs의 Embed API를 사용하여 영상의 멀티모달 임베딩을 생성하고, 나중에 필요할 때 검색할 수 있도록 Pinecone 벡터 데이터베이스에 저장할 수 있습니다. 이 임베딩은 시각, 청각 및 텍스트 정보를 포함하여 비디오 콘텐츠의 다양한 측면을 완벽히 포착하므로 광범위한 AI 애플리케이션에 매우 유용합니다.
텍스트 쿼리로 검색하기
다음으로 Twelve Labs의 Marengo 모델을 통해 텍스트를 임베딩하고, Pinecone 데이터베이스에서 유사한 콘텐츠를 찾는 함수를 정의하겠습니다.
get_text_embedding함수는 Twelve Labs Embed API를 사용해 텍스트 쿼리를 임베딩 벡터로 변환합니다.twelvelabs_client.embed.create메서드를 사용하여 텍스트에 대한 임베딩을 생성합니다.engine_name파라미터는 사용할 임베딩 모델("Marengo-retrieval-2.6")을 지정합니다.text_truncate파라미터는 "start"로 설정되며, 이는 텍스트 길이가 너무 길 경우 시작 부분부터 자르게 됨을 의미합니다.
retrieve_similar_content함수는 실질적인 검색을 수행하는 메인 함수입니다.입력 파라미터로 텍스트 쿼리와 반환할 결과 수(
top_k)를 전달받습니다.get_text_embedding을 호출하여 텍스트 쿼리를 임베딩 데이터로 변환합니다.twelve-labs라는 명칭의 Pinecone 인덱스에 연결합니다.쿼리 임베딩과 가장 유사한 벡터를 인덱스에서 검색하여, 메타데이터와 함께 반환할 결과 개수만큼 탐색합니다.
콘텐츠 검색은 텍스트 쿼리의 임베딩과 Pinecone에 미리 저장되어 있는 비디오 세그먼트의 임베딩 간의 유사도를 비교하는 방식으로 동작합니다. 이를 통해 대규모 비디오 데이터셋 전체를 빠르고 효율적으로 검색할 수 있습니다.
def get_text_embedding(text_query): # Twelve Labs Embed API supports text-to-embedding text_embedding = twelvelabs_client.embed.create( engine_name="Marengo-retrieval-2.6", text=text_query, text_truncate="start" ) return text_embedding.text_embedding.float def retrieve_similar_content(query, index_name="twelve-labs", top_k=5): """ Retrieve similar content based on query embedding """ # Generate query embedding query_embedding = get_text_embedding(query) # Connect to Pinecone index index = pc.Index(index_name) # Query Pinecone for similar vectors results = index.query(vector=query_embedding, top_k=top_k, include_metadata=True) return results
이제 샘플 텍스트 쿼리를 인자로 하여 retrieve_similar_content 함수를 호출하고, 검색해 낸 가장 유사한 콘텐츠의 자세한 정보들을 화면에 출력해 보겠습니다.
# Example usage text_query = "Lipstick" similar_content = retrieve_similar_content(text_query) print(f"Query: '{text_query}'") print(f"Top {len(similar_content['matches'])} similar content:") for i, match in enumerate(similar_content['matches']): print(f"{i+1}. Score: {match['score']:.4f}") print(f" Video File: {match['metadata']['video_file']}") print(f" Video ID: {match['metadata']['video_segment']}") print(f" Time range: {match['metadata']['start_time']} - {match['metadata']['end_time']} seconds") print(f" Scope: {match['metadata']['scope']}") print()
이 코드를 통해 비디오 콘텐츠상에서 텍스트 쿼리를 기반으로 의미 분석을 수행하는 시맨틱 검색(Semantic Search)이 가능해집니다. Twelve Labs의 강력한 멀티모달 임베딩 성능 덕분에 사용자가 검색한 정확한 단어가 비디오 내에 포함되어 있지 않더라도, 의미상 밀접하게 매칭되는 비디오 구간을 정확하게 찾아낼 수 있습니다.
이 코드를 실행하면 유사도 점수, 비디오 파일명, 비디오 ID, 해당 타임스탬프 구간, 임베딩 스코프를 포함한 검색 매칭 순위가 함께 노출됩니다. 이를 통해 고도화된 비디오 검색, 개인화 추천 시스템 등 다양한 형태의 서비스로 응용 및 확장할 수 있습니다.
비디오 포맷 제어
벡터 데이터베이스에 임베딩을 저장하고 검색 준비를 마친 단계에서, 우리의 첫 번째 실험은 전체 긴 비디오 단위가 아닌 세부 구간 클립 단위로 임베딩을 구성하여 쿼리와 분석 모델을 연동하는 것입니다. 임베딩 모델의 타임스탬프 처리 방식에 맞춰 비디오 파일을 여러 조각의 세그먼트로 나누어 보겠습니다.
아래 정의된 split_video 함수는 `av` 라이브러리를 이용하여 하나의 비디오를 지정된 시간 단위로 쪼개는 역할을 담당합니다. 처리 원리는 다음과 같습니다.
함수의 인자로 입력 비디오 경로, 저장 폴더, 세그먼트 생성 단위 시간(기본값: 6초)을 받습니다.
입력 영상을 연 다음, 해당 영상의 초당 프레임 수(FPS)를 기준으로 한 세그먼트에 들어갈 프레임 개수를 계측하고 영상 프레임을 순회합니다.
각 세그먼트 주기에 도달할 때마다 새로운 출력 파일(MP4) 컨테이너를 생성한 후 프레임 데이터를 복사하고 타임스탬프 값을 보정합니다.
조각난 결과 파일들은 일련번호가 포함된 별도의 MP4 형식으로 지정된 서브 폴더 안에 저장됩니다.
import av def split_video(input_path, output_dir, segment_duration=6): # Ensure output directory exists os.makedirs(output_dir, exist_ok=True) input_file_name = os.path.splitext(os.path.basename(input_path))[0] print(input_file_name) with av.open(input_path) as input_container: # Get video stream input_stream = input_container.streams.video[0] fps = input_stream.average_rate # Calculate how many frames are in each segment frames_per_segment = int(segment_duration * fps) segment_count = 0 frame_count = 0 output_container = None output_stream = None first_frame_timestamp = None for frame in input_container.decode(video=0): if frame_count % frames_per_segment == 0: # Close previous output container if it exists if output_container: output_container.close() # Create a new output container output_path = os.path.join(output_dir, f'{input_file_name}_segment_{segment_count:03d}.mp4') segment_count += 1 output_container = av.open(output_path, mode='w') output_stream = output_container.add_stream('h264', rate=fps) output_stream.width = frame.width output_stream.height = frame.height output_stream.pix_fmt = 'yuv420p' # Reset the first frame timestamp for the new segment first_frame_timestamp = frame.pts # Adjust the frame timestamp frame.pts -= first_frame_timestamp # Encode frame packet = output_stream.encode(frame) output_container.mux(packet) frame_count += 1 # Flush the encoder packet = output_stream.encode(None) output_container.mux(packet) # Close the last output container if output_container: output_container.close() split_video(input_path=single_video, output_dir=split_video_dir)
쿼리 준비하기
이제 생성형 모델과 상호작용하기 위한 모든 준비가 끝났습니다. 질문(Query)을 정의하고 데이터베이스로부터 관련된 콘텐츠를 검색해 오겠습니다.
query = "What is this advertisement selling?" similar_content = retrieve_similar_content(query)
Pegasus를 활용하여 비디오 영상 클립과 대화하기
Pegasus-1 모델을 성공적으로 구동하는 과정은 크게 세 기둥으로 구분할 수 있습니다.
Twelve Labs상에 업로드 비디오를 관리 및 매핑하기 위한 고유 인덱스 설정 — 이 단계는 서비스당 최초 한 번만 수행하면 충분합니다.
Twelve Labs 플랫폼에 비디오 파일 자체 업로드하기 — 비디오 파일당 한 번만 필요합니다.
최종 확인하고픈 질문(프롬프트)과 대상 비디오를 기반으로 Pegasus에 직접 쿼리하기
먼저 아래 코드로 플랫폼 내에 인덱스를 생성해 줍니다.
engines = [ { "name": "pegasus1.1", "options": ["visual", "conversation"] } ] index_name = "ads_index" indices_list = twelvelabs_client.index.list(name=index_name) if len(indices_list) == 0: index = twelvelabs_client.index.create( name=index_name, engines=engines, ) print(f"A new index has been created: id={index.id} name={index.name} engines={index.engines}") else: index = indices_list[0] print(f"Index already exists: id={index.id} name={index.name} engines={index.engines}")
이어서 원활한 연동을 위한 비디오 파일 업로드 유틸리티 로직을 구성합니다.
def upload_video_to_twelve_labs(video_path): task = twelvelabs_client.task.create( index_id=index.id, file = video_path ) print(f"Task created: id={task.id} status={task.status}") task.wait_for_done(sleep_interval=5, callback=on_task_update) if task.status != "ready": raise RuntimeError(f"Indexing failed with status {task.status}") print(f"The unique identifier of your video is {task.video_id}.") return task.video_id
이제 잘게 쪼갠 세그먼트 영상 디렉터리를 탐색하며, 루프를 통해 파일을 하나씩 Twelve Labs 상의 생성된 인덱스로 업로드합니다.
video_ids = {} for split_video_filename in os.listdir(split_video_dir): split_video_path = os.path.join(split_video_dir, split_video_filename) print(split_video_path) split_video_name = split_video_filename.split('.')[0] print(split_video_name) video_id = upload_video_to_twelve_labs(split_video_path) video_ids[split_video_name] = video_id print(video_ids)
Pegasus API 호출
이제 사전에 구현한 검색 결과값을 토대로 도출한 실제 동영상 클립 정보를 바인딩하고 간단히 쿼리를 전송하기만 하면 됩니다.
# retrieve the correct video_id for the relevant video video_segment = (int) (similar_content['matches'][0]['metadata']['video_segment']) print(f"Retrieved video segment: {video_segment}") base_filename = os.path.splitext(os.path.basename(single_video))[0] video_key = f"{base_filename}_segment_{video_segment:03d}" video_id = video_ids[video_key] res = twelvelabs_client.generate.text( video_id=video_id, prompt=query ) print(f"{res.data}")
오픈소스 LLaVA-NeXT-Video 모델 적용
대조군으로 오픈소스 모델을 활용하기 위해서는 다음과 같은 작업을 추가로 처리해야 합니다.
동영상을 NumPy 다차원 배열 형식으로 파싱하고 가공하기
모델 추론에 전송할 핵심 프레임 부분집합(Subset)을 균등 샘플링하기
사용자 GPU 자원에 필요한 오픈소스 라이브러리 및 가중치를 직접 다운로드하고 호스팅하기
사용 모델 고유의 전처리 포맷 처리 및 개별 질의 관리 수행하기
동영상을 NumPy 포맷으로 파싱
이 아래 기술된 read_video_pyav() 유틸리티는 PyAV 디코더를 활용하여 비디오의 특정 지정된 프레임셋만 선별 디코딩하는 함수입니다.
import av import numpy as np def read_video_pyav(container, indices): ''' Decode the video with PyAV decoder. Args: container (av.container.input.InputContainer): PyAV container. indices (List[int]): List of frame indices to decode. Returns: np.ndarray: np array of decoded frames of shape (num_frames, height, width, 3). ''' frames = [] container.seek(0) start_index = indices[0] end_index = indices[-1] for i, frame in enumerate(container.decode(video=0)): if i > end_index: break if i >= start_index and i in indices: frames.append(frame)
비디오 프레임 샘플링
아래 코드 구조는 다음과 같이 작동합니다.
get_total_frames(): 비디오 파일에 포함된 전체 원본 물리적 프레임의 총 개수를 반환합니다. 일부 균등 분할 계산 예외로 결과가 0으로 도출되는 것을 방지합니다.sample_video(): 전체 비디오 범위에 걸쳐 균등한 간격으로 특정된 가시 샘플 개수만큼 대표 프레임을 추출합니다.process_videos_in_folder(): 타겟 디렉터리 내의 동영상을 돌며 이 샘플링 로직을 통과시켜 연관 캐시 배열 정보를 빌드합니다.
def get_total_frames(video_path): """ Manually count the total number of frames in a video. Used in case uniformly sampling comes up as 0 frames. """ container = av.open(video_path) video_stream = container.streams.video[0] total_frames = 0 for frame in container.decode(video_stream): total_frames += 1 return total_frames def sample_video(video_path, num_samples=8): container = av.open(video_path) video_stream = container.streams.video[0] # sample uniformly num_samples frames from the video total_frames = container.streams.video[0].frames if total_frames == 0: total_frames = get_total_frames(video_path) indices = np.arange(0, total_frames, total_frames / num_samples).astype(int) sampled_frames = read_video_pyav(container, indices) return sampled_frames def process_videos_in_folder(folder_path): sample_info = {} # Supported video file extensions video_extensions = ('.mp4', '.avi', '.mov', '.mkv') for filename in os.listdir(folder_path): simple_video_name = os.path.splitext(os.path.basename(filename))[0] if filename.lower().endswith(video_extensions): video_path = os.path.join(folder_path, filename) try: print("Sampling " + video_path) sampled_clip = sample_video(video_path) sample_info[simple_video_name] = {"sampled_video": sampled_clip, "video_path" : video_path} except Exception as e: print(f"Error processing {filename}: {str(e)}") return sample_info sampled_video_info = process_videos_in_folder(split_video_dir)
모델 로딩
여기서는 Hugging Face의 Transformers 라이브러리를 통해 오픈소스 모델인 LLaVA-NeXT-Video와 프로세서를 불러옵니다. 불필요한 GPU 메모리 소모를 극적으로 줄이기 위해 4비트 양자화 방식을 활용해 적재합니다.
from transformers import BitsAndBytesConfig, LlavaNextVideoForConditionalGeneration, LlavaNextVideoProcessor import torch quantization_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16 ) processor = LlavaNextVideoProcessor.from_pretrained("llava-hf/LLaVA-NeXT-Video-7B-hf") model = LlavaNextVideoForConditionalGeneration.from_pretrained( "llava-hf/LLaVA-NeXT-Video-7B-hf", quantization_config=quantization_config, device_map='auto' )
모델 대상 질의 전송
Pinecone 데이터베이스의 유사 이미지 클립 반환 결과에 포함된 물리 오프셋 값을 역계산해 조각난 소스 세그먼트 파일명을 파싱해낼 유틸리티 기능입니다. video_segment_name_from_offset() 함수는 비디오 경로와 시작 시각 값을 받아 규칙화된 파일 포맷 명칭을 반환합니다.
def video_segment_name_from_offset(video_path, start_time, segment_length = 6): segment_number = int (start_time // segment_length) simple_video_name = os.path.splitext(os.path.basename(video_path))[0] return f"{simple_video_name}_segment_{segment_number:03d}"
이제 파싱된 클립을 가져온 뒤 모델에 던질 형식을 생성합니다. 순서에 맞춘 사용자와 텍스트-비디오 모달리티 대화 템플릿(Conversation structure) 형태로 입력을 구성하고, 전용 텐서 디큐잉 및 빌딩 단계를 거쳐 지시 추론을 수행하게 됩니다.
video_segment = similar_content['matches'][0]['metadata']['video_file'] print(video_segment) video_offset = similar_content['matches'][0]['metadata']['start_time'] video_segment_name = video_segment_name_from_offset(video_segment, video_offset) video_segment = sampled_video_info[video_segment_name]['sampled_video'] # Each "content" is a list of dicts and you can add image/video/text modalities conversation = [ { "role": "user", "content": [ {"type": "text", "text": query}, {"type": "video"}, ], }, ] prompt = processor.apply_chat_template(conversation, add_generation_prompt=True) prompt_len = len(prompt) inputs = processor([prompt], videos=[video_segment], padding=True, return_tensors="pt").to(model.device) generate_kwargs = {"max_new_tokens": 100, "do_sample": True, "top_p": 0.9} output = model.generate(**inputs, **generate_kwargs) generated_text = processor.batch_decode(output, skip_special_tokens=True) print(generated_text[0])
비교 평가
두 모델에서 뽑아낸 결과를 직접 비교해 보면, LLaVA-NeXT-Video와 비교해 Pegasus가 제공하는 답변이 타임 범위 및 상황 맥락적으로 훨씬 우수하고 정보 밀도가 높음을 확인할 수 있습니다.
다만, 데이터가 쪼개진 탓에 두 모델 모두 단편 조각 클립만 입력으로 받아서는 전체 비디오 내 종합 맥락을 관찰하는 데 다소 제약이 있습니다. 이제 두 방식에 완전한 크기의 비디오 전량을 제공하는 경우 어떻게 다른지 비교해 보겠습니다.
여러 비디오 다루기
구조화되지 않은 상태의 완전한 영상들이 디렉터리에 놓여 있는 환경에서 이 아카이브 전체에 결친 고차원 질의 연동 예시입니다. 이전 시점에는 세그먼테이션된 조각 데이터셋으로 질의 유사 클립을 정렬했다면, 이번에는 해당 클립의 원천이 되는 완전한 무삭제 원본 비디오 파일 자체를 모델에 피딩할 예정입니다. Pinecone 벡터 내 세그먼트 메타데이터에 파일 상위 계층 구조가 저장되어 있기에 손쉽게 매핑할 수 있습니다.
일단 타겟 영상을 Pinecone에 일괄 분석·인덱싱 처리하겠습니다.
ads_dir = os.path.join(base_folder_path,"ad_vids") video_list = [] #Make sure that we' don't waste time re-embedding the original single video: single_video_filename = os.path.splitext(os.path.basename(single_video))[0] for filename in os.listdir(ads_dir): if filename.endswith(".mp4") and single_video_filename not in filename: video_list.append(ads_dir + "/" + filename) print(video_list) for video in video_list: ingest_data(video)
이어서 데이터베이스에 전질의하여, 의미상 가장 밀접도가 높은 영상 후보군을 가려내기 위한 샘플 질문을 몇 개 작성해 보겠습니다.
full_database_questions = ["Who is the actor in the Miss Dior video?", "What ad is Selena Gomez in?", "What is the ad for Rare Beauty about?", "Why should people buy the Rare Beauty product according to their ad?"] question = full_database_questions[0] similar_content_from_question = retrieve_similar_content(question) video_name = similar_content_from_question['matches'][0]['metadata']['video_file']
Pegasus 구현
파이썬 SDK 연동 가이드를 따라 Pegasus 모델상에서 질의를 이어나가기 위한 절차를 살펴보겠습니다. 이미 인덱스는 앞에서 한 차례 가설했기에, 이번에는 쿼리에 매핑할 전체 영상만을 태스크 업로드하면 준비 작업이 완성됩니다.
Twelve Labs 측으로 원본 영상 일괄 업로드
디렉터리에 존재하는 여러 동영상 파일을 반복 순회하여 태스크를 요청하고, 식별 식별자인 비디오 ID들을 고유 변수에 매핑합니다.
for vid in os.listdir(ads_dir): vid_path = os.path.join(ads_dir, vid) vid_name = os.path.splitext(os.path.basename(vid_path))[0] print(vid_path) video_id = upload_video_to_twelve_labs(vid_path) video_ids[vid_name] = video_id
전체 비디오 ID 구조 기반으로 Pegasus 질의 실행
결과적으로 검색된 메타데이터 비디오 ID 고유 식별 명칭에 직접 질의 지시문(Prompt)을 투사하여 텍스트 응답을 정밀 획득해 내는 구문입니다.
video_id = video_ids[video_name] res = twelvelabs_client.generate.text( video_id=video_id, prompt=question ) print(f"{res.data}")
오픈소스 LLaVA-NeXT-Video 기반 질의 수행
통합 비디오 샘플링
가용한 원본 비디오 파일들 전부를 순차 샘플링 배열로 포맷 처리한 후, 사전에 추려 진 고유 명칭과 일치하는 임베딩 배열을 식별해 맵핑해야 합니다.
sampled_database_video_info = process_videos_in_folder(ads_dir) video_segment = sampled_database_video_info[video_name]['sampled_video']
추론 런타임 가동
이제 로컬 또는 탑재한 추론 엔진으로 해당 데이터 샘플상에 모델을 순회합니다.
사용자 정보 역할군 정의 및 질의(Question)와 대표 비디오 배열 정보를 대화 포맷으로 매핑합니다.
해당 모델 인터페이스 템플릿으로 변환 후, 입출력 디코드 시 조건 인자(`max_new_tokens`, `do_sample`, `top_p`) 세트를 토대로 연계를 시도합니다.
최종 디코딩 단계를 거쳐 추출한 프롬프트 완성문을 로깅 모듈로 터미널 화면에 노출합니다.
conversation = [ { "role": "user", "content": [ {"type": "text", "text": question}, {"type": "video"}, ], }, ] prompt = processor.apply_chat_template(conversation, add_generation_prompt=True) prompt_len = len(prompt) inputs = processor([prompt], videos=[video_segment], padding=True, return_tensors="pt").to(model.device) generate_kwargs = {"max_new_tokens": 100, "do_sample": True, "top_p": 0.9} output = model.generate(**inputs, **generate_kwargs) generated_text = processor.batch_decode(output, skip_special_tokens=True) print(generated_text[0])
비교 평가
두 가지 결과 파일을 직접 수급해 확인해 보면, Pegasus는 디올(Miss Dior) 광고 영상 맥락 속에 등장한 배우 "나탈리 포트만"의 존재를 명확히 판별하고 정확하게 대답합니다. 대조적으로 로컬 LLaVA-NeXT-Video 구조에서는 나탈리 포트만의 등장 유무 자체를 이해하지 못하거나 프레임 샘플 해상도 단계에서 정보를 놓쳐 식별에 실패합니다. 또한 질문에 명확히 응답하지 못하고 엉뚱한 설명을 장황하게 늘어놓는 경향(Hallucination)이 있으며, 응답 지연 시간(Latency)이 크게 길어져 실제 프로덕션 서비스에 바로 투입하기에는 한계가 있음을 알 수 있습니다.
결론 (Conclusion)
본 튜토리얼 아티클에서는 개별 영상 파일부터 가용할 수 있는 완전한 영상 데이터셋 군집 도메인 전체에 이르기까지 비디오와 유기적으로 대화하는 방법을 다루었습니다. 데이터셋 인덱싱 정렬 및 유사 클립 수급 통제 장치로서 Twelve Labs Embed API와 Pinecone 벡터 엔진을 융합하는 방법을 완벽히 이해하는 데 기여했습니다.
동시에 Twelve Labs 고유의 Pegasus 모델과 범용 비디오 멀티모달 오픈소스 진영인 LLaVA-NeXT-Video 간의 필수 인프라 비용 부담, 개발 엔지니어링 활용성(DX), 세부 정밀 탐색 신뢰도를 다각도로 비교해 보았습니다. Pegasus는 복잡한 하부 인프라 운영 부담 없이 지시 프롬프트 수행 부문에서 확실하게 정밀하고 깔끔한 사용 환경 메커니즘을 제공해 줌을 보여줍니다.
주요 모범 사례 (Best Practices)
안정적인 엔터프라이즈 환경 구축이 주요 목적인 경우, 전용 호스트 활용 측면에서 Pinecone의 Pod 기반 서비스 구조를 채택하는 것을 권장합니다.
오픈소스 탑재 시 샘플링 프레임의 크기를 무조건 확대하기보다 추론 연산 처리 정밀성 및 서비스 지연 속도 사이의 효율적 절충안(Trade-off) 지점을 세심히 고려해야 합니다.
로컬 운영이 필수적이라면 양자화 기법을 적극 접목해 속도 부하를 완화하는 것이 좋습니다. 사내 램 소모 사양 및 대답 완성 완성도를 모니링하면서 비즈니스 상황에 최적화된 선택을 하시길 바랍니다.
다음 도전 과제 (Next Steps)
수십만 건 이상의 방대한 영상 데이터 아카이브를 핸들링할 때 탐색 품질이 떨어질 수 있습니다. 이를 우회하고 정합성을 정화할 수 있는 솔루션은 다음과 같습니다.
임베딩 결과 레이어 최상단 영역에 단순 선형 보정기(Linear adapter)를 수립 보완하여 자사 고유 형태 도메인 데이터셋 규격에 정합합니다.
다양한 동영상에서 반환된 후보군이 있을 경우, 2차 단에서 라이트하게 Pegasus 추론을 돌려 후보 영상들의 타당성 우선순위를 재정렬(Re-ranking)합니다.
Pinecone 항목의 메타데이터 측면에 비디오 전 구간의 자동 생성된 요약문 데이터셋을 복합 구축하여 하이브리드 서치 방식을 통해 의미적 탐색 범주를 비약적으로 개선합니다. 자세한 사항은 Pinecone 메타데이터 연동 문서를 참조해주시기 바랍니다.
부록 (Appendix)
심화 학습과 직접 실습을 위한 유용한 참고 자료 모음입니다.
구현 소스 코드가 포함된 Colab Notebook 주소
세부 활용 Twelve Labs 가이드 문서
Pinecone 공식 통합 라이브러리 클라이언트 SDK 문서 및 클라우드 백서





