Week 4

Agentic AI Workflow

유튜브 채널 자동화 - 매일 자동으로 영상 생성 및 업로드

사전 준비사항

  • 유튜브 채널 주제 구상해오기
  • YouTube Studio에서 채널 생성하기
  • Google Cloud Console에서 YouTube Data API v3 활성화

실제 운영 사례: 역사의 목격자

매일 아침 자동으로 2분짜리 역사 쇼츠를 생성하고 YouTube에 업로드

@HistoryByWitness 채널 보기 →

자동화 파이프라인 구조

전체 워크플로우

GeminiElevenLabsImagenFFmpegYouTube API
단계도구역할
1. 스크립트Gemini 2.5 Pro2분 분량 영상 대본 생성
2. 음성ElevenLabs eleven_v3내레이션 음성 합성
3. 이미지Imagen 3장면별 이미지 5-8장 생성
4. 비디오FFmpeg이미지 + 오디오 조립 (Ken Burns 효과)
5. 업로드YouTube API v3자동 업로드 + 메타데이터

Step 1: 스크립트 생성

Gemini로 쇼츠 대본 생성

프롬프트 예시

"당신은 역사 다큐멘터리 작가입니다. '포레스트 검프' 스타일로, 역사적 순간에 실제로 있었던 평범한 사람의 시선에서 이야기를 들려주세요. 오늘의 주제: 1969년 7월 20일 아폴로 11호 달 착륙. 2분 분량, 5개 장면으로 구성해주세요. 각 장면은 [장면 N: 설명]과 내레이션으로 구분해주세요."

import { GoogleGenAI } from '@google/genai';

async function generateScript(topic: string) {
  const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

  const result = await ai.models.generateContent({
    model: 'gemini-2.5-pro',
    contents: [{
      role: 'user',
      parts: [{ text: `
        당신은 역사 다큐멘터리 작가입니다.
        '포레스트 검프' 스타일로 역사적 순간을 평범한 사람의 시선에서 전해주세요.

        주제: ${topic}
        길이: 2분 (약 300단어)
        형식: 5개 장면, 각 장면에 [장면 N: 이미지 설명]과 내레이션 포함

        JSON 형식으로 반환:
        {
          "title": "영상 제목",
          "scenes": [
            { "sceneNumber": 1, "imagePrompt": "이미지 생성 프롬프트", "narration": "내레이션" }
          ]
        }
      ` }]
    }],
  });

  return JSON.parse(result.response.text());
}

Step 2: 음성 생성 (ElevenLabs)

TTS 음성 합성

import ElevenLabs from 'elevenlabs';

const client = new ElevenLabs({ apiKey: process.env.ELEVENLABS_API_KEY });

async function generateNarration(text: string) {
  const audio = await client.textToSpeech.convert({
    voice_id: 'pNInz6obpgDQGcFmaJgB', // Adam (한국어 지원)
    model_id: 'eleven_v3', // 최신 다국어 모델
    text: text,
    voice_settings: {
      stability: 0.5,
      similarity_boost: 0.75,
    },
  });

  // audio는 ReadableStream
  const chunks = [];
  for await (const chunk of audio) {
    chunks.push(chunk);
  }
  return Buffer.concat(chunks);
}
비용 절약 팁: ElevenLabs는 문자 수 기준 과금됩니다. 2분 영상 (약 300단어)은 약 1,500자로, 월 10,000자 무료 티어 내에서 6-7개 영상 제작 가능.

Step 3: 이미지 생성 (Imagen)

장면별 이미지 생성

async function generateSceneImage(prompt: string) {
  const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });

  const result = await ai.models.generateImages({
    model: 'imagen-3.0-generate-002',
    prompt: `Historical illustration, cinematic style: ${prompt}`,
    config: {
      numberOfImages: 1,
      aspectRatio: '9:16', // 쇼츠용 세로 화면
    },
  });

  // base64 이미지 반환
  return result.generatedImages[0].image.imageBytes;
}

Step 4: 비디오 조립 (FFmpeg)

Ken Burns 효과로 비디오 생성

import ffmpeg from 'fluent-ffmpeg';

async function assembleVideo(images: string[], audioPath: string, outputPath: string) {
  // 각 이미지에 Ken Burns 효과 (줌/팬) 적용
  const duration = 24; // 2분 = 120초, 5장면 = 각 24초

  return new Promise((resolve, reject) => {
    let command = ffmpeg();

    // 각 이미지 입력 (Ken Burns 효과)
    images.forEach((img, i) => {
      command = command
        .input(img)
        .inputOptions(['-loop 1', `-t ${duration}`]);
    });

    command
      .input(audioPath) // 오디오 입력
      .complexFilter([
        // 각 이미지에 줌인 효과
        ...images.map((_, i) =>
          `[${i}:v]scale=1920:1080,zoompan=z='min(zoom+0.001,1.2)':d=${duration * 30}:s=1080x1920[${i}v]`
        ),
        // 모든 비디오 연결
        `[${images.map((_, i) => `${i}v`).join('][')}]concat=n=${images.length}:v=1:a=0[outv]`,
      ])
      .outputOptions([
        '-map [outv]',
        '-map 5:a', // 오디오 (마지막 입력)
        '-c:v libx264',
        '-c:a aac',
        '-shortest',
      ])
      .output(outputPath)
      .on('end', resolve)
      .on('error', reject)
      .run();
  });
}

Step 5: YouTube 업로드

YouTube Data API v3 연동

import { google } from 'googleapis';
import * as fs from 'fs';

async function uploadToYouTube(videoPath: string, metadata: VideoMetadata) {
  const oauth2Client = new google.auth.OAuth2(
    process.env.YOUTUBE_CLIENT_ID,
    process.env.YOUTUBE_CLIENT_SECRET,
  );

  oauth2Client.setCredentials({
    refresh_token: process.env.YOUTUBE_REFRESH_TOKEN,
  });

  const youtube = google.youtube({ version: 'v3', auth: oauth2Client });

  const response = await youtube.videos.insert({
    part: ['snippet', 'status'],
    requestBody: {
      snippet: {
        title: metadata.title,
        description: `${metadata.description}\n\n#역사 #쇼츠 #AI생성`,
        tags: ['역사', '쇼츠', 'shorts', 'AI', '역사의목격자'],
        categoryId: '27', // Education
      },
      status: {
        privacyStatus: 'public',
        selfDeclaredMadeForKids: false,
      },
    },
    media: {
      body: fs.createReadStream(videoPath),
    },
  });

  return response.data.id; // 업로드된 영상 ID
}
YouTube API 할당량: 일일 10,000 유닛. 영상 업로드는 약 1,600 유닛 사용. 하루 6개 정도 업로드 가능.

전체 자동화: Cloud Functions

매일 자동 실행 설정

import { onSchedule } from 'firebase-functions/v2/scheduler';

// 매일 아침 6시에 실행 (한국 시간)
export const dailyYouTubeShorts = onSchedule(
  {
    schedule: '0 6 * * *',
    timeZone: 'Asia/Seoul',
    memory: '2GiB', // 비디오 처리용 메모리 증가
    timeoutSeconds: 540, // 9분 타임아웃
  },
  async (event) => {
    // 1. 오늘의 역사 주제 선택 (중복 방지)
    const topic = await selectTodaysTopic();

    // 2. 스크립트 생성
    const script = await generateScript(topic);

    // 3. 음성 생성
    const audioBuffer = await generateNarration(script.fullNarration);

    // 4. 이미지 생성
    const images = await Promise.all(
      script.scenes.map(scene => generateSceneImage(scene.imagePrompt))
    );

    // 5. 비디오 조립
    const videoPath = await assembleVideo(images, audioBuffer);

    // 6. YouTube 업로드
    const videoId = await uploadToYouTube(videoPath, {
      title: script.title,
      description: script.description,
    });

    // 7. Firestore에 기록 (중복 방지용)
    await saveEpisodeRecord(topic, videoId);

    console.log(`Uploaded: https://youtube.com/shorts/${videoId}`);
  }
);

이번 주 과제

  • 유튜브 채널 생성하고 주제 결정하기
  • 파이프라인 각 단계별로 테스트 실행하기
  • 첫 번째 쇼츠 수동 업로드해보기
  • Cloud Function으로 자동화 배포하기
  • (도전) 중복 에피소드 방지 로직 구현하기
← 3주차로5주차로 →