Tutorials

Twelve Labs의 간편한 검색 API를 활용하여 동영상 내 특정 순간을 찾는 방법

안키트 카레 카레 (Ankit Khare)

개발자는 Twelve Labs Search API를 사용하여 수동으로 지루하게 영상을 훑어볼 필요 없이, 일상적인 언어 쿼리만으로 모든 비디오 내에서 원하는 정확한 순간을 찾아낼 수 있습니다. 이 튜토리얼에서는 비디오를 인덱싱하고, 시각적 요소, 대화, 화면에 나타나는 텍스트 전반에 걸쳐 시맨틱 검색을 실행하며, 그 결과를 Flask 앱에 표시하는 과정을 자세히 알아봅니다.

개발자는 Twelve Labs Search API를 사용하여 수동으로 지루하게 영상을 훑어볼 필요 없이, 일상적인 언어 쿼리만으로 모든 비디오 내에서 원하는 정확한 순간을 찾아낼 수 있습니다. 이 튜토리얼에서는 비디오를 인덱싱하고, 시각적 요소, 대화, 화면에 나타나는 텍스트 전반에 걸쳐 시맨틱 검색을 실행하며, 그 결과를 Flask 앱에 표시하는 과정을 자세히 알아봅니다.

In this article

No headings found on page

뉴스레터 구독하기

뉴스레터 구독하기

영상 이해 분야의 최신 기술 업데이트, 튜토리얼 및 인사이트를 받아보세요.

영상 이해 분야의 최신 기술 업데이트, 튜토리얼 및 인사이트를 받아보세요.

AI로 영상을 검색하고, 분석하고, 탐색하세요.

2023. 4. 7.

15분

링크 복사하기

최근 Twelve Labs는 GTC 2023에서 멀티모달 AI 부문의 선두 주자로 소개되었습니다. 당시 GTC 2023 영상을 보면서, 공동 창업자인 Soyoung 님이 우리 회사가 소개된 장면을 찾기 위해 머리를 싸매며 고군분투하는 모습을 보았습니다. 이 경험을 계기로, Twelve Labs의 고유한 API를 사용하여 '비디오 내부 검색'이라는 과제에 직접 도전해봐야겠다는 자극을 받았습니다. 그렇게 해서 시작된 주말 프로젝트의 튜토리얼을 공유하고자 합니다. 이번 가이드를 통해 Twelve Labs의 검색 API를 사용하여 영상 속에서 원하는 특정 순간을 찾아내는 과정을 자세히 소개해 드리겠습니다.

                     비디오 내부 검색을 한 단계 더 업그레이드하기 위한 청사진😎

소개

Twelve Labs는 비디오 이해(Video Understanding) 기능을 활용한 애플리케이션 개발을 돕기 위해, 강력한 API 제품군 형태의 멀티모달 파운데이션 모델을 제공합니다. 이 블로그 포스트에서는 자연어 쿼리를 사용하여 Twelve Labs API가 비디오 내에서 원하는 정확한 지점을 얼마나 자연스럽게 찾아내는지 살펴보겠습니다. 저는 테스트를 위해 대학원 시절 직접 제작한 "Machine Learning is Everywhere"라는 재미있는 영상을 로컬 드라이브에서 업로드해 보았습니다. 제목 그대로 80초 남짓한 이 비디오는 우리 삶 일상 곳곳에 스며든 머신러닝을 다룹니다. 예를 들어, 프로 탁구 선수가 쿠카(Kuka) 로봇과 대결하는 장면이나, 스케이트보드 트릭을 선보이는 남자의 모습을 머신러닝이 자동 요약하는 등 다양한 ML 적용 사례가 등장합니다. Twelve Labs API를 활용해, 이러한 영상 속 특정 장면을 단순 평문 형태의 자연어 검색만으로 어떻게 찾아낼 수 있는지 직접 시연해 보이겠습니다.

이번 튜토리얼의 목적은 검색 API를 직관적이고 편안하게 시작할 수 있도록 돕는 데 있습니다. 따라서 한 개의 비디오에서만 특정 장면을 검색하는 방식으로 기능을 최소화하고, 가볍고 친숙한 Flask 프레임워크 기반의 데모 앱을 구현하는 데 초점을 맞췄습니다. 하지만 아키텍처 자체가 확장성이 뛰어나기 때문에, 실제 프로젝트에서는 수백, 수천 개의 비디오를 한 번에 업로드하고 검색을 동시 실행하는 규모로도 완전하게 대응 가능합니다. 자, 이제 본격적인 흥미를 채우러 출발해 볼까요?

빠른 요약

  • 선행 조건: 회원가입을 하고 Twelve Labs API 키를 발급받은 뒤, 데모 애플리케이션 제작을 위한 필요 패키지를 설치합니다.

  • 비디오 업로드: 검색해볼 비디오가 준비되었나요? Twelve Labs 플랫폼으로 전송하면 간편하게 인덱싱을 완료한 뒤 고속 검색 준비를 마칩니다.

  • 시맨틱 비디오 검색: 자연어 질의문을 사용해 영상 속 기억에 남는 정확한 장면을 영리하게 추적합니다.

  • 데모 앱 제작: Flask를 사용해 HTML 템플릿을 화면에 띄우는 가벼운 Python 스크립트를 작성하고, 검색 결과를 정갈하게 보여줄 HTML 페이지를 구성해 봅니다.

💡 만약 개발자가 아니신 분이 읽고 계시더라도 전혀 걱정하실 필요 없습니다! 즉시 실행해 보고 결과를 확인해볼 수 있는 완제품 상태의 Jupyter Notebook 링크를 준비해 두었습니다. 또한 코드 한 줄도 작성하지 않고 시맨틱 비디오 검색의 강력함을 체험해 보고 싶으시다면, 저희 Playground를 직접 활용해 보세요. 무료 크레딧이 필요하시다면 언제든 저에게 연락해 주세요😄.

선행 조건

이 튜토리얼에서는 Jupyter Notebook을 기준으로 진행합니다. 로컬 컴퓨터에 Jupyter, Python 및 Pip가 이미 설정되어 있다고 가정하겠습니다. 진행 도중 예외가 발생하거나 가이드가 필요하면 주저하지 마시고 저희 Discord 서버를 찾아 노크해 주세요. 가장 빠르게 응답해 드립니다 🚅🏎️⚡️. 만약 Discord가 편하지 않으시다면 이메일 창구를 통해 문의를 남겨주셔도 좋습니다. Twelve Labs 계정을 생성하면 API 대시보드 화면에서 API Key를 확인할 수 있습니다. 이번 실습에서는 이미 존재하는 계정을 통해 API 호출을 진행하겠습니다. 간단하게 본인의 Secret Key를 담아 제공되는 API 엔드포인트 URL로 호출을 요청하면 되며, 필요한 설정 매개변수들은 다음과 같이 환경 변수로 깔끔하게 넘겨줄 수 있습니다.

<pre><code class="bash">%env API_KEY=<your_API_key>
%env API_URL=https://api.twelvelabs.io/v1.1
</code></pre>


의존성 설치:

<pre><code class="python">pip install requests
pip install flask
</code></pre>

비디오 업로드

첫 번째 단계로, 저의 로컬 컴퓨터에 보관 중이던 비디오 파일을 Twelve Labs 플랫폼으로 업로드하여 비디오 이해 기술을 적용하는 방식을 보여 드리겠습니다.

가져오기(Imports):

<pre><code class="python">import os
import requests
import glob
from pprint import pprint
</code></pre

다음과 같이 API 엔드포인트 URL과 발급받은 API Key를 변수에 바인딩합니다.

<pre><code class="python">API_URL = os.getenv("API_URL")
assert API_URL
</code></pre

<pre><code class="python">API_KEY = os.getenv("API_KEY")
assert API_KEY
</code></pre

인덱스(Index) API

이제 Index API를 호출하여 비디오 인덱스를 생성할 차례입니다. 비디오 인덱스는 한 개 이상의 비디오 파일 그룹을 하나로 묶고 공동의 타겟 검색 옵션을 정의함으로써, 인덱스 내에 탑재된 대용량 비디오에 대해 전반적인 시맨틱 매칭 처리를 손쉽게 수행할 수 있게 돕는 유연한 가상 컨테이너 역할을 합니다.

인덱스는 다음과 같은 필드 정보를 기반으로 구성됩니다:

<ul>
<li><b>이름(name)</b></li>
<li><b>엔진(engine)</b> - 현재 저희는 비디오 이해에 최적화된 최신 멀티모달 파운데이션 모델인 Marengo2를 기본 제공합니다.</li>
<li>한 가지 이상의 <b>인덱싱 옵션(indexing options)</b>:</li>
<ul>
<li><b>visual</b>: 이 옵션이 선택되면, API 서비스가 영상에 대해 오디오-비주얼 결합 분석을 수행하여 사물, 행동, 사운드, 모션, 장소, 정황 이벤트, 그리고 복합 오디오-비주얼 자연어 설명을 기준으로 검색할 수 있도록 지원합니다. 예를 들면 '환호하는 군중들'이나 '밤샘 작업을 마치고 퇴근하는 초췌한 개발자들' 같은 검색이 가능해집니다😆.</li>
<li><b>conversation</b>: 이 옵션을 켜두면 비디오 내부 임베디드 오디오 음성을 텍스트로 추출(녹취록 작성)한 뒤, 이를 바탕으로 시맨틱 NLP 의미론 분석을 진행합니다. 덕분에 대화 속에 포함된 구체적 어휘나 주제 흐름에 기반해 정확히 어느 타이밍에 말했는지를 정확하게 찾아내 줍니다. 예를 들면, 인덱싱된 대화 중 '어렸을 때 동생한테 거짓말한 순간' 같은 키워드로 정확히 특정해 찾아낼 수 있습니다😜.</li>
<li><b>text_in_video</b>: 이 옵션은 화면 안에 비춰진 표지판, 문자, 라벨, 자막, 프레젠테이션, 브랜드 로고, 문서 등을 OCR(광학 문자 인식) 기법을 거쳐 탐색하게 만들어 줍니다. 이를테면 축구 경기 영상 중 특정 스포츠 브랜드 로고가 노출된 프레임들이 언제인지 정확히 걸러내고 싶을 때 매우 유용합니다🏈.</li></ul></ul>

인덱스 생성 코드:

<pre><code class="python"># Construct the URL of the `/indexes` endpoint
INDEXES_URL = f"{API_URL}/indexes"

# Specify the name of the index
INDEX_NAME = "My University Days"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}

# Declare a dictionary named data
data = {
    "engine_id": "marengo2",  
    "index_options": ["visual", "conversation", "text_in_video"], 
    "index_name": INDEX_NAME,
}

# Create an index
response = requests.post(INDEXES_URL, headers=headers, json=data)

# Store the unique identifier of your index
INDEX_ID = response.json().get('_id')

# Print the status code and response
print(f'Status code: {response.status_code}')
pprint(response.json())
</code></pre

태스크(Task) API를 통한 비디오 업로드

Twelve Labs 플랫폼은 생성된 인덱스 내부로 비디오 파일을 전송하고 진행 경과를 추적 제어할 수 있는 Task API를 지원합니다.

<pre><code class="python">TASKS_URL = f"{API_URL}/tasks"

file_name = "Machine Learning is Everywhere" # indexed video will have this file name
file_path = "ml.mp4" # file name of the video being uploaded
file_stream = open(file_path,"rb")

data = {
    "index_id": INDEX_ID, 
    "language": "en"
}

file_param = [
    ("video_file", (file_name, file_stream, "application/octet-stream")),]

response = requests.post(TASKS_URL, headers=headers, data=data, files=file_param
</code></pre

비디오가 안정적으로 입고되는 즉시 시스템 내부에서 자동 인덱싱 변환 파이프라인이 즉각 가동됩니다. Twelve Labs는 뛰어난 시구간 분석 능력을 지닌 멀티모달 기반 파운데이션 모델을 기반으로 비디오에 담긴 행동 양상, 피사체의 움직임, 음성, 메타 텍스트 정보 등을 심층 분석하여 정교한 고밀도 비디오 임베딩(Embedding)을 영리하게 복구해 냅니다. 이 과정을 통하여 평상시 자연스럽게 사용하는 구어체 질의 형태나 라벨 가이드를 활용한 지식 접근이 현실화됩니다.

인덱싱 진행 상태 모니터링:

<pre><code class="python">import time

# Define starting time
start = time.time()
print("Start uploading video")

# Monitor the indexing process
TASK_STATUS_URL = f"{API_URL}/tasks/{TASK_ID}"
while True:
    response = requests.get(TASK_STATUS_URL, headers=headers)
    STATUS = response.json().get("status")
    if STATUS == "ready":
        print(f"Status code: {STATUS}")
        break
    time.sleep(10)

# Define ending time
end = time.time()
print("Finish uploading video")
print("Time elapsed (in seconds): ", end - start)
</code></pre

<pre><code class="python"># Retrieve the unique identifier of the video
VIDEO_ID = response.json().get('video_id')

# Print the status code, the unique identifier of the video, 
and the response
print(f"VIDEO ID: {VIDEO_ID}")
pprint(response.json())
</code></pre

<pre><code class="language-plaintext">VIDEO ID: 642621ffffa3551fb6d2f###
{'_id': '642621fc3205dc8a48ba8###',
 'created_at': '2023-03-30T23:57:48.877Z',
 'estimated_time': '2023-03-30T23:59:58.312Z',
 'index_id': '###621fb7b1f2230dfcd6###',
 'metadata': {'duration': 80.32,
              'filename': 'Machine Learning is Everywhere',
              'height': 720,
              'width': 1280},
 'status': 'ready',
 'type': 'index_task_info',
 'updated_at': '2023-03-31T00:00:34.412Z',
 'video_id': '642621ffffa3551fb6d2f###'}
</code></pre

데모 애플리케이션에서 인덱스의 고유 식별자(ID)를 참고할 수 있도록 환경 변수를 한 차례 더 등록해 줍니다:

<pre><code class="python">%env ENV_INDEX_ID = ###621fb7b1f2230dfcd6###
</code></pre

현재 설정한 인덱스에 보관된 비디오 원본 목록을 봅니다. 구조의 파악을 돕기 위해 의도적으로 인덱싱 파일을 1개 단위로 단순 제어했지만, 테스트용 무료 크레딧을 이용해 최대 10시간 정량 범위 내에서 자유롭게 영상 파일을 쌓아두고 전체 검색을 시도하는 것도 좋은 선택입니다.

<pre><code class="python"># Retrieve the unique identifier of the existing index
INDEX_ID = os.getenv("ENV_INDEX_ID")

# Set the header of the request
headers = {
    "x-api-key": API_KEY,
}

# List all the videos in an index
INDEXES_VIDEOS_URL = f"{API_URL}/indexes/{INDEX_ID}/videos"
response = requests.get(INDEXES_VIDEOS_URL, headers=headers)
print(f'Status code: {response.status_code}')
pprint(response.json())
</code></pre

<pre><code class="python">Status code: 200
{'data': [{'_id': '642621ffffa3551fb6d2f###',
           'created_at': '2023-03-30T23:57:48Z',
           'metadata': {'duration': 80.32,
                        'engine_id': 'marengo2',
                        'filename': 'Machine Learning is Everywhere',
                        'fps': 25,
                        'height': 720,
                        'size': 11877525,
                        'width': 1280},
           'updated_at': '2023-03-30T23:57:51Z'}],
 'page_info': {'limit_per_page': 10,
               'page': 1,
               'total_duration': 80.32,
               'total_page': 1,
               'total_results': 1}}
</code></pre

시맨틱 비디오 검색: 특정 순간 찾기

모델의 비디오 인덱싱 변환이 완료되고 고유 비디오 임베딩이 생성되는 즉시, Search API를 유용하게 사용할 수 있습니다. 해당 REST Endpoint는 사용자가 정교하게 던진 자연어에 담긴 문맥과 시맨틱 요소를 추출한 뒤, 일치도가 가장 높은 비디오 내부의 정확한 시작(start)과 종료(end) 타임코드를 도출해 냅니다. 최초 인덱스 구성 시점에 선언한 옵션 조합과 일치하도록, 시맨틱 비디오 검색 범위 역시 세부 선택하여 컨트롤할 수 있습니다. 예를 들어, 인덱스 생성 시 모든 옵션을 켰다면 소리-영상 정보와 대화 텍스트, 화면 속 OCR 자재들까지 모두 결합 탐색이 가능해집니다. 인덱스 레벨과 검색 레벨 모두에서 동일한 옵션을 개별적으로 제공하는 이유는 개발자의 자유도를 최대화하기 위함입니다. 사용 중인 컨텍스트의 요구 사항에 딱 맞는 다채로운 탐색 조건들을 유연하게 혼합 선별할 수 있습니다.

자, 그럼 가볍게 직관적인 자연어 검색어인 “a guy doing a trick on a skateboard”(스케이트보드 트릭을 구사하는 남자)를 넘겨 시각 검색(visual search)를 돌려보겠습니다.

<pre><code class="bash">Status code: 200
{'data': [{'confidence': 'high',
           'end': 49.34375,
           'metadata': [{'type': 'visual'}],
           'score': 83.24,
           'start': 41.65625,
           'video_id': '642621ffffa3551fb6d2f###'}],
 'page_info': {'limit_per_page': 10,
               'page_expired_at': '2023-03-31T22:41:42Z',
               'total_results': 1},
 'search_pool': {'index_id': '642621fb7b1f2230dfcd####',
                 'total_count': 1,
                 'total_duration': 80}}
</code></pre

조건에 매칭되는 비디오 특정 시점:

이 단계는 볼 때마다 감탄을 자아냅니다. 모델이 사람이 비디오를 보고 이해하는 방식에 극도로 가깝다는 점을 확연하게 증명하기 때문인데요. 위의 스크린샷 이미지처럼 시스템은 제가 정확하게 지칭하고 요구했던 지점의 최적 구간을 빈틈없이 포착해 냈습니다.

이 기세를 이어 "a guy playing table tennis with a robotic arm" (로봇 팔과 함께 탁구를 치고 있는 사람) 이라는 새로운 쿼리를 보낸 뒤, 시스템이 다시 한번 놀라운 정확도를 뽐내는지 직접 검증해 봅시다.

<pre><code class="python"># Construct the URL of the `/search` endpoint
SEARCH_URL = f"{API_URL}/search/"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}
query = "a guy playing table tennis with a robotic arm"
# Declare a dictionary named `data`
data = {
    "query": query,  # Specify your search query
    "index_id": INDEX_ID,  # Indicate the unique identifier of your index
    "search_options": ["visual"],  # Specify the search options
}

# Make a search request
response = requests.post(SEARCH_URL, headers=headers, json=data)
print(f"Status code: {response.status_code}")
pprint(response.json())
</code></pre


출력 결과:

<pre><code class="python">{'data': [{'confidence': 'high',
           'end': 14.6875,
           'metadata': [{'type': 'visual'}],
           'score': 90.62,
           'start': 9.75,
           'video_id': '642621ffffa3551fb6d2####'},
          {'confidence': 'high',
           'end': 9.75,
           'metadata': [{'type': 'visual'}],
           'score': 89.74,
           'start': 4.5,
           'video_id': '642621ffffa3551fb6d2####'}],
</code></pre


조건에 부합하는 타임프레임 결과 화면:

확실하게 정확해졌군요! 이번에도 시스템이 해당 사건이 담긴 최적의 지점을 완벽하게 잡아냈습니다.

💡여기 간략하고 재미있는 서브 챌린지가 있습니다. "a breakthrough in machine learning would be worth ten Microsofts"(머신러닝의 중대한 돌파구는 마이크로소프트 10개만큼의 가치가 있을 것이다)라고 검색어를 타이핑한 후에, Search option 설정값을 ["text_in_video"]로 바꾼 채 시각 탐색을 테스트해 보세요.

매번 시작과 중간 타임스탬프 값을 JSON 데이터 형태를 쳐다보며 직접 비교하는 일은 번잡할 수 있습니다. 이를 우아하게 연계 표시할 수 있는 아름다운 웹 기반 인터페이스 화면을 만들어보겠습니다. 검색 응답으로 넘어온 실시간 JSON 정보를 우리가 출력할 대상 비디오 단말 프레임에 곧장 동기화해 봅니다. 그럼 가볍게 제작해 볼까요?

데모 애플리케이션 빌딩

여기까지 비디오 이해 여정에 즐겁게 호응해 주시고 따라와 주셔서 진심으로 감사합니다 🎉🥳👏! 이제 마지막 피날레에 돌입하겠습니다. 이전 단계에서 생성한 동영상 탐색 JSON 타임스탬프 결과물들을 정형 분석한 뒤, 가장 아름답고 보기 편한 웹뷰 컴포넌트로 뿌려주는 초경량 Flask 앱을 조립해 보겠습니다. 저는 평소 데이터 분석과 밀접하게 일하며 Python 환경을 편애해 온 취향에 맞춰 Flask를 애용하지만, 여러분은 각자의 취향에 알맞은 다른 웹 개발 프레임워크를 마음껏 변형 택하셔도 완벽하게 호환 작동합니다.


우선 Jupyter Notebook 상에 필요한 패키지 라이브러리를 추가해 둡니다.

<pre><code class="python">import json
import pickle
</code></pre


우리는 Search API 추출 원시 응답값으로부터 각 검색 장면들의 시작 및 끝 타임프레임 시간을 안전하게 축적 보존하기 위한 두 가지 보조 리스트 자료형인 「starts」와 「ends」를 세팅하게 됩니다:

<pre><code class="python">data = response.json()

starts = []
ends = []

for item in data['data']:
    starts.append(item['start'])
    ends.append(item['end'])

print("starts:", starts)
print("ends:", ends)
</code></pre

<pre><code class="bash">starts: [34.90625, 0, 62.5625]
ends: [39.21875, 4.5, 65.90625]
</code></pre


타임스탬프 영역이 확보되었고 해당 테스트 비디오 역시 제 개인 유튜브 채널에 무사히 배포된 상태이므로, 이를 활용할 수 있는 간단한 방법들을 고안해볼 수 있습니다. 로컬 저장 장치에 있는 비디오 원천 파일을 활용해 필요한 클립을 웹 인터페이스로 직접 스트리밍할 수도 있고, 유튜브 채널 플레이어 연동 URL을 적재하는 것으로도 멋진 비주얼 출력이 똑같이 대응 가능합니다. 개인적으로 후자의 방식이 구현상 더욱 직관적이라 생각되므로 유튜브 임베딩 코드를 생성하고 그 내부에 매칭할 start 및 end 한계 타임스탬프 주소를 주입하는 방식을 취하도록 하겠습니다. 이렇게 설계하면 복잡한 플레이어 로직 없이도 필터링된 범위만큼만 장면이 출력됩니다. 다만 한 가지 소소하게 참고할 사항으로는, Youtube Embed 타임 파라미터는 소수점이 없는 정수 형태의 속성값만을 완벽 수공하므로 다음과 같이 반올림 캐스팅을 해줍니다.

<pre><code class="bash">starts_int = [int(f) for f in starts]
ends_int = [int(f) for f in ends]
</code></pre


이제 이 리스트와 우리가 입력했던 쿼리를 간단하게 파일로 저장(pickle)해 보겠습니다. 이렇게 해두면 곧바로 만들기 시작할 Flask 앱 파일에 매개변수를 넘겨주기가 아주 편합니다:

<pre><code class="python">with open("lists.pkl", "wb") as f:
    pickle.dump((starts_int, ends_int, query), f)
</code></pre>

좋습니다! Flask 기반으로 해당 유효 범위들을 끄집어낼 완치 동작 사전 세팅들이 순조롭게 해결되었습니다.

Flask 앱 구축 개발 단계

1. 새 Flask 프로젝트 디렉터리 빌딩: 전용 폴더를 만들어서 해당 안쪽 범위에 핵심 진입점이 되어줄 소스 코드 파일을 새로 생성해 올립니다.

<pre><code class="bash">mkdir my_flask_app
cd my_flask_app
touch app.py
</code></pre>

동작할 소스 및 템플릿 파일 배치 구성이 전부 들어선 폴더 구조의 배치 모습은 다음과 같을 것입니다.

<pre><code class="markdown">my_flask_app/
│   app.py
│   ml.mp4
│
└───templates/
    │   index.html
</code></pre>

테스트로 올릴 비디오 원시 mp4 파일은 작성 중인 my_flask_app 하위 내부 노드 경로 내에 나란히 안치해 주시기 바랍니다.

2. Flask 어플리케이션 소스 작성: app.py 파일에 핵심 비즈니스 호출 처리 로직을 추가 구성합니다. Jinja2 기반의 동적 템플릿 변환 기법에 실시간 생성한 시작-끝 정수 배열 스택 구조의 상태 정보들을 안전하게 맵핑해 줄 수 있도록 연동합니다.

<pre><code class="python">from flask import Flask, render_template
import pickle

with open("lists.pkl", "rb") as f:
    starts, ends, query = pickle.load(f)

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html", starts=starts, ends=ends, query=query)

if __name__ == "__main__":
    app.run(debug=True)
</code></pre

3. 전용 템플릿(templates) 디렉터리 구성: Jinja2 엔진 파일을 Flask 런타임에서 온전히 읽어 렌더링하기 위해서 어플리케이션 루트 위치와 일치하는 위상 하단에 templates 폴더 환경을 만듭니다. 이 폴더 안에 구현할 UI 마크업 뼈대가 상주할 예정입니다.

<pre><code class="bash">mkdir templates
</code></pre>

최종 관문은 사용자의 자연어 입력에 정확히 부합하는 장면으로 걸러진 클립 조각들을 웹에 정렬시켜 줄 index.html 화면 설계입니다. 코드로 작성해 올리기 전 제 Youtube 단말 채널에서 제공되는 정형화된 Embed Video 소스 형태를 미리 한 차례 점검해 보세요.

4. Jinja2 템플릿 가공: templates 디렉터리 내부에 index.html 파일을 가볍게 생성하는 명령을 수행해 줍니다.

<pre><code class="bash">touch index.html
</code></pre>

어플리케이션 인터페이스 환경을 채워줄 깔끔하고 명확한 주입식 코드 구성 예시입니다. HTML 마크업 블림 속에 루프문을 걸어 starts, ends 및 query 변수를 반복 출력시킵니다.

<pre><code class="language-html"><html>
  <head>
    <style>
      body {
        background-color: #F2F2F2;
        font-family: Arial, sans-serif;
        text-align: center;
      }
      h1 {
        margin-top: 40px;
      }
      .video-container {
        display: flex;
        flex-wrap: wrap;
        padding: 40px;
        justify-content: center;
      }
      .video-item {
        display: flex;
        flex-direction: column;
        align-items: center;
        width: 50%;
        height: 600px;
        margin: 20px;
        text-align: center;
      }
      .video-item iframe {
        width: 80%;
        height: 380px;
        margin: 20px;
      }
      .video-item p {
        font-size: 16px;
        margin-top: 10px;
        font-weight: bold;
      }
    </style>
  </head>
  <body>
    <h1>My Favorite Scenes</h1>
    <div class="video-container">
      {% for i in range(starts|length) %}
        <div class="video-item">
          <iframe width="560" height="315" src="https://www.youtube.com/embed/hdZ_tNtdB4c?start={{ starts[i] }}&amp;end={{ ends[i] }}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in- picture" allowfullscreen></iframe>
          <p>Start: {{ starts[i] }} | End: {{ ends[i] }}</p>
          <p>Query: {{ query }}</p>
        </div>
      {% endfor %}
    </div>
  </body>
</html>
</code></pre>

완벽합니다! 그럼 가볍게 Jupyter Notebook의 마지막 실행 블록 코드를 실행시켜 보지요:

<pre><code class="python">%run app.py
</code></pre>

콘솔 창에 아래와 같이 로컬 호스트 실행 서버 응답 메시지가 성공적으로 떨어진다면, 모든 흐름이 우리의 예상 시나리오대로 원활하게 안착한 상태입니다😊:

서버 주소인 http://127.0.0.1:5000 링크를 클릭하여 모니터를 열면, 우리가 인덱스 질의로 요구했던 세그먼트에 맞춘 최종 데모 뷰가 바로 노출됩니다.

어플리케이션 안의 미디어를 직접 가동해 보면 지정한 타임스탬프 슬롯과 연동하여, 우리가 찾고자 식별했던 정확한 정황 지점 장면부터 정확히 실행되는 것을 체감하실 수 있습니다.

로컬 PC 환경에서 곧바로 테스트해볼 수 있도록 Jupyter Notebook 및 필요 파일 일체를 담은 보조 다운로드 폴더 공간을 공유해 둡니다. - https://tinyurl.com/twelvelabs

더 도전해볼 만한 재미있는 과제들:

  1. 다양한 검색 옵션 조합(visual, conversation, text-in-video)을 마음껏 실험해 보며 검출 결과가 어떻게 달라지는지 체감해 보세요.

  2. 여러 개의 동영상 파일을 한데 모아 동시 업로딩한 다음, 전체 아카이브 대역을 한 번에 전역 검색(Search across videos)할 수 있도록 확장 개량해 보세요.

  3. 한 단계 더 나아가 index.html 기본 웹에 사용자가 직접 텍스트 인풋 폼으로 검색어를 입력하고, 실시간 반응형으로 결과를 렌더링하도록 훌륭하게 업그레이드해 보세요.

‍다음 이야기

다음 포스트에서는 여러 개의 단순 쿼리를 논리 연산자로 깔끔하게 조합하여 비디오 컬렉션 전체에서 더욱 정밀하게 복합 시맨틱 검색을 실행하는 방법을 심도 있게 나눠 보겠습니다. 다음 편도 많은 기대 부탁드립니다!

아, 그리고 마지막 하나 더! 멀티모달 파운데이션 모델에 대해 흥미가 있으시다면, 트렌드를 나누고 개척해 가는 동료 개발자 및 연구자분들과 교류할 수 있는 공식 Discord 커뮤니티에 꼭 찾아오세요. 자유롭게 지식을 나누고 질문하며 함께 성장하기에 참 좋은 곳입니다!

최근 Twelve Labs는 GTC 2023에서 멀티모달 AI 부문의 선두 주자로 소개되었습니다. 당시 GTC 2023 영상을 보면서, 공동 창업자인 Soyoung 님이 우리 회사가 소개된 장면을 찾기 위해 머리를 싸매며 고군분투하는 모습을 보았습니다. 이 경험을 계기로, Twelve Labs의 고유한 API를 사용하여 '비디오 내부 검색'이라는 과제에 직접 도전해봐야겠다는 자극을 받았습니다. 그렇게 해서 시작된 주말 프로젝트의 튜토리얼을 공유하고자 합니다. 이번 가이드를 통해 Twelve Labs의 검색 API를 사용하여 영상 속에서 원하는 특정 순간을 찾아내는 과정을 자세히 소개해 드리겠습니다.

                     비디오 내부 검색을 한 단계 더 업그레이드하기 위한 청사진😎

소개

Twelve Labs는 비디오 이해(Video Understanding) 기능을 활용한 애플리케이션 개발을 돕기 위해, 강력한 API 제품군 형태의 멀티모달 파운데이션 모델을 제공합니다. 이 블로그 포스트에서는 자연어 쿼리를 사용하여 Twelve Labs API가 비디오 내에서 원하는 정확한 지점을 얼마나 자연스럽게 찾아내는지 살펴보겠습니다. 저는 테스트를 위해 대학원 시절 직접 제작한 "Machine Learning is Everywhere"라는 재미있는 영상을 로컬 드라이브에서 업로드해 보았습니다. 제목 그대로 80초 남짓한 이 비디오는 우리 삶 일상 곳곳에 스며든 머신러닝을 다룹니다. 예를 들어, 프로 탁구 선수가 쿠카(Kuka) 로봇과 대결하는 장면이나, 스케이트보드 트릭을 선보이는 남자의 모습을 머신러닝이 자동 요약하는 등 다양한 ML 적용 사례가 등장합니다. Twelve Labs API를 활용해, 이러한 영상 속 특정 장면을 단순 평문 형태의 자연어 검색만으로 어떻게 찾아낼 수 있는지 직접 시연해 보이겠습니다.

이번 튜토리얼의 목적은 검색 API를 직관적이고 편안하게 시작할 수 있도록 돕는 데 있습니다. 따라서 한 개의 비디오에서만 특정 장면을 검색하는 방식으로 기능을 최소화하고, 가볍고 친숙한 Flask 프레임워크 기반의 데모 앱을 구현하는 데 초점을 맞췄습니다. 하지만 아키텍처 자체가 확장성이 뛰어나기 때문에, 실제 프로젝트에서는 수백, 수천 개의 비디오를 한 번에 업로드하고 검색을 동시 실행하는 규모로도 완전하게 대응 가능합니다. 자, 이제 본격적인 흥미를 채우러 출발해 볼까요?

빠른 요약

  • 선행 조건: 회원가입을 하고 Twelve Labs API 키를 발급받은 뒤, 데모 애플리케이션 제작을 위한 필요 패키지를 설치합니다.

  • 비디오 업로드: 검색해볼 비디오가 준비되었나요? Twelve Labs 플랫폼으로 전송하면 간편하게 인덱싱을 완료한 뒤 고속 검색 준비를 마칩니다.

  • 시맨틱 비디오 검색: 자연어 질의문을 사용해 영상 속 기억에 남는 정확한 장면을 영리하게 추적합니다.

  • 데모 앱 제작: Flask를 사용해 HTML 템플릿을 화면에 띄우는 가벼운 Python 스크립트를 작성하고, 검색 결과를 정갈하게 보여줄 HTML 페이지를 구성해 봅니다.

💡 만약 개발자가 아니신 분이 읽고 계시더라도 전혀 걱정하실 필요 없습니다! 즉시 실행해 보고 결과를 확인해볼 수 있는 완제품 상태의 Jupyter Notebook 링크를 준비해 두었습니다. 또한 코드 한 줄도 작성하지 않고 시맨틱 비디오 검색의 강력함을 체험해 보고 싶으시다면, 저희 Playground를 직접 활용해 보세요. 무료 크레딧이 필요하시다면 언제든 저에게 연락해 주세요😄.

선행 조건

이 튜토리얼에서는 Jupyter Notebook을 기준으로 진행합니다. 로컬 컴퓨터에 Jupyter, Python 및 Pip가 이미 설정되어 있다고 가정하겠습니다. 진행 도중 예외가 발생하거나 가이드가 필요하면 주저하지 마시고 저희 Discord 서버를 찾아 노크해 주세요. 가장 빠르게 응답해 드립니다 🚅🏎️⚡️. 만약 Discord가 편하지 않으시다면 이메일 창구를 통해 문의를 남겨주셔도 좋습니다. Twelve Labs 계정을 생성하면 API 대시보드 화면에서 API Key를 확인할 수 있습니다. 이번 실습에서는 이미 존재하는 계정을 통해 API 호출을 진행하겠습니다. 간단하게 본인의 Secret Key를 담아 제공되는 API 엔드포인트 URL로 호출을 요청하면 되며, 필요한 설정 매개변수들은 다음과 같이 환경 변수로 깔끔하게 넘겨줄 수 있습니다.

<pre><code class="bash">%env API_KEY=<your_API_key>
%env API_URL=https://api.twelvelabs.io/v1.1
</code></pre>


의존성 설치:

<pre><code class="python">pip install requests
pip install flask
</code></pre>

비디오 업로드

첫 번째 단계로, 저의 로컬 컴퓨터에 보관 중이던 비디오 파일을 Twelve Labs 플랫폼으로 업로드하여 비디오 이해 기술을 적용하는 방식을 보여 드리겠습니다.

가져오기(Imports):

<pre><code class="python">import os
import requests
import glob
from pprint import pprint
</code></pre

다음과 같이 API 엔드포인트 URL과 발급받은 API Key를 변수에 바인딩합니다.

<pre><code class="python">API_URL = os.getenv("API_URL")
assert API_URL
</code></pre

<pre><code class="python">API_KEY = os.getenv("API_KEY")
assert API_KEY
</code></pre

인덱스(Index) API

이제 Index API를 호출하여 비디오 인덱스를 생성할 차례입니다. 비디오 인덱스는 한 개 이상의 비디오 파일 그룹을 하나로 묶고 공동의 타겟 검색 옵션을 정의함으로써, 인덱스 내에 탑재된 대용량 비디오에 대해 전반적인 시맨틱 매칭 처리를 손쉽게 수행할 수 있게 돕는 유연한 가상 컨테이너 역할을 합니다.

인덱스는 다음과 같은 필드 정보를 기반으로 구성됩니다:

<ul>
<li><b>이름(name)</b></li>
<li><b>엔진(engine)</b> - 현재 저희는 비디오 이해에 최적화된 최신 멀티모달 파운데이션 모델인 Marengo2를 기본 제공합니다.</li>
<li>한 가지 이상의 <b>인덱싱 옵션(indexing options)</b>:</li>
<ul>
<li><b>visual</b>: 이 옵션이 선택되면, API 서비스가 영상에 대해 오디오-비주얼 결합 분석을 수행하여 사물, 행동, 사운드, 모션, 장소, 정황 이벤트, 그리고 복합 오디오-비주얼 자연어 설명을 기준으로 검색할 수 있도록 지원합니다. 예를 들면 '환호하는 군중들'이나 '밤샘 작업을 마치고 퇴근하는 초췌한 개발자들' 같은 검색이 가능해집니다😆.</li>
<li><b>conversation</b>: 이 옵션을 켜두면 비디오 내부 임베디드 오디오 음성을 텍스트로 추출(녹취록 작성)한 뒤, 이를 바탕으로 시맨틱 NLP 의미론 분석을 진행합니다. 덕분에 대화 속에 포함된 구체적 어휘나 주제 흐름에 기반해 정확히 어느 타이밍에 말했는지를 정확하게 찾아내 줍니다. 예를 들면, 인덱싱된 대화 중 '어렸을 때 동생한테 거짓말한 순간' 같은 키워드로 정확히 특정해 찾아낼 수 있습니다😜.</li>
<li><b>text_in_video</b>: 이 옵션은 화면 안에 비춰진 표지판, 문자, 라벨, 자막, 프레젠테이션, 브랜드 로고, 문서 등을 OCR(광학 문자 인식) 기법을 거쳐 탐색하게 만들어 줍니다. 이를테면 축구 경기 영상 중 특정 스포츠 브랜드 로고가 노출된 프레임들이 언제인지 정확히 걸러내고 싶을 때 매우 유용합니다🏈.</li></ul></ul>

인덱스 생성 코드:

<pre><code class="python"># Construct the URL of the `/indexes` endpoint
INDEXES_URL = f"{API_URL}/indexes"

# Specify the name of the index
INDEX_NAME = "My University Days"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}

# Declare a dictionary named data
data = {
    "engine_id": "marengo2",  
    "index_options": ["visual", "conversation", "text_in_video"], 
    "index_name": INDEX_NAME,
}

# Create an index
response = requests.post(INDEXES_URL, headers=headers, json=data)

# Store the unique identifier of your index
INDEX_ID = response.json().get('_id')

# Print the status code and response
print(f'Status code: {response.status_code}')
pprint(response.json())
</code></pre

태스크(Task) API를 통한 비디오 업로드

Twelve Labs 플랫폼은 생성된 인덱스 내부로 비디오 파일을 전송하고 진행 경과를 추적 제어할 수 있는 Task API를 지원합니다.

<pre><code class="python">TASKS_URL = f"{API_URL}/tasks"

file_name = "Machine Learning is Everywhere" # indexed video will have this file name
file_path = "ml.mp4" # file name of the video being uploaded
file_stream = open(file_path,"rb")

data = {
    "index_id": INDEX_ID, 
    "language": "en"
}

file_param = [
    ("video_file", (file_name, file_stream, "application/octet-stream")),]

response = requests.post(TASKS_URL, headers=headers, data=data, files=file_param
</code></pre

비디오가 안정적으로 입고되는 즉시 시스템 내부에서 자동 인덱싱 변환 파이프라인이 즉각 가동됩니다. Twelve Labs는 뛰어난 시구간 분석 능력을 지닌 멀티모달 기반 파운데이션 모델을 기반으로 비디오에 담긴 행동 양상, 피사체의 움직임, 음성, 메타 텍스트 정보 등을 심층 분석하여 정교한 고밀도 비디오 임베딩(Embedding)을 영리하게 복구해 냅니다. 이 과정을 통하여 평상시 자연스럽게 사용하는 구어체 질의 형태나 라벨 가이드를 활용한 지식 접근이 현실화됩니다.

인덱싱 진행 상태 모니터링:

<pre><code class="python">import time

# Define starting time
start = time.time()
print("Start uploading video")

# Monitor the indexing process
TASK_STATUS_URL = f"{API_URL}/tasks/{TASK_ID}"
while True:
    response = requests.get(TASK_STATUS_URL, headers=headers)
    STATUS = response.json().get("status")
    if STATUS == "ready":
        print(f"Status code: {STATUS}")
        break
    time.sleep(10)

# Define ending time
end = time.time()
print("Finish uploading video")
print("Time elapsed (in seconds): ", end - start)
</code></pre

<pre><code class="python"># Retrieve the unique identifier of the video
VIDEO_ID = response.json().get('video_id')

# Print the status code, the unique identifier of the video, 
and the response
print(f"VIDEO ID: {VIDEO_ID}")
pprint(response.json())
</code></pre

<pre><code class="language-plaintext">VIDEO ID: 642621ffffa3551fb6d2f###
{'_id': '642621fc3205dc8a48ba8###',
 'created_at': '2023-03-30T23:57:48.877Z',
 'estimated_time': '2023-03-30T23:59:58.312Z',
 'index_id': '###621fb7b1f2230dfcd6###',
 'metadata': {'duration': 80.32,
              'filename': 'Machine Learning is Everywhere',
              'height': 720,
              'width': 1280},
 'status': 'ready',
 'type': 'index_task_info',
 'updated_at': '2023-03-31T00:00:34.412Z',
 'video_id': '642621ffffa3551fb6d2f###'}
</code></pre

데모 애플리케이션에서 인덱스의 고유 식별자(ID)를 참고할 수 있도록 환경 변수를 한 차례 더 등록해 줍니다:

<pre><code class="python">%env ENV_INDEX_ID = ###621fb7b1f2230dfcd6###
</code></pre

현재 설정한 인덱스에 보관된 비디오 원본 목록을 봅니다. 구조의 파악을 돕기 위해 의도적으로 인덱싱 파일을 1개 단위로 단순 제어했지만, 테스트용 무료 크레딧을 이용해 최대 10시간 정량 범위 내에서 자유롭게 영상 파일을 쌓아두고 전체 검색을 시도하는 것도 좋은 선택입니다.

<pre><code class="python"># Retrieve the unique identifier of the existing index
INDEX_ID = os.getenv("ENV_INDEX_ID")

# Set the header of the request
headers = {
    "x-api-key": API_KEY,
}

# List all the videos in an index
INDEXES_VIDEOS_URL = f"{API_URL}/indexes/{INDEX_ID}/videos"
response = requests.get(INDEXES_VIDEOS_URL, headers=headers)
print(f'Status code: {response.status_code}')
pprint(response.json())
</code></pre

<pre><code class="python">Status code: 200
{'data': [{'_id': '642621ffffa3551fb6d2f###',
           'created_at': '2023-03-30T23:57:48Z',
           'metadata': {'duration': 80.32,
                        'engine_id': 'marengo2',
                        'filename': 'Machine Learning is Everywhere',
                        'fps': 25,
                        'height': 720,
                        'size': 11877525,
                        'width': 1280},
           'updated_at': '2023-03-30T23:57:51Z'}],
 'page_info': {'limit_per_page': 10,
               'page': 1,
               'total_duration': 80.32,
               'total_page': 1,
               'total_results': 1}}
</code></pre

시맨틱 비디오 검색: 특정 순간 찾기

모델의 비디오 인덱싱 변환이 완료되고 고유 비디오 임베딩이 생성되는 즉시, Search API를 유용하게 사용할 수 있습니다. 해당 REST Endpoint는 사용자가 정교하게 던진 자연어에 담긴 문맥과 시맨틱 요소를 추출한 뒤, 일치도가 가장 높은 비디오 내부의 정확한 시작(start)과 종료(end) 타임코드를 도출해 냅니다. 최초 인덱스 구성 시점에 선언한 옵션 조합과 일치하도록, 시맨틱 비디오 검색 범위 역시 세부 선택하여 컨트롤할 수 있습니다. 예를 들어, 인덱스 생성 시 모든 옵션을 켰다면 소리-영상 정보와 대화 텍스트, 화면 속 OCR 자재들까지 모두 결합 탐색이 가능해집니다. 인덱스 레벨과 검색 레벨 모두에서 동일한 옵션을 개별적으로 제공하는 이유는 개발자의 자유도를 최대화하기 위함입니다. 사용 중인 컨텍스트의 요구 사항에 딱 맞는 다채로운 탐색 조건들을 유연하게 혼합 선별할 수 있습니다.

자, 그럼 가볍게 직관적인 자연어 검색어인 “a guy doing a trick on a skateboard”(스케이트보드 트릭을 구사하는 남자)를 넘겨 시각 검색(visual search)를 돌려보겠습니다.

<pre><code class="bash">Status code: 200
{'data': [{'confidence': 'high',
           'end': 49.34375,
           'metadata': [{'type': 'visual'}],
           'score': 83.24,
           'start': 41.65625,
           'video_id': '642621ffffa3551fb6d2f###'}],
 'page_info': {'limit_per_page': 10,
               'page_expired_at': '2023-03-31T22:41:42Z',
               'total_results': 1},
 'search_pool': {'index_id': '642621fb7b1f2230dfcd####',
                 'total_count': 1,
                 'total_duration': 80}}
</code></pre

조건에 매칭되는 비디오 특정 시점:

이 단계는 볼 때마다 감탄을 자아냅니다. 모델이 사람이 비디오를 보고 이해하는 방식에 극도로 가깝다는 점을 확연하게 증명하기 때문인데요. 위의 스크린샷 이미지처럼 시스템은 제가 정확하게 지칭하고 요구했던 지점의 최적 구간을 빈틈없이 포착해 냈습니다.

이 기세를 이어 "a guy playing table tennis with a robotic arm" (로봇 팔과 함께 탁구를 치고 있는 사람) 이라는 새로운 쿼리를 보낸 뒤, 시스템이 다시 한번 놀라운 정확도를 뽐내는지 직접 검증해 봅시다.

<pre><code class="python"># Construct the URL of the `/search` endpoint
SEARCH_URL = f"{API_URL}/search/"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}
query = "a guy playing table tennis with a robotic arm"
# Declare a dictionary named `data`
data = {
    "query": query,  # Specify your search query
    "index_id": INDEX_ID,  # Indicate the unique identifier of your index
    "search_options": ["visual"],  # Specify the search options
}

# Make a search request
response = requests.post(SEARCH_URL, headers=headers, json=data)
print(f"Status code: {response.status_code}")
pprint(response.json())
</code></pre


출력 결과:

<pre><code class="python">{'data': [{'confidence': 'high',
           'end': 14.6875,
           'metadata': [{'type': 'visual'}],
           'score': 90.62,
           'start': 9.75,
           'video_id': '642621ffffa3551fb6d2####'},
          {'confidence': 'high',
           'end': 9.75,
           'metadata': [{'type': 'visual'}],
           'score': 89.74,
           'start': 4.5,
           'video_id': '642621ffffa3551fb6d2####'}],
</code></pre


조건에 부합하는 타임프레임 결과 화면:

확실하게 정확해졌군요! 이번에도 시스템이 해당 사건이 담긴 최적의 지점을 완벽하게 잡아냈습니다.

💡여기 간략하고 재미있는 서브 챌린지가 있습니다. "a breakthrough in machine learning would be worth ten Microsofts"(머신러닝의 중대한 돌파구는 마이크로소프트 10개만큼의 가치가 있을 것이다)라고 검색어를 타이핑한 후에, Search option 설정값을 ["text_in_video"]로 바꾼 채 시각 탐색을 테스트해 보세요.

매번 시작과 중간 타임스탬프 값을 JSON 데이터 형태를 쳐다보며 직접 비교하는 일은 번잡할 수 있습니다. 이를 우아하게 연계 표시할 수 있는 아름다운 웹 기반 인터페이스 화면을 만들어보겠습니다. 검색 응답으로 넘어온 실시간 JSON 정보를 우리가 출력할 대상 비디오 단말 프레임에 곧장 동기화해 봅니다. 그럼 가볍게 제작해 볼까요?

데모 애플리케이션 빌딩

여기까지 비디오 이해 여정에 즐겁게 호응해 주시고 따라와 주셔서 진심으로 감사합니다 🎉🥳👏! 이제 마지막 피날레에 돌입하겠습니다. 이전 단계에서 생성한 동영상 탐색 JSON 타임스탬프 결과물들을 정형 분석한 뒤, 가장 아름답고 보기 편한 웹뷰 컴포넌트로 뿌려주는 초경량 Flask 앱을 조립해 보겠습니다. 저는 평소 데이터 분석과 밀접하게 일하며 Python 환경을 편애해 온 취향에 맞춰 Flask를 애용하지만, 여러분은 각자의 취향에 알맞은 다른 웹 개발 프레임워크를 마음껏 변형 택하셔도 완벽하게 호환 작동합니다.


우선 Jupyter Notebook 상에 필요한 패키지 라이브러리를 추가해 둡니다.

<pre><code class="python">import json
import pickle
</code></pre


우리는 Search API 추출 원시 응답값으로부터 각 검색 장면들의 시작 및 끝 타임프레임 시간을 안전하게 축적 보존하기 위한 두 가지 보조 리스트 자료형인 「starts」와 「ends」를 세팅하게 됩니다:

<pre><code class="python">data = response.json()

starts = []
ends = []

for item in data['data']:
    starts.append(item['start'])
    ends.append(item['end'])

print("starts:", starts)
print("ends:", ends)
</code></pre

<pre><code class="bash">starts: [34.90625, 0, 62.5625]
ends: [39.21875, 4.5, 65.90625]
</code></pre


타임스탬프 영역이 확보되었고 해당 테스트 비디오 역시 제 개인 유튜브 채널에 무사히 배포된 상태이므로, 이를 활용할 수 있는 간단한 방법들을 고안해볼 수 있습니다. 로컬 저장 장치에 있는 비디오 원천 파일을 활용해 필요한 클립을 웹 인터페이스로 직접 스트리밍할 수도 있고, 유튜브 채널 플레이어 연동 URL을 적재하는 것으로도 멋진 비주얼 출력이 똑같이 대응 가능합니다. 개인적으로 후자의 방식이 구현상 더욱 직관적이라 생각되므로 유튜브 임베딩 코드를 생성하고 그 내부에 매칭할 start 및 end 한계 타임스탬프 주소를 주입하는 방식을 취하도록 하겠습니다. 이렇게 설계하면 복잡한 플레이어 로직 없이도 필터링된 범위만큼만 장면이 출력됩니다. 다만 한 가지 소소하게 참고할 사항으로는, Youtube Embed 타임 파라미터는 소수점이 없는 정수 형태의 속성값만을 완벽 수공하므로 다음과 같이 반올림 캐스팅을 해줍니다.

<pre><code class="bash">starts_int = [int(f) for f in starts]
ends_int = [int(f) for f in ends]
</code></pre


이제 이 리스트와 우리가 입력했던 쿼리를 간단하게 파일로 저장(pickle)해 보겠습니다. 이렇게 해두면 곧바로 만들기 시작할 Flask 앱 파일에 매개변수를 넘겨주기가 아주 편합니다:

<pre><code class="python">with open("lists.pkl", "wb") as f:
    pickle.dump((starts_int, ends_int, query), f)
</code></pre>

좋습니다! Flask 기반으로 해당 유효 범위들을 끄집어낼 완치 동작 사전 세팅들이 순조롭게 해결되었습니다.

Flask 앱 구축 개발 단계

1. 새 Flask 프로젝트 디렉터리 빌딩: 전용 폴더를 만들어서 해당 안쪽 범위에 핵심 진입점이 되어줄 소스 코드 파일을 새로 생성해 올립니다.

<pre><code class="bash">mkdir my_flask_app
cd my_flask_app
touch app.py
</code></pre>

동작할 소스 및 템플릿 파일 배치 구성이 전부 들어선 폴더 구조의 배치 모습은 다음과 같을 것입니다.

<pre><code class="markdown">my_flask_app/
│   app.py
│   ml.mp4
│
└───templates/
    │   index.html
</code></pre>

테스트로 올릴 비디오 원시 mp4 파일은 작성 중인 my_flask_app 하위 내부 노드 경로 내에 나란히 안치해 주시기 바랍니다.

2. Flask 어플리케이션 소스 작성: app.py 파일에 핵심 비즈니스 호출 처리 로직을 추가 구성합니다. Jinja2 기반의 동적 템플릿 변환 기법에 실시간 생성한 시작-끝 정수 배열 스택 구조의 상태 정보들을 안전하게 맵핑해 줄 수 있도록 연동합니다.

<pre><code class="python">from flask import Flask, render_template
import pickle

with open("lists.pkl", "rb") as f:
    starts, ends, query = pickle.load(f)

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html", starts=starts, ends=ends, query=query)

if __name__ == "__main__":
    app.run(debug=True)
</code></pre

3. 전용 템플릿(templates) 디렉터리 구성: Jinja2 엔진 파일을 Flask 런타임에서 온전히 읽어 렌더링하기 위해서 어플리케이션 루트 위치와 일치하는 위상 하단에 templates 폴더 환경을 만듭니다. 이 폴더 안에 구현할 UI 마크업 뼈대가 상주할 예정입니다.

<pre><code class="bash">mkdir templates
</code></pre>

최종 관문은 사용자의 자연어 입력에 정확히 부합하는 장면으로 걸러진 클립 조각들을 웹에 정렬시켜 줄 index.html 화면 설계입니다. 코드로 작성해 올리기 전 제 Youtube 단말 채널에서 제공되는 정형화된 Embed Video 소스 형태를 미리 한 차례 점검해 보세요.

4. Jinja2 템플릿 가공: templates 디렉터리 내부에 index.html 파일을 가볍게 생성하는 명령을 수행해 줍니다.

<pre><code class="bash">touch index.html
</code></pre>

어플리케이션 인터페이스 환경을 채워줄 깔끔하고 명확한 주입식 코드 구성 예시입니다. HTML 마크업 블림 속에 루프문을 걸어 starts, ends 및 query 변수를 반복 출력시킵니다.

<pre><code class="language-html"><html>
  <head>
    <style>
      body {
        background-color: #F2F2F2;
        font-family: Arial, sans-serif;
        text-align: center;
      }
      h1 {
        margin-top: 40px;
      }
      .video-container {
        display: flex;
        flex-wrap: wrap;
        padding: 40px;
        justify-content: center;
      }
      .video-item {
        display: flex;
        flex-direction: column;
        align-items: center;
        width: 50%;
        height: 600px;
        margin: 20px;
        text-align: center;
      }
      .video-item iframe {
        width: 80%;
        height: 380px;
        margin: 20px;
      }
      .video-item p {
        font-size: 16px;
        margin-top: 10px;
        font-weight: bold;
      }
    </style>
  </head>
  <body>
    <h1>My Favorite Scenes</h1>
    <div class="video-container">
      {% for i in range(starts|length) %}
        <div class="video-item">
          <iframe width="560" height="315" src="https://www.youtube.com/embed/hdZ_tNtdB4c?start={{ starts[i] }}&amp;end={{ ends[i] }}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in- picture" allowfullscreen></iframe>
          <p>Start: {{ starts[i] }} | End: {{ ends[i] }}</p>
          <p>Query: {{ query }}</p>
        </div>
      {% endfor %}
    </div>
  </body>
</html>
</code></pre>

완벽합니다! 그럼 가볍게 Jupyter Notebook의 마지막 실행 블록 코드를 실행시켜 보지요:

<pre><code class="python">%run app.py
</code></pre>

콘솔 창에 아래와 같이 로컬 호스트 실행 서버 응답 메시지가 성공적으로 떨어진다면, 모든 흐름이 우리의 예상 시나리오대로 원활하게 안착한 상태입니다😊:

서버 주소인 http://127.0.0.1:5000 링크를 클릭하여 모니터를 열면, 우리가 인덱스 질의로 요구했던 세그먼트에 맞춘 최종 데모 뷰가 바로 노출됩니다.

어플리케이션 안의 미디어를 직접 가동해 보면 지정한 타임스탬프 슬롯과 연동하여, 우리가 찾고자 식별했던 정확한 정황 지점 장면부터 정확히 실행되는 것을 체감하실 수 있습니다.

로컬 PC 환경에서 곧바로 테스트해볼 수 있도록 Jupyter Notebook 및 필요 파일 일체를 담은 보조 다운로드 폴더 공간을 공유해 둡니다. - https://tinyurl.com/twelvelabs

더 도전해볼 만한 재미있는 과제들:

  1. 다양한 검색 옵션 조합(visual, conversation, text-in-video)을 마음껏 실험해 보며 검출 결과가 어떻게 달라지는지 체감해 보세요.

  2. 여러 개의 동영상 파일을 한데 모아 동시 업로딩한 다음, 전체 아카이브 대역을 한 번에 전역 검색(Search across videos)할 수 있도록 확장 개량해 보세요.

  3. 한 단계 더 나아가 index.html 기본 웹에 사용자가 직접 텍스트 인풋 폼으로 검색어를 입력하고, 실시간 반응형으로 결과를 렌더링하도록 훌륭하게 업그레이드해 보세요.

‍다음 이야기

다음 포스트에서는 여러 개의 단순 쿼리를 논리 연산자로 깔끔하게 조합하여 비디오 컬렉션 전체에서 더욱 정밀하게 복합 시맨틱 검색을 실행하는 방법을 심도 있게 나눠 보겠습니다. 다음 편도 많은 기대 부탁드립니다!

아, 그리고 마지막 하나 더! 멀티모달 파운데이션 모델에 대해 흥미가 있으시다면, 트렌드를 나누고 개척해 가는 동료 개발자 및 연구자분들과 교류할 수 있는 공식 Discord 커뮤니티에 꼭 찾아오세요. 자유롭게 지식을 나누고 질문하며 함께 성장하기에 참 좋은 곳입니다!

최근 Twelve Labs는 GTC 2023에서 멀티모달 AI 부문의 선두 주자로 소개되었습니다. 당시 GTC 2023 영상을 보면서, 공동 창업자인 Soyoung 님이 우리 회사가 소개된 장면을 찾기 위해 머리를 싸매며 고군분투하는 모습을 보았습니다. 이 경험을 계기로, Twelve Labs의 고유한 API를 사용하여 '비디오 내부 검색'이라는 과제에 직접 도전해봐야겠다는 자극을 받았습니다. 그렇게 해서 시작된 주말 프로젝트의 튜토리얼을 공유하고자 합니다. 이번 가이드를 통해 Twelve Labs의 검색 API를 사용하여 영상 속에서 원하는 특정 순간을 찾아내는 과정을 자세히 소개해 드리겠습니다.

                     비디오 내부 검색을 한 단계 더 업그레이드하기 위한 청사진😎

소개

Twelve Labs는 비디오 이해(Video Understanding) 기능을 활용한 애플리케이션 개발을 돕기 위해, 강력한 API 제품군 형태의 멀티모달 파운데이션 모델을 제공합니다. 이 블로그 포스트에서는 자연어 쿼리를 사용하여 Twelve Labs API가 비디오 내에서 원하는 정확한 지점을 얼마나 자연스럽게 찾아내는지 살펴보겠습니다. 저는 테스트를 위해 대학원 시절 직접 제작한 "Machine Learning is Everywhere"라는 재미있는 영상을 로컬 드라이브에서 업로드해 보았습니다. 제목 그대로 80초 남짓한 이 비디오는 우리 삶 일상 곳곳에 스며든 머신러닝을 다룹니다. 예를 들어, 프로 탁구 선수가 쿠카(Kuka) 로봇과 대결하는 장면이나, 스케이트보드 트릭을 선보이는 남자의 모습을 머신러닝이 자동 요약하는 등 다양한 ML 적용 사례가 등장합니다. Twelve Labs API를 활용해, 이러한 영상 속 특정 장면을 단순 평문 형태의 자연어 검색만으로 어떻게 찾아낼 수 있는지 직접 시연해 보이겠습니다.

이번 튜토리얼의 목적은 검색 API를 직관적이고 편안하게 시작할 수 있도록 돕는 데 있습니다. 따라서 한 개의 비디오에서만 특정 장면을 검색하는 방식으로 기능을 최소화하고, 가볍고 친숙한 Flask 프레임워크 기반의 데모 앱을 구현하는 데 초점을 맞췄습니다. 하지만 아키텍처 자체가 확장성이 뛰어나기 때문에, 실제 프로젝트에서는 수백, 수천 개의 비디오를 한 번에 업로드하고 검색을 동시 실행하는 규모로도 완전하게 대응 가능합니다. 자, 이제 본격적인 흥미를 채우러 출발해 볼까요?

빠른 요약

  • 선행 조건: 회원가입을 하고 Twelve Labs API 키를 발급받은 뒤, 데모 애플리케이션 제작을 위한 필요 패키지를 설치합니다.

  • 비디오 업로드: 검색해볼 비디오가 준비되었나요? Twelve Labs 플랫폼으로 전송하면 간편하게 인덱싱을 완료한 뒤 고속 검색 준비를 마칩니다.

  • 시맨틱 비디오 검색: 자연어 질의문을 사용해 영상 속 기억에 남는 정확한 장면을 영리하게 추적합니다.

  • 데모 앱 제작: Flask를 사용해 HTML 템플릿을 화면에 띄우는 가벼운 Python 스크립트를 작성하고, 검색 결과를 정갈하게 보여줄 HTML 페이지를 구성해 봅니다.

💡 만약 개발자가 아니신 분이 읽고 계시더라도 전혀 걱정하실 필요 없습니다! 즉시 실행해 보고 결과를 확인해볼 수 있는 완제품 상태의 Jupyter Notebook 링크를 준비해 두었습니다. 또한 코드 한 줄도 작성하지 않고 시맨틱 비디오 검색의 강력함을 체험해 보고 싶으시다면, 저희 Playground를 직접 활용해 보세요. 무료 크레딧이 필요하시다면 언제든 저에게 연락해 주세요😄.

선행 조건

이 튜토리얼에서는 Jupyter Notebook을 기준으로 진행합니다. 로컬 컴퓨터에 Jupyter, Python 및 Pip가 이미 설정되어 있다고 가정하겠습니다. 진행 도중 예외가 발생하거나 가이드가 필요하면 주저하지 마시고 저희 Discord 서버를 찾아 노크해 주세요. 가장 빠르게 응답해 드립니다 🚅🏎️⚡️. 만약 Discord가 편하지 않으시다면 이메일 창구를 통해 문의를 남겨주셔도 좋습니다. Twelve Labs 계정을 생성하면 API 대시보드 화면에서 API Key를 확인할 수 있습니다. 이번 실습에서는 이미 존재하는 계정을 통해 API 호출을 진행하겠습니다. 간단하게 본인의 Secret Key를 담아 제공되는 API 엔드포인트 URL로 호출을 요청하면 되며, 필요한 설정 매개변수들은 다음과 같이 환경 변수로 깔끔하게 넘겨줄 수 있습니다.

<pre><code class="bash">%env API_KEY=<your_API_key>
%env API_URL=https://api.twelvelabs.io/v1.1
</code></pre>


의존성 설치:

<pre><code class="python">pip install requests
pip install flask
</code></pre>

비디오 업로드

첫 번째 단계로, 저의 로컬 컴퓨터에 보관 중이던 비디오 파일을 Twelve Labs 플랫폼으로 업로드하여 비디오 이해 기술을 적용하는 방식을 보여 드리겠습니다.

가져오기(Imports):

<pre><code class="python">import os
import requests
import glob
from pprint import pprint
</code></pre

다음과 같이 API 엔드포인트 URL과 발급받은 API Key를 변수에 바인딩합니다.

<pre><code class="python">API_URL = os.getenv("API_URL")
assert API_URL
</code></pre

<pre><code class="python">API_KEY = os.getenv("API_KEY")
assert API_KEY
</code></pre

인덱스(Index) API

이제 Index API를 호출하여 비디오 인덱스를 생성할 차례입니다. 비디오 인덱스는 한 개 이상의 비디오 파일 그룹을 하나로 묶고 공동의 타겟 검색 옵션을 정의함으로써, 인덱스 내에 탑재된 대용량 비디오에 대해 전반적인 시맨틱 매칭 처리를 손쉽게 수행할 수 있게 돕는 유연한 가상 컨테이너 역할을 합니다.

인덱스는 다음과 같은 필드 정보를 기반으로 구성됩니다:

<ul>
<li><b>이름(name)</b></li>
<li><b>엔진(engine)</b> - 현재 저희는 비디오 이해에 최적화된 최신 멀티모달 파운데이션 모델인 Marengo2를 기본 제공합니다.</li>
<li>한 가지 이상의 <b>인덱싱 옵션(indexing options)</b>:</li>
<ul>
<li><b>visual</b>: 이 옵션이 선택되면, API 서비스가 영상에 대해 오디오-비주얼 결합 분석을 수행하여 사물, 행동, 사운드, 모션, 장소, 정황 이벤트, 그리고 복합 오디오-비주얼 자연어 설명을 기준으로 검색할 수 있도록 지원합니다. 예를 들면 '환호하는 군중들'이나 '밤샘 작업을 마치고 퇴근하는 초췌한 개발자들' 같은 검색이 가능해집니다😆.</li>
<li><b>conversation</b>: 이 옵션을 켜두면 비디오 내부 임베디드 오디오 음성을 텍스트로 추출(녹취록 작성)한 뒤, 이를 바탕으로 시맨틱 NLP 의미론 분석을 진행합니다. 덕분에 대화 속에 포함된 구체적 어휘나 주제 흐름에 기반해 정확히 어느 타이밍에 말했는지를 정확하게 찾아내 줍니다. 예를 들면, 인덱싱된 대화 중 '어렸을 때 동생한테 거짓말한 순간' 같은 키워드로 정확히 특정해 찾아낼 수 있습니다😜.</li>
<li><b>text_in_video</b>: 이 옵션은 화면 안에 비춰진 표지판, 문자, 라벨, 자막, 프레젠테이션, 브랜드 로고, 문서 등을 OCR(광학 문자 인식) 기법을 거쳐 탐색하게 만들어 줍니다. 이를테면 축구 경기 영상 중 특정 스포츠 브랜드 로고가 노출된 프레임들이 언제인지 정확히 걸러내고 싶을 때 매우 유용합니다🏈.</li></ul></ul>

인덱스 생성 코드:

<pre><code class="python"># Construct the URL of the `/indexes` endpoint
INDEXES_URL = f"{API_URL}/indexes"

# Specify the name of the index
INDEX_NAME = "My University Days"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}

# Declare a dictionary named data
data = {
    "engine_id": "marengo2",  
    "index_options": ["visual", "conversation", "text_in_video"], 
    "index_name": INDEX_NAME,
}

# Create an index
response = requests.post(INDEXES_URL, headers=headers, json=data)

# Store the unique identifier of your index
INDEX_ID = response.json().get('_id')

# Print the status code and response
print(f'Status code: {response.status_code}')
pprint(response.json())
</code></pre

태스크(Task) API를 통한 비디오 업로드

Twelve Labs 플랫폼은 생성된 인덱스 내부로 비디오 파일을 전송하고 진행 경과를 추적 제어할 수 있는 Task API를 지원합니다.

<pre><code class="python">TASKS_URL = f"{API_URL}/tasks"

file_name = "Machine Learning is Everywhere" # indexed video will have this file name
file_path = "ml.mp4" # file name of the video being uploaded
file_stream = open(file_path,"rb")

data = {
    "index_id": INDEX_ID, 
    "language": "en"
}

file_param = [
    ("video_file", (file_name, file_stream, "application/octet-stream")),]

response = requests.post(TASKS_URL, headers=headers, data=data, files=file_param
</code></pre

비디오가 안정적으로 입고되는 즉시 시스템 내부에서 자동 인덱싱 변환 파이프라인이 즉각 가동됩니다. Twelve Labs는 뛰어난 시구간 분석 능력을 지닌 멀티모달 기반 파운데이션 모델을 기반으로 비디오에 담긴 행동 양상, 피사체의 움직임, 음성, 메타 텍스트 정보 등을 심층 분석하여 정교한 고밀도 비디오 임베딩(Embedding)을 영리하게 복구해 냅니다. 이 과정을 통하여 평상시 자연스럽게 사용하는 구어체 질의 형태나 라벨 가이드를 활용한 지식 접근이 현실화됩니다.

인덱싱 진행 상태 모니터링:

<pre><code class="python">import time

# Define starting time
start = time.time()
print("Start uploading video")

# Monitor the indexing process
TASK_STATUS_URL = f"{API_URL}/tasks/{TASK_ID}"
while True:
    response = requests.get(TASK_STATUS_URL, headers=headers)
    STATUS = response.json().get("status")
    if STATUS == "ready":
        print(f"Status code: {STATUS}")
        break
    time.sleep(10)

# Define ending time
end = time.time()
print("Finish uploading video")
print("Time elapsed (in seconds): ", end - start)
</code></pre

<pre><code class="python"># Retrieve the unique identifier of the video
VIDEO_ID = response.json().get('video_id')

# Print the status code, the unique identifier of the video, 
and the response
print(f"VIDEO ID: {VIDEO_ID}")
pprint(response.json())
</code></pre

<pre><code class="language-plaintext">VIDEO ID: 642621ffffa3551fb6d2f###
{'_id': '642621fc3205dc8a48ba8###',
 'created_at': '2023-03-30T23:57:48.877Z',
 'estimated_time': '2023-03-30T23:59:58.312Z',
 'index_id': '###621fb7b1f2230dfcd6###',
 'metadata': {'duration': 80.32,
              'filename': 'Machine Learning is Everywhere',
              'height': 720,
              'width': 1280},
 'status': 'ready',
 'type': 'index_task_info',
 'updated_at': '2023-03-31T00:00:34.412Z',
 'video_id': '642621ffffa3551fb6d2f###'}
</code></pre

데모 애플리케이션에서 인덱스의 고유 식별자(ID)를 참고할 수 있도록 환경 변수를 한 차례 더 등록해 줍니다:

<pre><code class="python">%env ENV_INDEX_ID = ###621fb7b1f2230dfcd6###
</code></pre

현재 설정한 인덱스에 보관된 비디오 원본 목록을 봅니다. 구조의 파악을 돕기 위해 의도적으로 인덱싱 파일을 1개 단위로 단순 제어했지만, 테스트용 무료 크레딧을 이용해 최대 10시간 정량 범위 내에서 자유롭게 영상 파일을 쌓아두고 전체 검색을 시도하는 것도 좋은 선택입니다.

<pre><code class="python"># Retrieve the unique identifier of the existing index
INDEX_ID = os.getenv("ENV_INDEX_ID")

# Set the header of the request
headers = {
    "x-api-key": API_KEY,
}

# List all the videos in an index
INDEXES_VIDEOS_URL = f"{API_URL}/indexes/{INDEX_ID}/videos"
response = requests.get(INDEXES_VIDEOS_URL, headers=headers)
print(f'Status code: {response.status_code}')
pprint(response.json())
</code></pre

<pre><code class="python">Status code: 200
{'data': [{'_id': '642621ffffa3551fb6d2f###',
           'created_at': '2023-03-30T23:57:48Z',
           'metadata': {'duration': 80.32,
                        'engine_id': 'marengo2',
                        'filename': 'Machine Learning is Everywhere',
                        'fps': 25,
                        'height': 720,
                        'size': 11877525,
                        'width': 1280},
           'updated_at': '2023-03-30T23:57:51Z'}],
 'page_info': {'limit_per_page': 10,
               'page': 1,
               'total_duration': 80.32,
               'total_page': 1,
               'total_results': 1}}
</code></pre

시맨틱 비디오 검색: 특정 순간 찾기

모델의 비디오 인덱싱 변환이 완료되고 고유 비디오 임베딩이 생성되는 즉시, Search API를 유용하게 사용할 수 있습니다. 해당 REST Endpoint는 사용자가 정교하게 던진 자연어에 담긴 문맥과 시맨틱 요소를 추출한 뒤, 일치도가 가장 높은 비디오 내부의 정확한 시작(start)과 종료(end) 타임코드를 도출해 냅니다. 최초 인덱스 구성 시점에 선언한 옵션 조합과 일치하도록, 시맨틱 비디오 검색 범위 역시 세부 선택하여 컨트롤할 수 있습니다. 예를 들어, 인덱스 생성 시 모든 옵션을 켰다면 소리-영상 정보와 대화 텍스트, 화면 속 OCR 자재들까지 모두 결합 탐색이 가능해집니다. 인덱스 레벨과 검색 레벨 모두에서 동일한 옵션을 개별적으로 제공하는 이유는 개발자의 자유도를 최대화하기 위함입니다. 사용 중인 컨텍스트의 요구 사항에 딱 맞는 다채로운 탐색 조건들을 유연하게 혼합 선별할 수 있습니다.

자, 그럼 가볍게 직관적인 자연어 검색어인 “a guy doing a trick on a skateboard”(스케이트보드 트릭을 구사하는 남자)를 넘겨 시각 검색(visual search)를 돌려보겠습니다.

<pre><code class="bash">Status code: 200
{'data': [{'confidence': 'high',
           'end': 49.34375,
           'metadata': [{'type': 'visual'}],
           'score': 83.24,
           'start': 41.65625,
           'video_id': '642621ffffa3551fb6d2f###'}],
 'page_info': {'limit_per_page': 10,
               'page_expired_at': '2023-03-31T22:41:42Z',
               'total_results': 1},
 'search_pool': {'index_id': '642621fb7b1f2230dfcd####',
                 'total_count': 1,
                 'total_duration': 80}}
</code></pre

조건에 매칭되는 비디오 특정 시점:

이 단계는 볼 때마다 감탄을 자아냅니다. 모델이 사람이 비디오를 보고 이해하는 방식에 극도로 가깝다는 점을 확연하게 증명하기 때문인데요. 위의 스크린샷 이미지처럼 시스템은 제가 정확하게 지칭하고 요구했던 지점의 최적 구간을 빈틈없이 포착해 냈습니다.

이 기세를 이어 "a guy playing table tennis with a robotic arm" (로봇 팔과 함께 탁구를 치고 있는 사람) 이라는 새로운 쿼리를 보낸 뒤, 시스템이 다시 한번 놀라운 정확도를 뽐내는지 직접 검증해 봅시다.

<pre><code class="python"># Construct the URL of the `/search` endpoint
SEARCH_URL = f"{API_URL}/search/"

# Set the header of the request
headers = {
    "x-api-key": API_KEY
}
query = "a guy playing table tennis with a robotic arm"
# Declare a dictionary named `data`
data = {
    "query": query,  # Specify your search query
    "index_id": INDEX_ID,  # Indicate the unique identifier of your index
    "search_options": ["visual"],  # Specify the search options
}

# Make a search request
response = requests.post(SEARCH_URL, headers=headers, json=data)
print(f"Status code: {response.status_code}")
pprint(response.json())
</code></pre


출력 결과:

<pre><code class="python">{'data': [{'confidence': 'high',
           'end': 14.6875,
           'metadata': [{'type': 'visual'}],
           'score': 90.62,
           'start': 9.75,
           'video_id': '642621ffffa3551fb6d2####'},
          {'confidence': 'high',
           'end': 9.75,
           'metadata': [{'type': 'visual'}],
           'score': 89.74,
           'start': 4.5,
           'video_id': '642621ffffa3551fb6d2####'}],
</code></pre


조건에 부합하는 타임프레임 결과 화면:

확실하게 정확해졌군요! 이번에도 시스템이 해당 사건이 담긴 최적의 지점을 완벽하게 잡아냈습니다.

💡여기 간략하고 재미있는 서브 챌린지가 있습니다. "a breakthrough in machine learning would be worth ten Microsofts"(머신러닝의 중대한 돌파구는 마이크로소프트 10개만큼의 가치가 있을 것이다)라고 검색어를 타이핑한 후에, Search option 설정값을 ["text_in_video"]로 바꾼 채 시각 탐색을 테스트해 보세요.

매번 시작과 중간 타임스탬프 값을 JSON 데이터 형태를 쳐다보며 직접 비교하는 일은 번잡할 수 있습니다. 이를 우아하게 연계 표시할 수 있는 아름다운 웹 기반 인터페이스 화면을 만들어보겠습니다. 검색 응답으로 넘어온 실시간 JSON 정보를 우리가 출력할 대상 비디오 단말 프레임에 곧장 동기화해 봅니다. 그럼 가볍게 제작해 볼까요?

데모 애플리케이션 빌딩

여기까지 비디오 이해 여정에 즐겁게 호응해 주시고 따라와 주셔서 진심으로 감사합니다 🎉🥳👏! 이제 마지막 피날레에 돌입하겠습니다. 이전 단계에서 생성한 동영상 탐색 JSON 타임스탬프 결과물들을 정형 분석한 뒤, 가장 아름답고 보기 편한 웹뷰 컴포넌트로 뿌려주는 초경량 Flask 앱을 조립해 보겠습니다. 저는 평소 데이터 분석과 밀접하게 일하며 Python 환경을 편애해 온 취향에 맞춰 Flask를 애용하지만, 여러분은 각자의 취향에 알맞은 다른 웹 개발 프레임워크를 마음껏 변형 택하셔도 완벽하게 호환 작동합니다.


우선 Jupyter Notebook 상에 필요한 패키지 라이브러리를 추가해 둡니다.

<pre><code class="python">import json
import pickle
</code></pre


우리는 Search API 추출 원시 응답값으로부터 각 검색 장면들의 시작 및 끝 타임프레임 시간을 안전하게 축적 보존하기 위한 두 가지 보조 리스트 자료형인 「starts」와 「ends」를 세팅하게 됩니다:

<pre><code class="python">data = response.json()

starts = []
ends = []

for item in data['data']:
    starts.append(item['start'])
    ends.append(item['end'])

print("starts:", starts)
print("ends:", ends)
</code></pre

<pre><code class="bash">starts: [34.90625, 0, 62.5625]
ends: [39.21875, 4.5, 65.90625]
</code></pre


타임스탬프 영역이 확보되었고 해당 테스트 비디오 역시 제 개인 유튜브 채널에 무사히 배포된 상태이므로, 이를 활용할 수 있는 간단한 방법들을 고안해볼 수 있습니다. 로컬 저장 장치에 있는 비디오 원천 파일을 활용해 필요한 클립을 웹 인터페이스로 직접 스트리밍할 수도 있고, 유튜브 채널 플레이어 연동 URL을 적재하는 것으로도 멋진 비주얼 출력이 똑같이 대응 가능합니다. 개인적으로 후자의 방식이 구현상 더욱 직관적이라 생각되므로 유튜브 임베딩 코드를 생성하고 그 내부에 매칭할 start 및 end 한계 타임스탬프 주소를 주입하는 방식을 취하도록 하겠습니다. 이렇게 설계하면 복잡한 플레이어 로직 없이도 필터링된 범위만큼만 장면이 출력됩니다. 다만 한 가지 소소하게 참고할 사항으로는, Youtube Embed 타임 파라미터는 소수점이 없는 정수 형태의 속성값만을 완벽 수공하므로 다음과 같이 반올림 캐스팅을 해줍니다.

<pre><code class="bash">starts_int = [int(f) for f in starts]
ends_int = [int(f) for f in ends]
</code></pre


이제 이 리스트와 우리가 입력했던 쿼리를 간단하게 파일로 저장(pickle)해 보겠습니다. 이렇게 해두면 곧바로 만들기 시작할 Flask 앱 파일에 매개변수를 넘겨주기가 아주 편합니다:

<pre><code class="python">with open("lists.pkl", "wb") as f:
    pickle.dump((starts_int, ends_int, query), f)
</code></pre>

좋습니다! Flask 기반으로 해당 유효 범위들을 끄집어낼 완치 동작 사전 세팅들이 순조롭게 해결되었습니다.

Flask 앱 구축 개발 단계

1. 새 Flask 프로젝트 디렉터리 빌딩: 전용 폴더를 만들어서 해당 안쪽 범위에 핵심 진입점이 되어줄 소스 코드 파일을 새로 생성해 올립니다.

<pre><code class="bash">mkdir my_flask_app
cd my_flask_app
touch app.py
</code></pre>

동작할 소스 및 템플릿 파일 배치 구성이 전부 들어선 폴더 구조의 배치 모습은 다음과 같을 것입니다.

<pre><code class="markdown">my_flask_app/
│   app.py
│   ml.mp4
│
└───templates/
    │   index.html
</code></pre>

테스트로 올릴 비디오 원시 mp4 파일은 작성 중인 my_flask_app 하위 내부 노드 경로 내에 나란히 안치해 주시기 바랍니다.

2. Flask 어플리케이션 소스 작성: app.py 파일에 핵심 비즈니스 호출 처리 로직을 추가 구성합니다. Jinja2 기반의 동적 템플릿 변환 기법에 실시간 생성한 시작-끝 정수 배열 스택 구조의 상태 정보들을 안전하게 맵핑해 줄 수 있도록 연동합니다.

<pre><code class="python">from flask import Flask, render_template
import pickle

with open("lists.pkl", "rb") as f:
    starts, ends, query = pickle.load(f)

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html", starts=starts, ends=ends, query=query)

if __name__ == "__main__":
    app.run(debug=True)
</code></pre

3. 전용 템플릿(templates) 디렉터리 구성: Jinja2 엔진 파일을 Flask 런타임에서 온전히 읽어 렌더링하기 위해서 어플리케이션 루트 위치와 일치하는 위상 하단에 templates 폴더 환경을 만듭니다. 이 폴더 안에 구현할 UI 마크업 뼈대가 상주할 예정입니다.

<pre><code class="bash">mkdir templates
</code></pre>

최종 관문은 사용자의 자연어 입력에 정확히 부합하는 장면으로 걸러진 클립 조각들을 웹에 정렬시켜 줄 index.html 화면 설계입니다. 코드로 작성해 올리기 전 제 Youtube 단말 채널에서 제공되는 정형화된 Embed Video 소스 형태를 미리 한 차례 점검해 보세요.

4. Jinja2 템플릿 가공: templates 디렉터리 내부에 index.html 파일을 가볍게 생성하는 명령을 수행해 줍니다.

<pre><code class="bash">touch index.html
</code></pre>

어플리케이션 인터페이스 환경을 채워줄 깔끔하고 명확한 주입식 코드 구성 예시입니다. HTML 마크업 블림 속에 루프문을 걸어 starts, ends 및 query 변수를 반복 출력시킵니다.

<pre><code class="language-html"><html>
  <head>
    <style>
      body {
        background-color: #F2F2F2;
        font-family: Arial, sans-serif;
        text-align: center;
      }
      h1 {
        margin-top: 40px;
      }
      .video-container {
        display: flex;
        flex-wrap: wrap;
        padding: 40px;
        justify-content: center;
      }
      .video-item {
        display: flex;
        flex-direction: column;
        align-items: center;
        width: 50%;
        height: 600px;
        margin: 20px;
        text-align: center;
      }
      .video-item iframe {
        width: 80%;
        height: 380px;
        margin: 20px;
      }
      .video-item p {
        font-size: 16px;
        margin-top: 10px;
        font-weight: bold;
      }
    </style>
  </head>
  <body>
    <h1>My Favorite Scenes</h1>
    <div class="video-container">
      {% for i in range(starts|length) %}
        <div class="video-item">
          <iframe width="560" height="315" src="https://www.youtube.com/embed/hdZ_tNtdB4c?start={{ starts[i] }}&amp;end={{ ends[i] }}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in- picture" allowfullscreen></iframe>
          <p>Start: {{ starts[i] }} | End: {{ ends[i] }}</p>
          <p>Query: {{ query }}</p>
        </div>
      {% endfor %}
    </div>
  </body>
</html>
</code></pre>

완벽합니다! 그럼 가볍게 Jupyter Notebook의 마지막 실행 블록 코드를 실행시켜 보지요:

<pre><code class="python">%run app.py
</code></pre>

콘솔 창에 아래와 같이 로컬 호스트 실행 서버 응답 메시지가 성공적으로 떨어진다면, 모든 흐름이 우리의 예상 시나리오대로 원활하게 안착한 상태입니다😊:

서버 주소인 http://127.0.0.1:5000 링크를 클릭하여 모니터를 열면, 우리가 인덱스 질의로 요구했던 세그먼트에 맞춘 최종 데모 뷰가 바로 노출됩니다.

어플리케이션 안의 미디어를 직접 가동해 보면 지정한 타임스탬프 슬롯과 연동하여, 우리가 찾고자 식별했던 정확한 정황 지점 장면부터 정확히 실행되는 것을 체감하실 수 있습니다.

로컬 PC 환경에서 곧바로 테스트해볼 수 있도록 Jupyter Notebook 및 필요 파일 일체를 담은 보조 다운로드 폴더 공간을 공유해 둡니다. - https://tinyurl.com/twelvelabs

더 도전해볼 만한 재미있는 과제들:

  1. 다양한 검색 옵션 조합(visual, conversation, text-in-video)을 마음껏 실험해 보며 검출 결과가 어떻게 달라지는지 체감해 보세요.

  2. 여러 개의 동영상 파일을 한데 모아 동시 업로딩한 다음, 전체 아카이브 대역을 한 번에 전역 검색(Search across videos)할 수 있도록 확장 개량해 보세요.

  3. 한 단계 더 나아가 index.html 기본 웹에 사용자가 직접 텍스트 인풋 폼으로 검색어를 입력하고, 실시간 반응형으로 결과를 렌더링하도록 훌륭하게 업그레이드해 보세요.

‍다음 이야기

다음 포스트에서는 여러 개의 단순 쿼리를 논리 연산자로 깔끔하게 조합하여 비디오 컬렉션 전체에서 더욱 정밀하게 복합 시맨틱 검색을 실행하는 방법을 심도 있게 나눠 보겠습니다. 다음 편도 많은 기대 부탁드립니다!

아, 그리고 마지막 하나 더! 멀티모달 파운데이션 모델에 대해 흥미가 있으시다면, 트렌드를 나누고 개척해 가는 동료 개발자 및 연구자분들과 교류할 수 있는 공식 Discord 커뮤니티에 꼭 찾아오세요. 자유롭게 지식을 나누고 질문하며 함께 성장하기에 참 좋은 곳입니다!