
Tutorials
Twelve Labs와 함께 올림픽 비디오 분류 애플리케이션 구축하기

흐리시케시 야다브(Hrishikesh Yadav)
이 튜토리얼에서는 별도의 모델 학습 없이도 Twelve Labs의 Classification API와 Marengo 2.6을 활용해 스트림릿(Streamlit) 인터페이스에서 스포츠 영상을 미리 정의된 클래스 또는 사용자 지정 클래스로 자동 분류하는 '올림픽 비디오 클립 분류 애플리케이션'을 구축하는 과정을 단계별로 알아봅니다.
이 튜토리얼에서는 별도의 모델 학습 없이도 Twelve Labs의 Classification API와 Marengo 2.6을 활용해 스트림릿(Streamlit) 인터페이스에서 스포츠 영상을 미리 정의된 클래스 또는 사용자 지정 클래스로 자동 분류하는 '올림픽 비디오 클립 분류 애플리케이션'을 구축하는 과정을 단계별로 알아봅니다.

In this article
No headings found on page
뉴스레터 구독하기
뉴스레터 구독하기
영상 이해 분야의 최신 기술 업데이트, 튜토리얼 및 인사이트를 받아보세요.
영상 이해 분야의 최신 기술 업데이트, 튜토리얼 및 인사이트를 받아보세요.
AI로 영상을 검색하고, 분석하고, 탐색하세요.
2024. 9. 3.
15분
링크 복사하기
올림픽 동영상 영상을 정리하는 것만으로 금메달을 따는 꿈을 꾸어본 적이 있으신가요? 🥇 이제 그 시상대 위에 설 수 있는 기회가 왔습니다!
올림픽 동영상 클립 분류 애플리케이션은 스포츠 영상 분류라는 번거롭고 시간이 많이 걸리는 과정을 간소화하기 위해 설계되었습니다. Twelve Labs의 Marengo 2.6 임베딩 모델을 통해, 이 앱은 비디오 클립 내의 올림픽 스포츠 종목을 빠르게 분류해 냅니다.
화면 상의 텍스트, 음성 대화, 시각 요소를 모두 분석하여 동영상 클립을 손쉽게 카테고리별로 정렬합니다. 본 튜토리얼에서는 연구자, 스포츠 마니아, 방송사 관계자가 올림픽 콘텐츠를 다루는 방식을 완전히 혁신할 Streamlit 애플리케이션 구축 과정을 단계별로 안내합니다. 사용자가 직접 정의한 카테고리에 따라 동영상을 분류하는 앱을 구현하는 방법을 배우게 되며, 상세한 앱 시연 동영상은 아래에서 확인하실 수 있습니다.
애플리케이션 데모는 여기서 직접 실행해 볼 수 있습니다: 올림픽 분류 앱 데모. 또한, 이 Replit 템플릿을 통해 코드를 직접 작동시켜 볼 수도 있습니다.
사전 준비 단계
Twelve Labs Playground에 가입하고 API 키를 생성하세요.
노트북과 본 애플리케이션의 소스 코드가 포함된 GitHub 저장소를 확인하세요.
본 Streamlit 애플리케이션은 Python, HTML, JavaScript를 사용합니다.

애플리케이션 작동 원리
이 섹션에서는 올림픽 비디오 클립 분류를 위한 애플리케이션 파이프라인의 전체적인 흐름을 설명합니다.
이 애플리케이션은 다양한 활용 시나리오를 가진 분류 검색 엔진을 기반으로 개발되었습니다. 여기서는 올림픽 스포츠 동영상 클립을 분류하고 필요한 부분만 검색해 내는 작업에 집중합니다. 가장 핵심적인 첫 단계를 바로 인덱스(Index)를 생성하는 것입니다.
시작하려면 Twelve Labs Playground에 접속하세요.
새 인덱스(New Index)를 생성하고 비디오 인덱싱 및 분류 작업에 최적화된
Marengo 2.6 (Embedding Engine)을 선택합니다.분류하고자 하는 모든 스포츠 비디오를 해당 인덱스에 업로드하고 나면 다음 단계로 진행할 수 있습니다.
아래 섹션에서는 생성된 Index ID를 연동하고 실제로 구현하는 과정을 구체적으로 설명합니다. 먼저 전체적인 애플리케이션 워크플로우를 요약해 드립니다.
사용자는 다중 선택(Multi-select) 메뉴에서 시청하고자 하는 스포츠 카테고리를 직접 선택하거나, 본인만의 맞춤 클래스(Custom class)를 새로 정의하여 추가할 수 있습니다.
선택된 클래스는 해당 인덱스 내의 비디오 클립을 검색하고 분류하기 위해
INDEX ID및 기타 옵션(예:include_clips등)과 함께 classify 엔드포인트로 전송됩니다.API 응답 데이터에는 비디오 ID, 매칭된 클래스 이름, 신뢰도(Confidence Level), 시작 및 종료 시간, 썸네일 URL 등이 함께 포함됩니다.
검색 및 분류 결과로 반환된 비디오 ID의 실제 비디오 스트리밍 URL을 가져오기 위해 비디오 상세 정보 엔드포인트에 전송 요청을 수행하게 됩니다.

기본 준비 사항
Twelve Labs Playground에서 계정을 생성하고 인덱스를 만들어 줍니다.
분류 작업에 가장 적합한 엔진인 Marengo 2.6 (Embedding Engine)을 설정해 줍니다. 이 엔진은 높은 수준의 비디오 검색 및 비디오 분류 성능을 발휘하여 뛰어난 비디오 이해도의 강력한 기반을 제공합니다.
분류할 올림픽 스포츠 동영상을 업로드하세요. 테스트용 영상이 필요하시다면 본 가이드에서 제공하는 샘플 클립인 스포츠 비디오 클립을 다운로드하여 활용해 보세요.

Twelve Labs Playground 대시보드에서 고유 API 키를 복사합니다.
올림픽 클립을 새로 생성해 둔 인덱스 화면에 들어가면 웹 브라우저 주소창에서 고유 인덱스 ID를 확인할 수 있습니다: https://playground.twelvelabs.io/indexes/{index_id}.
메인 애플리케이션 파일과 같은 경로에 API Key와 INDEX_ID 값을 기술한 .env 파일을 생성해 설정해 둡니다.
여기까지 모든 준비 작업이 완료되었다면, 본격적으로 실제 애플리케이션 개발 단계로 넘어가 보겠습니다!
Twelvelabs_API=your_api_key_here API_URL=your_api_url_here INDEX_ID=your_index_id_here
분류 기능 상세 가이드
1 - 라이브러리 임포트, 검증 및 클라이언트 개발 설정
가장 먼저 필요한 라이브러리를 임포트하고 올바른 실행 환경 조성을 위한 환경 변수를 세팅해 줍니다. 다음과 같이 작성합니다.
import os import requests import glob import requests from twelvelabs import TwelveLabs from twelvelabs.models.task import Task import dotenv API_KEY=os.getenv("Twelvelabs_API") API_URL=os.getenv("API_URL") INDEX_ID=os.getenv("INDEX_ID") client = TwelveLabs(api_key=API_KEY) # URL of the /indexes endpoint INDEXES_URL = f"{API_URL}/indexes" # Setting headers variables default_header = { "x-api-key": API_KEY }
의존성 라이브러리를 성공적으로 가셔왔고, 환경 변수가 정확하게 로드되었으며 대시보드 크리덴셜을 사용해 올바른 API 클라이언트 초기화가 구성되었습니다. 또한, 인덱스에 접근하기 위한 베이스 URL과 API 요청을 위한 기본 헤더 설정도 완료되었습니다.
기초 뼈대가 든든하게 갖춰졌으므로, 이제 인덱스 제어 및 보다 정밀한 스포츠 비디오 분류 작업에 착수해 보겠습니다.
2 - 분류 클래스 그룹 설정하기
올림픽 스포츠 영상 클립들을 체계적으로 분류하는 데 사용할 기본 클래스(카테고리명 및 관련 프롬프트 조합) 목록은 다음과 같습니다.
# Categories for the classification of the Olympics Sports CLASSES = [ { "name": "AquaticSports", "prompts": [ "swimming competition", "diving event", "water polo match", "synchronized swimming", "open water swimming" ] }, { "name": "AthleticEvents", "prompts": [ "track and field", "marathon running", "long jump competition", "javelin throw", "high jump event" ] }, { "name": "GymnasticsEvents", "prompts": [ "artistic gymnastics", "rhythmic gymnastics", "trampoline gymnastics", "balance beam routine", "floor exercise performance" ] }, { "name": "CombatSports", "prompts": [ "boxing match", "judo competition", "wrestling bout", "taekwondo fight", "fencing duel" ] }, { "name": "TeamSports", "prompts": [ "basketball game", "volleyball match", "football (soccer) match", "handball game", "field hockey competition" ] }, { "name": "CyclingSports", "prompts": [ "road cycling race", "track cycling event", "mountain bike competition", "BMX racing", "cycling time trial" ] }, { "name": "RacquetSports", "prompts": [ "tennis match", "badminton game", "table tennis competition", "squash game", "tennis doubles match" ] }, { "name": "RowingAndSailing", "prompts": [ "rowing competition", "sailing race", "canoe sprint", "kayak event", "windsurfing competition" ] } ]
3 - 특정 인덱스 내 검색된 모든 동영상을 분류하는 유틸리티 함수
# Utility function def print_page(page): for data in page: print(f"video_id={data.video_id}") for cl in data.classes: print( f" name={cl.name} score={cl.score} duration_ratio={cl.duration_ratio} clips={cl.clips.model_dump_json(indent=2)}" ) result = client.classify.index( index_id=INDEX_ID, options=["visual"], classes=CLASSES, include_clips=True )
실제 분류를 트리거하기 위해 client.classify.index() 메소드를 노출해 호출합니다. 사전에 설정해 둔 INDEX_ID 값과 함께 비디오 내 시각 정보(options=["visual"])를 참조하도록 구성하고 메타데이터로 각 타임스탬프 기반 Clip 상세 내역도 응답에 반환받도록 처리합니다. 분류 타겟 배열로는 구성해둔 CLASSES를 제공합니다.
print_page(result) 헬프 메소드는 이러한 반환 결과들을 가독성 높은 출력 형식으로 나타내어 줍니다. 비디오 루프를 돌며 video_id를 출력하고, 매칭된 분류명 가각의 스코어 점수, 해당 스포츠가 재생된 전체 듀레이션 비율(Duration Ratio), 그리고 개별 비디오 클립 데이터들의 구조화된 상태 리스트를 프린트해 결과를 명확히 확인하도록 돕습니다.
4 - 정돈된 형태로 분류 스코어 결과를 표출해 주는 출력 핸들러 작성
print_classification_result() 헬퍼는 Twelve Labs API의 최종 비디오 감별 결과 구조 데이터를 사람이 인지하기 가장 쉬운 형태로 가시화합니다. 각 비디오 소스를 반복 순회하며 대상 ID, 감지된 최종 카테고리 정보 및 매칭 점수를 시각적으로 정리합니다. 또한 각 클래스별 신뢰 점수와 구간 점유 비중을 정밀하게 추출하며, 각 비디오 내부에서 발견된 가장 연관성이 우수한 상위 5개의 세부 클립 목록을 우선 정렬해 각각 시작/끝 시점, 그리고 매칭 유도가 이뤄진 CLASSES 프롬프트를 깔끔하게 화면에 찍어 줍니다. 5개 이상의 상세 검출 클립이 더 존재하면 생략 카운팅도 알려줍니다.
def print_classification_result(result) -> None: for video_data in result.data: print(f"Video ID: {video_data.video_id}") print("=" * 50) for class_data in video_data.classes: print(f" Class: {class_data.name}") print(f" Score: {class_data.score:.2f}") print(f" Duration Ratio: {class_data.duration_ratio:.2f}") print(" Clips:") sorted_clips = sorted(class_data.clips, key=lambda x: x.score, reverse=True) for i, clip in enumerate(sorted_clips[:5], 1): # Print top 5 clips print(f" {i}. Score: {clip.score:.2f}") print(f" Start: {clip.start:.2f}s, End: {clip.end:.2f}s") print(f" Prompt: {clip.prompt}") if len(sorted_clips) > 5: print(f" ... and {len(sorted_clips) - 5} more clips") print("-" * 40) print("\n") print(f"Total results: {result.page_info.total_results}") print(f"Page expires at: {result.page_info.page_expired_at}") print(f"Next page token: {result.page_info.next_page_token}")
위의 실행 결과 예시는 다음과 같습니다 -
Video ID: 66c9b03be53394f4aaed82c1 ================================================== Class: AquaticSports Score: 96.08 Duration Ratio: 0.90 Clips: 1. Score: 85.74 Start: 19.30s, End: 42.00s Prompt: water polo match 2. Score: 85.38 Start: 56.30s, End: 160.41s Prompt: water polo match 3. Score: 85.25 Start: 19.30s, End: 124.07s Prompt: synchronized swimming 4. Score: 85.13 Start: 0.00s, End: 24.83s Prompt: swimming competition 5. Score: 85.08 Start: 124.10s, End: 160.41s Prompt: synchronized swimming ... and 19 more clips
5 - 인덱스 내 전체 동영상에서 특정 스포츠 종류만 정밀 구분하기
특정 클래스 전용 필터 적용 테스트를 위해, 전체 업로드 파일 중 오직 "수상 스포츠(AquaticSports)" 관련 콘텐츠만 콕 집어내는 분류를 예시로 보겠습니다. 절차는 매우 직관적입니다. 타겟 클래스명과 매핑 프롬프트 범주를 단독 지정해 API 호출을 던진 후, 결과만 걸러서 관측해 봅니다.
이렇듯 목적이 명료하게 좁혀진 검사는 해당 스포츠가 등장한 핵심 영상을 찾는 데 한층 더 정확하고 고품질의 감별 성과를 보장합니다. 간이 디버깅용 도구인 print_page()가 분류된 원시 결과를 요약하여 출력해 줍니다.
CLASS = [ { "name": "AquaticSports", "prompts": [ "swimming competition", "diving event", "water polo match", "synchronized swimming", "open water swimming" ] } ] def print_page(page): for data in page: print(f"video_id={data.video_id}") for cl in data.classes: print( f" name={cl.name} score={cl.score} duration_ratio={cl.duration_ratio} detailed_scores={cl.detailed_scores.model_dump_json(indent=2)}" ) result = client.classify.index( index_id=INDEX_ID, options=["visual"], classes=CLASS, include_clips=True, show_detailed_score=True ) print_classification_result(result)
위 코드의 실행 결과 데이터를 보면 지정된 대상 CLASS인 수상 스포츠 검사 범주에 명확하게 들어맞는 올림픽 비디오들이 나열됩니다.
Video ID: 66c9b03be53394f4aaed82c1 ================================================== Class: AquaticSports Score: 96.08 Duration Ratio: 0.90 Clips: 1. Score: 85.74 Start: 19.30s, End: 42.00s Prompt: water polo match 2. Score: 85.38 Start: 56.30s, End: 160.41s Prompt: water polo match 3. Score: 85.25 Start: 19.30s, End: 124.07s Prompt: synchronized swimming 4. Score: 85.13 Start: 0.00s, End: 24.83s Prompt: swimming competition 5. Score: 85.08 Start: 124.10s, End: 160.41s Prompt: synchronized swimming ... and 19 more clips
Twelve Labs 공식 SDK를 사용하여 분류 엔드포인트를 효과적으로 다루고 조율하는 방법을 마스터하셨으니, 이제 실사용자를 위한 어진 가독성을 갖춘 Streamlit 전체 애플리케이션 프레젠테이션 설계 단계로 넘어가 보겠습니다.
Streamlit 기반 웹 애플리케이션 구축
본 시스템의 웹 프로토타입 구현 환경으로는 심플한 설정과 놀라운 초고속 MVP 도출 생산성을 자랑하는 Streamlit을 낙점했습니다. 단 몇 줄의 직관적인 코드로 뛰어난 대화형 인터랙티브 웹 포털 화면을 찍어낼 수 있어 비디오 인공지능 분류 결과를 깔끔하게 보여주기 가장 안성맞춤입니다. Twelve Labs SDK와 연동하면 단 수 분 이내에 근사한 웹 GUI를 만들 수 있습니다.
Streamlit 실행에 앞서 개발 가상 환경이 제대로 켜져 있는지 체크한 주 아래 쉘 명령어를 기동해 의존성 모듈 설치를 진행합니다.
pip install streamlit
보다 다양한 프레임워크 핵심 팁을 보시려면, 공식 Streamlit 설명서 웹사이트를 확인하세요.
프로젝트 구성을 위해 다음과 같이 디렉터리와 파일을 레이아웃하여 소스를 생성합니다.
. ├── .env ├── app.py ├── requirements.txt └── .gitignore
이 중 프로젝트 의존 패키지를 서술할 requirements.txt의 내용물은 아래와 같습니다.
streamlit twelvelabs requests python-dotenv
이 짧은 몇 줄의 환경 추가로 스포츠 영상 분류기를 돌려줄 백앤드 구조 준비가 완료되었습니다.
메인 실행파일인 app.py에는 핵심적인 제어 기능 블록들이 명쾌하게 탑재됩니다.
get_initial_classes(): 시스템에 사전에 장착될 올림픽 전용 스포츠 카테고리를 자동 맵핑합니다.get_custom_classes()및add_custom_class(): 사용자 임의로 더 유연하게 탐지 필터를 개량/확장해 맞춤 타겟을 올릴 수 있게 지원하는 조력 함수입니다.classify_videos(): 입력받은 분석 기준 배열을 기반으로 인공지능이 추론을 시작하도록 Twelve Labs API 서버와 원활히 교신합니다.get_video_urls(): 정확히 일치하거나 분류된 대상 고유 Video ID들로부터 실시간 영상 주소를 조회하여 리턴합니다.render_video(): 사용자 웹 브라우저에서 끊김에 대처 가능한 HLS.js 플러그인 연동 HTML5 플레이어 쉘을 동적 마운트합니다.
# Import Necessary Dependencies import streamlit as st from twelvelabs import TwelveLabs import requests import os from dotenv import load_dotenv load_dotenv() # Get the API Key from the Dashboard - https://playground.twelvelabs.io/dashboard/api-key API_KEY = os.getenv("API_KEY") # Create the INDEX ID as specified in the README.md and get the INDEX_ID INDEX_ID = os.getenv("INDEX_ID") client = TwelveLabs(api_key=API_KEY) # Background Setting of the Application page_element = """ <style> [data-testid="stAppViewContainer"] { background-image: url("https://wallpapercave.com/wp/wp3589963.jpg"); background-size: cover; } [data-testid="stHeader"] { background-color: rgba(0,0,0,0); } [data-testid="stToolbar"] { right: 2rem; background-image: url(""); background-size: cover; } </style> """ st.markdown(page_element, unsafe_allow_html=True) # Classes to classify the video into, there are categories name and # the prompts which specifc finds that factor to label that category @st.cache_data def get_initial_classes(): return [ {"name": "AquaticSports", "prompts": ["swimming competition", "diving event", "water polo match", "synchronized swimming", "open water swimming"]}, {"name": "AthleticEvents", "prompts": ["track and field", "marathon running", "long jump competition", "javelin throw", "high jump event"]}, {"name": "GymnasticsEvents", "prompts": ["artistic gymnastics", "rhythmic gymnastics", "trampoline gymnastics", "balance beam routine", "floor exercise performance"]}, {"name": "CombatSports", "prompts": ["boxing match", "judo competition", "wrestling bout", "taekwondo fight", "fencing duel"]}, {"name": "TeamSports", "prompts": ["basketball game", "volleyball match", "football (soccer) match", "handball game", "field hockey competition"]}, {"name": "CyclingSports", "prompts": ["road cycling race", "track cycling event", "mountain bike competition", "BMX racing", "cycling time trial"]}, {"name": "RacquetSports", "prompts": ["tennis match", "badminton game", "table tennis competition", "squash game", "tennis doubles match"]}, {"name": "RowingAndSailing", "prompts": ["rowing competition", "sailing race", "canoe sprint", "kayak event", "windsurfing competition"]} ] # Session State for the custom classes def get_custom_classes(): if 'custom_classes' not in st.session_state: st.session_state.custom_classes = [] return st.session_state.custom_classes # Utitlity Function to add the custom classes in app def add_custom_class(name, prompts): custom_classes = get_custom_classes() custom_classes.append({"name": name, "prompts": prompts}) st.session_state.custom_classes = custom_classes st.session_state.new_class_added = True # Utitlity Function to classify all the videos in the specified Index def classify_videos(selected_classes): return client.classify.index( index_id=INDEX_ID, options=["visual"], classes=selected_classes, include_clips=True ) # To get the video urls from the resultant video id def get_video_urls(video_ids): base_url = f"https://api.twelvelabs.io/v1.2/indexes/{INDEX_ID}/videos/{{}}" headers = {"x-api-key": API_KEY, "Content-Type": "application/json"} video_urls = {} for video_id in video_ids: try: response = requests.get(base_url.format(video_id), headers=headers) response.raise_for_status() data = response.json() if 'hls' in data and 'video_url' in data['hls']: video_urls[video_id] = data['hls']['video_url'] else: st.warning(f"No video URL found for video ID: {video_id}") except requests.exceptions.RequestException as e: st.error(f"Failed to get data for video ID: {video_id}. Error: {str(e)}") return video_urls # Utitlity Function to Render the Video by the resultant video url def render_video(video_url): hls_player = f""" <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> <div style="width: 100%; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);"> <video id="video" controls style="width: 100%; height: auto;"></video> </div> <script> var video = document.getElementById('video'); var videoSrc = "{video_url}"; if (Hls.isSupported()) {{ var hls = new Hls(); hls.loadSource(videoSrc); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function() {{ video.pause(); }}); }} else if (video.canPlayType('application/vnd.apple.mpegurl')) {{ video.src = videoSrc; video.addEventListener('loadedmetadata', function() {{ video.pause(); }}); }} </script> """ st.components.v1.html(hls_player, height=300)
get_initial_classes() 정적 메서드는 변경되지 않을 올림픽 전역 운동 종목 대상을 전달합니다. Streamlit은 똑똑하게 이 데이터를 즉각 캐싱 처리해, 시스템 구동 중 한 번만 이 구조체 평가가 일어나도록 제어하여 응답 성능을 비약적으로 보전합니다. 캐시 데이터 로드로 부하를 줄일 수 있습니다.
정적인 get_initial_classes()가 캐싱 보전을 사용하는 반면, 사용자가 상호작용하는 get_custom_classes()는 유동적인 업데이트 보전을 위해 의도적으로 캐싱 처리에서 제외됩니다. 수동 제작 분류군 정보는 동적으로 더해지거나 고쳐져야만 하기 때문입니다.
get_video_urls()는 일련의 매칭 동영상 ID들을 접수 후, Twelve Labs REST API를 반복 질의하여 HLS 비디오 스트리밍 주소 목록으로 깔끔하게 변환 출력하는 임무를 성실히 해내며 전송 실패 등 각종 에러 안전망을 제공합니다.
render_video() 또한 뛰어난 하위 범용 웹브라우저 재생 방식을 보증하고자 HLS.js 플러그인 랩퍼 소스를 동적 생성한 후 네이티브 미디어를 실행하지 못하는 단말 환경에서도 훌륭히 재생되도록 돕습니다.
classify_videos()는 마침내 수합 완료된 관조하려 한 관심 분류군 필터 조합을 바탕으로 본 Twelve Labs 인덱스 대상 분류 작업을 실행합니다. 오직 영상의 시각적 행동과 상황(Visual)의 매칭 신호 분석에 타겟을 둡니다.
# Main Function def main(): # Basic Markdown Setup for the Application st.markdown(""" <style> .big-font { font-size: 40px !important; font-weight: bold; color: #000000; text-align: center; margin-bottom: 30px; } .subheader { font-size: 24px; font-weight: bold; color: #424242; margin-top: 20px; margin-bottom: 10px; } .stButton>button { width: 100%; } .video-info { background-color: #f0f0f0; border-radius: 10px; padding: 15px; margin-bottom: 20px; } .custom-box { background-color: #f9f9f9; border-radius: 10px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .stTabs [data-baseweb="tab-list"] { gap: 24px; } .stTabs [data-baseweb="tab"] { height: 50px; white-space: pre-wrap; background-color: #f0f2f6; border-radius: 4px 4px 0px 0px; gap: 1px; padding-top: 10px; padding-bottom: 10px; } .stTabs [data-baseweb="tab-list"] button[aria-selected="true"] { background-color: #e8eaed; } </style> """, unsafe_allow_html=True) st.markdown('<p class="big-font">Olympics Classification w/t Twelve Labs</p>', unsafe_allow_html=True) # Updation of the classes CLASSES = get_initial_classes() + get_custom_classes() # Nav Tabs Creation tab1, tab2 = st.tabs(["Select Classes", "Add Custom Class"]) with tab1: st.markdown('<p class="subheader">Select Classes</p>', unsafe_allow_html=True) with st.container(): class_names = [cls["name"] for cls in CLASSES] # Multiselect option from the CLASSES selected_classes = st.multiselect("Choose one or more Olympic sports categories:", class_names) if st.button("Classify Videos", key="classify_button"): if selected_classes: with st.spinner("Classifying videos..."): selected_classes_with_prompts = [cls for cls in CLASSES if cls["name"] in selected_classes] res = classify_videos(selected_classes_with_prompts) video_ids = [data.video_id for data in res.data] # Retrieving the video urls from the resultant video which matches to the selected CLASSES video_urls = get_video_urls(video_ids) st.markdown('<p class="subheader">Classified Videos</p>', unsafe_allow_html=True) # Iterating over to showcase the information for every resulatant video for i, video_data in enumerate(res.data, 1): video_id = video_data.video_id video_url = video_urls.get(video_id, "URL not found") st.markdown(f"### Video {i}") st.markdown('<div class="video-info">', unsafe_allow_html=True) st.markdown(f"**Video ID:** {video_id}") for class_data in video_data.classes: st.markdown(f""" **Class:** {class_data.name} - Score: {class_data.score:.2f} - Duration Ratio: {class_data.duration_ratio:.2f} """) if video_url != "URL not found": render_video(video_url) else: st.warning("Video URL not available. Unable to render video.") st.markdown("---") st.success(f"Total videos classified: {len(res.data)}") else: st.warning("Please select at least one class.") st.markdown('</div>', unsafe_allow_html=True) # Nav Tab for the addition of the Custom Classes to select from with tab2: st.markdown('<p class="subheader">Add Custom Class</p>', unsafe_allow_html=True) with st.container(): custom_class_name = st.text_input("Enter custom class name") custom_class_prompts = st.text_input("Enter custom class prompts (comma-separated)") if st.button("Add Custom Class"): if custom_class_name and custom_class_prompts: add_custom_class(custom_class_name, custom_class_prompts.split(',')) st.success(f"Custom class '{custom_class_name}' added successfully!") st.experimental_rerun() else: st.warning("Please enter both class name and prompts.") st.markdown('</div>', unsafe_allow_html=True) if st.session_state.get('new_class_added', False): st.session_state.new_class_added = False st.experimental_rerun() if __name__ == "__main__": main()
이 애플리케이션의 메인 엔트리 포인트(Main function)는 인터페이스 레이아웃 구성과 매끄러운 반응형 CSS 연동, 제어 핸들러 호출 순서를 잡는데 주력합니다. 사용자는 "Add Custom Class" 탭에서 추가 전용 맞춤 타겟 입력을 받아 수시로 유동적인 추론 범주 확장을 도모할 수 있으며, 준비된 "Select Classes" 탭을 열어 신속한 비디오 분류 가동 범위 정렬을 시도해 볼 수 있습니다.
이제 준비된 사용자가 실제 "Classify Videos" 버튼을 정식 누르면 애플리케이션은 즉시 아래 조치를 실행합니다.
사용자가 선택 보전한 목적 클래스들을 정밀 매칭합니다.
전용 헬퍼 함수(
classify_videos())를 통해 Twelve Labs 클라우드로 질의 검사를 수행합니다.추출 검출된 개별 결과 비디오 고유 HLS 미디어 주소 목록을 즉각 다시 받아옵니다.
신뢰도 지표를 깔끔이 표출하며 미디어를 브라우저 상에 한눈에 재생해 시현합니다.
훌륭히 빌드되어 완성된 비디오 포털의 최종 모습 -

영역을 탐색하기 위한 아이디어 추천
본 튜토리얼을 기반으로 파이프라인과 비디오 인지 성능을 습득하셨다면, 일상이나 비즈니스 내 다양한 분야에 직접 이를 접목하여 새로운 프로젝트를 기획해 보실 수 있습니다. 여기 몇 가지 영감을 제공해 드립니다.
🔍 멀티모달 비디오 검색 포털: 테라바이트급 동영상 라이브러리 속에서 누구나 특정 대화나 인물 행동, 상황만을 지정해 연관 씬을 초단위로 짚어낼 수 있는 커스텀 검색기를 구축할 수 있습니다.
🎥 모니터링 이벤트 감지 분석기: 감시용 카메라 피드 데이터 스트림을 가동하고 이상 상황이나 고유 행동 패턴 등을 똑똑히 잡아내 분류하는 도구를 설계합니다.
💃 모션 및 안무 댄스 코칭 시스템: 트렌디한 챌린지 댄스 영상들에서 시그니처 댄스 동작이나 구체적인 안무 스타일만을 고유 분류하고 지도하는 기제를 구현합니다.
마치며
본 블로그의 핵심 타겟은 올림픽이라는 친숙한 소스를 통해 Twelve Labs 분류 AI 모델을 직접 작동시키며 비디오 분류 엔진의 파이프라인 개발 방법을 아주 현실적이고 알기 쉽게 풀어내는 것이었습니다. 튜토리얼을 즐겁게 수행해 주셔서 기쁩니다. 여러분이 해결해 나갈 다음 세대 비디오 어플리케이션 발상과 더욱 탁월해질 다양한 경험적 개선안들을 한시바삐 만나보고 싶습니다!
올림픽 동영상 영상을 정리하는 것만으로 금메달을 따는 꿈을 꾸어본 적이 있으신가요? 🥇 이제 그 시상대 위에 설 수 있는 기회가 왔습니다!
올림픽 동영상 클립 분류 애플리케이션은 스포츠 영상 분류라는 번거롭고 시간이 많이 걸리는 과정을 간소화하기 위해 설계되었습니다. Twelve Labs의 Marengo 2.6 임베딩 모델을 통해, 이 앱은 비디오 클립 내의 올림픽 스포츠 종목을 빠르게 분류해 냅니다.
화면 상의 텍스트, 음성 대화, 시각 요소를 모두 분석하여 동영상 클립을 손쉽게 카테고리별로 정렬합니다. 본 튜토리얼에서는 연구자, 스포츠 마니아, 방송사 관계자가 올림픽 콘텐츠를 다루는 방식을 완전히 혁신할 Streamlit 애플리케이션 구축 과정을 단계별로 안내합니다. 사용자가 직접 정의한 카테고리에 따라 동영상을 분류하는 앱을 구현하는 방법을 배우게 되며, 상세한 앱 시연 동영상은 아래에서 확인하실 수 있습니다.
애플리케이션 데모는 여기서 직접 실행해 볼 수 있습니다: 올림픽 분류 앱 데모. 또한, 이 Replit 템플릿을 통해 코드를 직접 작동시켜 볼 수도 있습니다.
사전 준비 단계
Twelve Labs Playground에 가입하고 API 키를 생성하세요.
노트북과 본 애플리케이션의 소스 코드가 포함된 GitHub 저장소를 확인하세요.
본 Streamlit 애플리케이션은 Python, HTML, JavaScript를 사용합니다.

애플리케이션 작동 원리
이 섹션에서는 올림픽 비디오 클립 분류를 위한 애플리케이션 파이프라인의 전체적인 흐름을 설명합니다.
이 애플리케이션은 다양한 활용 시나리오를 가진 분류 검색 엔진을 기반으로 개발되었습니다. 여기서는 올림픽 스포츠 동영상 클립을 분류하고 필요한 부분만 검색해 내는 작업에 집중합니다. 가장 핵심적인 첫 단계를 바로 인덱스(Index)를 생성하는 것입니다.
시작하려면 Twelve Labs Playground에 접속하세요.
새 인덱스(New Index)를 생성하고 비디오 인덱싱 및 분류 작업에 최적화된
Marengo 2.6 (Embedding Engine)을 선택합니다.분류하고자 하는 모든 스포츠 비디오를 해당 인덱스에 업로드하고 나면 다음 단계로 진행할 수 있습니다.
아래 섹션에서는 생성된 Index ID를 연동하고 실제로 구현하는 과정을 구체적으로 설명합니다. 먼저 전체적인 애플리케이션 워크플로우를 요약해 드립니다.
사용자는 다중 선택(Multi-select) 메뉴에서 시청하고자 하는 스포츠 카테고리를 직접 선택하거나, 본인만의 맞춤 클래스(Custom class)를 새로 정의하여 추가할 수 있습니다.
선택된 클래스는 해당 인덱스 내의 비디오 클립을 검색하고 분류하기 위해
INDEX ID및 기타 옵션(예:include_clips등)과 함께 classify 엔드포인트로 전송됩니다.API 응답 데이터에는 비디오 ID, 매칭된 클래스 이름, 신뢰도(Confidence Level), 시작 및 종료 시간, 썸네일 URL 등이 함께 포함됩니다.
검색 및 분류 결과로 반환된 비디오 ID의 실제 비디오 스트리밍 URL을 가져오기 위해 비디오 상세 정보 엔드포인트에 전송 요청을 수행하게 됩니다.

기본 준비 사항
Twelve Labs Playground에서 계정을 생성하고 인덱스를 만들어 줍니다.
분류 작업에 가장 적합한 엔진인 Marengo 2.6 (Embedding Engine)을 설정해 줍니다. 이 엔진은 높은 수준의 비디오 검색 및 비디오 분류 성능을 발휘하여 뛰어난 비디오 이해도의 강력한 기반을 제공합니다.
분류할 올림픽 스포츠 동영상을 업로드하세요. 테스트용 영상이 필요하시다면 본 가이드에서 제공하는 샘플 클립인 스포츠 비디오 클립을 다운로드하여 활용해 보세요.

Twelve Labs Playground 대시보드에서 고유 API 키를 복사합니다.
올림픽 클립을 새로 생성해 둔 인덱스 화면에 들어가면 웹 브라우저 주소창에서 고유 인덱스 ID를 확인할 수 있습니다: https://playground.twelvelabs.io/indexes/{index_id}.
메인 애플리케이션 파일과 같은 경로에 API Key와 INDEX_ID 값을 기술한 .env 파일을 생성해 설정해 둡니다.
여기까지 모든 준비 작업이 완료되었다면, 본격적으로 실제 애플리케이션 개발 단계로 넘어가 보겠습니다!
Twelvelabs_API=your_api_key_here API_URL=your_api_url_here INDEX_ID=your_index_id_here
분류 기능 상세 가이드
1 - 라이브러리 임포트, 검증 및 클라이언트 개발 설정
가장 먼저 필요한 라이브러리를 임포트하고 올바른 실행 환경 조성을 위한 환경 변수를 세팅해 줍니다. 다음과 같이 작성합니다.
import os import requests import glob import requests from twelvelabs import TwelveLabs from twelvelabs.models.task import Task import dotenv API_KEY=os.getenv("Twelvelabs_API") API_URL=os.getenv("API_URL") INDEX_ID=os.getenv("INDEX_ID") client = TwelveLabs(api_key=API_KEY) # URL of the /indexes endpoint INDEXES_URL = f"{API_URL}/indexes" # Setting headers variables default_header = { "x-api-key": API_KEY }
의존성 라이브러리를 성공적으로 가셔왔고, 환경 변수가 정확하게 로드되었으며 대시보드 크리덴셜을 사용해 올바른 API 클라이언트 초기화가 구성되었습니다. 또한, 인덱스에 접근하기 위한 베이스 URL과 API 요청을 위한 기본 헤더 설정도 완료되었습니다.
기초 뼈대가 든든하게 갖춰졌으므로, 이제 인덱스 제어 및 보다 정밀한 스포츠 비디오 분류 작업에 착수해 보겠습니다.
2 - 분류 클래스 그룹 설정하기
올림픽 스포츠 영상 클립들을 체계적으로 분류하는 데 사용할 기본 클래스(카테고리명 및 관련 프롬프트 조합) 목록은 다음과 같습니다.
# Categories for the classification of the Olympics Sports CLASSES = [ { "name": "AquaticSports", "prompts": [ "swimming competition", "diving event", "water polo match", "synchronized swimming", "open water swimming" ] }, { "name": "AthleticEvents", "prompts": [ "track and field", "marathon running", "long jump competition", "javelin throw", "high jump event" ] }, { "name": "GymnasticsEvents", "prompts": [ "artistic gymnastics", "rhythmic gymnastics", "trampoline gymnastics", "balance beam routine", "floor exercise performance" ] }, { "name": "CombatSports", "prompts": [ "boxing match", "judo competition", "wrestling bout", "taekwondo fight", "fencing duel" ] }, { "name": "TeamSports", "prompts": [ "basketball game", "volleyball match", "football (soccer) match", "handball game", "field hockey competition" ] }, { "name": "CyclingSports", "prompts": [ "road cycling race", "track cycling event", "mountain bike competition", "BMX racing", "cycling time trial" ] }, { "name": "RacquetSports", "prompts": [ "tennis match", "badminton game", "table tennis competition", "squash game", "tennis doubles match" ] }, { "name": "RowingAndSailing", "prompts": [ "rowing competition", "sailing race", "canoe sprint", "kayak event", "windsurfing competition" ] } ]
3 - 특정 인덱스 내 검색된 모든 동영상을 분류하는 유틸리티 함수
# Utility function def print_page(page): for data in page: print(f"video_id={data.video_id}") for cl in data.classes: print( f" name={cl.name} score={cl.score} duration_ratio={cl.duration_ratio} clips={cl.clips.model_dump_json(indent=2)}" ) result = client.classify.index( index_id=INDEX_ID, options=["visual"], classes=CLASSES, include_clips=True )
실제 분류를 트리거하기 위해 client.classify.index() 메소드를 노출해 호출합니다. 사전에 설정해 둔 INDEX_ID 값과 함께 비디오 내 시각 정보(options=["visual"])를 참조하도록 구성하고 메타데이터로 각 타임스탬프 기반 Clip 상세 내역도 응답에 반환받도록 처리합니다. 분류 타겟 배열로는 구성해둔 CLASSES를 제공합니다.
print_page(result) 헬프 메소드는 이러한 반환 결과들을 가독성 높은 출력 형식으로 나타내어 줍니다. 비디오 루프를 돌며 video_id를 출력하고, 매칭된 분류명 가각의 스코어 점수, 해당 스포츠가 재생된 전체 듀레이션 비율(Duration Ratio), 그리고 개별 비디오 클립 데이터들의 구조화된 상태 리스트를 프린트해 결과를 명확히 확인하도록 돕습니다.
4 - 정돈된 형태로 분류 스코어 결과를 표출해 주는 출력 핸들러 작성
print_classification_result() 헬퍼는 Twelve Labs API의 최종 비디오 감별 결과 구조 데이터를 사람이 인지하기 가장 쉬운 형태로 가시화합니다. 각 비디오 소스를 반복 순회하며 대상 ID, 감지된 최종 카테고리 정보 및 매칭 점수를 시각적으로 정리합니다. 또한 각 클래스별 신뢰 점수와 구간 점유 비중을 정밀하게 추출하며, 각 비디오 내부에서 발견된 가장 연관성이 우수한 상위 5개의 세부 클립 목록을 우선 정렬해 각각 시작/끝 시점, 그리고 매칭 유도가 이뤄진 CLASSES 프롬프트를 깔끔하게 화면에 찍어 줍니다. 5개 이상의 상세 검출 클립이 더 존재하면 생략 카운팅도 알려줍니다.
def print_classification_result(result) -> None: for video_data in result.data: print(f"Video ID: {video_data.video_id}") print("=" * 50) for class_data in video_data.classes: print(f" Class: {class_data.name}") print(f" Score: {class_data.score:.2f}") print(f" Duration Ratio: {class_data.duration_ratio:.2f}") print(" Clips:") sorted_clips = sorted(class_data.clips, key=lambda x: x.score, reverse=True) for i, clip in enumerate(sorted_clips[:5], 1): # Print top 5 clips print(f" {i}. Score: {clip.score:.2f}") print(f" Start: {clip.start:.2f}s, End: {clip.end:.2f}s") print(f" Prompt: {clip.prompt}") if len(sorted_clips) > 5: print(f" ... and {len(sorted_clips) - 5} more clips") print("-" * 40) print("\n") print(f"Total results: {result.page_info.total_results}") print(f"Page expires at: {result.page_info.page_expired_at}") print(f"Next page token: {result.page_info.next_page_token}")
위의 실행 결과 예시는 다음과 같습니다 -
Video ID: 66c9b03be53394f4aaed82c1 ================================================== Class: AquaticSports Score: 96.08 Duration Ratio: 0.90 Clips: 1. Score: 85.74 Start: 19.30s, End: 42.00s Prompt: water polo match 2. Score: 85.38 Start: 56.30s, End: 160.41s Prompt: water polo match 3. Score: 85.25 Start: 19.30s, End: 124.07s Prompt: synchronized swimming 4. Score: 85.13 Start: 0.00s, End: 24.83s Prompt: swimming competition 5. Score: 85.08 Start: 124.10s, End: 160.41s Prompt: synchronized swimming ... and 19 more clips
5 - 인덱스 내 전체 동영상에서 특정 스포츠 종류만 정밀 구분하기
특정 클래스 전용 필터 적용 테스트를 위해, 전체 업로드 파일 중 오직 "수상 스포츠(AquaticSports)" 관련 콘텐츠만 콕 집어내는 분류를 예시로 보겠습니다. 절차는 매우 직관적입니다. 타겟 클래스명과 매핑 프롬프트 범주를 단독 지정해 API 호출을 던진 후, 결과만 걸러서 관측해 봅니다.
이렇듯 목적이 명료하게 좁혀진 검사는 해당 스포츠가 등장한 핵심 영상을 찾는 데 한층 더 정확하고 고품질의 감별 성과를 보장합니다. 간이 디버깅용 도구인 print_page()가 분류된 원시 결과를 요약하여 출력해 줍니다.
CLASS = [ { "name": "AquaticSports", "prompts": [ "swimming competition", "diving event", "water polo match", "synchronized swimming", "open water swimming" ] } ] def print_page(page): for data in page: print(f"video_id={data.video_id}") for cl in data.classes: print( f" name={cl.name} score={cl.score} duration_ratio={cl.duration_ratio} detailed_scores={cl.detailed_scores.model_dump_json(indent=2)}" ) result = client.classify.index( index_id=INDEX_ID, options=["visual"], classes=CLASS, include_clips=True, show_detailed_score=True ) print_classification_result(result)
위 코드의 실행 결과 데이터를 보면 지정된 대상 CLASS인 수상 스포츠 검사 범주에 명확하게 들어맞는 올림픽 비디오들이 나열됩니다.
Video ID: 66c9b03be53394f4aaed82c1 ================================================== Class: AquaticSports Score: 96.08 Duration Ratio: 0.90 Clips: 1. Score: 85.74 Start: 19.30s, End: 42.00s Prompt: water polo match 2. Score: 85.38 Start: 56.30s, End: 160.41s Prompt: water polo match 3. Score: 85.25 Start: 19.30s, End: 124.07s Prompt: synchronized swimming 4. Score: 85.13 Start: 0.00s, End: 24.83s Prompt: swimming competition 5. Score: 85.08 Start: 124.10s, End: 160.41s Prompt: synchronized swimming ... and 19 more clips
Twelve Labs 공식 SDK를 사용하여 분류 엔드포인트를 효과적으로 다루고 조율하는 방법을 마스터하셨으니, 이제 실사용자를 위한 어진 가독성을 갖춘 Streamlit 전체 애플리케이션 프레젠테이션 설계 단계로 넘어가 보겠습니다.
Streamlit 기반 웹 애플리케이션 구축
본 시스템의 웹 프로토타입 구현 환경으로는 심플한 설정과 놀라운 초고속 MVP 도출 생산성을 자랑하는 Streamlit을 낙점했습니다. 단 몇 줄의 직관적인 코드로 뛰어난 대화형 인터랙티브 웹 포털 화면을 찍어낼 수 있어 비디오 인공지능 분류 결과를 깔끔하게 보여주기 가장 안성맞춤입니다. Twelve Labs SDK와 연동하면 단 수 분 이내에 근사한 웹 GUI를 만들 수 있습니다.
Streamlit 실행에 앞서 개발 가상 환경이 제대로 켜져 있는지 체크한 주 아래 쉘 명령어를 기동해 의존성 모듈 설치를 진행합니다.
pip install streamlit
보다 다양한 프레임워크 핵심 팁을 보시려면, 공식 Streamlit 설명서 웹사이트를 확인하세요.
프로젝트 구성을 위해 다음과 같이 디렉터리와 파일을 레이아웃하여 소스를 생성합니다.
. ├── .env ├── app.py ├── requirements.txt └── .gitignore
이 중 프로젝트 의존 패키지를 서술할 requirements.txt의 내용물은 아래와 같습니다.
streamlit twelvelabs requests python-dotenv
이 짧은 몇 줄의 환경 추가로 스포츠 영상 분류기를 돌려줄 백앤드 구조 준비가 완료되었습니다.
메인 실행파일인 app.py에는 핵심적인 제어 기능 블록들이 명쾌하게 탑재됩니다.
get_initial_classes(): 시스템에 사전에 장착될 올림픽 전용 스포츠 카테고리를 자동 맵핑합니다.get_custom_classes()및add_custom_class(): 사용자 임의로 더 유연하게 탐지 필터를 개량/확장해 맞춤 타겟을 올릴 수 있게 지원하는 조력 함수입니다.classify_videos(): 입력받은 분석 기준 배열을 기반으로 인공지능이 추론을 시작하도록 Twelve Labs API 서버와 원활히 교신합니다.get_video_urls(): 정확히 일치하거나 분류된 대상 고유 Video ID들로부터 실시간 영상 주소를 조회하여 리턴합니다.render_video(): 사용자 웹 브라우저에서 끊김에 대처 가능한 HLS.js 플러그인 연동 HTML5 플레이어 쉘을 동적 마운트합니다.
# Import Necessary Dependencies import streamlit as st from twelvelabs import TwelveLabs import requests import os from dotenv import load_dotenv load_dotenv() # Get the API Key from the Dashboard - https://playground.twelvelabs.io/dashboard/api-key API_KEY = os.getenv("API_KEY") # Create the INDEX ID as specified in the README.md and get the INDEX_ID INDEX_ID = os.getenv("INDEX_ID") client = TwelveLabs(api_key=API_KEY) # Background Setting of the Application page_element = """ <style> [data-testid="stAppViewContainer"] { background-image: url("https://wallpapercave.com/wp/wp3589963.jpg"); background-size: cover; } [data-testid="stHeader"] { background-color: rgba(0,0,0,0); } [data-testid="stToolbar"] { right: 2rem; background-image: url(""); background-size: cover; } </style> """ st.markdown(page_element, unsafe_allow_html=True) # Classes to classify the video into, there are categories name and # the prompts which specifc finds that factor to label that category @st.cache_data def get_initial_classes(): return [ {"name": "AquaticSports", "prompts": ["swimming competition", "diving event", "water polo match", "synchronized swimming", "open water swimming"]}, {"name": "AthleticEvents", "prompts": ["track and field", "marathon running", "long jump competition", "javelin throw", "high jump event"]}, {"name": "GymnasticsEvents", "prompts": ["artistic gymnastics", "rhythmic gymnastics", "trampoline gymnastics", "balance beam routine", "floor exercise performance"]}, {"name": "CombatSports", "prompts": ["boxing match", "judo competition", "wrestling bout", "taekwondo fight", "fencing duel"]}, {"name": "TeamSports", "prompts": ["basketball game", "volleyball match", "football (soccer) match", "handball game", "field hockey competition"]}, {"name": "CyclingSports", "prompts": ["road cycling race", "track cycling event", "mountain bike competition", "BMX racing", "cycling time trial"]}, {"name": "RacquetSports", "prompts": ["tennis match", "badminton game", "table tennis competition", "squash game", "tennis doubles match"]}, {"name": "RowingAndSailing", "prompts": ["rowing competition", "sailing race", "canoe sprint", "kayak event", "windsurfing competition"]} ] # Session State for the custom classes def get_custom_classes(): if 'custom_classes' not in st.session_state: st.session_state.custom_classes = [] return st.session_state.custom_classes # Utitlity Function to add the custom classes in app def add_custom_class(name, prompts): custom_classes = get_custom_classes() custom_classes.append({"name": name, "prompts": prompts}) st.session_state.custom_classes = custom_classes st.session_state.new_class_added = True # Utitlity Function to classify all the videos in the specified Index def classify_videos(selected_classes): return client.classify.index( index_id=INDEX_ID, options=["visual"], classes=selected_classes, include_clips=True ) # To get the video urls from the resultant video id def get_video_urls(video_ids): base_url = f"https://api.twelvelabs.io/v1.2/indexes/{INDEX_ID}/videos/{{}}" headers = {"x-api-key": API_KEY, "Content-Type": "application/json"} video_urls = {} for video_id in video_ids: try: response = requests.get(base_url.format(video_id), headers=headers) response.raise_for_status() data = response.json() if 'hls' in data and 'video_url' in data['hls']: video_urls[video_id] = data['hls']['video_url'] else: st.warning(f"No video URL found for video ID: {video_id}") except requests.exceptions.RequestException as e: st.error(f"Failed to get data for video ID: {video_id}. Error: {str(e)}") return video_urls # Utitlity Function to Render the Video by the resultant video url def render_video(video_url): hls_player = f""" <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> <div style="width: 100%; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);"> <video id="video" controls style="width: 100%; height: auto;"></video> </div> <script> var video = document.getElementById('video'); var videoSrc = "{video_url}"; if (Hls.isSupported()) {{ var hls = new Hls(); hls.loadSource(videoSrc); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function() {{ video.pause(); }}); }} else if (video.canPlayType('application/vnd.apple.mpegurl')) {{ video.src = videoSrc; video.addEventListener('loadedmetadata', function() {{ video.pause(); }}); }} </script> """ st.components.v1.html(hls_player, height=300)
get_initial_classes() 정적 메서드는 변경되지 않을 올림픽 전역 운동 종목 대상을 전달합니다. Streamlit은 똑똑하게 이 데이터를 즉각 캐싱 처리해, 시스템 구동 중 한 번만 이 구조체 평가가 일어나도록 제어하여 응답 성능을 비약적으로 보전합니다. 캐시 데이터 로드로 부하를 줄일 수 있습니다.
정적인 get_initial_classes()가 캐싱 보전을 사용하는 반면, 사용자가 상호작용하는 get_custom_classes()는 유동적인 업데이트 보전을 위해 의도적으로 캐싱 처리에서 제외됩니다. 수동 제작 분류군 정보는 동적으로 더해지거나 고쳐져야만 하기 때문입니다.
get_video_urls()는 일련의 매칭 동영상 ID들을 접수 후, Twelve Labs REST API를 반복 질의하여 HLS 비디오 스트리밍 주소 목록으로 깔끔하게 변환 출력하는 임무를 성실히 해내며 전송 실패 등 각종 에러 안전망을 제공합니다.
render_video() 또한 뛰어난 하위 범용 웹브라우저 재생 방식을 보증하고자 HLS.js 플러그인 랩퍼 소스를 동적 생성한 후 네이티브 미디어를 실행하지 못하는 단말 환경에서도 훌륭히 재생되도록 돕습니다.
classify_videos()는 마침내 수합 완료된 관조하려 한 관심 분류군 필터 조합을 바탕으로 본 Twelve Labs 인덱스 대상 분류 작업을 실행합니다. 오직 영상의 시각적 행동과 상황(Visual)의 매칭 신호 분석에 타겟을 둡니다.
# Main Function def main(): # Basic Markdown Setup for the Application st.markdown(""" <style> .big-font { font-size: 40px !important; font-weight: bold; color: #000000; text-align: center; margin-bottom: 30px; } .subheader { font-size: 24px; font-weight: bold; color: #424242; margin-top: 20px; margin-bottom: 10px; } .stButton>button { width: 100%; } .video-info { background-color: #f0f0f0; border-radius: 10px; padding: 15px; margin-bottom: 20px; } .custom-box { background-color: #f9f9f9; border-radius: 10px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .stTabs [data-baseweb="tab-list"] { gap: 24px; } .stTabs [data-baseweb="tab"] { height: 50px; white-space: pre-wrap; background-color: #f0f2f6; border-radius: 4px 4px 0px 0px; gap: 1px; padding-top: 10px; padding-bottom: 10px; } .stTabs [data-baseweb="tab-list"] button[aria-selected="true"] { background-color: #e8eaed; } </style> """, unsafe_allow_html=True) st.markdown('<p class="big-font">Olympics Classification w/t Twelve Labs</p>', unsafe_allow_html=True) # Updation of the classes CLASSES = get_initial_classes() + get_custom_classes() # Nav Tabs Creation tab1, tab2 = st.tabs(["Select Classes", "Add Custom Class"]) with tab1: st.markdown('<p class="subheader">Select Classes</p>', unsafe_allow_html=True) with st.container(): class_names = [cls["name"] for cls in CLASSES] # Multiselect option from the CLASSES selected_classes = st.multiselect("Choose one or more Olympic sports categories:", class_names) if st.button("Classify Videos", key="classify_button"): if selected_classes: with st.spinner("Classifying videos..."): selected_classes_with_prompts = [cls for cls in CLASSES if cls["name"] in selected_classes] res = classify_videos(selected_classes_with_prompts) video_ids = [data.video_id for data in res.data] # Retrieving the video urls from the resultant video which matches to the selected CLASSES video_urls = get_video_urls(video_ids) st.markdown('<p class="subheader">Classified Videos</p>', unsafe_allow_html=True) # Iterating over to showcase the information for every resulatant video for i, video_data in enumerate(res.data, 1): video_id = video_data.video_id video_url = video_urls.get(video_id, "URL not found") st.markdown(f"### Video {i}") st.markdown('<div class="video-info">', unsafe_allow_html=True) st.markdown(f"**Video ID:** {video_id}") for class_data in video_data.classes: st.markdown(f""" **Class:** {class_data.name} - Score: {class_data.score:.2f} - Duration Ratio: {class_data.duration_ratio:.2f} """) if video_url != "URL not found": render_video(video_url) else: st.warning("Video URL not available. Unable to render video.") st.markdown("---") st.success(f"Total videos classified: {len(res.data)}") else: st.warning("Please select at least one class.") st.markdown('</div>', unsafe_allow_html=True) # Nav Tab for the addition of the Custom Classes to select from with tab2: st.markdown('<p class="subheader">Add Custom Class</p>', unsafe_allow_html=True) with st.container(): custom_class_name = st.text_input("Enter custom class name") custom_class_prompts = st.text_input("Enter custom class prompts (comma-separated)") if st.button("Add Custom Class"): if custom_class_name and custom_class_prompts: add_custom_class(custom_class_name, custom_class_prompts.split(',')) st.success(f"Custom class '{custom_class_name}' added successfully!") st.experimental_rerun() else: st.warning("Please enter both class name and prompts.") st.markdown('</div>', unsafe_allow_html=True) if st.session_state.get('new_class_added', False): st.session_state.new_class_added = False st.experimental_rerun() if __name__ == "__main__": main()
이 애플리케이션의 메인 엔트리 포인트(Main function)는 인터페이스 레이아웃 구성과 매끄러운 반응형 CSS 연동, 제어 핸들러 호출 순서를 잡는데 주력합니다. 사용자는 "Add Custom Class" 탭에서 추가 전용 맞춤 타겟 입력을 받아 수시로 유동적인 추론 범주 확장을 도모할 수 있으며, 준비된 "Select Classes" 탭을 열어 신속한 비디오 분류 가동 범위 정렬을 시도해 볼 수 있습니다.
이제 준비된 사용자가 실제 "Classify Videos" 버튼을 정식 누르면 애플리케이션은 즉시 아래 조치를 실행합니다.
사용자가 선택 보전한 목적 클래스들을 정밀 매칭합니다.
전용 헬퍼 함수(
classify_videos())를 통해 Twelve Labs 클라우드로 질의 검사를 수행합니다.추출 검출된 개별 결과 비디오 고유 HLS 미디어 주소 목록을 즉각 다시 받아옵니다.
신뢰도 지표를 깔끔이 표출하며 미디어를 브라우저 상에 한눈에 재생해 시현합니다.
훌륭히 빌드되어 완성된 비디오 포털의 최종 모습 -

영역을 탐색하기 위한 아이디어 추천
본 튜토리얼을 기반으로 파이프라인과 비디오 인지 성능을 습득하셨다면, 일상이나 비즈니스 내 다양한 분야에 직접 이를 접목하여 새로운 프로젝트를 기획해 보실 수 있습니다. 여기 몇 가지 영감을 제공해 드립니다.
🔍 멀티모달 비디오 검색 포털: 테라바이트급 동영상 라이브러리 속에서 누구나 특정 대화나 인물 행동, 상황만을 지정해 연관 씬을 초단위로 짚어낼 수 있는 커스텀 검색기를 구축할 수 있습니다.
🎥 모니터링 이벤트 감지 분석기: 감시용 카메라 피드 데이터 스트림을 가동하고 이상 상황이나 고유 행동 패턴 등을 똑똑히 잡아내 분류하는 도구를 설계합니다.
💃 모션 및 안무 댄스 코칭 시스템: 트렌디한 챌린지 댄스 영상들에서 시그니처 댄스 동작이나 구체적인 안무 스타일만을 고유 분류하고 지도하는 기제를 구현합니다.
마치며
본 블로그의 핵심 타겟은 올림픽이라는 친숙한 소스를 통해 Twelve Labs 분류 AI 모델을 직접 작동시키며 비디오 분류 엔진의 파이프라인 개발 방법을 아주 현실적이고 알기 쉽게 풀어내는 것이었습니다. 튜토리얼을 즐겁게 수행해 주셔서 기쁩니다. 여러분이 해결해 나갈 다음 세대 비디오 어플리케이션 발상과 더욱 탁월해질 다양한 경험적 개선안들을 한시바삐 만나보고 싶습니다!
올림픽 동영상 영상을 정리하는 것만으로 금메달을 따는 꿈을 꾸어본 적이 있으신가요? 🥇 이제 그 시상대 위에 설 수 있는 기회가 왔습니다!
올림픽 동영상 클립 분류 애플리케이션은 스포츠 영상 분류라는 번거롭고 시간이 많이 걸리는 과정을 간소화하기 위해 설계되었습니다. Twelve Labs의 Marengo 2.6 임베딩 모델을 통해, 이 앱은 비디오 클립 내의 올림픽 스포츠 종목을 빠르게 분류해 냅니다.
화면 상의 텍스트, 음성 대화, 시각 요소를 모두 분석하여 동영상 클립을 손쉽게 카테고리별로 정렬합니다. 본 튜토리얼에서는 연구자, 스포츠 마니아, 방송사 관계자가 올림픽 콘텐츠를 다루는 방식을 완전히 혁신할 Streamlit 애플리케이션 구축 과정을 단계별로 안내합니다. 사용자가 직접 정의한 카테고리에 따라 동영상을 분류하는 앱을 구현하는 방법을 배우게 되며, 상세한 앱 시연 동영상은 아래에서 확인하실 수 있습니다.
애플리케이션 데모는 여기서 직접 실행해 볼 수 있습니다: 올림픽 분류 앱 데모. 또한, 이 Replit 템플릿을 통해 코드를 직접 작동시켜 볼 수도 있습니다.
사전 준비 단계
Twelve Labs Playground에 가입하고 API 키를 생성하세요.
노트북과 본 애플리케이션의 소스 코드가 포함된 GitHub 저장소를 확인하세요.
본 Streamlit 애플리케이션은 Python, HTML, JavaScript를 사용합니다.

애플리케이션 작동 원리
이 섹션에서는 올림픽 비디오 클립 분류를 위한 애플리케이션 파이프라인의 전체적인 흐름을 설명합니다.
이 애플리케이션은 다양한 활용 시나리오를 가진 분류 검색 엔진을 기반으로 개발되었습니다. 여기서는 올림픽 스포츠 동영상 클립을 분류하고 필요한 부분만 검색해 내는 작업에 집중합니다. 가장 핵심적인 첫 단계를 바로 인덱스(Index)를 생성하는 것입니다.
시작하려면 Twelve Labs Playground에 접속하세요.
새 인덱스(New Index)를 생성하고 비디오 인덱싱 및 분류 작업에 최적화된
Marengo 2.6 (Embedding Engine)을 선택합니다.분류하고자 하는 모든 스포츠 비디오를 해당 인덱스에 업로드하고 나면 다음 단계로 진행할 수 있습니다.
아래 섹션에서는 생성된 Index ID를 연동하고 실제로 구현하는 과정을 구체적으로 설명합니다. 먼저 전체적인 애플리케이션 워크플로우를 요약해 드립니다.
사용자는 다중 선택(Multi-select) 메뉴에서 시청하고자 하는 스포츠 카테고리를 직접 선택하거나, 본인만의 맞춤 클래스(Custom class)를 새로 정의하여 추가할 수 있습니다.
선택된 클래스는 해당 인덱스 내의 비디오 클립을 검색하고 분류하기 위해
INDEX ID및 기타 옵션(예:include_clips등)과 함께 classify 엔드포인트로 전송됩니다.API 응답 데이터에는 비디오 ID, 매칭된 클래스 이름, 신뢰도(Confidence Level), 시작 및 종료 시간, 썸네일 URL 등이 함께 포함됩니다.
검색 및 분류 결과로 반환된 비디오 ID의 실제 비디오 스트리밍 URL을 가져오기 위해 비디오 상세 정보 엔드포인트에 전송 요청을 수행하게 됩니다.

기본 준비 사항
Twelve Labs Playground에서 계정을 생성하고 인덱스를 만들어 줍니다.
분류 작업에 가장 적합한 엔진인 Marengo 2.6 (Embedding Engine)을 설정해 줍니다. 이 엔진은 높은 수준의 비디오 검색 및 비디오 분류 성능을 발휘하여 뛰어난 비디오 이해도의 강력한 기반을 제공합니다.
분류할 올림픽 스포츠 동영상을 업로드하세요. 테스트용 영상이 필요하시다면 본 가이드에서 제공하는 샘플 클립인 스포츠 비디오 클립을 다운로드하여 활용해 보세요.

Twelve Labs Playground 대시보드에서 고유 API 키를 복사합니다.
올림픽 클립을 새로 생성해 둔 인덱스 화면에 들어가면 웹 브라우저 주소창에서 고유 인덱스 ID를 확인할 수 있습니다: https://playground.twelvelabs.io/indexes/{index_id}.
메인 애플리케이션 파일과 같은 경로에 API Key와 INDEX_ID 값을 기술한 .env 파일을 생성해 설정해 둡니다.
여기까지 모든 준비 작업이 완료되었다면, 본격적으로 실제 애플리케이션 개발 단계로 넘어가 보겠습니다!
Twelvelabs_API=your_api_key_here API_URL=your_api_url_here INDEX_ID=your_index_id_here
분류 기능 상세 가이드
1 - 라이브러리 임포트, 검증 및 클라이언트 개발 설정
가장 먼저 필요한 라이브러리를 임포트하고 올바른 실행 환경 조성을 위한 환경 변수를 세팅해 줍니다. 다음과 같이 작성합니다.
import os import requests import glob import requests from twelvelabs import TwelveLabs from twelvelabs.models.task import Task import dotenv API_KEY=os.getenv("Twelvelabs_API") API_URL=os.getenv("API_URL") INDEX_ID=os.getenv("INDEX_ID") client = TwelveLabs(api_key=API_KEY) # URL of the /indexes endpoint INDEXES_URL = f"{API_URL}/indexes" # Setting headers variables default_header = { "x-api-key": API_KEY }
의존성 라이브러리를 성공적으로 가셔왔고, 환경 변수가 정확하게 로드되었으며 대시보드 크리덴셜을 사용해 올바른 API 클라이언트 초기화가 구성되었습니다. 또한, 인덱스에 접근하기 위한 베이스 URL과 API 요청을 위한 기본 헤더 설정도 완료되었습니다.
기초 뼈대가 든든하게 갖춰졌으므로, 이제 인덱스 제어 및 보다 정밀한 스포츠 비디오 분류 작업에 착수해 보겠습니다.
2 - 분류 클래스 그룹 설정하기
올림픽 스포츠 영상 클립들을 체계적으로 분류하는 데 사용할 기본 클래스(카테고리명 및 관련 프롬프트 조합) 목록은 다음과 같습니다.
# Categories for the classification of the Olympics Sports CLASSES = [ { "name": "AquaticSports", "prompts": [ "swimming competition", "diving event", "water polo match", "synchronized swimming", "open water swimming" ] }, { "name": "AthleticEvents", "prompts": [ "track and field", "marathon running", "long jump competition", "javelin throw", "high jump event" ] }, { "name": "GymnasticsEvents", "prompts": [ "artistic gymnastics", "rhythmic gymnastics", "trampoline gymnastics", "balance beam routine", "floor exercise performance" ] }, { "name": "CombatSports", "prompts": [ "boxing match", "judo competition", "wrestling bout", "taekwondo fight", "fencing duel" ] }, { "name": "TeamSports", "prompts": [ "basketball game", "volleyball match", "football (soccer) match", "handball game", "field hockey competition" ] }, { "name": "CyclingSports", "prompts": [ "road cycling race", "track cycling event", "mountain bike competition", "BMX racing", "cycling time trial" ] }, { "name": "RacquetSports", "prompts": [ "tennis match", "badminton game", "table tennis competition", "squash game", "tennis doubles match" ] }, { "name": "RowingAndSailing", "prompts": [ "rowing competition", "sailing race", "canoe sprint", "kayak event", "windsurfing competition" ] } ]
3 - 특정 인덱스 내 검색된 모든 동영상을 분류하는 유틸리티 함수
# Utility function def print_page(page): for data in page: print(f"video_id={data.video_id}") for cl in data.classes: print( f" name={cl.name} score={cl.score} duration_ratio={cl.duration_ratio} clips={cl.clips.model_dump_json(indent=2)}" ) result = client.classify.index( index_id=INDEX_ID, options=["visual"], classes=CLASSES, include_clips=True )
실제 분류를 트리거하기 위해 client.classify.index() 메소드를 노출해 호출합니다. 사전에 설정해 둔 INDEX_ID 값과 함께 비디오 내 시각 정보(options=["visual"])를 참조하도록 구성하고 메타데이터로 각 타임스탬프 기반 Clip 상세 내역도 응답에 반환받도록 처리합니다. 분류 타겟 배열로는 구성해둔 CLASSES를 제공합니다.
print_page(result) 헬프 메소드는 이러한 반환 결과들을 가독성 높은 출력 형식으로 나타내어 줍니다. 비디오 루프를 돌며 video_id를 출력하고, 매칭된 분류명 가각의 스코어 점수, 해당 스포츠가 재생된 전체 듀레이션 비율(Duration Ratio), 그리고 개별 비디오 클립 데이터들의 구조화된 상태 리스트를 프린트해 결과를 명확히 확인하도록 돕습니다.
4 - 정돈된 형태로 분류 스코어 결과를 표출해 주는 출력 핸들러 작성
print_classification_result() 헬퍼는 Twelve Labs API의 최종 비디오 감별 결과 구조 데이터를 사람이 인지하기 가장 쉬운 형태로 가시화합니다. 각 비디오 소스를 반복 순회하며 대상 ID, 감지된 최종 카테고리 정보 및 매칭 점수를 시각적으로 정리합니다. 또한 각 클래스별 신뢰 점수와 구간 점유 비중을 정밀하게 추출하며, 각 비디오 내부에서 발견된 가장 연관성이 우수한 상위 5개의 세부 클립 목록을 우선 정렬해 각각 시작/끝 시점, 그리고 매칭 유도가 이뤄진 CLASSES 프롬프트를 깔끔하게 화면에 찍어 줍니다. 5개 이상의 상세 검출 클립이 더 존재하면 생략 카운팅도 알려줍니다.
def print_classification_result(result) -> None: for video_data in result.data: print(f"Video ID: {video_data.video_id}") print("=" * 50) for class_data in video_data.classes: print(f" Class: {class_data.name}") print(f" Score: {class_data.score:.2f}") print(f" Duration Ratio: {class_data.duration_ratio:.2f}") print(" Clips:") sorted_clips = sorted(class_data.clips, key=lambda x: x.score, reverse=True) for i, clip in enumerate(sorted_clips[:5], 1): # Print top 5 clips print(f" {i}. Score: {clip.score:.2f}") print(f" Start: {clip.start:.2f}s, End: {clip.end:.2f}s") print(f" Prompt: {clip.prompt}") if len(sorted_clips) > 5: print(f" ... and {len(sorted_clips) - 5} more clips") print("-" * 40) print("\n") print(f"Total results: {result.page_info.total_results}") print(f"Page expires at: {result.page_info.page_expired_at}") print(f"Next page token: {result.page_info.next_page_token}")
위의 실행 결과 예시는 다음과 같습니다 -
Video ID: 66c9b03be53394f4aaed82c1 ================================================== Class: AquaticSports Score: 96.08 Duration Ratio: 0.90 Clips: 1. Score: 85.74 Start: 19.30s, End: 42.00s Prompt: water polo match 2. Score: 85.38 Start: 56.30s, End: 160.41s Prompt: water polo match 3. Score: 85.25 Start: 19.30s, End: 124.07s Prompt: synchronized swimming 4. Score: 85.13 Start: 0.00s, End: 24.83s Prompt: swimming competition 5. Score: 85.08 Start: 124.10s, End: 160.41s Prompt: synchronized swimming ... and 19 more clips
5 - 인덱스 내 전체 동영상에서 특정 스포츠 종류만 정밀 구분하기
특정 클래스 전용 필터 적용 테스트를 위해, 전체 업로드 파일 중 오직 "수상 스포츠(AquaticSports)" 관련 콘텐츠만 콕 집어내는 분류를 예시로 보겠습니다. 절차는 매우 직관적입니다. 타겟 클래스명과 매핑 프롬프트 범주를 단독 지정해 API 호출을 던진 후, 결과만 걸러서 관측해 봅니다.
이렇듯 목적이 명료하게 좁혀진 검사는 해당 스포츠가 등장한 핵심 영상을 찾는 데 한층 더 정확하고 고품질의 감별 성과를 보장합니다. 간이 디버깅용 도구인 print_page()가 분류된 원시 결과를 요약하여 출력해 줍니다.
CLASS = [ { "name": "AquaticSports", "prompts": [ "swimming competition", "diving event", "water polo match", "synchronized swimming", "open water swimming" ] } ] def print_page(page): for data in page: print(f"video_id={data.video_id}") for cl in data.classes: print( f" name={cl.name} score={cl.score} duration_ratio={cl.duration_ratio} detailed_scores={cl.detailed_scores.model_dump_json(indent=2)}" ) result = client.classify.index( index_id=INDEX_ID, options=["visual"], classes=CLASS, include_clips=True, show_detailed_score=True ) print_classification_result(result)
위 코드의 실행 결과 데이터를 보면 지정된 대상 CLASS인 수상 스포츠 검사 범주에 명확하게 들어맞는 올림픽 비디오들이 나열됩니다.
Video ID: 66c9b03be53394f4aaed82c1 ================================================== Class: AquaticSports Score: 96.08 Duration Ratio: 0.90 Clips: 1. Score: 85.74 Start: 19.30s, End: 42.00s Prompt: water polo match 2. Score: 85.38 Start: 56.30s, End: 160.41s Prompt: water polo match 3. Score: 85.25 Start: 19.30s, End: 124.07s Prompt: synchronized swimming 4. Score: 85.13 Start: 0.00s, End: 24.83s Prompt: swimming competition 5. Score: 85.08 Start: 124.10s, End: 160.41s Prompt: synchronized swimming ... and 19 more clips
Twelve Labs 공식 SDK를 사용하여 분류 엔드포인트를 효과적으로 다루고 조율하는 방법을 마스터하셨으니, 이제 실사용자를 위한 어진 가독성을 갖춘 Streamlit 전체 애플리케이션 프레젠테이션 설계 단계로 넘어가 보겠습니다.
Streamlit 기반 웹 애플리케이션 구축
본 시스템의 웹 프로토타입 구현 환경으로는 심플한 설정과 놀라운 초고속 MVP 도출 생산성을 자랑하는 Streamlit을 낙점했습니다. 단 몇 줄의 직관적인 코드로 뛰어난 대화형 인터랙티브 웹 포털 화면을 찍어낼 수 있어 비디오 인공지능 분류 결과를 깔끔하게 보여주기 가장 안성맞춤입니다. Twelve Labs SDK와 연동하면 단 수 분 이내에 근사한 웹 GUI를 만들 수 있습니다.
Streamlit 실행에 앞서 개발 가상 환경이 제대로 켜져 있는지 체크한 주 아래 쉘 명령어를 기동해 의존성 모듈 설치를 진행합니다.
pip install streamlit
보다 다양한 프레임워크 핵심 팁을 보시려면, 공식 Streamlit 설명서 웹사이트를 확인하세요.
프로젝트 구성을 위해 다음과 같이 디렉터리와 파일을 레이아웃하여 소스를 생성합니다.
. ├── .env ├── app.py ├── requirements.txt └── .gitignore
이 중 프로젝트 의존 패키지를 서술할 requirements.txt의 내용물은 아래와 같습니다.
streamlit twelvelabs requests python-dotenv
이 짧은 몇 줄의 환경 추가로 스포츠 영상 분류기를 돌려줄 백앤드 구조 준비가 완료되었습니다.
메인 실행파일인 app.py에는 핵심적인 제어 기능 블록들이 명쾌하게 탑재됩니다.
get_initial_classes(): 시스템에 사전에 장착될 올림픽 전용 스포츠 카테고리를 자동 맵핑합니다.get_custom_classes()및add_custom_class(): 사용자 임의로 더 유연하게 탐지 필터를 개량/확장해 맞춤 타겟을 올릴 수 있게 지원하는 조력 함수입니다.classify_videos(): 입력받은 분석 기준 배열을 기반으로 인공지능이 추론을 시작하도록 Twelve Labs API 서버와 원활히 교신합니다.get_video_urls(): 정확히 일치하거나 분류된 대상 고유 Video ID들로부터 실시간 영상 주소를 조회하여 리턴합니다.render_video(): 사용자 웹 브라우저에서 끊김에 대처 가능한 HLS.js 플러그인 연동 HTML5 플레이어 쉘을 동적 마운트합니다.
# Import Necessary Dependencies import streamlit as st from twelvelabs import TwelveLabs import requests import os from dotenv import load_dotenv load_dotenv() # Get the API Key from the Dashboard - https://playground.twelvelabs.io/dashboard/api-key API_KEY = os.getenv("API_KEY") # Create the INDEX ID as specified in the README.md and get the INDEX_ID INDEX_ID = os.getenv("INDEX_ID") client = TwelveLabs(api_key=API_KEY) # Background Setting of the Application page_element = """ <style> [data-testid="stAppViewContainer"] { background-image: url("https://wallpapercave.com/wp/wp3589963.jpg"); background-size: cover; } [data-testid="stHeader"] { background-color: rgba(0,0,0,0); } [data-testid="stToolbar"] { right: 2rem; background-image: url(""); background-size: cover; } </style> """ st.markdown(page_element, unsafe_allow_html=True) # Classes to classify the video into, there are categories name and # the prompts which specifc finds that factor to label that category @st.cache_data def get_initial_classes(): return [ {"name": "AquaticSports", "prompts": ["swimming competition", "diving event", "water polo match", "synchronized swimming", "open water swimming"]}, {"name": "AthleticEvents", "prompts": ["track and field", "marathon running", "long jump competition", "javelin throw", "high jump event"]}, {"name": "GymnasticsEvents", "prompts": ["artistic gymnastics", "rhythmic gymnastics", "trampoline gymnastics", "balance beam routine", "floor exercise performance"]}, {"name": "CombatSports", "prompts": ["boxing match", "judo competition", "wrestling bout", "taekwondo fight", "fencing duel"]}, {"name": "TeamSports", "prompts": ["basketball game", "volleyball match", "football (soccer) match", "handball game", "field hockey competition"]}, {"name": "CyclingSports", "prompts": ["road cycling race", "track cycling event", "mountain bike competition", "BMX racing", "cycling time trial"]}, {"name": "RacquetSports", "prompts": ["tennis match", "badminton game", "table tennis competition", "squash game", "tennis doubles match"]}, {"name": "RowingAndSailing", "prompts": ["rowing competition", "sailing race", "canoe sprint", "kayak event", "windsurfing competition"]} ] # Session State for the custom classes def get_custom_classes(): if 'custom_classes' not in st.session_state: st.session_state.custom_classes = [] return st.session_state.custom_classes # Utitlity Function to add the custom classes in app def add_custom_class(name, prompts): custom_classes = get_custom_classes() custom_classes.append({"name": name, "prompts": prompts}) st.session_state.custom_classes = custom_classes st.session_state.new_class_added = True # Utitlity Function to classify all the videos in the specified Index def classify_videos(selected_classes): return client.classify.index( index_id=INDEX_ID, options=["visual"], classes=selected_classes, include_clips=True ) # To get the video urls from the resultant video id def get_video_urls(video_ids): base_url = f"https://api.twelvelabs.io/v1.2/indexes/{INDEX_ID}/videos/{{}}" headers = {"x-api-key": API_KEY, "Content-Type": "application/json"} video_urls = {} for video_id in video_ids: try: response = requests.get(base_url.format(video_id), headers=headers) response.raise_for_status() data = response.json() if 'hls' in data and 'video_url' in data['hls']: video_urls[video_id] = data['hls']['video_url'] else: st.warning(f"No video URL found for video ID: {video_id}") except requests.exceptions.RequestException as e: st.error(f"Failed to get data for video ID: {video_id}. Error: {str(e)}") return video_urls # Utitlity Function to Render the Video by the resultant video url def render_video(video_url): hls_player = f""" <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script> <div style="width: 100%; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);"> <video id="video" controls style="width: 100%; height: auto;"></video> </div> <script> var video = document.getElementById('video'); var videoSrc = "{video_url}"; if (Hls.isSupported()) {{ var hls = new Hls(); hls.loadSource(videoSrc); hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function() {{ video.pause(); }}); }} else if (video.canPlayType('application/vnd.apple.mpegurl')) {{ video.src = videoSrc; video.addEventListener('loadedmetadata', function() {{ video.pause(); }}); }} </script> """ st.components.v1.html(hls_player, height=300)
get_initial_classes() 정적 메서드는 변경되지 않을 올림픽 전역 운동 종목 대상을 전달합니다. Streamlit은 똑똑하게 이 데이터를 즉각 캐싱 처리해, 시스템 구동 중 한 번만 이 구조체 평가가 일어나도록 제어하여 응답 성능을 비약적으로 보전합니다. 캐시 데이터 로드로 부하를 줄일 수 있습니다.
정적인 get_initial_classes()가 캐싱 보전을 사용하는 반면, 사용자가 상호작용하는 get_custom_classes()는 유동적인 업데이트 보전을 위해 의도적으로 캐싱 처리에서 제외됩니다. 수동 제작 분류군 정보는 동적으로 더해지거나 고쳐져야만 하기 때문입니다.
get_video_urls()는 일련의 매칭 동영상 ID들을 접수 후, Twelve Labs REST API를 반복 질의하여 HLS 비디오 스트리밍 주소 목록으로 깔끔하게 변환 출력하는 임무를 성실히 해내며 전송 실패 등 각종 에러 안전망을 제공합니다.
render_video() 또한 뛰어난 하위 범용 웹브라우저 재생 방식을 보증하고자 HLS.js 플러그인 랩퍼 소스를 동적 생성한 후 네이티브 미디어를 실행하지 못하는 단말 환경에서도 훌륭히 재생되도록 돕습니다.
classify_videos()는 마침내 수합 완료된 관조하려 한 관심 분류군 필터 조합을 바탕으로 본 Twelve Labs 인덱스 대상 분류 작업을 실행합니다. 오직 영상의 시각적 행동과 상황(Visual)의 매칭 신호 분석에 타겟을 둡니다.
# Main Function def main(): # Basic Markdown Setup for the Application st.markdown(""" <style> .big-font { font-size: 40px !important; font-weight: bold; color: #000000; text-align: center; margin-bottom: 30px; } .subheader { font-size: 24px; font-weight: bold; color: #424242; margin-top: 20px; margin-bottom: 10px; } .stButton>button { width: 100%; } .video-info { background-color: #f0f0f0; border-radius: 10px; padding: 15px; margin-bottom: 20px; } .custom-box { background-color: #f9f9f9; border-radius: 10px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .stTabs [data-baseweb="tab-list"] { gap: 24px; } .stTabs [data-baseweb="tab"] { height: 50px; white-space: pre-wrap; background-color: #f0f2f6; border-radius: 4px 4px 0px 0px; gap: 1px; padding-top: 10px; padding-bottom: 10px; } .stTabs [data-baseweb="tab-list"] button[aria-selected="true"] { background-color: #e8eaed; } </style> """, unsafe_allow_html=True) st.markdown('<p class="big-font">Olympics Classification w/t Twelve Labs</p>', unsafe_allow_html=True) # Updation of the classes CLASSES = get_initial_classes() + get_custom_classes() # Nav Tabs Creation tab1, tab2 = st.tabs(["Select Classes", "Add Custom Class"]) with tab1: st.markdown('<p class="subheader">Select Classes</p>', unsafe_allow_html=True) with st.container(): class_names = [cls["name"] for cls in CLASSES] # Multiselect option from the CLASSES selected_classes = st.multiselect("Choose one or more Olympic sports categories:", class_names) if st.button("Classify Videos", key="classify_button"): if selected_classes: with st.spinner("Classifying videos..."): selected_classes_with_prompts = [cls for cls in CLASSES if cls["name"] in selected_classes] res = classify_videos(selected_classes_with_prompts) video_ids = [data.video_id for data in res.data] # Retrieving the video urls from the resultant video which matches to the selected CLASSES video_urls = get_video_urls(video_ids) st.markdown('<p class="subheader">Classified Videos</p>', unsafe_allow_html=True) # Iterating over to showcase the information for every resulatant video for i, video_data in enumerate(res.data, 1): video_id = video_data.video_id video_url = video_urls.get(video_id, "URL not found") st.markdown(f"### Video {i}") st.markdown('<div class="video-info">', unsafe_allow_html=True) st.markdown(f"**Video ID:** {video_id}") for class_data in video_data.classes: st.markdown(f""" **Class:** {class_data.name} - Score: {class_data.score:.2f} - Duration Ratio: {class_data.duration_ratio:.2f} """) if video_url != "URL not found": render_video(video_url) else: st.warning("Video URL not available. Unable to render video.") st.markdown("---") st.success(f"Total videos classified: {len(res.data)}") else: st.warning("Please select at least one class.") st.markdown('</div>', unsafe_allow_html=True) # Nav Tab for the addition of the Custom Classes to select from with tab2: st.markdown('<p class="subheader">Add Custom Class</p>', unsafe_allow_html=True) with st.container(): custom_class_name = st.text_input("Enter custom class name") custom_class_prompts = st.text_input("Enter custom class prompts (comma-separated)") if st.button("Add Custom Class"): if custom_class_name and custom_class_prompts: add_custom_class(custom_class_name, custom_class_prompts.split(',')) st.success(f"Custom class '{custom_class_name}' added successfully!") st.experimental_rerun() else: st.warning("Please enter both class name and prompts.") st.markdown('</div>', unsafe_allow_html=True) if st.session_state.get('new_class_added', False): st.session_state.new_class_added = False st.experimental_rerun() if __name__ == "__main__": main()
이 애플리케이션의 메인 엔트리 포인트(Main function)는 인터페이스 레이아웃 구성과 매끄러운 반응형 CSS 연동, 제어 핸들러 호출 순서를 잡는데 주력합니다. 사용자는 "Add Custom Class" 탭에서 추가 전용 맞춤 타겟 입력을 받아 수시로 유동적인 추론 범주 확장을 도모할 수 있으며, 준비된 "Select Classes" 탭을 열어 신속한 비디오 분류 가동 범위 정렬을 시도해 볼 수 있습니다.
이제 준비된 사용자가 실제 "Classify Videos" 버튼을 정식 누르면 애플리케이션은 즉시 아래 조치를 실행합니다.
사용자가 선택 보전한 목적 클래스들을 정밀 매칭합니다.
전용 헬퍼 함수(
classify_videos())를 통해 Twelve Labs 클라우드로 질의 검사를 수행합니다.추출 검출된 개별 결과 비디오 고유 HLS 미디어 주소 목록을 즉각 다시 받아옵니다.
신뢰도 지표를 깔끔이 표출하며 미디어를 브라우저 상에 한눈에 재생해 시현합니다.
훌륭히 빌드되어 완성된 비디오 포털의 최종 모습 -

영역을 탐색하기 위한 아이디어 추천
본 튜토리얼을 기반으로 파이프라인과 비디오 인지 성능을 습득하셨다면, 일상이나 비즈니스 내 다양한 분야에 직접 이를 접목하여 새로운 프로젝트를 기획해 보실 수 있습니다. 여기 몇 가지 영감을 제공해 드립니다.
🔍 멀티모달 비디오 검색 포털: 테라바이트급 동영상 라이브러리 속에서 누구나 특정 대화나 인물 행동, 상황만을 지정해 연관 씬을 초단위로 짚어낼 수 있는 커스텀 검색기를 구축할 수 있습니다.
🎥 모니터링 이벤트 감지 분석기: 감시용 카메라 피드 데이터 스트림을 가동하고 이상 상황이나 고유 행동 패턴 등을 똑똑히 잡아내 분류하는 도구를 설계합니다.
💃 모션 및 안무 댄스 코칭 시스템: 트렌디한 챌린지 댄스 영상들에서 시그니처 댄스 동작이나 구체적인 안무 스타일만을 고유 분류하고 지도하는 기제를 구현합니다.
마치며
본 블로그의 핵심 타겟은 올림픽이라는 친숙한 소스를 통해 Twelve Labs 분류 AI 모델을 직접 작동시키며 비디오 분류 엔진의 파이프라인 개발 방법을 아주 현실적이고 알기 쉽게 풀어내는 것이었습니다. 튜토리얼을 즐겁게 수행해 주셔서 기쁩니다. 여러분이 해결해 나갈 다음 세대 비디오 어플리케이션 발상과 더욱 탁월해질 다양한 경험적 개선안들을 한시바삐 만나보고 싶습니다!





