Profile image
Jinyoung
Dev

동영상 자막 자동 번역기 만들기(영문 ➡️ 한글)

동영상 자막 자동 번역기 만들기(영문 ➡️ 한글)
0 views
41 min read

최근 들어서 Andrej Karpathy의 유튜브 영상을 몇 개 보고 있습니다.

Andrej Karpathy는 인공지능과 딥러닝 분야에서 세계적으로 유명한 연구자 중의 한 명입니다. 예전부터 이름은 많이 들어왔는데 최근 들어 유튜브 추천 알고리즘에 떠서 몇 개의 동영상을 봤습니다.

제가 Andrej Karpathy의 영상들을 우연히 보고 이 사람의 영상을 앞으로 좀 챙겨봐야겠다 라고 생각하게 된 이유가 딱 한 가지 있습니다. 설명을 정말 잘 합니다. 말을 유창하게 한다는 느낌보다는 이 필드에서 엄청난 성취를 거뒀고 지금도 주요 연구자로서 활동하기 때문인지 필드 밖의 일반 사람들에게 매우 친절하고 이해하기 쉽게 설명을 해줍니다.

이번에 보려고 하는 영상은 2025년 2월 6일에 공개된 Deep Dive into LLMs like ChatGPT 라는 영상입니다. 2025년 3월 10일 현재 조회수가 160만 회가 넘는 매우 핫한 영상으로서 페이스북이나 다른 기술 블로그 등에서 수 많은 사람들에 의해 언급되기도 했습니다.

이 영상은 조회수가 높기도 하고 다른 LLM 관련 커뮤니티에서도 많이 회자되어서 평소에 한 번 봐야겠다 하는 생각을 갖고 있었습니다. 하지만 동시에 언어 장벽을 생각하지 않을 수 없었습니다.
영어 영상을 실시간으로 보면서 이해한다는 것이 아예 불가능한 건 아니지만 아무래도 기술적인 단어와 표현들, 그리고 제가 잘 모르는 필드에 대한 영상이기 때문에 제 모국어에 비해 이해도가 많이 떨어지는 것 역시 사실입니다.

그래서 이참에 그냥 영상 자막 번역기를 만들어보기로 했습니다. 유튜브 영상을 다운 받아서 자막을 만들고 자막을 번역하고 번역된 자막을 다시 영상에 입히면 되지 않을까 하는 막연한 생각으로 시작했습니다.

이 글은 영상의 음성을 인식하여 자막을 만들고 원래 언어와 다른 언어의 자막을 만들어서 영상에 입히는 기능을 어떻게 구현했는지에 대한 과정을 다룹니다. 코드가 일부 삽입되기는 하지만 코드를 읽는 것은 중요하지 않고 Cursor(LLM)을 이용하여 어떤 과정을 거쳐서 최종 프로그램을 완성했는지에 집중합니다.


LLM chat - 감 잡기

Cursor의 Chat 기능을 통해 만들기 시작했습니다. 프롬프트는 다음과 같습니다.

아래 요구사항을 만족하는 TypeScript 코드를 작성해봅시다.

  • 영상을 읽어서 해당 영상의 화자가 말 하는 언어(영어)를 한국어로 번역하여 한국어를 자막으로 달아서 새로운 영상을 만드는 것입니다.

일단 위 요구사항을 만족시키기 위해 어떤 컨셉으로 코드를 구성해야 할지 구성 요소부터 논의해봅시다.

LLM은 다음과 같이 답변합니다.

# 필요한 주요 구성 요소
1. 영상 처리
  - 입력 영상을 읽고 처리할 라이브러리 (FFmpeg)
  - TypeScript에서는 fluent-ffmpeg와 같은 라이브러리 사용 가능

2. 음성 인식 (Speech-to-Text)
  - 영상에서 음성을 추출하고 텍스트로 변환
  - 옵션: Google Cloud Speech-to-Text, AWS Transcribe, OpenAI Whisper API 등

3. 번역 API
  - 영어 텍스트를 한국어로 번역
  - 옵션: Google Translate API, DeepL API, Papago API 등

4. 자막 생성 및 영상 합성
  - 번역된 텍스트를 타임스탬프와 함께 자막 파일 (SRT/VTT)로 생성
  - 자막을 영상에 합성하여 최종 결과물 생성

# 사용할 기술 스택 및 라이브러리
1. 기본 환경:
  - Node.js + TypeScript
  - npm 또는 yarn 패키지 관리자

2. 영상 처리:
  - fluent-ffmpeg: FFmpeg를 Node.js에서 사용할 수 있게 해주는 라이브러리
  - @ffmpeg-installer/ffmpeg: FFmpeg 바이너리를 자동으로 설치

3. 음성 인식 및 번역:
  - OpenAI Whisper API: 고품질 음성 인식
  - Google Cloud Translate 또는 DeepL API: 번역

4. 자막 생성 및 처리:
  - subtitle 또는 subtitles-parser: SRT/VTT 파일 생성 및 처리
  - node-srt: SRT 파일 생성

5. 유틸리티:
  - dotenv: 환경 변수 관리
  - axios: HTTP 요청

# 프로젝트 구조 제안
project/
├── src/
│ ├── index.ts # 메인 애플리케이션 진입점
│ ├── videoProcessor.ts # 영상 처리 관련 기능
│ ├── speechRecognizer.ts # 음성 인식 관련 기능
│ ├── translator.ts # 번역 관련 기능
│ ├── subtitleGenerator.ts # 자막 생성 관련 기능
│ └── types/ # 타입 정의
├── package.json
├── tsconfig.json
└── .env # API 키 등 환경 변수

# 처리 흐름
1. 입력 영상에서 오디오 추출
2. 오디오를 사용하여 음성 인식(STT) 수행, 영어 텍스트와 타임스탬프 얻기
3. 인식된 영어 텍스트를 한국어로 번역
4. 번역된 한국어 텍스트와 타임스탬프를 사용하여 자막 파일(SRT) 생성
5. 원본 영상과 자막 파일을 합성하여 최종 영상 생성

이 구성은 어떻게 생각하시나요? 코드 작성을 시작하기 전에 다른 요구사항이나 고려할 사항이 있으신가요?

이제 이 정도 퀄리티의 답변을 받아도 놀라지 않을 때가 된 것도 같은데, 여전히 놀라지 않을 수 없습니다.


몸풀기 기능 개발

본격적으로 기능 개발을 시작하기 전에 일단 아주 아주 간단한 기능을 만들어보고 싶었습니다.

LLM이 답변한 위 결과물을 100% 신뢰할 수는 없었기 때문입니다. 간혹가다 아예 없는 라이브러리를 있다고 하는 경우도 있고 존재는 하되 원래 목적과 다르게 사용하는 경우도 있었습니다.

해서, 일단은 아래 프롬프트를 입력했습니다.

일단 아주 간단한 코드부터 작성해봅시다. 영상의 앞 부분(10초) 정도만 잘라서 영상에 '안녕하세요' 라는 한글 자막을 다는 것입니다.

LLM은 첫번째 대화와 👆 이 프롬프트를 기반으로 아래와 같이 답변합니다.

1. 프로젝트 초기화

먼저 프로젝트 디렉토리를 생성하고 초기화하겠습니다.

mkdir -p simple-video-subtitle && cd simple-video-subtitle && npm init -y

2. 필요한 패키지 설치

필요한 패키지들을 설치하겠습니다. 영상 처리를 위해 FFmpeg와 TypeScript 환경을 설정합니다.

npm install fluent-ffmpeg @ffmpeg-installer/ffmpeg typescript ts-node @types/node @types/fluent-ffmpeg

3. TypeScript 설정 파일 생성 & 프로젝트 구조 생성

npx tsc --init
mkdir -p src

4. package.json 스크립트 수정

package.json 스크립트를 업데이트하겠습니다:

  "npm": "10.9.2"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "ts-node src/index.ts"
  },
  "keywords": [],
  "author": "",

5. 소스 코드 작성

먼저 메인 파일을 만들어 영상의 처음 10초를 잘라서 '안녕하세요' 자막을 추가하는 코드를 작성하겠습니다:

전체 소스 코드를 확인하세요
index.ts
import ffmpeg from 'fluent-ffmpeg';
import { path as ffmpegPath } from '@ffmpeg-installer/ffmpeg';
import * as fs from 'fs';
import * as path from 'path';

// FFmpeg 경로 설정
ffmpeg.setFfmpegPath(ffmpegPath);

// 디렉토리 생성 함수
function ensureDirectoryExists(directory: string): void {
  if (!fs.existsSync(directory)) {
    fs.mkdirSync(directory, { recursive: true });
  }
}

// 자막 SRT 파일 생성 함수
function createSimpleSubtitle(outputPath: string): string {
  const srtContent = `1
00:00:00,000 --> 00:00:10,000
안녕하세요`;

  const srtPath = path.join(outputPath, 'subtitle.srt');
  fs.writeFileSync(srtPath, srtContent);
  return srtPath;
}

// 메인 함수
async function main() {
  // 입력 및 출력 경로 설정
  const inputDir = path.join(__dirname, "..", 'input');
  const outputDir = path.join(__dirname, "..", 'output');

  // 디렉토리 확인
  ensureDirectoryExists(inputDir);
  ensureDirectoryExists(outputDir);

  // 입력 비디오 파일 경로 (사용자가 이 경로에 비디오 파일을 넣어야 함)
  const inputVideoPath = path.join(inputDir, 'input.mp4');

  // 출력 비디오 파일 경로
  const outputVideoPath = path.join(outputDir, "output_with_subtitle.mp4");

  // 입력 파일이 존재하는지 확인
  if (!fs.existsSync(inputVideoPath)) {
    console.error(`입력 비디오 파일이 없습니다: ${inputVideoPath}`);
    console.log("input 폴더에 input.mp4 파일을 추가해주세요.");
    return;
  }

  // 자막 파일 생성
  const subtitlePath = createSimpleSubtitle(outputDir);

  console.log("영상 처리 시작...");

  return new Promise<void>((resolve, reject) => {
    ffmpeg(inputVideoPath)
      .setDuration(10) // 처음 10초만 자르기
      .videoFilters(`subtitles=${subtitlePath}`) // 문자열 형식으로 필터 지정
      .output(outputVideoPath)
      .on("end", () => {
        console.log(`처리 완료! 출력 파일: ${outputVideoPath}`);
        resolve();
      })
      .on("error", (err) => {
        console.error("오류 발생:", err);
        reject(err);
      })
      .run();
  });
}

// 프로그램 실행
main().catch((err) => {
  console.error("프로그램 실행 중 오류 발생:", err);
});

소스 코드의 경우 중간에 오류가 하나 있었지만 이것조차도 스스로 인식해서 수정합니다.

최종 소스 코드를 기반으로 1분짜리 영상을 input으로 해서 테스트 해본 결과 매우 잘 작동합니다. 여기서 2가지를 확인할 수 있었습니다.

  1. 요구사항을 토대로 수립한 프로그램 계획이 꽤 합리적이고
  2. 지금까지 생성된 코드 또는 참조하는 라이브러리에 문제가 없다는 것

특히 아무 오류 없이 깔끔하게 실행된다는 것에 고무됐습니다. 이제 본격적으로 동영상 자막 자동 번역기를 만들 때가 되었습니다.


본격적인 기능 구현 시작

index.ts 파일은 프로그램의 진입점으로서만 역할을 하고 주요 로직은 translate_video.ts 라는 파일에 두고 싶었습니다. 아래와 같은 프롬프트를 시작으로 시작했습니다.

아주 좋습니다!

이제, 본격적으로 원래 목표했던 프로그램을 작성해봅시다. src 디렉토리 하위에 translate_video.ts 라는 파일을 만들고 이 파일에 코드를 작성하세요

1. Dependency 추가

네, 영상의 음성을 인식하고 번역하여 자막을 추가하는 기능을 구현해보겠습니다. 이를 위해서는 다음과 같은 추가 패키지들이 필요합니다. 먼저 필요한 패키지들을 설치하겠습니다:

npm install @google-cloud/speech openai @google-cloud/translate dotenv

2. translate_video.ts 코드 작성

이제 translate_video.ts 파일을 생성하고 코드를 작성하겠습니다:

전체 소스 코드를 확인하세요
import ffmpeg from "fluent-ffmpeg";
import { path as ffmpegPath } from "@ffmpeg-installer/ffmpeg";
import * as fs from "fs";
import * as path from "path";
import * as speech from "@google-cloud/speech";
import { OpenAI } from "openai";
import { v2 as translate } from "@google-cloud/translate";
import * as dotenv from "dotenv";

// 환경 변수 로드
dotenv.config();

// FFmpeg 경로 설정
ffmpeg.setFfmpegPath(ffmpegPath);

// API 클라이언트 초기화
const speechClient = new speech.SpeechClient();
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const translateClient = new translate.Translate({
  key: process.env.GOOGLE_TRANSLATE_API_KEY,
});

interface SubtitleSegment {
  start: number;
  end: number;
  text: string;
}

// 디렉토리 생성 함수
function ensureDirectoryExists(directory: string): void {
  if (!fs.existsSync(directory)) {
    fs.mkdirSync(directory, { recursive: true });
  }
}

// 오디오 추출 함수
async function extractAudio(
  inputPath: string,
  outputPath: string
): Promise<void> {
  return new Promise((resolve, reject) => {
    ffmpeg(inputPath)
      .toFormat("wav")
      .outputOptions("-acodec pcm_s16le")
      .outputOptions("-ac 1")
      .outputOptions("-ar 16000")
      .save(outputPath)
      .on("end", () => resolve())
      .on("error", reject);
  });
}

// 음성 인식 함수
async function transcribeAudio(audioPath: string): Promise<SubtitleSegment[]> {
  const audioBytes = fs.readFileSync(audioPath).toString("base64");

  const audio = {
    content: audioBytes,
  };
  const config = {
    encoding: "LINEAR16",
    sampleRateHertz: 16000,
    languageCode: "en-US",
    enableWordTimeOffsets: true,
  };
  const request = {
    audio: audio,
    config: config,
  };

  try {
    const [response] = await speechClient.recognize(request);
    const segments: SubtitleSegment[] = [];

    response.results?.forEach((result) => {
      result.alternatives?.[0].words?.forEach((wordInfo) => {
        const start = Number(wordInfo.startTime?.seconds || 0);
        const end = Number(wordInfo.endTime?.seconds || 0);
        const text = wordInfo.word || "";

        if (
          segments.length > 0 &&
          segments[segments.length - 1].end === start
        ) {
          // 이전 세그먼트와 연결
          segments[segments.length - 1].text += " " + text;
          segments[segments.length - 1].end = end;
        } else {
          // 새 세그먼트 생성
          segments.push({ start, end, text });
        }
      });
    });

    return segments;
  } catch (error) {
    console.error("음성 인식 오류:", error);
    throw error;
  }
}

// 텍스트 번역 함수
async function translateText(text: string): Promise<string> {
  try {
    const [translation] = await translateClient.translate(text, {
      from: "en",
      to: "ko",
    });
    return translation;
  } catch (error) {
    console.error("번역 오류:", error);
    throw error;
  }
}

// SRT 파일 생성 함수
function createSRTFile(segments: SubtitleSegment[], outputPath: string): void {
  let srtContent = "";
  segments.forEach((segment, index) => {
    const startTime = formatSRTTime(segment.start);
    const endTime = formatSRTTime(segment.end);

    srtContent += `${index + 1}\n`;
    srtContent += `${startTime} --> ${endTime}\n`;
    srtContent += `${segment.text}\n\n`;
  });

  fs.writeFileSync(outputPath, srtContent);
}

// SRT 시간 포맷 함수
function formatSRTTime(seconds: number): string {
  const pad = (num: number): string => num.toString().padStart(2, "0");
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const secs = Math.floor(seconds % 60);
  const ms = Math.floor((seconds % 1) * 1000);

  return `${pad(hours)}:${pad(minutes)}:${pad(secs)},${ms
    .toString()
    .padStart(3, "0")}`;
}

// 메인 처리 함수
export async function translateVideo(
  inputVideoPath: string,
  outputVideoPath: string
): Promise<void> {
  try {
    // 작업 디렉토리 설정
    const workDir = path.join(path.dirname(inputVideoPath), "temp");
    ensureDirectoryExists(workDir);

    // 오디오 추출
    console.log("오디오 추출 중...");
    const audioPath = path.join(workDir, "audio.wav");
    await extractAudio(inputVideoPath, audioPath);

    // 음성 인식
    console.log("음성 인식 중...");
    const segments = await transcribeAudio(audioPath);

    // 번역
    console.log("텍스트 번역 중...");
    const translatedSegments: SubtitleSegment[] = [];
    for (const segment of segments) {
      const translatedText = await translateText(segment.text);
      translatedSegments.push({
        ...segment,
        text: translatedText,
      });
    }

    // 자막 파일 생성
    console.log("자막 파일 생성 중...");
    const subtitlePath = path.join(workDir, "subtitle.srt");
    createSRTFile(translatedSegments, subtitlePath);

    // 자막 합성
    console.log("자막 합성 중...");
    return new Promise((resolve, reject) => {
      ffmpeg(inputVideoPath)
        .videoFilters(`subtitles=${subtitlePath}`)
        .output(outputVideoPath)
        .on("end", () => {
          // 임시 파일 정리
          fs.rmSync(workDir, { recursive: true, force: true });
          console.log("처리 완료!");
          resolve();
        })
        .on("error", (err: Error) => {
          console.error("오류 발생:", err);
          reject(err);
        })
        .run();
    });
  } catch (error) {
    console.error("비디오 처리 중 오류 발생:", error);
    throw error;
  }
}

마지막으로, 환경 변수 설정을 위한 .env 파일을 생성하겠습니다:

# Google Cloud API 키 설정
GOOGLE_APPLICATION_CREDENTIALS="path/to/your/google-credentials.json"
GOOGLE_TRANSLATE_API_KEY="your-google-translate-api-key"

# OpenAI API 키 설정
OPENAI_API_KEY="your-openai-api-key"

3. 코드 설명

이제 프로젝트의 구조와 기능이 완성되었습니다! 주요 기능은 다음과 같습니다:

1. 오디오 추출: FFmpeg를 사용하여 영상에서 오디오를 추출합니다.
2. 음성 인식: Google Cloud Speech-to-Text API를 사용하여 영어 음성을 텍스트로 변환합니다.
3. 번역: Google Cloud Translate API를 사용하여 영어 텍스트를 한국어로 번역합니다.
4. 자막 생성: 번역된 텍스트를 SRT 형식의 자막 파일로 생성합니다.
5. 자막 합성: FFmpeg를 사용하여 원본 영상에 자막을 합성합니다.

사용하기 위해서는 다음 단계가 필요합니다:

1. .env 파일에 필요한 API 키들을 설정합니다:
   - Google Cloud 서비스 계정 키 파일 경로
   - Google Translate API 키
   - OpenAI API 키 (향후 확장을 위해)

2. input 폴더에 처리할 영상 파일을 input.mp4 이름으로 넣습니다.
3. 다음 명령어로 실행합니다:
   `npm start`

코드 실행

컴파일 오류가 전부 수정된 버전의 소스 코드가 완성되기까지 몇 번의 추가적인 대화가 필요했지만 어쨌든 굉장히 빠르게 코드가 준비되었습니다.

그래서 곧바로 코드를 실행해봤습니다. 그후에 3가지 문제점이 발견되었습니다.

  1. 인증 문제

    • 이 프로그램은 음성에서 텍스트를 분리하기 위해 Google Clould Speech-to-Text API를 사용하고 텍스트 번역을 위해 Google Cloud Translate API를 사용합니다.
    • LLM이 처음으로 작성해준 버전은 Google Cloud API 인증을 위해 여러가지 복잡한 키 설정이 필요했습니다.
  2. API 사용법 문제 (1)

    • LLM이 작성한 버전의 코드에서는 Speech-to-Text API를 호출할 때, 1분 미만의 음성만 지원하도록 되어 있었습니다
    • 그래서 수십분이 넘어가는 영상을 input으로 지정하면 에러 로그가 출력되면서 프로그램이 중간에 죽는 문제가 있습니다.
  3. API 사용법 문제 (2)

    • Speech-to-Text API 호출 시, 음성 파일의 용량이 큰 경우 바로 전송할 수 없고 GCS(Google Cloud Storage)를 통해서 전달해야 한다고 합니다.
    • 이 문제 역시 프로그램 실행을 중간에 멈추게 만듭니다.

인증 문제 해결

일단 인증 키를 발급 받기 위해 Google cloud console에 접속 하여 이런저런 프로젝트 설정을 뒤적여가며 키를 발급 받았습니다. 회사 프로젝트로 GCP를 사용해본 경험이 있었기 때문에 이 부분은 쉽게 해결이 가능할 것 같았습니다.

하지만 인증을 위해 Key를 따로 발급 받고 관리하는 것이 부담스러웠습니다. 또한 인증을 위해 Key 설정을 코드에 두는 것 보다는 CLI를 통해 로그인 하고 바로 코드에 인증 정보가 적용되게 하고 싶었습니다. 이 부분은 제가 이미 알고 있는 것이었기 때문에 코드를 직접 제가 수정했습니다.

GCP API 인증과 관련해 자세한 것은 이 문서를 참조하세요

API 사용법 문제 해결

Speech-to-Text API 사용과 관련한 에러 로그가 콘솔창에 출력되었기 때문에 해당 부분을 선택한 후에 Chat 인터페이스에 추가했습니다.

LLM은 곧바로 에러의 원인과 함께 코드 수정까지 자동으로 수행했습니다. 앞서 파악했던 API 사용법 문제 2가지를 순차적으로 해결해줬습니다.

특히 처음에 파악하지 못했던 GCS를 사용해야 한다는 것을 파악하고 자동으로 GCS 라이브러리를 설치하고 해당 라이브버리를 사용하여 음성 파일을 Storage에 업로드 하고 업로드 된 파일의 uri를 Speech-to-Text API의 input parameter에 설정까지 합니다.


두번째 코드 실행

이제 어느정도 코드가 완성단계에 이르렀다는 느낌을 받습니다.

5분짜리 짧은 영상을 input으로 넣고 프로그램을 실행해봤습니다. 프로그램 실행 로그가 출력되면서 영상에서 음성을 분리하고 음성에서 텍스트를 추출하고 추출된 것을 한글로 번역하는 과정까지 그리고 마지막으로 번역된 한글 자막을 영상에 입히는 것까지 전부 오류 로그 없이 수행되었습니다!

떨리는 마음으로 출력 파일(동영상)을 실행해봤습니다.

first result

일단 2가지 문제가 바로 눈에 들어옵니다.

  • ChatGPT 라는 단어가 Chachi PT로 번역되었습니다. 이것은 음성을 텍스트로 변환하는 과정에 문제가 있는 것 같습니다. 화자는 확실히 ChatGPT 라고 발음했습니다.
  • 자막이 너무 길게 화면에 출력 됩니다. 이것은 음성을 텍스트로 변환한 후에 결과값을 그대로 자막 데이터로 변환했기 때문입니다.

이 중에서 자막이 너무 길게 표시되는 것을 수정하기로 했습니다. 그래서 아래의 프롬프트를 추가로 입력했습니다. LLM에 좀 더 풍부한 Context를 제공하기 위해 이미지(위 이미지 그대로)를 채팅 인터페이스에 입력했습니다.

최종 결과물 영상 자막이 이상합니다.

자막이 이상한 이유는 아래와 같이 너무 긴 시간을 하나의 자막으로 처리하기 때문입니다. 사람이 편하게 읽을 수 있는 정도로 자막을 잘라서 처리하세요

LLM은 아래와 같이 답변합니다.

네, 자막을 더 읽기 쉽게 나누도록 하겠습니다.
보통 자막은 1-3초 정도의 길이로 나누는 것이 좋습니다.
음성 인식 결과를 처리할 때 세그먼트를 더 작은 단위로 나누는 로직을 추가하겠습니다.

그리고 splitSegments 라는 함수를 작성해주고는 원래 로직에서 이 함수를 호출하여 자막 길이를 기계적으로 자르도록 프로그램을 자동으로 수정합니다.


세번째 코드 실행

이제 세번째 프로그램 실행을 실행해봅니다.
이렇게 짧은 시간에 어느정도 동작하는 프로그램을 만들 수 있다는 게 놀랍습니다.

하지만, 세번째 코드 실행 결과에서도 약간의 아쉬움이 있었습니다.

3rd wrong...

영상의 거의 첫부분에 화자가 이런 문장을 말 합니다.
it is obviously magical and amazing in some respects, it's a really good at some things, not very good at other things and there's also a lot of sharp edges to be aware of.

한국어로는
그것은 분명히 마법처럼 놀라운 면이 있으며, 어떤 면에서는 정말 뛰어나지만, 다른 면에서는 그다지 좋지 않고, 조심해야 할 날카로운 부분도 많다. 라고 번역할 수 있을 것입니다.

문제는 음성에서 추출한 텍스트를 기계적으로 초 단위로 자르다보니 영상의 17초와 20초 사이의 Transcript가 'respects, it's a really good at some things, not very'로 설정된 것입니다. 그래서 이 문장만으로 영문에서 한국어로 번역이 이뤄졌고 최종 자막이 존경합니다.로 시작하게 된 것입니다. (respects => 존경합니다) 🙃

어떤 문제가 있는지 확실히 식별했으니 이제 또 하나의 프롬프트를 작성합니다. 이번에는 이미지가 아니라 음성에서 텍스트를 추출한 결과물인 transcription.json 파일을 input으로 지정했습니다.

@translate_video.ts @transcription.json

이 파일은 음성 -> 텍스트 변환 결과물입니다. 자세히 보면 하나의 문장이 중간에 끊겨 있는 것을 알 수 있습니다.

  1. how do I make this video for a while it
  2. is a comprehensive but generation audience intoduction

그렇기 때문에 결과적으로 최종 번역 품질이 좋지 않습니다. 음성 -> 텍스트 변환 로직을 개선해서 하나의 문장이 중간에 끊기지 않게 해봅시다.

LLM은 아래와 같이 답변합니다.

네, 말씀하신 대로 문장이 중간에 끊기면 번역 품질이 떨어지게 됩니다.
음성 인식 결과를 문장 단위로 재구성하도록 수정하겠습니다.

새로 groupWordsIntoSentences 라는 함수를 작성하여 기존의 기계적으로 문장을 초 단위로 자르는 함수를 대체하게 했습니다.

또 놀라운 것은 Speech-to-Text API를 수정하여 '자동 문장 부호 추가 활성화' 설정을 켰다는 것입니다. enableAutomaticPunctuation 설정은 Google Cloud Speech-to-Text API에서 자동으로 문장 부호(예: 마침표, 쉼표, 물음표 등)를 추가할지 여부를 결정하는 옵션입니다. 예를 들면, 다음과 같습니다.

  • 음성 입력: "Hello how are you I'm fine thank you"
  • 출력: "Hello, how are you? I'm fine, thank you."

네번째 코드 실행

점점 더 합리적으로 문장 데이터를 처리하게 되는 느낌입니다.

위에서 새로 만든 groupWordsIntoSentences 라는 함수와 새롭게 설정한 API 파라미터가 잘 작동하면 좋겠습니다.

이제 네번째 코드를 실행할 차례입니다.

4rd 1st image

4rd 2nd image

영상을 5분까지 시청했을 때, 내용을 이해하는데 크게 무리가 없는 수준입니다. 일부 단어 처리가 어색하거나 번역이 너무 직역으로 된 것 같은 문장들이 있긴 하지만 전체적으로 꽤 훌륭한 수준이라고 생각합니다.

맨 처음 프롬프트를 입력하고 이번 네번째 코드까지 실행하는데 대략 30분 정도 소요되었습니다. 영상 처리에 대해 전혀 문외한인 제가 몇 번의 프롬프트 만으로 쓸만한 영상 자동 번역기 프로그램을 만들게 된 것입니다


회고 (Retrospect)

프로그램 개발이 완료되었으니 회고를 해봅시다!

소요 비용

소요 비용에 대해서도 분석해보겠습니다.

GCP billing
  • Google Cloud Speech API
    • 975원
  • Google Cloud Storage
    • 0원
  • Google Translate
    • 6,936원
    • 매월 500,000자까지는 무료이기 때문에 전액 할인으로 실제 금액은 0원

이 금액은 개발 과정에서 테스트 하느라 API를 호출한 것과 3시간 넘는 원래 동영상을 처리하는 전체 과정에서 소요된 것을 포함합니다.

다만, 로직에 조금 문제가 있는지 비용이 과도하게 발생했습니다. 특히 Google Translate 비용이 조금 과도한 것 같습니다. 이 부분은 추후에 개선할 여지가 있겠습니다.

번역 품질 결과 비교 (vs YouTube 자동 번역)

YouTube에서 영상을 볼 때, 영상의 자막을 실시간으로 번역할 수 있습니다. 이 실시간 번역과 이번에 만든 자동 번역기를 통한 번역의 품질을 비교해보겠습니다.

Youtube vs translator - 1st
  • 위쪽이 YouTube에서 실시간 자막 번역 설정을 켠 상태입니다. Cloud 라는 단어를 구름이라고 번역합니다. 반면에 아래쪽 자동 번역기를 통한 자막은 큰 문제가 없습니다.
Youtube vs translator - 2nd
  • 마찬가지로 위쪽 YouTube 실시간 번역은 조금 어색합니다. 실시간으로 번역해서인지 문장을 잘게 나눠서 번역하는 느낌입니다. 아래쪽 자동 번역기는 어느 정도는 문장 단위로 인식하여 번역하고 있습니다.

전체적으로 YouTube의 실시간 자막 번역 품질보다는 자동 번역기 품질이 더 괜찮습니다. 다만 전체 영상을 보고 전부 비교한 것은 아니고 영상의 특정 시간대를 랜덤하게 골라서 비교해봤습니다.

느낀점

  1. 문제 인식의 주도권

    • 자막의 길이가 너무 길어서 화면을 가린다는 것을 제가 인지하여 해결책을 LLM에게 물었습니다. 그리고 나서는 또 문장이 잘려서 번역되기 때문에 최종 자막 품질이 좋지 않다는 것을 제가 인지했습니다. 그래서 그 부분을 해결해야 한다고 지정해서 프롬프트를 작성했습니다.
    • 이 2번의 과정 모두 사람이 문제를 인지하고 문제 해결의 방법을 LLM에게 알려주는 방식을 거쳤습니다.
    • 이것을 LLM이 스스로 하도록 할 수 있을까? 라는 생각을 하게 됩니다. 문제의 해결 방법뿐만 아니라 문제 자체를 LLM이 인식하는 것입니다.
    • LLM은 소스 코드에 문제가 있다는 것은 (특히 문법 오류) 매우 잘 인식합니다. 그것은 Linter 또는 IDE에서 문법 오류 로그를 실시간으로 출력하고 LLM이 그것을 곧바로 읽을 수 있기 때문입니다. 그리고 모델 학습 단계에서 수 많은 코드와 관련 문서가 포함되었기 때문일 것입니다.
    • '영상의 자막이 너무 길다 ➡️ 적당한 길이로 짧게 잘라야 한다' 라는 의식의 흐름을 LLM이 할 수 있을까요? 저는 이 부분이 궁금해졌습니다.
    • 이번에 이 프로그램을 작성하기위해 LLM을 사용한 방법은 문제 인식은 사람이 하고 해결 방법에 대해서만 LLM을 이용한 것입니다. 다음에는 문제의 인식 단계에서부터 LLM을 참여시켜봐야 겠다는 생각이 듭니다.
  2. 프롬프트의 중요성

    • 이 프로그램을 만들기 위해 제가 입력한 최초 프롬프트는 꽤 단순합니다.

      아래 요구사항을 만족하는 TypeScript 코드를 작성해봅시다.

      • 영상을 읽어서 해당 영상의 화자가 말 하는 언어(영어)를 한국어로 번역하여 한국어를 자막으로 달아서 새로운 영상을 만드는 것입니다.

      일단 위 요구사항을 만족시키기 위해 어떤 컨셉으로 코드를 구성해야 할지 구성 요소부터 논의해봅시다.

    • 만약 이 최초 프롬프트가 좀 더 풍부했더라면 어땠을까요?

      • input 영상의 길이가 3시간이 넘는다는 것
      • 자막의 길이는 너무 짧지도 않고 너무 길지도 않아서 사람이 읽기 편한 정도여야 한다는 것
    • 그러나 이러한 문제들은 최초에 제가 이 프로그램을 만들기 시작할 때는 생각하지 못했던 것들입니다

      • 너무 막연하게 생각했던 것이죠. "영어로 말 하는 영상을 한글 자막을 보고 싶다"
      • 이 때, 조금만 더 생각해서 내가 원하는 게 무엇인지 구체화 한 후에 프롬프트 작성을 시작했다면 훨씬 더 빨리 끝낼 수 있었을 거라는 생각을 합니다. 어쩌면 한방에 프로그램을 완성시켰을 수도 있습니다 🧙‍♂️
  3. LLM이 생성한 코드를 어느정도까지 읽어야 할까?

    • 이 문제도 한 번 생각해볼만한 것입니다. 프로그램을 완성시켜나가는 중간 중간 LLM이 작성한 수 많은 코드를 사람이 어느 정도까지 깊게 읽어가야 할 것인가 입니다.
    • 저는 프로그램을 만드는 과정에서 코드를 어느 정도는 읽어보려고 했습니다. 그래서 시간이 조금 더 소요되기는 했지만 말이죠. 코드가 적어도 어떤 구조로 작성되었고 어떤 라이브러리를 사용하고 있는지를 알고 있어야 문제가 있을 때 고칠 수 있을 것 같았습니다.
    • 한 줄 한 줄 단위로 깊게 읽을 것인지 아니면 아예 읽지도 않고 LLM에게 모든 것을 맡길 것인지는 사람의 선택입니다. 그리고 이것은 프로그램의 성격에 따라 달라질 것입니다.
    • 저는 적어도 동작의 원리를 파악할 정도는 코드를 읽어가며 진행해야 한다고 생각합니다.
  4. 어설프게 동작하는 프로그램

    • 작년에 싱가포르 노동법 기반의 RAG 시스템을 만들면서 LLM을 본격적으로 프로그래밍에 사용하기 시작했습니다. RAG 시스템 자체도 LLM을 사용하고 그 시스템 개발에도 LLM을 사용하면서 깨달았던 것이 하나 있습니다.
    • 어설프게 동작하는 프로그램은 정말 빠르게 만들 수 있다는 것입니다. 이 자동 번역기는 두 번의 프롬프트만으로 '동작'할 수 있도록 만들어졌습니다.
    • 초기 개념증명 과정에서 몇 가지 아이디어와 프롬프트만으로 '동작'하는 프로그램을 만들 수 있습니다. 하지만 이 정도는 아직 Production 단계에서는 사용하기 힘듭니다. 여기서 더 polishing 과정을 거쳐야 상용화할 수 있는 제품을 만들 수 있습니다.
    • 초기 LLM의 작업물 기반으로 LLM과 함께 그 작업물의 수준을 끌어올리기 위해서는 LLM이 작성한 코드를 '어느 정도'는 이해하면서 진행 해야 합니다.
    • 그래야 초기 작업물 기반으로 더 개선된 제품을 만들 수 있기 때문입니다.

이제 제가 원래 의도했던대로 Deep Dive into LLMs like ChatGPT 영상을 볼 준비가 되었습니다. 이 영상을 제가 얼마나 이해할 수 있을지 모르겠지만 본래 영어 영상일때보다는 이해를 더 많이 할 수 있지 않을까 기대됩니다.

Comments (0)

Checking login status...

No comments yet. Be the first to comment!