들어가며
지난 두 글에서 영상 생성 AI 시장 정리(Grok·Veo·Kling·Runway 4파전)와 Grok Imagine API 통합을 다뤘습니다. 그런데 실제로 운영해보면 알게 됩니다 — 영상 "생성"은 전체 파이프라인의 30%밖에 안 됩니다. 나머지 70%는 자막, 트랜지션, BGM, 워터마크 같은 후처리에 들어갑니다.
이 글은 AI가 생성한 짧은 클립들을 받아서 "바로 X·인스타·유튜브 숏츠에 올릴 수 있는 완성된 숏폼"으로 만드는 자동화 파이프라인을 다룹니다. 핵심 도구는 두 가지입니다.
- FFmpeg: 영상 합치기, 자막 burn-in, 워터마크, BGM 합성, 포맷 변환의 표준
- OpenAI Whisper / GPT-4o Transcribe: 음성을 자막으로 변환, $0.003~$0.006/분의 합리적 단가
이 둘만으로 "AI 영상 클립 → 완성된 숏폼" 변환을 풀-자동화할 수 있습니다. Node.js와 Spring Boot 모두 다루며, 실제 운영에서 부딪히는 함정도 함께 정리합니다.
1. 전체 파이프라인 구성도
최종 워크플로우는 다음과 같습니다.
[1] AI 영상 클립 N개
↓ (FFmpeg)
[2] 클립별 오디오 추출 (.wav)
↓ (Whisper API)
[3] 자막 생성 (.srt 또는 .ass)
↓ (FFmpeg)
[4] 자막 burn-in + 클립 스티칭
↓ (FFmpeg)
[5] BGM 합성 + 워터마크 + 페이드인/아웃
↓
[6] 최종 .mp4 (X/Instagram/YouTube Shorts 호환)
이 전체를 한 번의 스크립트 실행으로 끝나도록 만드는 것이 목표입니다.
2. 1단계 - 클립별 오디오 추출
Whisper에 영상을 직접 넣을 수도 있지만, 영상은 용량이 크고 처리 시간이 느립니다. 오디오만 분리해서 보내는 게 비용·속도 면에서 압도적으로 유리합니다.
FFmpeg 명령어
# clip1.mp4 → clip1.wav (16kHz mono - Whisper 권장 포맷)
ffmpeg -i clip1.mp4 \
-vn \ # 비디오 제거
-ac 1 \ # 모노
-ar 16000 \ # 16kHz 샘플링
-c:a pcm_s16le \ # 16-bit PCM
clip1.wav
16kHz 모노 PCM은 Whisper가 가장 잘 처리하는 포맷입니다. 44.1kHz 스테레오 그대로 보내면 용량만 5~6배 커지고 정확도는 그대로입니다.
Node.js로 일괄 추출
// extract-audio.js
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { readdir } from 'node:fs/promises';
const run = promisify(execFile);
async function extractAudio(videoPath) {
const audioPath = videoPath.replace(/\.mp4$/, '.wav');
await run('ffmpeg', [
'-y', '-i', videoPath,
'-vn', '-ac', '1', '-ar', '16000',
'-c:a', 'pcm_s16le',
audioPath
]);
return audioPath;
}
// clips/ 디렉터리 일괄 처리
const files = (await readdir('clips/')).filter(f => f.endsWith('.mp4'));
const audioPaths = await Promise.all(
files.map(f => extractAudio(`clips/${f}`))
);
console.log('Extracted:', audioPaths);
3. 2단계 - Whisper로 자막 생성
2026년 4월 기준, OpenAI 음성→텍스트 옵션은 세 가지입니다.
| 모델 | 가격 | 특징 |
|---|---|---|
whisper-1 |
$0.006/분 | 레거시, 99개 언어, srt/vtt 직접 출력 |
gpt-4o-transcribe |
$0.006/분 | WER 개선, 화자 분리 옵션 포함 |
gpt-4o-mini-transcribe |
$0.003/분 | 최저가, 짧은 클립에 충분한 정확도 |
10초 클립 처리 비용은 $0.001 미만입니다. 예산 걱정은 사실상 없습니다.
SRT 자막 직접 받기
// transcribe.js
import OpenAI from 'openai';
import { createReadStream } from 'node:fs';
import { writeFile } from 'node:fs/promises';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function transcribeToSrt(audioPath) {
const response = await openai.audio.transcriptions.create({
file: createReadStream(audioPath),
model: 'gpt-4o-mini-transcribe',
response_format: 'srt',
language: 'ko' // 명시하면 정확도 상승
});
const srtPath = audioPath.replace(/\.wav$/, '.srt');
await writeFile(srtPath, response);
return srtPath;
}
SRT 출력 예시
1
00:00:00,000 --> 00:00:02,500
신제품 헤드폰을 360도 보여드립니다
2
00:00:02,500 --> 00:00:05,000
가벼운 무게에 강력한 노이즈 캔슬링까지
3
00:00:05,000 --> 00:00:08,000
오늘부터 30% 할인 시작합니다
단어 단위 타임스탬프가 필요할 때
SRT 기본 출력은 segment 단위(보통 2~5초)입니다. 카라오케 스타일로 단어 한 글자씩 강조하고 싶다면 verbose_json으로 받아서 직접 SRT/ASS를 생성합니다.
const detailed = await openai.audio.transcriptions.create({
file: createReadStream(audioPath),
model: 'whisper-1',
response_format: 'verbose_json',
timestamp_granularities: ['word', 'segment'],
language: 'ko'
});
// detailed.words = [{ word, start, end }, ...]
// 이걸로 ASS 파일을 직접 빌드하면 단어별 컬러 강조 가능
4. 3단계 - 자막 Burn-in
SRT 파일이 준비됐으면 영상에 "태우는(burn-in)" 단계입니다. "소프트 자막(별도 트랙)"과 "하드 자막(영상에 합성)"을 구분해야 합니다.
| 방식 | 장점 | 단점 | 추천 상황 |
|---|---|---|---|
| 소프트 자막 (mov_text) | 껐다 켤 수 있음, 다국어 가능 | 플랫폼 호환성 낮음 (Insta/X 미지원) | YouTube, Vimeo |
| 하드 자막 (burn-in) | 모든 플랫폼 호환, 디자인 자유 | 다국어 불가, 재인코딩 필요 | SNS 숏폼 (Insta·X·TikTok) |
SNS 숏폼은 거의 100% 하드 자막입니다.
기본 Burn-in
ffmpeg -i clip1.mp4 \
-vf "subtitles=clip1.srt" \
-c:a copy \
clip1_subtitled.mp4
주의: -c:v copy는 절대 사용 불가입니다. 자막 burn-in은 비디오 프레임을 다시 그려야 하므로 재인코딩이 필수입니다.
스타일 적용 (force_style)
ffmpeg -i clip1.mp4 \
-vf "subtitles=clip1.srt:force_style='\
FontName=Pretendard,\
Fontsize=22,\
PrimaryColour=&H00FFFFFF,\
OutlineColour=&H00000000,\
BackColour=&HA0000000,\
BorderStyle=4,\
Outline=2,\
MarginV=50'" \
-c:a copy \
clip1_styled.mp4
색상은 &HAABBGGRR 형태(ASS 표준)입니다. 일반 RGB와 순서가 반대(BGR)인 점에 유의하세요. 흰 글씨에 검정 외곽선 + 반투명 배경박스가 SNS 숏폼의 표준 스타일입니다.
9:16 세로형 영상에서 자막 위치
MarginV 값으로 하단 여백을 조정합니다. 인스타그램/틱톡 UI(좋아요/공유 버튼)에 가려지지 않도록 MarginV=180 정도로 하단을 띄우는 게 안전합니다.
5. 4단계 - 클립 스티칭 + 트랜지션
Grok Imagine 한 클립의 최대 길이는 10초입니다. 30초 콘텐츠를 만들려면 3~4개 클립을 자연스럽게 이어붙여야 합니다.
단순 연결 (concat demuxer)
# clips.txt 파일 생성
cat > clips.txt << EOF
file 'clip1_subtitled.mp4'
file 'clip2_subtitled.mp4'
file 'clip3_subtitled.mp4'
EOF
# 무손실 연결 (모든 클립이 같은 코덱·해상도일 때만)
ffmpeg -f concat -safe 0 -i clips.txt -c copy combined.mp4
크로스페이드 트랜지션 (xfade)
단순 연결은 끊김이 보입니다. xfade 필터로 부드럽게 이어붙입니다.
# 8초 클립 3개를 0.5초 페이드로 연결 → 총 23.5초
ffmpeg \
-i clip1.mp4 -i clip2.mp4 -i clip3.mp4 \
-filter_complex "
[0][1]xfade=transition=fade:duration=0.5:offset=7.5[v01];
[v01][2]xfade=transition=fade:duration=0.5:offset=15[vout];
[0:a][1:a]acrossfade=d=0.5[a01];
[a01][2:a]acrossfade=d=0.5[aout]
" \
-map "[vout]" -map "[aout]" \
combined.mp4
offset 값은 "앞 클립의 어느 지점부터 페이드를 시작할지"입니다. 8초 클립이라면 7.5초에 페이드 시작 → 0.5초 페이드 후 8초에 다음 클립 시작됩니다.
다양한 트랜지션
| transition 값 | 효과 |
|---|---|
fade |
크로스페이드 (가장 안전) |
wipeleft / wiperight |
좌우 와이프 |
slideup / slidedown |
슬라이드 |
circleopen / circleclose |
원형 마스크 |
pixelize |
픽셀화 후 다음 컷 |
6. 5단계 - BGM, 워터마크, 페이드
마지막 단계는 "포장"입니다. 콘텐츠 톤에 맞는 BGM과 브랜드 워터마크를 합성합니다.
BGM 합성 (원본 음성 유지)
# 원본 보이스 100%, BGM 25%로 믹스
ffmpeg \
-i combined.mp4 \
-i bgm.mp3 \
-filter_complex "
[0:a]volume=1.0[a0];
[1:a]volume=0.25,aloop=loop=-1:size=2e+09[a1];
[a0][a1]amix=inputs=2:duration=first[aout]
" \
-map 0:v -map "[aout]" \
-c:v copy -c:a aac -b:a 192k \
with_bgm.mp4
핵심 포인트:
aloop=loop=-1: BGM이 영상보다 짧으면 무한 반복amix duration=first: 영상 길이에 맞춰 자르기 (BGM 끝까지 안 가도 됨)- BGM 볼륨은 0.2~0.3이 표준 (그 이상이면 음성을 가림)
워터마크 (PNG 오버레이)
# 우상단 코너에 로고 삽입
ffmpeg \
-i with_bgm.mp4 \
-i logo.png \
-filter_complex "[1:v]scale=120:-1[wm];[0:v][wm]overlay=W-w-20:20" \
-c:a copy \
with_logo.mp4
W-w-20:20은 "비디오 너비에서 워터마크 너비를 빼고 20픽셀 안쪽, 위에서 20픽셀" — 우상단 위치입니다. 좌하단은 20:H-h-20.
페이드인/아웃
# 처음 0.5초 페이드인, 끝 0.5초 페이드아웃 (총 길이 23.5초 가정)
ffmpeg \
-i with_logo.mp4 \
-vf "fade=t=in:st=0:d=0.5,fade=t=out:st=23:d=0.5" \
-af "afade=t=in:st=0:d=0.5,afade=t=out:st=23:d=0.5" \
-c:v libx264 -preset fast -crf 23 \
-c:a aac -b:a 192k \
final.mp4
7. 통합 파이프라인 - Node.js
지금까지의 단계를 하나의 클래스로 묶습니다.
// pipeline.js
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { writeFile, mkdir } from 'node:fs/promises';
import { createReadStream } from 'node:fs';
import OpenAI from 'openai';
import path from 'node:path';
const run = promisify(execFile);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export class ShortFormBuilder {
constructor(workDir = './work') {
this.workDir = workDir;
}
async build({ clips, bgm, logo, outputPath, transition = 'fade' }) {
await mkdir(this.workDir, { recursive: true });
// 1) 클립별 자막 burn-in
const subtitled = [];
for (let i = 0; i < clips.length; i++) {
const audioPath = await this.extractAudio(clips[i], i);
const srtPath = await this.transcribe(audioPath, i);
const out = path.join(this.workDir, `clip_${i}_sub.mp4`);
await this.burnSubtitles(clips[i], srtPath, out);
subtitled.push(out);
}
// 2) 클립 스티칭
const combined = path.join(this.workDir, 'combined.mp4');
await this.stitch(subtitled, combined, transition);
// 3) BGM 합성
const withBgm = path.join(this.workDir, 'with_bgm.mp4');
await this.addBgm(combined, bgm, withBgm);
// 4) 워터마크
const withLogo = path.join(this.workDir, 'with_logo.mp4');
await this.addWatermark(withBgm, logo, withLogo);
// 5) 페이드 + 최종 인코딩
const totalDuration = clips.length * 8 - (clips.length - 1) * 0.5;
await this.finalize(withLogo, outputPath, totalDuration);
return outputPath;
}
async extractAudio(videoPath, idx) {
const out = path.join(this.workDir, `audio_${idx}.wav`);
await run('ffmpeg', ['-y', '-i', videoPath, '-vn', '-ac', '1',
'-ar', '16000', '-c:a', 'pcm_s16le', out]);
return out;
}
async transcribe(audioPath, idx) {
const srt = await openai.audio.transcriptions.create({
file: createReadStream(audioPath),
model: 'gpt-4o-mini-transcribe',
response_format: 'srt',
language: 'ko'
});
const srtPath = path.join(this.workDir, `sub_${idx}.srt`);
await writeFile(srtPath, srt);
return srtPath;
}
async burnSubtitles(videoPath, srtPath, outPath) {
const style = "FontName=Pretendard,Fontsize=22,PrimaryColour=&H00FFFFFF," +
"OutlineColour=&H00000000,BackColour=&HA0000000,BorderStyle=4," +
"Outline=2,MarginV=180";
await run('ffmpeg', ['-y', '-i', videoPath,
'-vf', `subtitles=${srtPath}:force_style='${style}'`,
'-c:a', 'copy', outPath]);
}
async stitch(clips, outPath, transition) {
// 클립이 모두 8초라고 가정. 실 운영에서는 ffprobe로 길이 측정 필요
const inputs = clips.flatMap(c => ['-i', c]);
const filters = [];
let last = '0';
for (let i = 1; i < clips.length; i++) {
const offset = i * 7.5;
filters.push(`[${last}][${i}]xfade=transition=${transition}:duration=0.5:offset=${offset}[v${i}]`);
last = `v${i}`;
}
await run('ffmpeg', ['-y', ...inputs,
'-filter_complex', filters.join(';'),
'-map', `[${last}]`, outPath]);
}
async addBgm(videoPath, bgmPath, outPath) {
await run('ffmpeg', ['-y', '-i', videoPath, '-i', bgmPath,
'-filter_complex',
'[0:a]volume=1.0[a0];[1:a]volume=0.25,aloop=loop=-1:size=2e+09[a1];' +
'[a0][a1]amix=inputs=2:duration=first[aout]',
'-map', '0:v', '-map', '[aout]',
'-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', outPath]);
}
async addWatermark(videoPath, logoPath, outPath) {
await run('ffmpeg', ['-y', '-i', videoPath, '-i', logoPath,
'-filter_complex', '[1:v]scale=120:-1[wm];[0:v][wm]overlay=W-w-20:20',
'-c:a', 'copy', outPath]);
}
async finalize(videoPath, outPath, duration) {
const fadeOutStart = duration - 0.5;
await run('ffmpeg', ['-y', '-i', videoPath,
'-vf', `fade=t=in:st=0:d=0.5,fade=t=out:st=${fadeOutStart}:d=0.5`,
'-af', `afade=t=in:st=0:d=0.5,afade=t=out:st=${fadeOutStart}:d=0.5`,
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
'-c:a', 'aac', '-b:a', '192k', outPath]);
}
}
// 사용
const builder = new ShortFormBuilder();
await builder.build({
clips: ['raw/clip1.mp4', 'raw/clip2.mp4', 'raw/clip3.mp4'],
bgm: 'assets/upbeat.mp3',
logo: 'assets/logo.png',
outputPath: 'final.mp4',
transition: 'fade'
});
console.log('Done: final.mp4');
8. Spring Boot 통합
Java 서비스에서는 ProcessBuilder로 FFmpeg를 호출하고, OpenAI Java SDK로 Whisper를 사용합니다.
@Service
@RequiredArgsConstructor
public class ShortFormService {
private final OpenAiClient openAi; // 자체 래퍼 또는 community SDK
public Path build(BuildRequest req) throws Exception {
Path workDir = Files.createTempDirectory("shortform-");
List<Path> subtitled = new ArrayList<>();
for (int i = 0; i < req.clips().size(); i++) {
Path clip = req.clips().get(i);
Path audio = extractAudio(clip, workDir, i);
Path srt = transcribe(audio, workDir, i);
Path out = workDir.resolve("clip_" + i + "_sub.mp4");
burnSubtitles(clip, srt, out);
subtitled.add(out);
}
Path combined = workDir.resolve("combined.mp4");
stitch(subtitled, combined, req.transition());
Path withBgm = workDir.resolve("with_bgm.mp4");
addBgm(combined, req.bgm(), withBgm);
finalize(withBgm, req.output());
return req.output();
}
private Path extractAudio(Path video, Path workDir, int idx) throws Exception {
Path out = workDir.resolve("audio_" + idx + ".wav");
runFfmpeg("-y", "-i", video.toString(),
"-vn", "-ac", "1", "-ar", "16000",
"-c:a", "pcm_s16le", out.toString());
return out;
}
private Path transcribe(Path audio, Path workDir, int idx) throws Exception {
String srt = openAi.transcribeToSrt(audio, "gpt-4o-mini-transcribe", "ko");
Path srtPath = workDir.resolve("sub_" + idx + ".srt");
Files.writeString(srtPath, srt);
return srtPath;
}
private void runFfmpeg(String... args) throws Exception {
List<String> cmd = new ArrayList<>();
cmd.add("ffmpeg");
cmd.addAll(Arrays.asList(args));
Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start();
if (p.waitFor() != 0) {
String err = new String(p.getInputStream().readAllBytes());
throw new IllegalStateException("FFmpeg failed: " + err);
}
}
public record BuildRequest(List<Path> clips, Path bgm, Path logo,
Path output, String transition) {}
}
9. 비용·성능 최적화
(1) Whisper 비용 - $0.003/분으로 충분한 경우가 많다
10초 클립 30개를 처리해도 5분 분량이라 $0.015입니다. 단, 영상이 시간당 100개를 넘기는 대량 처리에서는 self-hosted Whisper(faster-whisper)를 고려하세요. GPU 1장이면 시간당 3시간 분량을 처리할 수 있습니다.
(2) FFmpeg 인코딩 속도
| preset | 속도 | 용량 | 품질 | 추천 상황 |
|---|---|---|---|---|
ultrafast |
매우 빠름 | 큼 | 낮음 | 중간 결과물, 미리보기 |
fast |
빠름 | 중간 | 좋음 | 최종 결과물 표준 |
medium |
보통 | 작음 | 매우 좋음 | 품질 우선 환경 |
slow |
느림 | 가장 작음 | 최고 | 아카이브용 |
SNS 숏폼은 -preset fast -crf 23이 표준입니다. 30초 영상이라면 보통 5~10초에 인코딩이 끝납니다.
(3) 병렬 처리
클립별 자막 생성은 독립 작업이므로 병렬 처리할 수 있습니다. Node.js에서는 Promise.all, Spring Boot에서는 CompletableFuture를 활용하세요. 단, OpenAI API rate limit(분당 50회)에 주의해야 합니다.
// 클립 10개를 동시 자막 생성 (rate limit 안 넘기기 위해 chunk)
const chunks = chunk(clips, 5);
for (const c of chunks) {
await Promise.all(c.map(transcribe));
await sleep(1000); // 분당 50회 페이스 유지
}
10. 흔한 함정과 해결법
(1) "Pretendard 폰트가 자막에 안 적용돼요"
FFmpeg는 OS의 fontconfig를 통해 폰트를 찾습니다. macOS는 ~/Library/Fonts/에, Linux는 ~/.fonts/에 폰트를 설치해야 합니다. Docker 컨테이너에서는 다음 패키지가 필요합니다.
# Dockerfile (debian/ubuntu 기반)
RUN apt-get update && apt-get install -y \
ffmpeg \
fontconfig \
fonts-noto-cjk
COPY ./Pretendard-Bold.ttf /usr/share/fonts/truetype/
RUN fc-cache -fv
(2) "클립 길이가 제각각이라 stitch가 깨져요"
위 예제는 "모든 클립 8초"를 가정했습니다. 실 운영에서는 ffprobe로 각 클립 길이를 먼저 측정해서 offset을 동적으로 계산해야 합니다.
async function getDuration(videoPath) {
const { stdout } = await run('ffprobe', [
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
videoPath
]);
return parseFloat(stdout.trim());
}
// 사용
const durations = await Promise.all(clips.map(getDuration));
let cumOffset = 0;
const offsets = durations.slice(0, -1).map(d => cumOffset += d - 0.5);
(3) "모바일에서 영상이 거꾸로 재생돼요"
일부 카메라/AI 모델은 메타데이터 회전 정보(rotate)를 포함합니다. 호환성을 위해 메타데이터 회전을 "실제 픽셀 회전"으로 변환해야 합니다.
ffmpeg -i input.mp4 -metadata:s:v:0 rotate=0 -c copy fixed.mp4
# 또는 실제 회전 적용
ffmpeg -i input.mp4 -vf "transpose=2" output.mp4
(4) "인스타그램에서 음성이 모노로 나와요"
인스타그램 리믹스/숏폼은 스테레오를 권장합니다. 최종 단계에서 모노→스테레오 변환을 추가하세요.
-af "pan=stereo|c0=c0|c1=c0" # 모노를 양쪽 채널에 복제
(5) "SRT 자막에 한글이 깨져요"
SRT 파일을 UTF-8 BOM 없이 저장해야 합니다. Node.js의 writeFile은 기본 UTF-8이라 문제없지만, Windows 환경에서 메모장으로 편집하면 BOM이 붙을 수 있습니다. iconv-lite로 명시적으로 처리하세요.
11. 출력 포맷별 권장 설정
플랫폼별 표준 사양 정리입니다.
| 플랫폼 | 비율 | 해상도 | 최대 길이 | 비트레이트 |
|---|---|---|---|---|
| Instagram Reels | 9:16 | 1080×1920 | 90초 | 3~5 Mbps |
| YouTube Shorts | 9:16 | 1080×1920 | 60초 | 4~8 Mbps |
| TikTok | 9:16 | 1080×1920 | 3분 | 4~6 Mbps |
| X (트위터) | 16:9 또는 9:16 | 1280×720+ | 2분 20초 | 5 Mbps 권장 |
| 1:1 또는 16:9 | 1920×1080 | 10분 | 5~10 Mbps |
Grok Imagine은 720p 출력이라 1080p로 업스케일이 필요합니다. scale=1080:1920:flags=lanczos 필터를 마지막 인코딩에 추가하면 됩니다.
마치며
AI 영상 후처리 자동화의 핵심 정리:
- FFmpeg + Whisper 두 도구로 90% 끝납니다. 자막 생성·burn-in·스티칭·BGM·워터마크·페이드까지 — 모두 명령어 한 줄씩의 조합입니다. CapCut 같은 GUI 도구는 "한 번 만들기"엔 좋지만, "매일 100개 생성"엔 자동화가 답입니다.
- 비용은 거의 무시 가능합니다.
gpt-4o-mini-transcribe기준 10초 클립 자막 생성은 $0.0005입니다. 1만 개 만들어도 $5. 실질 비용은 영상 생성(Grok Imagine 분당 $4.20)에 묶여 있습니다. - SNS 숏폼은 하드 자막이 표준입니다. Instagram·X·TikTok 모두 소프트 자막을 거의 무시합니다. burn-in이 강제 사항이라 인코딩 비용은 어쩔 수 없습니다.
- Docker 컨테이너의 폰트 함정을 잊지 마세요.
fc-cache를 안 돌리면 한글이 □□□로 나옵니다. CI/CD에 자주 걸리는 함정입니다. - 병렬 처리 + Rate Limit: Whisper API 분당 50회 한도. 50개 이상 동시 처리하려면 chunk 기반 페이싱이 필요합니다. 또는 self-hosted faster-whisper로 우회.
이 시리즈가 영상 AI 시장 정리(1편) → Grok 단독 통합(2편) → 후처리 자동화(3편)로 이어졌습니다. 다음 글에서는 이 전체 파이프라인을 GitHub Actions + 스케줄 트리거로 완전 무인 운영하는 방법을 다루겠습니다. 매주 월요일 오전 10시에 자동으로 영상이 생성되고, 자막이 붙고, BGM이 합성되고, X에 게시되는 "잠자는 동안 콘텐츠가 만들어지는" 워크플로우를 코드로 보여드릴 예정입니다.
'최신 트렌드' 카테고리의 다른 글
| AI 콘텐츠 자기 학습 루프 - X 메트릭을 GPT에 피드백해 다음 주 콘텐츠를 자동 개선하는 시스템 (1) | 2026.04.30 |
|---|---|
| AI 영상 콘텐츠 완전 무인 자동화 - GitHub Actions + cron으로 매주 월요일 X에 자동 게시 (1) | 2026.04.30 |
| Grok Imagine 완전 정복 - xAI API로 영상 생성부터 X 자동 게시까지 (1) | 2026.04.29 |
| 2026년 4월 영상 생성 AI 총정리 - Sora 2 종료 후 Grok·Veo·Kling·Runway 4파전 (0) | 2026.04.29 |
| 플랫폼 엔지니어링·내부 개발자 플랫폼(IDP) 실전 - Backstage·Port·Humanitec로 셀프서비스 포털 구축하기 (1) | 2026.04.27 |