Week 4
Agentic AI Workflow
유튜브 채널 자동화 - 매일 자동으로 영상 생성 및 업로드
사전 준비사항
- 유튜브 채널 주제 구상해오기
- YouTube Studio에서 채널 생성하기
- Google Cloud Console에서 YouTube Data API v3 활성화
자동화 파이프라인 구조
전체 워크플로우
Gemini→ElevenLabs→Imagen→FFmpeg→YouTube API
| 단계 | 도구 | 역할 |
|---|---|---|
| 1. 스크립트 | Gemini 2.5 Pro | 2분 분량 영상 대본 생성 |
| 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으로 자동화 배포하기
- (도전) 중복 에피소드 방지 로직 구현하기