초대하고 적립

초대 보상 안내

초대 링크를 공유하세요. 친구가 링크로 가입하고 충전하면 이후 충전마다 표시된 보상을 받을 수 있습니다.

Gemini API를 활용한 장시간 비디오 인덱싱: 타임스탬프, 시각 정보 및 누락 구간 감사

Gemini API를 활용하여 장시간 비디오 녹화본을 인덱싱하는 엔지니어링 워크플로입니다. Files API를 통한 대용량 파일 업로드, Interactions API에서의 에이전틱(Agentic) 모드 초안 생성, 구조화된 데이터 행의 프로그래밍 방식 유효성 검증, 그리고 타임라인 누락 구간에 대한 정밀한 타깃 검수 파이프라인을 제시합니다.

목차
Gemini API를 활용한 장시간 비디오 인덱싱: 타임스탬프, 시각 정보 및 누락 구간 감사

기술 발표, 워크숍, 아키텍처 리뷰 회의, 스크린캐스트와 같은 장시간 비디오 녹화본은 발표자의 음성과 화면상의 시각 자료 전반에 걸쳐 고밀도 정보를 담고 있습니다. 일반적인 고수준 요약은 포괄적인 주제만 다루기 때문에, 엔지니어가 특정 터미널 명령어나 설정 파라미터를 찾으려면 긴 녹화 영상을 직접 앞뒤로 탐색(scrubbing)해야 하는 비효율이 발생합니다.

Gemini 멀티모달 모델은 오디오와 비디오 스트림을 동시에 분석하여 타임스탬프가 포함된 구조화된 이벤트 인덱스를 생성할 수 있습니다. 하지만 모델의 초기 1차 추론 결과는 어디까지나 **초안 후보군(draft candidate set)**일 뿐이며, 즉시 실무에 투입 가능한 완성형 참조 색인이 아닙니다. 신뢰할 수 있는 인덱스를 구축하려면 Files API를 통한 1회 업로드 및 URI 재사용, 초안 구간 추출, 출력 스키마의 엄격한 유효성 검증, 의심스러운 누락 구간 감사, 그리고 타깃 정밀 보정으로 이어지는 체계적인 파이프라인이 필수적입니다.


아키텍처: 비디오 전달 방식 및 처리 모드

현재 Google Video Understanding 공식 문서에서는 주요 예제에 Interactions APIgoogle-genai 라이브러리를 사용하고 있습니다. 하위 호환성을 위해 기존 generate_content 메서드도 여전히 지원되지만, Interactions API를 사용하면 멀티모달 처리 파라미터를 더욱 투명하게 제어할 수 있습니다.

1. 비디오 전달 방식

  • Files API (장시간 비디오 권장): 수 분에서 수 시간에 이르는 영상에 가장 적합합니다. 파일을 한 번만 업로드하면 서버에서 인덱싱이 수행되며, 이후 원시 바이트를 재전송할 필요 없이 URI를 통해 여러 차례 반복 참조할 수 있습니다.
  • Google Cloud Storage (GCS): 이미 Google Cloud 인프라 내에 보관되어 있는 기존 비디오 아카이브를 활용할 때 적합합니다.
  • 인라인 데이터(Inline Data): 요청 페이로드 본문에 원시 바이트를 직접 포함하여 전송하는 방식입니다. 공식 문서에는 환경에 따라 허용되는 페이로드 제한이 명시되어 있으며, 1시간 분량의 영상을 안정적으로 처리할 때는 원시 비디오를 인라인으로 스트리밍하는 대신 Files API나 Cloud Storage를 사용하는 것이 안정적이고 실용적인 선택입니다.

2. 처리 모드: 정적 모드 vs 에이전틱 모드

  • 정적 처리(Static Processing):
    기본적으로 모델은 초당 1프레임(1 FPS)의 고정 주기로 프레임을 샘플링합니다. 영상의 1초마다 토큰으로 변환되므로 60분 분량의 영상에서는 상당한 양의 컨텍스트 부하가 누적됩니다. 이때 유의할 점은 1 FPS 샘플링 방식이 1초 미만의 창 전환이나 순식간에 사라지는 툴팁과 같은 짧은 시각적 이벤트를 놓칠 수 있으며, 미세한 인터랙션을 모두 빠짐없이 포착한다고 보장할 수 없다는 것입니다.
  • 에이전틱 비디오 이해(Agentic Video Understanding):
    모델이 비디오를 동적으로 탐색하면서 필요에 따라 특정 프레임과 오디오 세그먼트를 능동적으로 가져옵니다. 이를 통해 처리되는 컨텍스트 토큰의 양을 대폭 줄일 수 있습니다.
    한계점: 에이전틱 휴리스틱은 음성 신호와 의미적 트리거에 크게 의존합니다. 발표자가 음성 설명 없이 화면에서 묵묵히 작업(명령어 입력, 다이어그램 검토 등)을 수행할 경우, 휴리스틱이 해당 구간을 유휴 배경으로 판단하여 세부 비주얼 프레임 조회를 건너뛸 수 있습니다.

1단계. 비디오 업로드 및 상태 폴링

Files API로 전송된 파일은 업로드 즉시 추론에 사용할 수 없습니다. 서버 측에서 미디어 컨테이너를 해제하고 오디오 및 비디오 트랙을 인덱싱하는 작업이 선행되어야 하기 때문입니다. 따라서 애플리케이션은 해당 리소스의 상태가 ACTIVE가 될 때까지 주기적으로 상태를 폴링해야 합니다.

import time
from google import genai

client = genai.Client()

video_path = "tech_workshop_60min.mp4"
video_file = client.files.upload(file=video_path)

while not video_file.state or video_file.state.name != "ACTIVE":
    if video_file.state and video_file.state.name == "FAILED":
        error_info = getattr(video_file, "error", None)
        raise RuntimeError(
            f"파일 상태가 FAILED로 변경되었습니다. 세부 정보: {error_info}. "
            "코덱, 컨테이너 무결성을 확인하거나 다시 시도하세요."
        )
    time.sleep(5)
    video_file = client.files.get(name=video_file.name)

print("비디오 처리 준비가 완료되었습니다.")

2단계. Interactions API를 통한 초안 인덱스 요청

장시간 녹화본의 경우 "processing": "agentic" 파라미터와 함께 client.interactions.create 메서드를 호출합니다. 프롬프트에는 엄격한 표 형식 출력을 강제하여 타임스탬프 범위(MM:SS - MM:SS), 이벤트 유형(visual, speech, hybrid), 간결한 음성 발화 요지(CLAIM), 화면상의 동작(ACTION)을 명시하도록 구성합니다.

index_prompt = """
당신은 동영상 기술 인덱싱 도구입니다. 
00:00부터 60:00까지의 전체 60분 녹화본에 대해 시간순 이벤트 인덱스를 한국어로 생성하세요.

응답 구조 요구사항:
1. 결과를 구분자 |를 사용하는 TSV 형식으로 한 줄씩 출력하세요.
2. 각 행은 정확히 5개의 필드를 포함해야 합니다:
   START_TIME (MM:SS) | END_TIME (MM:SS) | TYPE (visual/speech/hybrid) | CLAIM | ACTION
3. 너무 긴 시간 구간을 하나의 항목으로 병합하지 마세요. 슬라이드 전환, 터미널 작업, 에러, 연사의 결론을 기록하세요.
4. 음성이 없는 경우 CLAIM 필드에 NONE을 지정하세요. 화면에 움직임이 없는 경우 ACTION에 STATIC을 지정하세요.
5. 서두나 부가 설명 없이 오직 데이터 행만 출력하세요. CLAIM 및 ACTION 필드의 모든 설명은 한국어로 작성해야 합니다.
"""

interaction = client.interactions.create(
    model="gemini-3.8-flash",
    input=[
        {
            "type": "video",
            "uri": video_file.uri,
            "mime_type": video_file.mime_type,
            "processing": "agentic"
        },
        {
            "type": "text",
            "text": index_prompt
        }
    ]
)

raw_index_output = interaction.output_text

에이전틱 탐색 단계에 대한 참고 사항:
interaction.steps 내부에 processing_call 항목이 존재한다는 것은 모델이 영상을 연속된 스트림으로 순차 처리한 것이 아니라 타임라인을 동적으로 탐색했음을 의미합니다. 하지만 이러한 호출의 존재는 모델이 탐색 작업을 수행했다는 사실만 확인할 뿐, 타임라인의 모든 중요 이벤트에 대한 출력의 완전성(output completeness)을 보장하지는 않습니다.


3단계. 출력 구조 예시 (가상 초안 데이터)

다음은 기대하는 데이터 스키마를 보여주는 모델 출력의 가상 예시입니다.

00:00 | 02:15 | hybrid | 도입부 안내, 샤딩 클러스터 마이그레이션 아젠다 소개 | 발표 타이틀 슬라이드, 연사 웹캠 창
02:16 | 05:40 | visual | NONE | Miro에서 주문 서비스 아키텍처 다이어그램으로 화면 전환
05:41 | 09:12 | speech | 분산 트랜잭션 대신 Saga 패턴을 선택한 이유 설명 | Miro의 정지된 다이어그램 화면, 커서 움직임 없음
27:30 | 29:10 | visual | NONE | 콘솔 창 열기, DB 복제본 배포 스크립트 실행
29:11 | 32:45 | hybrid | 네트워크 연결 유실 시 split-brain 시나리오 분석 | 터미널에 etcd 로그 출력, 시간 초과 항목 강조 표시
54:10 | 57:25 | speech | 데이터 복제 허용 지연 시간에 대한 질의응답 | 연사 연락처가 포함된 최종 슬라이드
57:26 | 60:00 | hybrid | 세션 요약 및 저장소 링크 안내 | 화면에 QR 코드 표시, 화상 회의 종료

4단계. 마크업 유효성 검증 및 로컬 검색

LLM의 원시 응답은 별도의 검증 절차 없이는 구조화된 데이터 세트로 신뢰할 수 없습니다. 안정적인 파서는 유효하지 않은 레코드를 조용히 폐기하지 않고, 수동 수정을 위해 별도로 격리해야 합니다. 유효성 검증기는 정확히 5개의 필드가 존재하는지, 이벤트 유형(visual, speech, hybrid)이 올바른지, 그리고 타임스탬프가 영상 전체 길이 범위 내에서 올바른 MM:SS 형식을 만족하는지 강제합니다.

import csv
import re
from typing import List, Dict, Tuple

TIME_PATTERN = re.compile(r"^(\d{2}):([0-5]\d)$")
ALLOWED_TYPES = {"visual", "speech", "hybrid"}

def time_to_seconds(t_str: str) -> int:
    match = TIME_PATTERN.match(t_str)
    if not match:
        raise ValueError(f"잘못된 시간 형식: {t_str}")
    m, s = map(int, match.groups())
    return m * 60 + s

def parse_and_validate_tsv(
    raw_text: str, 
    max_duration_sec: int = 3600
) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]:
    valid_rows = []
    malformed_rows = []
    
    lines = [line.strip() for line in raw_text.strip().splitlines() if line.strip()]
    
    for idx, line in enumerate(lines, start=1):
        cols = [c.strip() for c in line.split("|")]
        
        if len(cols) != 5:
            malformed_rows.append({
                "line": idx,
                "content": line,
                "error": f"5개 필드를 예상했으나 {len(cols)}개가 입력되었습니다"
            })
            continue
            
        start_str, end_str, event_type, claim, action = cols
        
        if event_type.lower() not in ALLOWED_TYPES:
            malformed_rows.append({
                "line": idx,
                "content": line,
                "error": f"유효하지 않은 이벤트 유형: {event_type}"
            })
            continue
            
        try:
            s_sec = time_to_seconds(start_str)
            e_sec = time_to_seconds(end_str)
        except ValueError as err:
            malformed_rows.append({
                "line": idx,
                "content": line,
                "error": str(err)
            })
            continue
            
        if s_sec > e_sec:
            malformed_rows.append({
                "line": idx,
                "content": line,
                "error": f"시작 시간({start_str})이 종료 시간({end_str})보다 큽니다"
            })
            continue
            
        if e_sec > max_duration_sec:
            malformed_rows.append({
                "line": idx,
                "content": line,
                "error": f"타임코드 {end_str}는 전체 재생 시간({max_duration_sec}초)을 초과합니다"
            })
            continue
            
        valid_rows.append({
            "start": start_str,
            "end": end_str,
            "start_sec": s_sec,
            "end_sec": e_sec,
            "type": event_type.lower(),
            "claim": claim,
            "action": action
        })
        
    return valid_rows, malformed_rows

def search_index(
    rows: List[Dict[str, str]], 
    keyword: str = None, 
    event_type: str = None
) -> List[Dict[str, str]]:
    results = []
    for r in rows:
        if event_type and r["type"] != event_type.lower():
            continue
        if keyword:
            kw = keyword.lower()
            if kw not in r["claim"].lower() and kw not in r["action"].lower():
                continue
        results.append(r)
    return results

valid_index, errors = parse_and_validate_tsv(raw_index_output, max_duration_sec=3600)

if errors:
    print(f"잘못된 행이 {len(errors)}개 발견되었습니다. 수동으로 수정해야 합니다:")
    for err in errors:
        print(f"행 {err['line']}: {err['error']} -> {err['content']}")
else:
    print(f"모든 행이 유효합니다. 인덱스 레코드 수: {len(valid_index)}")

5단계. 커버리지 감사 및 누락 구간 식별

인덱스를 배포하거나 확정하기 전에 타임라인에 사각지대가 없는지 감사를 진행해야 합니다.

  1. 기준 구간(Benchmark Zones) 샘플 점검:
    영상의 시작부(도입부 및 타이틀 슬라이드), 중간 지점(일반적으로 라이브 데모나 아키텍처 토론이 집중되는 구간), 그리고 종료부(Q&A 및 마무리 인사)를 수동으로 교차 확인합니다.
  2. 타임스탬프 간격 분석:
    인덱스 항목 간 허용 가능한 시간 간격에 대한 만능 기준값은 없으며, 허용 오차는 유스케이스에 따라 달라집니다. 고밀도 스크린캐스트에서는 90초의 공백이 중요한 설정 단계 누락을 의미할 수 있지만, 개괄적인 강의에서는 단일 주제가 5분 동안 이어져도 완전히 정상일 수 있습니다. 특정 구간의 간격이 발표 흐름과 맞지 않게 지나치게 벌어져 있다면 의심 구간으로 플래그를 지정하십시오.
  3. 무음 시각 이벤트 점검:
    발표자가 음성 설명 없이 화면 시연만 진행한 경우, 에이전틱 모드에서 해당 구간이 누락될 수 있습니다. 이러한 시간대는 타깃 정적 검수 단계로 전달해야 합니다.

6단계. 정적 클립을 통한 의심 구간의 타깃 정밀 검수

의심스러운 구간을 재평가할 때 60분 분량의 영상 전체를 다시 실행할 필요는 없습니다. 클립 슬라이싱 기능을 활용하면 start_offsetend_offset을 사용하여 정적 모드 분석을 초 단위의 정확한 구간으로 한정할 수 있습니다.

방식의 한계점:
1 FPS 정적 모드는 고정된 샘플링 그리드를 제공하므로 광범위한 에이전틱 탐색 과정에서 놓친 작업을 발견하는 데 도움을 줍니다. 하지만 완벽한 재현율(recall)을 보장하지는 않습니다. 1초 미만으로 빠르게 지나간 시각적 변경 사항은 샘플링 프레임 사이에 누락될 수 있습니다.

start_sec = 1200
end_sec = 1380

targeted_inspection = client.interactions.create(
    model="gemini-3.8-flash",
    input=[
        {
            "type": "video",
            "uri": video_file.uri,
            "mime_type": video_file.mime_type,
            "processing": {
                "type": "static",
                "start_offset": start_sec,
                "end_offset": end_sec,
                "fps": 1.0
            }
        },
        {
            "type": "text",
            "text": (
                "화면의 변경 사항을 시간 순서대로 한국어로 설명하세요. "
                "창 전환 등의 설명은 한국어로 기록하되, 터미널 명령어와 시스템 메시지는 원문 그대로 유지하세요."
            )
        }
    ]
)

print("구간 세부 검사 결과:")
print(targeted_inspection.output_text)

새롭게 추출된 항목은 수동으로 또는 반자동 검토 단계를 거쳐 검증된 인덱스에 병합합니다.

errors 목록의 모든 항목을 해결하고 원본 영상과 대조하여 타임스탬프를 검증했다면 최종 인덱스를 내보냅니다. 아래 코드 스니펫은 파서에 여전히 오류가 남아 있을 경우 의도적으로 실행을 중단하므로, 실행 전에 valid_index에 검증 및 수정이 완료된 데이터가 반영되어 있는지 확인하십시오.

if errors:
    raise ValueError("errors에 있는 행을 수정한 후 내보내기 전에 다시 확인하세요")

with open("video_index.csv", "w", newline="", encoding="utf-8") as output_file:
    writer = csv.DictWriter(
        output_file,
        fieldnames=["start", "end", "type", "claim", "action"],
        extrasaction="ignore",
    )
    writer.writeheader()
    writer.writerows(valid_index)

이렇게 생성된 CSV 파일을 통해 사용자는 특정 발화 내용이나 화면 동작을 검색하고 원본 영상의 해당 구간으로 즉시 이동할 수 있습니다. 다만 인간 편집자가 원본 녹화본을 바탕으로 단순한 샘플링 경계뿐만 아니라 어떤 이벤트가 선택되었는지와 해당 이벤트 경계를 모두 직접 확인할 때까지는 여전히 초안 상태로 취급해야 합니다.


문제 해결 및 엣지 케이스

  • Files API 업로드 중 FAILED 상태 발생:
    API 진단 정보 없이 임의로 원인을 단정하지 마십시오. 실패 원인은 지원되지 않는 컨테이너 포맷, 손상된 파일 헤더, 일시적인 인프라 오류 등 다양합니다. SDK를 통해 file.error 속성을 확인하고, ffprobe로 로컬 파일 재생 가능 여부를 점검하며, 필요시 ffmpeg(-c:v libx264 -c:a aac)를 사용해 스트림을 표준화한 뒤 재시도하십시오.
  • 401 Unauthorized 또는 네트워크 끊김 현상:
    401 오류는 처리 세션 만료가 아니라 인증 실패(유효하지 않거나 누락된 API 키, 혹은 설정되지 않은 GEMINI_API_KEY 환경 변수)를 명확히 의미합니다. 장시간 호출 중 클라이언트 HTTP 연결 시간 초과를 방지하려면 stream=True를 활성화하여 스트리밍 모드를 사용하십시오.
  • 전체 영상 길이를 초과하는 타임스탬프:
    컨텍스트의 복잡도가 증가함에 따라 이러한 드리프트가 발생할 수 있습니다. 프롬프트에 엄격한 지침을 명시하고 파서에서 프로그래밍 방식의 유효성 검사(e_sec > max_duration_sec)를 수행하여 이를 방지하십시오.
  • TSV 구조적 드리프트:
    형식이 깨지는 경우 비정상 라인을 malformed_rows로 분류하고, 시스템 프롬프트에 1~2개의 퓨샷(few-shot) 참조 예시 행을 제공하십시오.

배포 전 검증 워크플로

  1. 에셋 준비 상태 확인: Files API에서 파일 상태가 ACTIVE로 전환되었는지 확인합니다.
  2. 초기 초안 생성: Interactions API의 agentic 처리 모드를 사용하여 기준 인덱스를 생성합니다.
  3. 프로그래밍 방식 유효성 검증 수행: 모든 행이 필수 5개 필드, 유효한 이벤트 유형, MM:SS 형식을 만족하는지 확인합니다. 수정이 필요한 비정상 라인은 격리합니다.
  4. 커버리지 감사: 기준 구간(시작, 중간, 끝)을 점검하고 발표 진행 속도와 비교하여 타임스탬프 밀도를 평가합니다.
  5. 타깃 정밀 검수 진행: 정적 클립(start_offset/end_offset)을 사용하여 의심스러운 공백 구간이나 음성 없는 화면 세그먼트를 재검토합니다.
  6. 수동 무작위 보정 수행: 비디오 플레이어에서 무작위 타임스탬프를 탐색하며, 유스케이스의 요구에 따라 각 이벤트의 실제 시작 시간을 관련 원본 오디오/비디오와 대조하여 확인합니다. 오디오 전용 이벤트도 발생할 수 있으므로 일치하는 시각적 변화를 요구하지는 마십시오.

요청 스키마, 처리 모드 및 샘플링 제약 조건에 대한 자세한 내용은 공식 Gemini Video Understanding 문서를 참조하십시오.

LLM 워크플로를 최적화할 준비가 되셨나요?

하나의 API로 모델을 연결하고 키와 AI 비용을 관리하세요.

무료로 시작하기