한 번에 끝내는 Video Understanding/Generation: “프레임 분석 파이프라인” 설계 패턴
비디오 AI를 프로젝트에 붙일 때 제일 먼저 부딪히는 문제는 “모델이 똑똑하냐”가 아니라 입력 비디오를 어떤 단위(shot/clip/frame)로 쪼개고, 무엇을 저장하며, 어떻게 재질의(re-query)할지입니다.
들어가며
비디오 AI를 프로젝트에 붙일 때 제일 먼저 부딪히는 문제는 “모델이 똑똑하냐”가 아니라 입력 비디오를 어떤 단위(shot/clip/frame)로 쪼개고, 무엇을 저장하며, 어떻게 재질의(re-query)할지입니다. 긴 비디오에서 uniform sampling만으로는 중요한 구간을 놓치고(정보 손실), 반대로 프레임을 많이 넣으면 컨텍스트/비용이 터집니다. 이 때문에 2026년에는 “video LLM” 자체보다 프레임 선택 + 타임스탬프 기반 증거(evidence) 구성 + temporal grounding이 핵심 기술로 굳어지는 흐름입니다. (예: long video temporal reasoning/grounding 계열)1
언제 쓰면 좋나:
- 긴 비디오(수십 분~수시간) QA/검색/요약, 공정/제조/교육 영상처럼 “특정 순간을 정확히 찾아야” 하는 도메인(시간 축이 본질)1
- 감사/컴플라이언스처럼 “모델 답변”보다 근거 구간(타임스탬프) 제시가 중요한 제품2
언제 쓰면 안 되나:
- 10~30초 짧은 클립만 다루고, 지연/비용이 매우 민감하며, “대충” 이해해도 되는 경우: 과한 파이프라인은 운영 복잡도만 올립니다.
- 생성(T2V/I2V/V2V)에서 “품질”이 절대 기준인데, 인프라/비용 제약이 심한 경우: 최신 연구는 consistency를 위해 메모리/계층 latent/멀티샷 구조 등을 쓰며 계산량이 커지기 쉽습니다.3
🔧 핵심 개념
1) “프레임”이 아니라 Shot/Clip/Evidence로 다루는 이유
2026년 long-video 쪽은 공통적으로:
- 비디오를 짧은 clip으로 나눠 로컬 특징을 만들고
- 이를 겹치는 윈도우/전역 집계로 long-range를 만든 뒤
- 최종적으로 LLM이 답을 만들되 시간 축을 명시적으로 취급합니다. (Temporal reasoning/long video 모델 설계)1
여기서 실무 핵심은 “모델 입력 토큰”이 아니라 증거 단위(evidence unit) 입니다.
- evidence = {shot_id, start/end_ts, keyframes, transcript span, extracted objects/actions, embeddings}
- 질의는 evidence를 먼저 좁히고 → 좁혀진 구간만 고해상도로 다시 분석합니다.
2) Temporal Grounding: “정답”보다 정답 구간이 제품을 살린다
Video Temporal Grounding(VTG)은 “이 질문의 답이 나오는 시간이 언제냐”를 맞히는 문제고, 멀티모달 LLM이 이를 강하게 밀고 있습니다.2
실전에서 VTG를 넣으면 좋은 점:
- hallucination이 나와도 근거 구간을 함께 리턴하게 강제 가능
- 사용자 경험: “요약”보다 “클릭하면 그 장면으로 점프”가 훨씬 강함
구현 패턴: 1) 1차 패스: 저비용으로 shot boundary + 거친 keyframe + ASR 2) 2차 패스: 질문 기반 retrieval(embedding)로 후보 shot top-k 3) 3차 패스: 후보 shot만 고밀도 프레임/객체/행동 추출 + LLM 답변(+timestamp)
3) Frame Selection: uniform sampling의 시대는 끝
오픈소스 쪽에서도 scene-change-aware sampling(ffmpeg scdet 등)로 shot을 자르고, 그 안에서만 프레임을 뽑아 구조화 JSON을 만드는 패턴이 보입니다.4
또한 “학습 없이(inference-only) long video LVLM 입력을 최적화”하는 frame selection 프레임워크도 2026 CVPR/ECCV 라인에서 강하게 나옵니다.5
차이점 정리:
- Uniform: 구현 쉬움 / 중요한 순간 누락 / 토큰 낭비
- Scene boundary 기반: 정보 밀도 ↑ / 파이프라인 복잡도 ↑ / shot 내부 이벤트(짧은 동작) 놓칠 수 있음
- Query-aware(tool-augmented, 재샘플링): 비용 제어가 가장 좋음 / 시스템 설계 난이도 ↑6
4) Generation(생성) 쪽: 2026년 키워드는 Consistency를 “구조적으로” 해결
비디오 생성은 이제 화질보다 semantic/identity/camera/multi-shot consistency가 병목이고, 이를 시스템적으로 분해(divide-and-conquer)하거나, 멀티샷 오토리그레시브/캐시/계층 latent로 푸는 접근이 활발합니다.7
편집(V2V)에서는 “멀티턴 편집에서 턴이 바뀔 때마다 일관성이 무너지는” cross-turn consistency를 메모리로 잡는 방향이 등장합니다.3
실무 관점 결론:
- 생성 모델을 “그냥 호출”하면 일관성 요구사항에서 금방 깨집니다.
- 이해(grounding) 파이프라인을 같이 깔아 “무엇이 유지돼야 하는지(캐릭터/배경/카메라)”를 구조화해두는 팀이 결국 이깁니다.
💻 실전 코드
아래는 “긴 비디오에서 질문 기반으로 근거 구간을 찾아 답변”하는 현실적인 파이프라인 스켈레톤입니다.
- 전제: 로컬에서 비디오 파일을 처리
- 구성: 1) ffmpeg로 shot boundary 탐지 2) shot마다 keyframe N장 추출 3) whisper로 ASR(타임스탬프 포함) → shot과 align 4) (선택) OpenAI-compatible vision model로 keyframe 캡션/객체를 JSON evidence로 저장 5) 질의 시 evidence embedding으로 top-k shot을 뽑고, 그 구간만 재분석
0) 의존성/설치
1
2
3
4
5
6
# macOS 예시
brew install ffmpeg
python -m venv .venv
source .venv/bin/activate
pip install -U faster-whisper opencv-python numpy pydantic rich faiss-cpu
1) Shot boundary + keyframe 추출 (scene-change-aware)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# video_index.py
import json, os, subprocess, math
from dataclasses import dataclass, asdict
from typing import List, Tuple
@dataclass
class Shot:
shot_id: int
start: float
end: float
keyframes: List[str]
def run(cmd: List[str]) -> str:
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, check=True)
return p.stdout
def detect_shots(video_path: str, threshold: float = 0.35) -> List[Tuple[float, float]]:
"""
ffmpeg scdet 기반. 출력 로그에서 pts_time 파싱 후 shot 구간 생성.
"""
cmd = [
"ffmpeg", "-hide_banner", "-i", video_path,
"-vf", f"select='gt(scene,{threshold})',showinfo",
"-an", "-f", "null", "-"
]
out = run(cmd)
times = []
for line in out.splitlines():
if "pts_time:" in line:
# ... pts_time:12.345 ...
t = float(line.split("pts_time:")[1].split()[0])
times.append(t)
# shot boundary = [0] + times + [duration]
dur = probe_duration(video_path)
boundaries = [0.0] + sorted(set([t for t in times if 0 < t < dur])) + [dur]
shots = [(boundaries[i], boundaries[i+1]) for i in range(len(boundaries)-1) if boundaries[i+1] - boundaries[i] > 0.5]
return shots
def probe_duration(video_path: str) -> float:
cmd = [
"ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1", video_path
]
out = run(cmd).strip()
return float(out)
def extract_keyframes(video_path: str, start: float, end: float, out_dir: str, shot_id: int, fps: float = 0.5) -> List[str]:
os.makedirs(out_dir, exist_ok=True)
# shot 구간만 잘라서 저fps로 프레임 추출 (키프레임 "근사"용)
pattern = os.path.join(out_dir, f"shot{shot_id:04d}_%03d.jpg")
cmd = [
"ffmpeg", "-hide_banner", "-y",
"-ss", str(start), "-to", str(end), "-i", video_path,
"-vf", f"fps={fps},scale=640:-1",
pattern
]
run(cmd)
# 생성 파일 목록
files = sorted([os.path.join(out_dir, f) for f in os.listdir(out_dir) if f.startswith(f"shot{shot_id:04d}_")])
return files
def build_index(video_path: str, out_dir: str = "index_out") -> List[Shot]:
shots = detect_shots(video_path)
shot_objs: List[Shot] = []
frames_dir = os.path.join(out_dir, "frames")
os.makedirs(out_dir, exist_ok=True)
for i, (s, e) in enumerate(shots):
kfs = extract_keyframes(video_path, s, e, frames_dir, i, fps=0.5)
shot_objs.append(Shot(shot_id=i, start=s, end=e, keyframes=kfs))
with open(os.path.join(out_dir, "shots.json"), "w", encoding="utf-8") as f:
json.dump([asdict(x) for x in shot_objs], f, ensure_ascii=False, indent=2)
return shot_objs
if __name__ == "__main__":
import sys
video_path = sys.argv[1]
build_index(video_path)
print("Wrote: index_out/shots.json")
예상 출력:
index_out/shots.json에 shot별 start/endindex_out/frames/shot0000_001.jpg ...식으로 keyframe 이미지들
2) ASR + shot 정렬(타임스탬프 증거 만들기)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# asr_align.py
import json, os
from dataclasses import dataclass, asdict
from typing import List, Dict, Any
from faster_whisper import WhisperModel
@dataclass
class Utterance:
start: float
end: float
text: str
def load_shots(path="index_out/shots.json") -> List[Dict[str, Any]]:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def transcribe(video_path: str, model_size="small") -> List[Utterance]:
model = WhisperModel(model_size, device="cpu", compute_type="int8")
segments, _info = model.transcribe(video_path, vad_filter=True)
uts = []
for s in segments:
uts.append(Utterance(start=float(s.start), end=float(s.end), text=s.text.strip()))
return uts
def align_utterances_to_shots(shots, uts: List[Utterance]):
# 단순 overlap 기준 (실무에선 더 정교하게)
for sh in shots:
s, e = sh["start"], sh["end"]
sh["asr"] = [asdict(u) for u in uts if not (u.end < s or u.start > e)]
return shots
if __name__ == "__main__":
import sys
video_path = sys.argv[1]
shots = load_shots()
uts = transcribe(video_path, model_size="small")
shots = align_utterances_to_shots(shots, uts)
os.makedirs("index_out", exist_ok=True)
with open("index_out/evidence.json", "w", encoding="utf-8") as f:
json.dump(shots, f, ensure_ascii=False, indent=2)
print("Wrote: index_out/evidence.json")
3) 질의: evidence를 벡터 검색으로 좁히고(top-k), 그 구간만 고비용 분석
여기서는 “질의→후보 shot 리스트”까지만 구현합니다(실무에서는 이 top-k에 대해 vision model caption/objects/grounding을 추가 호출).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# query.py
import json
import numpy as np
import faiss
def load_evidence(path="index_out/evidence.json"):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def shot_text(shot):
asr = " ".join([u["text"] for u in shot.get("asr", [])])
# keyframe 경로 자체도 힌트가 될 수 있어 함께 보관(실전에서는 frame caption을 넣는 게 핵심)
return f"[{shot['start']:.2f}-{shot['end']:.2f}] {asr}"
# 데모용: 임베딩은 실제론 text embedding 모델로 교체.
# 여기선 해시 기반 pseudo-embedding(실행가능하지만 성능 목적 X)
def embed(text: str, dim=384):
v = np.zeros(dim, dtype=np.float32)
for w in text.lower().split():
v[hash(w) % dim] += 1.0
n = np.linalg.norm(v) + 1e-8
return v / n
def build_faiss(shots):
X = np.stack([embed(shot_text(s)) for s in shots])
index = faiss.IndexFlatIP(X.shape[1])
index.add(X)
return index, X
def search(shots, index, query: str, topk=5):
q = embed(query)[None, :]
scores, ids = index.search(q, topk)
out = []
for score, i in zip(scores[0], ids[0]):
sh = shots[int(i)]
out.append({
"shot_id": sh["shot_id"],
"start": sh["start"],
"end": sh["end"],
"score": float(score),
"preview": shot_text(sh)[:200]
})
return out
if __name__ == "__main__":
shots = load_evidence()
index, _X = build_faiss(shots)
q = "안전장비를 착용하지 않은 순간이 언제 나와?"
results = search(shots, index, q, topk=5)
print(json.dumps(results, ensure_ascii=False, indent=2))
여기까지의 “현실성” 포인트:
- shot 단위 저장(나중에 timestamp jump/UI와 바로 연결)
- 비용은 “top-k shot 재분석”으로 제어
- 다음 단계로 Video Temporal Grounding 모델(또는 MLLM 기반 timestamp 출력)을 붙이면 제품이 됩니다. (VTG 흐름 자체가 2026년 핵심 축)2
⚡ 실전 팁 & 함정
Best Practice (2~3개)
1) Evidence JSON 스키마를 먼저 고정
- {start/end, transcript span, keyframes, (optional) objects/actions, embeddings, provenance}를 “DB row”처럼 설계하세요.
- 이게 있어야 모델을 바꿔도 파이프라인이 안 무너집니다.
2) 2-pass가 기본: cheap pass → expensive pass
- cheap: scdet + low-fps frames + ASR
- expensive: top-k 구간만 high-fps + detection/segmentation + caption + grounding
- Tool-augmented 방식(필요할 때만 프레임을 더 샘플링)이 long video에서 특히 강합니다.6
3) Temporal Grounding을 “출력 계약(Output Contract)”으로
- 답변 텍스트만 반환하지 말고, 항상 (start,end)와 근거 프레임/자막 span을 같이 반환하게 하세요.
- VTG/TimeLens 계열이 말하는 방향은 결국 “시간을 맞히는 능력”이 장기적으로 중요하다는 겁니다.8
흔한 함정/안티패턴
- uniform sampling 고정: 도메인에 따라 “중요 이벤트는 짧고 희소”합니다. 특히 제조/감시/스포츠는 치명적.
- 프레임만 저장하고 transcript/타임스탬프 provenance를 안 남김: 나중에 디버깅/재현 불가.
- 생성 모델을 먼저 붙이는 것: 편집/생성에서 consistency 이슈가 커서(멀티샷/멀티턴) 결국 이해/메모리 구조가 필요해집니다.3
비용/성능/안정성 트레이드오프
- scdet threshold를 낮추면 shot이 잘게 쪼개져 recall은 좋아지지만 인덱싱/저장/후속 분석 비용이 증가
- ASR 품질이 낮으면 retrieval이 흔들려서 top-k가 망가짐 → “텍스트 기반 retrieval”에 과도하게 의존하면 위험
- 생성 파이프라인에서 consistency를 잡으려면(메모리/계층 latent/멀티샷) 대체로 VRAM과 latency를 먹습니다.9
🚀 마무리
2026년 8월 기준으로 video AI의 실전 승부처는 “모델 이름”이 아니라 프레임 분석 파이프라인입니다. 긴 비디오에서 성능/비용/신뢰성을 동시에 잡으려면:
- (1) shot/clip 기반 evidence를 만들고
- (2) query-aware로 top-k 구간만 고비용 분석하며
- (3) 답변에는 timestamp grounding을 기본 포함시키는 설계 이 3가지를 먼저 고정하는 게 가장 ROI가 큽니다.1
도입 판단 기준(체크리스트):
- “유저가 답을 믿으려면 근거 구간이 필요하다” → temporal grounding 필수
- “비디오가 길고 이벤트가 희소하다” → scene/semantic boundary 기반 frame selection + 2-pass 필수5
- “생성/편집에서 동일 인물/배경/카메라를 유지해야 한다” → consistency를 구조적으로 다루는 접근(메모리/멀티샷/분해형)이 필요3
다음 학습 추천:
- Video Temporal Grounding/TimeLens류 논문 흐름으로 “timestamp 출력”을 제품 요구사항으로 바꾸는 연습8
- long video reasoning에서 tool-augmented(필요 시 재샘플링) 패턴을 시스템으로 구현6
- 생성은 consistency 논문(멀티샷/멀티턴 편집/계층 latent) 위주로 보는 게 실무에 바로 도움3
https://aclanthology.org/2026.findings-acl.70/ ↩︎ ↩︎2 ↩︎3 ↩︎4
https://research.adobe.com/publication/memory-v2v-memory-augmented-video-to-video-diffusion-for-consistent-multi-turn-editing/ ↩︎ ↩︎2 ↩︎3 ↩︎4 ↩︎5
https://openaccess.thecvf.com/content/CVPR2026/papers/Zhang_TimeLens_Rethinking_Video_Temporal_Grounding_with_Multimodal_LLMs_CVPR_2026_paper.pdf ↩︎ ↩︎2