Tutorials

유튜브 동영상의 텍스트 요약을 자동으로 생성하는 방법

김미란

이 튜토리얼은 Twelve Labs의 Generate API를 활용하여 인덱싱된 모든 비디오의 요약본, 챕터, 하이라이트를 자동으로 생성하고, React 프론트엔드를 통해 원하는 출력 형식을 선택하고 타임스탬프가 포함된 결과를 확인할 수 있는 'YouTube 비디오 요약' 앱의 구축 과정을 단계별로 안내합니다.

이 튜토리얼은 Twelve Labs의 Generate API를 활용하여 인덱싱된 모든 비디오의 요약본, 챕터, 하이라이트를 자동으로 생성하고, React 프론트엔드를 통해 원하는 출력 형식을 선택하고 타임스탬프가 포함된 결과를 확인할 수 있는 'YouTube 비디오 요약' 앱의 구축 과정을 단계별로 안내합니다.

목차

No headings found on page

뉴스레터 구독하기

뉴스레터 구독하기

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

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

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

2024. 2. 26.

10분

링크 복사하기

마케팅 배경을 가진 저로서는 인플루언서의 세계에 항상 깊은 관심을 가지고 있었습니다. 인플루언서로서 겪는 어려움 중 하나는 훌륭한 콘텐츠를 제작하고 제공하기 위한 영감을 지속적으로 얻는 것이라는 점을 깨달았습니다. 특히 유튜브 인플루언서의 경우, 구조와 핵심 요점, 하이라이트를 이해하기 위해 다른 사람들의 비디오를 수없이 많이 시청해야 하므로 이 문제가 더욱 두드러집니다. 

Twelve Labs의 Generate API를 발견했을 때, 저는 이 과제를 해결할 수 있는 상당한 잠재력을 바로 알아보았습니다. 저는 이 API를 활용하여 YouTube 동영상의 요약, 챕터, 하이라이트를 생성하는 간단한 앱을 제작하기로 결심했습니다. 이 앱은 포괄적인 보고서를 작성하여 각 동영상에 대한 구조화된 분석을 제공합니다. 이는 콘텐츠 분석 프로세스를 간소화할 뿐만 아니라 생각을 더 잘 정리할 수 있도록 도와줍니다.

두말할 필요 없이, 바로 본론으로 들어가 보겠습니다!

사전 요구 사항

  • Twelve Labs API 키가 필요합니다. 아직 키가 없다면 Twelve Labs Playground를 방문하여 가입하고 API 키를 생성하세요. 

  • 이 앱을 위한 모든 파일이 포함된 리포지토리는 GitHub에서 확인할 수 있습니다.

  • (우대 사항) JavaScript, Node, React 및 React Query에 대한 기본적인 지식이 권장됩니다. 하지만 이러한 기술에 익숙하지 않더라도 걱정하지 마세요. 이 글의 핵심은 이 앱이 Twelve Labs API를 어떻게 활용하는지 확인하는 것입니다!

앱의 구조

이 앱은 SummarizeVideo, VideoUrlUploadForm, Video, InputForm, Result의 5가지 주요 컴포넌트로 구성되어 있습니다. 

  • SummarizeVideo: 다른 컴포넌트들의 상위 컨테이너 역할을 합니다. 하위 컴포넌트들과 공유되는 주요 상태(state)를 관리합니다.

  • VideoUrlUploadForm: YouTube 동영상 URL을 입력받아 Twelve Labs API를 통해 동영상을 인덱싱하는 간단한 폼입니다. 인덱싱 프로세스가 완료될 때까지 인덱싱 진행 상태와 함께 프로세스 중인 동영상을 보여줍니다. 

  • Video: 입력된 URL의 동영상을 표시합니다. 세 개의 서로 다른 컴포넌트 내에서 재사용됩니다.

  • InputForm: 요약(Summary), 챕터(Chapters), 하이라이트(Highlights)의 3가지 체크박스로 구성된 폼입니다. 사용자는 각 필드를 자유롭게 체크하거나 해제할 수 있습니다. 

  • Result: Twelve Labs API(‘/summarize’ 엔드포인트)를 호출하여 inputForm에서 체크한 필드의 결과를 보여줍니다.

이 앱에는 API 호출과 관련된 모든 코드가 저장된 server와 상태, 캐시, 데이터 페칭을 관리하기 위한 커스텀 React Query 훅 모음인 apiHooks.js도 포함되어 있습니다.

이제 Twelve Labs API와 함께 이러한 컴포넌트들이 어떻게 작동하는지 살펴보겠습니다.

Twelve Labs API와 앱의 연동 방식

1 - 인덱스의 가장 최근 동영상 표시하기

요약을 생성할 때, 이 앱은 인덱스에 가장 최근에 업로드된 단 하나의 동영상만 처리합니다. 따라서 앱이 마운트되면 기본적으로 지정된 인덱스의 가장 최근 동영상을 보여줍니다. 아래는 그 작동 프로세스입니다. 

  1. App.js에서 주어진 인덱스의 모든 동영상을 가져옵니다 (GET Videos)

  2. 응답에서 첫 번째 동영상의 ID를 추출하여 SummarizeVideo.js로 전달합니다

  3. 동영상 ID를 사용해 동영상의 세부 정보를 가져오고, 동영상 소스 URL을 추출하여 Video.js로 전달합니다 (GET Video)

따라서 동영상을 가져와 페이지에 첫 번째 동영상을 표시하는 이 흐름에서 Twelve Labs API에 두 번의 GET 요청을 보내게 됩니다. 각 단계를 자세히 살펴보겠습니다.

1.1 - App.js에서 지정된 인덱스의 모든 동영상 가져오기 (GET Videos)

내부에서 동영상은 서버에 요청을 보내는 react query 훅인 useGetVideos를 호출하여 반환됩니다. 그런 다음 서버는 Twelve Labs API에 GET 요청을 보내 인덱스의 모든 동영상을 가져옵니다. (💡자세한 내용은 API 문서인 GET Videos에서 확인하세요)

Server.js (52 - 72행)

/** Get videos */

app.get("/indexes/:indexId/videos", async (request, response, next) => {
 const params = {
   page_limit: request.query.page_limit,
 };


 try {
   const options = {
     method: "GET",
     url: `${API_BASE_URL}/indexes/${request.params.indexId}/videos`,
     headers: { ...HEADERS },
     data: { params },
   };
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message = error.response?.data?.message || "Error Getting Videos";
   return next({ status, message });
 }
});

반환되는 데이터(video)는 아래와 같은 형태입니다.

{
	"data": [
		{
			"_id": "65caf0fa48db9fa780cb3fc2",
			"created_at": "2024-02-13T04:32:54Z",
			"updated_at": "2024-02-13T04:32:58Z",
			"indexed_at": "2024-02-13T04:40:15Z",
			"metadata": {
				"duration": 130,
				"engine_ids": [
					"pegasus1",
					"marengo2.5"
				],
				"filename": "Adidas CEO Herbert Hainer: How I Work",
				"fps": 30,
				"height": 720,
				"size": 11149582,
				"width": 1280
			},
			{},			
}

1.2 - 응답에서 첫 번째 동영상의 ID를 추출하여 SummarizeVideo.js로 전달하기

반환된 동영상 목록 정보를 기반으로 첫 번째 동영상의 ID를 SummizeVideo.js 컴포넌트로 전달합니다.

App.js (34 - 38행)

           <SummarizeVideo
             index={apiConfig.INDEX_ID}
             videoId={videos.data[0]?._id || null} //passing down the id
             refetchVideos={refetchVideos}
           &sol;

1.3 - 동영상 ID를 사용해 세부 정보를 가져오고, 동영상 소스 URL을 추출하여 Video.js로 전달하기 (GET Video)

이전 단계에서 동영상을 가져온 방법과 유사하게, 동영상의 세부 정보를 가져오기 위해 서버에 요청을 보내는 react query 훅인 useGetVideo를 사용합니다. 그런 다음 서버는 Twelve Labs API에 GET 요청을 보내 특정 동영상에 대한 상세 정보를 가져옵니다. (💡자세한 내용은 API 문서인 GET Video에서 확인하세요)

Server.js (74 - 95행)

/** Get a video of an index */
app.get(
 "/indexes/:indexId/videos/:videoId",
 async (request, response, next) => {
   const indexId = request.params.indexId;
   const videoId = request.params.videoId;


   try {
     const options = {
       method: "GET",
       url: `${API_BASE_URL}/indexes/${indexId}/videos/${videoId}`,
       headers: { ...HEADERS },
     };
     const apiResponse = await axios.request(options);
     response.json(apiResponse.data);
   } catch (error) {
     const status = error.response?.status || 500;
     const message = error.response?.data?.message || "Error Getting a Video";
     return next({ status, message });
   }
 }
);


이 요청은 우리가 필요로 하는 Youtube URL을 포함한 동영상의 상세 정보를 반환합니다! GET Videos 응답에서는 Youtube URL을 사용할 수 없었다는 점을 기억하세요. 따라서 여기서 다시 GET Video 요청을 날리게 되는 것입니다.

 {
	"_id": "65caf0fa48db9fa780cb3fc2",
	"created_at": "2024-02-13T04:32:54Z",
	"updated_at": "2024-02-13T04:32:58Z",
	"indexed_at": "2024-02-13T04:40:15Z",
	"metadata": {
		"duration": 130,
		"engine_ids": [
			"pegasus1",
			"marengo2.5"
		],
		"filename": "Adidas CEO Herbert Hainer: How I Work",
		"fps": 30,
		"height": 720,
		"size": 11149582,
		"video_title": "Adidas CEO Herbert Hainer: How I Work",
		"width": 1280
	},
	"hls": {
		"video_url": "...",
		"thumbnail_urls": [
			"..."
		],
		"status": "COMPLETE",
		"updated_at": "2024-02-13T04:33:29.993Z"
	},
	"source": {
		"type": "youtube",
		"name": "The Wall Street Journal",
		"url": "https://www.youtube.com/watch?v=sHD0YxASbGQ" // here!
	}
}

반환된 데이터(video) 정보를 기반으로 URL을 Video 컴포넌트에 전달하며, 이 컴포넌트 내에서 URL을 기반으로 React Player가 렌더링됩니다. 

SummarizeVideo.js (101 - 105행)

     {video && (
             <Video
               url={video.source?.url} // passing down the url
               width={"381px"}
               height={"214px"}

2 - YouTube URL을 통한 동영상 업로드 및 인덱싱

이 앱에서는 API 버전 1.2 덕분에 YouTube URL을 제출하는 것만으로 동영상을 쉽게 업로드하고 인덱싱할 수 있습니다! 동영상 인덱싱 요청(우리는 이를 ‘태스크(task)’라고 부름)이 제출되면 인덱싱 태스크의 진행 상황을 모니터링할 수 있습니다. 저는 또한 사용자가 인덱싱 중에 동영상을 확인하고 시청할 수 있도록 비디오 플레이어가 노출되도록 구현했습니다. 

  1. VideoUrlUploadForm.js에서 YouTube URL을 사용하여 동영상 인덱싱 태스크를 생성합니다 (POST Task)

  2. 동영상 정보를 가져와 VideoUrlUploadForm.js에 동영상을 표시합니다 (*ytdl-core 라이브러리를 사용했습니다) 

  3. Task.js에서 인덱싱 작업의 진행 상황을 수신하여 표시합니다 (GET Task)

각 단계를 차례대로 살펴보겠습니다.

2.1 - VideoUrlUploadForm.js에서 YouTube URL을 사용해 동영상 인덱싱 작업 생성하기

사용자가 YouTube URL을 사용해 VideoUrlUploadForm을 제출하면 taskVideo가 세팅됩니다. taskVideo가 존재할 때 indexYouTubeVideo가 실행되도록 useEffect를 추가했습니다. 

indexYouTubeVideo는 서버로 POST 요청을 보내고, 이는 다시 Twelve Labs API의 ‘/tasks/external-provider’ 엔드포인트로 POST 요청을 전달합니다. (💡자세한 내용은 API 문서인 POST Task에서 확인하세요)

Server.js (134 - 152행)

/** Index a Youtube video for analysis, returning a task ID */
app.post("/index", async (request, response, next) => {
 const options = {
   method: "POST",
   url: `${API_BASE_URL}/tasks/external-provider`,
   headers: { ...HEADERS, accept: "application/json" },
   data: request.body.body,
 };


 try {
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message =
     error.response?.data?.message || "Error indexing a YouTube Video";
   return next({ status, message });
 }
});


이제 방금 생성된 동영상 태스크의 ID가 반환됩니다.

{
	"_id": "65a9df3f627beda40b8dfa56"
}

2.2 - VideoUrlUploadForm.js에서 동영상 정보를 가져와 동영상 표시하기

동영상 인덱싱 작업이 진행되는 동안 사용자에게 인덱싱 중인 작업 비디오를 보여줍니다. 

VideoUrlUploadForm.js (120 - 126행)

{taskVideo && (
           <div className="videoUrlUploadForm__taskVideoWrapper">
             <Video
               url={taskVideo.video_url}
               width={"381px"}
               height={"214px"}

사용자가 YouTube URL이 포함된 양식을 제출하면 getVideoInfofetchVideoInfo(React Query Hook)를 통해 동영상 정보를 검색한 다음 해당 정보로 taskVideo를 설정합니다. 

VideoUrlUploadForm.js (74 - 88행)

/** Get information of a video and set it as task */
 async function handleSubmit(evt) {
   evt.preventDefault();
   try {
     if (!videoUrl?.trim()) {
       throw new Error("Please enter a valid video URL");
     }
     const videoInfo = await getVideoInfo(videoUrl); //get video info
     setTaskVideo(videoInfo); //set TaskVideo with the video info we got
     inputRef.current.value = "";
     resetPrompts();
   } catch (error) {
     setError(error.message);
   }
 }

동영상 정보를 가져오는 데 ytdl-core 라이브러리(getURLVideoID 및 getBasicInfo)를 사용하고 있습니다. 

Server.js (119 - 132행)

/** Get video information from a YouTube URL using ytdl */
app.get("/video-info", async (request, response, next) => {
 try {
   let url = request.query.url;
   const videoId = ytdl.getURLVideoID(url);
   const videoInfo = await ytdl.getBasicInfo(videoId);
   response.json(videoInfo.videoDetails);
 } catch (error) {
   const status = error.response?.status || 500;
   const message =
     error.response?.data?.message || "Error getting info of a video";
   return next({ status, message });
 }
});

2.3 - Task.js에서 인덱싱 작업 진행률을 수신하여 표시하기

‘/index’에 대한 POST 요청이 태스크 ID를 반환했다는 것, 기억하시나요? 우리는 이 태스크 ID를 사용하여 작업의 세부 정보를 가져오고 사용자에게 지속적으로 진행 상태를 실시간 업데이트하여 알릴 것입니다. 

따라서 taskId가 있으면 Task 컴포넌트가 렌더링됩니다. 

VideoUrlUploadForm.js (130 - 137행)

{taskId && (
               <Task
                 taskId={taskId}
                 refetchVideos={refetchVideos}
                 index={index}
                 setTaskVideo={setTaskVideo}

Task 컴포넌트 내부에서, 우리는 Twelve Labs API에 GET 요청을 보내는 useGetTask React Query 훅을 사용하여 데이터를 수신합니다. (💡자세한 내용은 API 문서인 GET Task에서 확인하세요)

server.js (154 - 171행)

/** Check the status of a specific indexing task */
app.get("/tasks/:taskId", async (request, response, next) => {
 const taskId = request.params.taskId;


 try {
   const options = {
     method: "GET",
     url: `${API_BASE_URL}/tasks/${taskId}`,
     headers: { ...HEADERS },
   };
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message = error.response?.data?.message || "Error getting a task";
   return next({ status, message });
 }
});

이 요청은 다음과 같은 작업 상세 정보를 반환합니다.

{
	"_id": "65a9fc79627beda40b8dfa7b",
	"index_id": "653c0592480f870fb3bb01be",
	"video_id": "65a9fc7e4981af6e637c8e59",
	"status": "indexing",
	"metadata": {...},
	"created_at": "2024-01-19T04:37:13.724Z",
	"updated_at": "2024-01-19T04:41:10.606Z",
	"estimated_time": "2024-01-19T04:41:36.601Z",
	"type": "index_task_info",
	"process": {
		"upload_percentage": 0,
		"remain_seconds": 0
	},
	"hls": {...	}
}

상태가 “ready”가 될 때까지 useGetTask 훅은 5000ms마다 데이터를 다시 가져오므로, 사용자는 인덱싱 진행 상황을 실시간으로 확인할 수 있습니다. 아래에서 제가 useQuery의 refetchInterval 프로퍼티를 어떻게 사용했는지 확인해보세요. 

apiHooks.js (128 - 142행)

export function useGetTask(taskId) {
 return useQuery({
   queryKey: [keys.TASK, taskId],
   queryFn: () =>
     apiConfig.SERVER.get(`${apiConfig.TASKS_URL}/${taskId}`).then(
       (res) => res.data
     ),
   refetchInterval: (data) => {
     return data?.status === "ready" || data?.status === "failed"
       ? false
       : 5000;
   },
   refetchIntervalInBackground: true,
 });
}

3 - 사용자 입력을 받아 결과 생성 및 보여주기

가장 핵심적이면서도 흥미로운 파트인 요약, 챕터, 하이라이트 생성 부분입니다! 우리는 사용자의 입력을 받은 후 Twelve Labs API의 summarize 엔드포인트를 사용하여 동영상의 텍스트 요약, 챕터 및 하이라이트를 생성합니다. 

  1. InputForm.js의 체크박스 폼에서 사용자 입력을 받습니다.

  2. 사용자가 선택한 각 필드 프롬프트를 기반으로 Result.js에서 ‘/summary’ API를 호출합니다 (POST Summaries, chapters, or highlights)

  3. Result.js에 그 결과를 출력합니다.

각 단계를 구체적으로 살펴보겠습니다.

3.1 - InputForm.js의 체크박스 폼에서 사용자 입력 받기

InputForm은 요약(Summary), 챕터(Chapters), 하이라이트(Highlights)라는 세 개의 체크박스 필드로 구성된 단순한 폼입니다. 사용자가 각 필드를 체크하거나 해제할 때마다, 각 필드 프롬프트가 type 프로퍼티와 함께 설정됩니다. 다음 단계에서 ‘/summary’에 요청을 보낼 때 이 ‘type’ 프로퍼티가 필수적이기 때문입니다. 

InputForm.js (33 - 49행)

   if (summaryRef.current?.checked) {
     setField1Prompt({ type: field1 });
   } else {
     setField1Prompt(null);
   }


   if (chaptersRef.current?.checked) {
     setField2Prompt({ type: field2 });
   } else {
     setField2Prompt(null);
   }


   if (highlightsRef.current?.checked) {
     setField3Prompt({ type: field3 });
   } else {
     setField3Prompt(null);
   }

3.2 - 사용자 입력을 기반으로 Result.js에서 ‘/summary’ API 호출하기 (POST Summaries, chapters, or highlights)

폼이 제출되고 유효한 비디오 ID 및 필드 프롬프트(type)가 확보되면, Result.js에서 useGenerate 훅이 호출됩니다. 그러면 이 훅들은 Twelve Labs API로 요청을 직접 보내는 서버에 요청을 전송하게 됩니다. (💡자세한 내용은 API 문서인 POST Summaries, chapters, or highlights에서 확인하세요)

server.js (97 - 117행)

/** Summarize a video */
app.post("/videos/:videoId/summarize", async (request, response, next) => {
 const videoId = request.params.videoId;
 let type = request.body.data;


 try {
   const options = {
     method: "POST",
     url: `${API_BASE_URL}/summarize`,
     headers: { ...HEADERS, accept: "application/json" },
     data: { ...type, video_id: videoId },
   };
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message =
     error.response?.data?.message || "Error Summarizing a Video";
   return next({ status, message });
 }
});

이 요청은

마케팅 배경을 가진 저로서는 인플루언서의 세계에 항상 깊은 관심을 가지고 있었습니다. 인플루언서로서 겪는 어려움 중 하나는 훌륭한 콘텐츠를 제작하고 제공하기 위한 영감을 지속적으로 얻는 것이라는 점을 깨달았습니다. 특히 유튜브 인플루언서의 경우, 구조와 핵심 요점, 하이라이트를 이해하기 위해 다른 사람들의 비디오를 수없이 많이 시청해야 하므로 이 문제가 더욱 두드러집니다. 

Twelve Labs의 Generate API를 발견했을 때, 저는 이 과제를 해결할 수 있는 상당한 잠재력을 바로 알아보았습니다. 저는 이 API를 활용하여 YouTube 동영상의 요약, 챕터, 하이라이트를 생성하는 간단한 앱을 제작하기로 결심했습니다. 이 앱은 포괄적인 보고서를 작성하여 각 동영상에 대한 구조화된 분석을 제공합니다. 이는 콘텐츠 분석 프로세스를 간소화할 뿐만 아니라 생각을 더 잘 정리할 수 있도록 도와줍니다.

두말할 필요 없이, 바로 본론으로 들어가 보겠습니다!

사전 요구 사항

  • Twelve Labs API 키가 필요합니다. 아직 키가 없다면 Twelve Labs Playground를 방문하여 가입하고 API 키를 생성하세요. 

  • 이 앱을 위한 모든 파일이 포함된 리포지토리는 GitHub에서 확인할 수 있습니다.

  • (우대 사항) JavaScript, Node, React 및 React Query에 대한 기본적인 지식이 권장됩니다. 하지만 이러한 기술에 익숙하지 않더라도 걱정하지 마세요. 이 글의 핵심은 이 앱이 Twelve Labs API를 어떻게 활용하는지 확인하는 것입니다!

앱의 구조

이 앱은 SummarizeVideo, VideoUrlUploadForm, Video, InputForm, Result의 5가지 주요 컴포넌트로 구성되어 있습니다. 

  • SummarizeVideo: 다른 컴포넌트들의 상위 컨테이너 역할을 합니다. 하위 컴포넌트들과 공유되는 주요 상태(state)를 관리합니다.

  • VideoUrlUploadForm: YouTube 동영상 URL을 입력받아 Twelve Labs API를 통해 동영상을 인덱싱하는 간단한 폼입니다. 인덱싱 프로세스가 완료될 때까지 인덱싱 진행 상태와 함께 프로세스 중인 동영상을 보여줍니다. 

  • Video: 입력된 URL의 동영상을 표시합니다. 세 개의 서로 다른 컴포넌트 내에서 재사용됩니다.

  • InputForm: 요약(Summary), 챕터(Chapters), 하이라이트(Highlights)의 3가지 체크박스로 구성된 폼입니다. 사용자는 각 필드를 자유롭게 체크하거나 해제할 수 있습니다. 

  • Result: Twelve Labs API(‘/summarize’ 엔드포인트)를 호출하여 inputForm에서 체크한 필드의 결과를 보여줍니다.

이 앱에는 API 호출과 관련된 모든 코드가 저장된 server와 상태, 캐시, 데이터 페칭을 관리하기 위한 커스텀 React Query 훅 모음인 apiHooks.js도 포함되어 있습니다.

이제 Twelve Labs API와 함께 이러한 컴포넌트들이 어떻게 작동하는지 살펴보겠습니다.

Twelve Labs API와 앱의 연동 방식

1 - 인덱스의 가장 최근 동영상 표시하기

요약을 생성할 때, 이 앱은 인덱스에 가장 최근에 업로드된 단 하나의 동영상만 처리합니다. 따라서 앱이 마운트되면 기본적으로 지정된 인덱스의 가장 최근 동영상을 보여줍니다. 아래는 그 작동 프로세스입니다. 

  1. App.js에서 주어진 인덱스의 모든 동영상을 가져옵니다 (GET Videos)

  2. 응답에서 첫 번째 동영상의 ID를 추출하여 SummarizeVideo.js로 전달합니다

  3. 동영상 ID를 사용해 동영상의 세부 정보를 가져오고, 동영상 소스 URL을 추출하여 Video.js로 전달합니다 (GET Video)

따라서 동영상을 가져와 페이지에 첫 번째 동영상을 표시하는 이 흐름에서 Twelve Labs API에 두 번의 GET 요청을 보내게 됩니다. 각 단계를 자세히 살펴보겠습니다.

1.1 - App.js에서 지정된 인덱스의 모든 동영상 가져오기 (GET Videos)

내부에서 동영상은 서버에 요청을 보내는 react query 훅인 useGetVideos를 호출하여 반환됩니다. 그런 다음 서버는 Twelve Labs API에 GET 요청을 보내 인덱스의 모든 동영상을 가져옵니다. (💡자세한 내용은 API 문서인 GET Videos에서 확인하세요)

Server.js (52 - 72행)

/** Get videos */

app.get("/indexes/:indexId/videos", async (request, response, next) => {
 const params = {
   page_limit: request.query.page_limit,
 };


 try {
   const options = {
     method: "GET",
     url: `${API_BASE_URL}/indexes/${request.params.indexId}/videos`,
     headers: { ...HEADERS },
     data: { params },
   };
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message = error.response?.data?.message || "Error Getting Videos";
   return next({ status, message });
 }
});

반환되는 데이터(video)는 아래와 같은 형태입니다.

{
	"data": [
		{
			"_id": "65caf0fa48db9fa780cb3fc2",
			"created_at": "2024-02-13T04:32:54Z",
			"updated_at": "2024-02-13T04:32:58Z",
			"indexed_at": "2024-02-13T04:40:15Z",
			"metadata": {
				"duration": 130,
				"engine_ids": [
					"pegasus1",
					"marengo2.5"
				],
				"filename": "Adidas CEO Herbert Hainer: How I Work",
				"fps": 30,
				"height": 720,
				"size": 11149582,
				"width": 1280
			},
			{},			
}

1.2 - 응답에서 첫 번째 동영상의 ID를 추출하여 SummarizeVideo.js로 전달하기

반환된 동영상 목록 정보를 기반으로 첫 번째 동영상의 ID를 SummizeVideo.js 컴포넌트로 전달합니다.

App.js (34 - 38행)

           <SummarizeVideo
             index={apiConfig.INDEX_ID}
             videoId={videos.data[0]?._id || null} //passing down the id
             refetchVideos={refetchVideos}
           &sol;

1.3 - 동영상 ID를 사용해 세부 정보를 가져오고, 동영상 소스 URL을 추출하여 Video.js로 전달하기 (GET Video)

이전 단계에서 동영상을 가져온 방법과 유사하게, 동영상의 세부 정보를 가져오기 위해 서버에 요청을 보내는 react query 훅인 useGetVideo를 사용합니다. 그런 다음 서버는 Twelve Labs API에 GET 요청을 보내 특정 동영상에 대한 상세 정보를 가져옵니다. (💡자세한 내용은 API 문서인 GET Video에서 확인하세요)

Server.js (74 - 95행)

/** Get a video of an index */
app.get(
 "/indexes/:indexId/videos/:videoId",
 async (request, response, next) => {
   const indexId = request.params.indexId;
   const videoId = request.params.videoId;


   try {
     const options = {
       method: "GET",
       url: `${API_BASE_URL}/indexes/${indexId}/videos/${videoId}`,
       headers: { ...HEADERS },
     };
     const apiResponse = await axios.request(options);
     response.json(apiResponse.data);
   } catch (error) {
     const status = error.response?.status || 500;
     const message = error.response?.data?.message || "Error Getting a Video";
     return next({ status, message });
   }
 }
);


이 요청은 우리가 필요로 하는 Youtube URL을 포함한 동영상의 상세 정보를 반환합니다! GET Videos 응답에서는 Youtube URL을 사용할 수 없었다는 점을 기억하세요. 따라서 여기서 다시 GET Video 요청을 날리게 되는 것입니다.

 {
	"_id": "65caf0fa48db9fa780cb3fc2",
	"created_at": "2024-02-13T04:32:54Z",
	"updated_at": "2024-02-13T04:32:58Z",
	"indexed_at": "2024-02-13T04:40:15Z",
	"metadata": {
		"duration": 130,
		"engine_ids": [
			"pegasus1",
			"marengo2.5"
		],
		"filename": "Adidas CEO Herbert Hainer: How I Work",
		"fps": 30,
		"height": 720,
		"size": 11149582,
		"video_title": "Adidas CEO Herbert Hainer: How I Work",
		"width": 1280
	},
	"hls": {
		"video_url": "...",
		"thumbnail_urls": [
			"..."
		],
		"status": "COMPLETE",
		"updated_at": "2024-02-13T04:33:29.993Z"
	},
	"source": {
		"type": "youtube",
		"name": "The Wall Street Journal",
		"url": "https://www.youtube.com/watch?v=sHD0YxASbGQ" // here!
	}
}

반환된 데이터(video) 정보를 기반으로 URL을 Video 컴포넌트에 전달하며, 이 컴포넌트 내에서 URL을 기반으로 React Player가 렌더링됩니다. 

SummarizeVideo.js (101 - 105행)

     {video && (
             <Video
               url={video.source?.url} // passing down the url
               width={"381px"}
               height={"214px"}

2 - YouTube URL을 통한 동영상 업로드 및 인덱싱

이 앱에서는 API 버전 1.2 덕분에 YouTube URL을 제출하는 것만으로 동영상을 쉽게 업로드하고 인덱싱할 수 있습니다! 동영상 인덱싱 요청(우리는 이를 ‘태스크(task)’라고 부름)이 제출되면 인덱싱 태스크의 진행 상황을 모니터링할 수 있습니다. 저는 또한 사용자가 인덱싱 중에 동영상을 확인하고 시청할 수 있도록 비디오 플레이어가 노출되도록 구현했습니다. 

  1. VideoUrlUploadForm.js에서 YouTube URL을 사용하여 동영상 인덱싱 태스크를 생성합니다 (POST Task)

  2. 동영상 정보를 가져와 VideoUrlUploadForm.js에 동영상을 표시합니다 (*ytdl-core 라이브러리를 사용했습니다) 

  3. Task.js에서 인덱싱 작업의 진행 상황을 수신하여 표시합니다 (GET Task)

각 단계를 차례대로 살펴보겠습니다.

2.1 - VideoUrlUploadForm.js에서 YouTube URL을 사용해 동영상 인덱싱 작업 생성하기

사용자가 YouTube URL을 사용해 VideoUrlUploadForm을 제출하면 taskVideo가 세팅됩니다. taskVideo가 존재할 때 indexYouTubeVideo가 실행되도록 useEffect를 추가했습니다. 

indexYouTubeVideo는 서버로 POST 요청을 보내고, 이는 다시 Twelve Labs API의 ‘/tasks/external-provider’ 엔드포인트로 POST 요청을 전달합니다. (💡자세한 내용은 API 문서인 POST Task에서 확인하세요)

Server.js (134 - 152행)

/** Index a Youtube video for analysis, returning a task ID */
app.post("/index", async (request, response, next) => {
 const options = {
   method: "POST",
   url: `${API_BASE_URL}/tasks/external-provider`,
   headers: { ...HEADERS, accept: "application/json" },
   data: request.body.body,
 };


 try {
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message =
     error.response?.data?.message || "Error indexing a YouTube Video";
   return next({ status, message });
 }
});


이제 방금 생성된 동영상 태스크의 ID가 반환됩니다.

{
	"_id": "65a9df3f627beda40b8dfa56"
}

2.2 - VideoUrlUploadForm.js에서 동영상 정보를 가져와 동영상 표시하기

동영상 인덱싱 작업이 진행되는 동안 사용자에게 인덱싱 중인 작업 비디오를 보여줍니다. 

VideoUrlUploadForm.js (120 - 126행)

{taskVideo && (
           <div className="videoUrlUploadForm__taskVideoWrapper">
             <Video
               url={taskVideo.video_url}
               width={"381px"}
               height={"214px"}

사용자가 YouTube URL이 포함된 양식을 제출하면 getVideoInfofetchVideoInfo(React Query Hook)를 통해 동영상 정보를 검색한 다음 해당 정보로 taskVideo를 설정합니다. 

VideoUrlUploadForm.js (74 - 88행)

/** Get information of a video and set it as task */
 async function handleSubmit(evt) {
   evt.preventDefault();
   try {
     if (!videoUrl?.trim()) {
       throw new Error("Please enter a valid video URL");
     }
     const videoInfo = await getVideoInfo(videoUrl); //get video info
     setTaskVideo(videoInfo); //set TaskVideo with the video info we got
     inputRef.current.value = "";
     resetPrompts();
   } catch (error) {
     setError(error.message);
   }
 }

동영상 정보를 가져오는 데 ytdl-core 라이브러리(getURLVideoID 및 getBasicInfo)를 사용하고 있습니다. 

Server.js (119 - 132행)

/** Get video information from a YouTube URL using ytdl */
app.get("/video-info", async (request, response, next) => {
 try {
   let url = request.query.url;
   const videoId = ytdl.getURLVideoID(url);
   const videoInfo = await ytdl.getBasicInfo(videoId);
   response.json(videoInfo.videoDetails);
 } catch (error) {
   const status = error.response?.status || 500;
   const message =
     error.response?.data?.message || "Error getting info of a video";
   return next({ status, message });
 }
});

2.3 - Task.js에서 인덱싱 작업 진행률을 수신하여 표시하기

‘/index’에 대한 POST 요청이 태스크 ID를 반환했다는 것, 기억하시나요? 우리는 이 태스크 ID를 사용하여 작업의 세부 정보를 가져오고 사용자에게 지속적으로 진행 상태를 실시간 업데이트하여 알릴 것입니다. 

따라서 taskId가 있으면 Task 컴포넌트가 렌더링됩니다. 

VideoUrlUploadForm.js (130 - 137행)

{taskId && (
               <Task
                 taskId={taskId}
                 refetchVideos={refetchVideos}
                 index={index}
                 setTaskVideo={setTaskVideo}

Task 컴포넌트 내부에서, 우리는 Twelve Labs API에 GET 요청을 보내는 useGetTask React Query 훅을 사용하여 데이터를 수신합니다. (💡자세한 내용은 API 문서인 GET Task에서 확인하세요)

server.js (154 - 171행)

/** Check the status of a specific indexing task */
app.get("/tasks/:taskId", async (request, response, next) => {
 const taskId = request.params.taskId;


 try {
   const options = {
     method: "GET",
     url: `${API_BASE_URL}/tasks/${taskId}`,
     headers: { ...HEADERS },
   };
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message = error.response?.data?.message || "Error getting a task";
   return next({ status, message });
 }
});

이 요청은 다음과 같은 작업 상세 정보를 반환합니다.

{
	"_id": "65a9fc79627beda40b8dfa7b",
	"index_id": "653c0592480f870fb3bb01be",
	"video_id": "65a9fc7e4981af6e637c8e59",
	"status": "indexing",
	"metadata": {...},
	"created_at": "2024-01-19T04:37:13.724Z",
	"updated_at": "2024-01-19T04:41:10.606Z",
	"estimated_time": "2024-01-19T04:41:36.601Z",
	"type": "index_task_info",
	"process": {
		"upload_percentage": 0,
		"remain_seconds": 0
	},
	"hls": {...	}
}

상태가 “ready”가 될 때까지 useGetTask 훅은 5000ms마다 데이터를 다시 가져오므로, 사용자는 인덱싱 진행 상황을 실시간으로 확인할 수 있습니다. 아래에서 제가 useQuery의 refetchInterval 프로퍼티를 어떻게 사용했는지 확인해보세요. 

apiHooks.js (128 - 142행)

export function useGetTask(taskId) {
 return useQuery({
   queryKey: [keys.TASK, taskId],
   queryFn: () =>
     apiConfig.SERVER.get(`${apiConfig.TASKS_URL}/${taskId}`).then(
       (res) => res.data
     ),
   refetchInterval: (data) => {
     return data?.status === "ready" || data?.status === "failed"
       ? false
       : 5000;
   },
   refetchIntervalInBackground: true,
 });
}

3 - 사용자 입력을 받아 결과 생성 및 보여주기

가장 핵심적이면서도 흥미로운 파트인 요약, 챕터, 하이라이트 생성 부분입니다! 우리는 사용자의 입력을 받은 후 Twelve Labs API의 summarize 엔드포인트를 사용하여 동영상의 텍스트 요약, 챕터 및 하이라이트를 생성합니다. 

  1. InputForm.js의 체크박스 폼에서 사용자 입력을 받습니다.

  2. 사용자가 선택한 각 필드 프롬프트를 기반으로 Result.js에서 ‘/summary’ API를 호출합니다 (POST Summaries, chapters, or highlights)

  3. Result.js에 그 결과를 출력합니다.

각 단계를 구체적으로 살펴보겠습니다.

3.1 - InputForm.js의 체크박스 폼에서 사용자 입력 받기

InputForm은 요약(Summary), 챕터(Chapters), 하이라이트(Highlights)라는 세 개의 체크박스 필드로 구성된 단순한 폼입니다. 사용자가 각 필드를 체크하거나 해제할 때마다, 각 필드 프롬프트가 type 프로퍼티와 함께 설정됩니다. 다음 단계에서 ‘/summary’에 요청을 보낼 때 이 ‘type’ 프로퍼티가 필수적이기 때문입니다. 

InputForm.js (33 - 49행)

   if (summaryRef.current?.checked) {
     setField1Prompt({ type: field1 });
   } else {
     setField1Prompt(null);
   }


   if (chaptersRef.current?.checked) {
     setField2Prompt({ type: field2 });
   } else {
     setField2Prompt(null);
   }


   if (highlightsRef.current?.checked) {
     setField3Prompt({ type: field3 });
   } else {
     setField3Prompt(null);
   }

3.2 - 사용자 입력을 기반으로 Result.js에서 ‘/summary’ API 호출하기 (POST Summaries, chapters, or highlights)

폼이 제출되고 유효한 비디오 ID 및 필드 프롬프트(type)가 확보되면, Result.js에서 useGenerate 훅이 호출됩니다. 그러면 이 훅들은 Twelve Labs API로 요청을 직접 보내는 서버에 요청을 전송하게 됩니다. (💡자세한 내용은 API 문서인 POST Summaries, chapters, or highlights에서 확인하세요)

server.js (97 - 117행)

/** Summarize a video */
app.post("/videos/:videoId/summarize", async (request, response, next) => {
 const videoId = request.params.videoId;
 let type = request.body.data;


 try {
   const options = {
     method: "POST",
     url: `${API_BASE_URL}/summarize`,
     headers: { ...HEADERS, accept: "application/json" },
     data: { ...type, video_id: videoId },
   };
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message =
     error.response?.data?.message || "Error Summarizing a Video";
   return next({ status, message });
 }
});

이 요청은

마케팅 배경을 가진 저로서는 인플루언서의 세계에 항상 깊은 관심을 가지고 있었습니다. 인플루언서로서 겪는 어려움 중 하나는 훌륭한 콘텐츠를 제작하고 제공하기 위한 영감을 지속적으로 얻는 것이라는 점을 깨달았습니다. 특히 유튜브 인플루언서의 경우, 구조와 핵심 요점, 하이라이트를 이해하기 위해 다른 사람들의 비디오를 수없이 많이 시청해야 하므로 이 문제가 더욱 두드러집니다. 

Twelve Labs의 Generate API를 발견했을 때, 저는 이 과제를 해결할 수 있는 상당한 잠재력을 바로 알아보았습니다. 저는 이 API를 활용하여 YouTube 동영상의 요약, 챕터, 하이라이트를 생성하는 간단한 앱을 제작하기로 결심했습니다. 이 앱은 포괄적인 보고서를 작성하여 각 동영상에 대한 구조화된 분석을 제공합니다. 이는 콘텐츠 분석 프로세스를 간소화할 뿐만 아니라 생각을 더 잘 정리할 수 있도록 도와줍니다.

두말할 필요 없이, 바로 본론으로 들어가 보겠습니다!

사전 요구 사항

  • Twelve Labs API 키가 필요합니다. 아직 키가 없다면 Twelve Labs Playground를 방문하여 가입하고 API 키를 생성하세요. 

  • 이 앱을 위한 모든 파일이 포함된 리포지토리는 GitHub에서 확인할 수 있습니다.

  • (우대 사항) JavaScript, Node, React 및 React Query에 대한 기본적인 지식이 권장됩니다. 하지만 이러한 기술에 익숙하지 않더라도 걱정하지 마세요. 이 글의 핵심은 이 앱이 Twelve Labs API를 어떻게 활용하는지 확인하는 것입니다!

앱의 구조

이 앱은 SummarizeVideo, VideoUrlUploadForm, Video, InputForm, Result의 5가지 주요 컴포넌트로 구성되어 있습니다. 

  • SummarizeVideo: 다른 컴포넌트들의 상위 컨테이너 역할을 합니다. 하위 컴포넌트들과 공유되는 주요 상태(state)를 관리합니다.

  • VideoUrlUploadForm: YouTube 동영상 URL을 입력받아 Twelve Labs API를 통해 동영상을 인덱싱하는 간단한 폼입니다. 인덱싱 프로세스가 완료될 때까지 인덱싱 진행 상태와 함께 프로세스 중인 동영상을 보여줍니다. 

  • Video: 입력된 URL의 동영상을 표시합니다. 세 개의 서로 다른 컴포넌트 내에서 재사용됩니다.

  • InputForm: 요약(Summary), 챕터(Chapters), 하이라이트(Highlights)의 3가지 체크박스로 구성된 폼입니다. 사용자는 각 필드를 자유롭게 체크하거나 해제할 수 있습니다. 

  • Result: Twelve Labs API(‘/summarize’ 엔드포인트)를 호출하여 inputForm에서 체크한 필드의 결과를 보여줍니다.

이 앱에는 API 호출과 관련된 모든 코드가 저장된 server와 상태, 캐시, 데이터 페칭을 관리하기 위한 커스텀 React Query 훅 모음인 apiHooks.js도 포함되어 있습니다.

이제 Twelve Labs API와 함께 이러한 컴포넌트들이 어떻게 작동하는지 살펴보겠습니다.

Twelve Labs API와 앱의 연동 방식

1 - 인덱스의 가장 최근 동영상 표시하기

요약을 생성할 때, 이 앱은 인덱스에 가장 최근에 업로드된 단 하나의 동영상만 처리합니다. 따라서 앱이 마운트되면 기본적으로 지정된 인덱스의 가장 최근 동영상을 보여줍니다. 아래는 그 작동 프로세스입니다. 

  1. App.js에서 주어진 인덱스의 모든 동영상을 가져옵니다 (GET Videos)

  2. 응답에서 첫 번째 동영상의 ID를 추출하여 SummarizeVideo.js로 전달합니다

  3. 동영상 ID를 사용해 동영상의 세부 정보를 가져오고, 동영상 소스 URL을 추출하여 Video.js로 전달합니다 (GET Video)

따라서 동영상을 가져와 페이지에 첫 번째 동영상을 표시하는 이 흐름에서 Twelve Labs API에 두 번의 GET 요청을 보내게 됩니다. 각 단계를 자세히 살펴보겠습니다.

1.1 - App.js에서 지정된 인덱스의 모든 동영상 가져오기 (GET Videos)

내부에서 동영상은 서버에 요청을 보내는 react query 훅인 useGetVideos를 호출하여 반환됩니다. 그런 다음 서버는 Twelve Labs API에 GET 요청을 보내 인덱스의 모든 동영상을 가져옵니다. (💡자세한 내용은 API 문서인 GET Videos에서 확인하세요)

Server.js (52 - 72행)

/** Get videos */

app.get("/indexes/:indexId/videos", async (request, response, next) => {
 const params = {
   page_limit: request.query.page_limit,
 };


 try {
   const options = {
     method: "GET",
     url: `${API_BASE_URL}/indexes/${request.params.indexId}/videos`,
     headers: { ...HEADERS },
     data: { params },
   };
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message = error.response?.data?.message || "Error Getting Videos";
   return next({ status, message });
 }
});

반환되는 데이터(video)는 아래와 같은 형태입니다.

{
	"data": [
		{
			"_id": "65caf0fa48db9fa780cb3fc2",
			"created_at": "2024-02-13T04:32:54Z",
			"updated_at": "2024-02-13T04:32:58Z",
			"indexed_at": "2024-02-13T04:40:15Z",
			"metadata": {
				"duration": 130,
				"engine_ids": [
					"pegasus1",
					"marengo2.5"
				],
				"filename": "Adidas CEO Herbert Hainer: How I Work",
				"fps": 30,
				"height": 720,
				"size": 11149582,
				"width": 1280
			},
			{},			
}

1.2 - 응답에서 첫 번째 동영상의 ID를 추출하여 SummarizeVideo.js로 전달하기

반환된 동영상 목록 정보를 기반으로 첫 번째 동영상의 ID를 SummizeVideo.js 컴포넌트로 전달합니다.

App.js (34 - 38행)

           <SummarizeVideo
             index={apiConfig.INDEX_ID}
             videoId={videos.data[0]?._id || null} //passing down the id
             refetchVideos={refetchVideos}
           &sol;

1.3 - 동영상 ID를 사용해 세부 정보를 가져오고, 동영상 소스 URL을 추출하여 Video.js로 전달하기 (GET Video)

이전 단계에서 동영상을 가져온 방법과 유사하게, 동영상의 세부 정보를 가져오기 위해 서버에 요청을 보내는 react query 훅인 useGetVideo를 사용합니다. 그런 다음 서버는 Twelve Labs API에 GET 요청을 보내 특정 동영상에 대한 상세 정보를 가져옵니다. (💡자세한 내용은 API 문서인 GET Video에서 확인하세요)

Server.js (74 - 95행)

/** Get a video of an index */
app.get(
 "/indexes/:indexId/videos/:videoId",
 async (request, response, next) => {
   const indexId = request.params.indexId;
   const videoId = request.params.videoId;


   try {
     const options = {
       method: "GET",
       url: `${API_BASE_URL}/indexes/${indexId}/videos/${videoId}`,
       headers: { ...HEADERS },
     };
     const apiResponse = await axios.request(options);
     response.json(apiResponse.data);
   } catch (error) {
     const status = error.response?.status || 500;
     const message = error.response?.data?.message || "Error Getting a Video";
     return next({ status, message });
   }
 }
);


이 요청은 우리가 필요로 하는 Youtube URL을 포함한 동영상의 상세 정보를 반환합니다! GET Videos 응답에서는 Youtube URL을 사용할 수 없었다는 점을 기억하세요. 따라서 여기서 다시 GET Video 요청을 날리게 되는 것입니다.

 {
	"_id": "65caf0fa48db9fa780cb3fc2",
	"created_at": "2024-02-13T04:32:54Z",
	"updated_at": "2024-02-13T04:32:58Z",
	"indexed_at": "2024-02-13T04:40:15Z",
	"metadata": {
		"duration": 130,
		"engine_ids": [
			"pegasus1",
			"marengo2.5"
		],
		"filename": "Adidas CEO Herbert Hainer: How I Work",
		"fps": 30,
		"height": 720,
		"size": 11149582,
		"video_title": "Adidas CEO Herbert Hainer: How I Work",
		"width": 1280
	},
	"hls": {
		"video_url": "...",
		"thumbnail_urls": [
			"..."
		],
		"status": "COMPLETE",
		"updated_at": "2024-02-13T04:33:29.993Z"
	},
	"source": {
		"type": "youtube",
		"name": "The Wall Street Journal",
		"url": "https://www.youtube.com/watch?v=sHD0YxASbGQ" // here!
	}
}

반환된 데이터(video) 정보를 기반으로 URL을 Video 컴포넌트에 전달하며, 이 컴포넌트 내에서 URL을 기반으로 React Player가 렌더링됩니다. 

SummarizeVideo.js (101 - 105행)

     {video && (
             <Video
               url={video.source?.url} // passing down the url
               width={"381px"}
               height={"214px"}

2 - YouTube URL을 통한 동영상 업로드 및 인덱싱

이 앱에서는 API 버전 1.2 덕분에 YouTube URL을 제출하는 것만으로 동영상을 쉽게 업로드하고 인덱싱할 수 있습니다! 동영상 인덱싱 요청(우리는 이를 ‘태스크(task)’라고 부름)이 제출되면 인덱싱 태스크의 진행 상황을 모니터링할 수 있습니다. 저는 또한 사용자가 인덱싱 중에 동영상을 확인하고 시청할 수 있도록 비디오 플레이어가 노출되도록 구현했습니다. 

  1. VideoUrlUploadForm.js에서 YouTube URL을 사용하여 동영상 인덱싱 태스크를 생성합니다 (POST Task)

  2. 동영상 정보를 가져와 VideoUrlUploadForm.js에 동영상을 표시합니다 (*ytdl-core 라이브러리를 사용했습니다) 

  3. Task.js에서 인덱싱 작업의 진행 상황을 수신하여 표시합니다 (GET Task)

각 단계를 차례대로 살펴보겠습니다.

2.1 - VideoUrlUploadForm.js에서 YouTube URL을 사용해 동영상 인덱싱 작업 생성하기

사용자가 YouTube URL을 사용해 VideoUrlUploadForm을 제출하면 taskVideo가 세팅됩니다. taskVideo가 존재할 때 indexYouTubeVideo가 실행되도록 useEffect를 추가했습니다. 

indexYouTubeVideo는 서버로 POST 요청을 보내고, 이는 다시 Twelve Labs API의 ‘/tasks/external-provider’ 엔드포인트로 POST 요청을 전달합니다. (💡자세한 내용은 API 문서인 POST Task에서 확인하세요)

Server.js (134 - 152행)

/** Index a Youtube video for analysis, returning a task ID */
app.post("/index", async (request, response, next) => {
 const options = {
   method: "POST",
   url: `${API_BASE_URL}/tasks/external-provider`,
   headers: { ...HEADERS, accept: "application/json" },
   data: request.body.body,
 };


 try {
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message =
     error.response?.data?.message || "Error indexing a YouTube Video";
   return next({ status, message });
 }
});


이제 방금 생성된 동영상 태스크의 ID가 반환됩니다.

{
	"_id": "65a9df3f627beda40b8dfa56"
}

2.2 - VideoUrlUploadForm.js에서 동영상 정보를 가져와 동영상 표시하기

동영상 인덱싱 작업이 진행되는 동안 사용자에게 인덱싱 중인 작업 비디오를 보여줍니다. 

VideoUrlUploadForm.js (120 - 126행)

{taskVideo && (
           <div className="videoUrlUploadForm__taskVideoWrapper">
             <Video
               url={taskVideo.video_url}
               width={"381px"}
               height={"214px"}

사용자가 YouTube URL이 포함된 양식을 제출하면 getVideoInfofetchVideoInfo(React Query Hook)를 통해 동영상 정보를 검색한 다음 해당 정보로 taskVideo를 설정합니다. 

VideoUrlUploadForm.js (74 - 88행)

/** Get information of a video and set it as task */
 async function handleSubmit(evt) {
   evt.preventDefault();
   try {
     if (!videoUrl?.trim()) {
       throw new Error("Please enter a valid video URL");
     }
     const videoInfo = await getVideoInfo(videoUrl); //get video info
     setTaskVideo(videoInfo); //set TaskVideo with the video info we got
     inputRef.current.value = "";
     resetPrompts();
   } catch (error) {
     setError(error.message);
   }
 }

동영상 정보를 가져오는 데 ytdl-core 라이브러리(getURLVideoID 및 getBasicInfo)를 사용하고 있습니다. 

Server.js (119 - 132행)

/** Get video information from a YouTube URL using ytdl */
app.get("/video-info", async (request, response, next) => {
 try {
   let url = request.query.url;
   const videoId = ytdl.getURLVideoID(url);
   const videoInfo = await ytdl.getBasicInfo(videoId);
   response.json(videoInfo.videoDetails);
 } catch (error) {
   const status = error.response?.status || 500;
   const message =
     error.response?.data?.message || "Error getting info of a video";
   return next({ status, message });
 }
});

2.3 - Task.js에서 인덱싱 작업 진행률을 수신하여 표시하기

‘/index’에 대한 POST 요청이 태스크 ID를 반환했다는 것, 기억하시나요? 우리는 이 태스크 ID를 사용하여 작업의 세부 정보를 가져오고 사용자에게 지속적으로 진행 상태를 실시간 업데이트하여 알릴 것입니다. 

따라서 taskId가 있으면 Task 컴포넌트가 렌더링됩니다. 

VideoUrlUploadForm.js (130 - 137행)

{taskId && (
               <Task
                 taskId={taskId}
                 refetchVideos={refetchVideos}
                 index={index}
                 setTaskVideo={setTaskVideo}

Task 컴포넌트 내부에서, 우리는 Twelve Labs API에 GET 요청을 보내는 useGetTask React Query 훅을 사용하여 데이터를 수신합니다. (💡자세한 내용은 API 문서인 GET Task에서 확인하세요)

server.js (154 - 171행)

/** Check the status of a specific indexing task */
app.get("/tasks/:taskId", async (request, response, next) => {
 const taskId = request.params.taskId;


 try {
   const options = {
     method: "GET",
     url: `${API_BASE_URL}/tasks/${taskId}`,
     headers: { ...HEADERS },
   };
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message = error.response?.data?.message || "Error getting a task";
   return next({ status, message });
 }
});

이 요청은 다음과 같은 작업 상세 정보를 반환합니다.

{
	"_id": "65a9fc79627beda40b8dfa7b",
	"index_id": "653c0592480f870fb3bb01be",
	"video_id": "65a9fc7e4981af6e637c8e59",
	"status": "indexing",
	"metadata": {...},
	"created_at": "2024-01-19T04:37:13.724Z",
	"updated_at": "2024-01-19T04:41:10.606Z",
	"estimated_time": "2024-01-19T04:41:36.601Z",
	"type": "index_task_info",
	"process": {
		"upload_percentage": 0,
		"remain_seconds": 0
	},
	"hls": {...	}
}

상태가 “ready”가 될 때까지 useGetTask 훅은 5000ms마다 데이터를 다시 가져오므로, 사용자는 인덱싱 진행 상황을 실시간으로 확인할 수 있습니다. 아래에서 제가 useQuery의 refetchInterval 프로퍼티를 어떻게 사용했는지 확인해보세요. 

apiHooks.js (128 - 142행)

export function useGetTask(taskId) {
 return useQuery({
   queryKey: [keys.TASK, taskId],
   queryFn: () =>
     apiConfig.SERVER.get(`${apiConfig.TASKS_URL}/${taskId}`).then(
       (res) => res.data
     ),
   refetchInterval: (data) => {
     return data?.status === "ready" || data?.status === "failed"
       ? false
       : 5000;
   },
   refetchIntervalInBackground: true,
 });
}

3 - 사용자 입력을 받아 결과 생성 및 보여주기

가장 핵심적이면서도 흥미로운 파트인 요약, 챕터, 하이라이트 생성 부분입니다! 우리는 사용자의 입력을 받은 후 Twelve Labs API의 summarize 엔드포인트를 사용하여 동영상의 텍스트 요약, 챕터 및 하이라이트를 생성합니다. 

  1. InputForm.js의 체크박스 폼에서 사용자 입력을 받습니다.

  2. 사용자가 선택한 각 필드 프롬프트를 기반으로 Result.js에서 ‘/summary’ API를 호출합니다 (POST Summaries, chapters, or highlights)

  3. Result.js에 그 결과를 출력합니다.

각 단계를 구체적으로 살펴보겠습니다.

3.1 - InputForm.js의 체크박스 폼에서 사용자 입력 받기

InputForm은 요약(Summary), 챕터(Chapters), 하이라이트(Highlights)라는 세 개의 체크박스 필드로 구성된 단순한 폼입니다. 사용자가 각 필드를 체크하거나 해제할 때마다, 각 필드 프롬프트가 type 프로퍼티와 함께 설정됩니다. 다음 단계에서 ‘/summary’에 요청을 보낼 때 이 ‘type’ 프로퍼티가 필수적이기 때문입니다. 

InputForm.js (33 - 49행)

   if (summaryRef.current?.checked) {
     setField1Prompt({ type: field1 });
   } else {
     setField1Prompt(null);
   }


   if (chaptersRef.current?.checked) {
     setField2Prompt({ type: field2 });
   } else {
     setField2Prompt(null);
   }


   if (highlightsRef.current?.checked) {
     setField3Prompt({ type: field3 });
   } else {
     setField3Prompt(null);
   }

3.2 - 사용자 입력을 기반으로 Result.js에서 ‘/summary’ API 호출하기 (POST Summaries, chapters, or highlights)

폼이 제출되고 유효한 비디오 ID 및 필드 프롬프트(type)가 확보되면, Result.js에서 useGenerate 훅이 호출됩니다. 그러면 이 훅들은 Twelve Labs API로 요청을 직접 보내는 서버에 요청을 전송하게 됩니다. (💡자세한 내용은 API 문서인 POST Summaries, chapters, or highlights에서 확인하세요)

server.js (97 - 117행)

/** Summarize a video */
app.post("/videos/:videoId/summarize", async (request, response, next) => {
 const videoId = request.params.videoId;
 let type = request.body.data;


 try {
   const options = {
     method: "POST",
     url: `${API_BASE_URL}/summarize`,
     headers: { ...HEADERS, accept: "application/json" },
     data: { ...type, video_id: videoId },
   };
   const apiResponse = await axios.request(options);
   response.json(apiResponse.data);
 } catch (error) {
   const status = error.response?.status || 500;
   const message =
     error.response?.data?.message || "Error Summarizing a Video";
   return next({ status, message });
 }
});

이 요청은