
動画AI・LLM開発|ffmpegを使った動画チャンク(chunking)の落とし穴
動画LLM開発で発生する時間精度問題とその解決策。NTSC 29.97fpsでのフレーム単位分割手法により、Premiere Proで正確に表示される60秒チャンクを実現する方法を解説します。
公開日2025.12.30
更新日2025.12.30
はじめに
動画AI・LLM開発では、動画を一定時間のチャンクに分割する処理が必須です。しかし、単純な時間ベース分割では予想外の問題が発生します。
実際に発生した問題の例
- 60秒で分割したはずが、54.40秒、2.72秒、60.01秒のバラバラな長さになる
- Premiere Proで54:10、59:28と表示され、期待する1:00:00にならない
- LLMへの時間情報が不正確になり、分析精度が低下する
この記事では、動画チャンク分割の落とし穴と、フレーム単位による解決策を実装レベルで解説します。
目次
対象読者
- 動画AI・LLMアプリケーションを開発している方
- ffmpegを使った動画処理に課題を感じている方
- 動画編集ソフトとの互換性を重視する方
前提知識
- JavaScriptまたはTypeScriptの基本的な理解
- ffmpegの基本的な使用経験
- 動画のフレームレートに関する基本知識
動画の内部構造とキーフレーム
動画チャンク分割の問題を理解するには、まず動画の内部構造を知る必要があります。
キーフレーム(Iフレーム)とは
動画ファイルは、効率的な圧縮のために以下の3種類のフレームで構成されています:
🔧 キーフレーム(Iフレーム)
- 完全な画像情報を持つフレーム
- 他のフレームに依存せず、単独で画像を再構成可能
- ファイルサイズが大きいため、1〜2秒間隔で配置
📊 予測フレーム(Pフレーム)
- 前のフレームとの差分情報のみ保持
- キーフレームやPフレームを参照して画像を構成
🎯 双予測フレーム(Bフレーム)
- 前後のフレームを参照して差分を記録
- 最も効率的な圧縮が可能
GOP(Group of Pictures)構造
これらのフレームは「GOP」という単位でグループ化されています:

GOP構造の重要なポイント
- 各GOPはキーフレーム(I)から開始
- GOP内のフレームは相互依存関係がある
- 動画カットはキーフレーム位置でのみ正確に実行可能
時間ベース分割で発生する問題
上記のように、指定した時間位置にキーフレームが存在しない場合:
❌ 問題1: 不正確なカット位置
指定時間: 60.00秒
実際のキーフレーム: 59.73秒 または 60.27秒
結果: 59.73秒 または 60.27秒でカット❌ 問題2: 依存フレームの欠損
GOP途中でカット → 参照フレームが不完全 → 再生エラーまたは品質劣化動画チャンク分割の落とし穴
時間ベース分割の問題点
一般的な動画分割では、以下のようなアプローチを取りがちです:
// ❌ 問題のあるアプローチ
ffmpeg(inputPath)
.seekInput(startTime) // 開始時刻
.duration(chunkDuration) // 長さ
.save(outputPath)この方式で発生する問題
🔧 GOP境界による時間のズレ
- 前述したように、正確なカットはキーフレーム位置でのみ可能
- 時間指定でのカットは最近のキーフレームに"スナップ"される
- 60秒指定でも実際は59.73秒や60.27秒でカットされる
⚠️ NTSCフレームレート(29.97fps)の特殊性
- 29.97fpsは厳密には30000/1001fps
- 1秒 = 29.97フレームではなく、複雑な計算が必要
- 時間ベースの計算では累積誤差が発生
❌ fluent-ffmpegライブラリのバグ
-ss 0(開始時刻0秒)で分割すると、期待より短いチャンクが生成される- 最初のチャンクが54秒になる現象が確認されている
Premiere Proでの表示問題
時間ベース分割されたチャンクをPremiere Proに読み込むと:
期待値: 1:00:00, 1:00:00, 1:00:00...
実際 : 54:10, 59:28, 59:29...この表示の違いは、プロ向け編集ソフトがフレーム単位で時間を管理しているためです。
フレーム単位分割による解決策
NTSC 29.97fps動画での正確な計算
60秒のチャンクをフレーム単位で正確に作成するには:
// ✅ 正しいアプローチ:フレーム単位での計算
function calculateFramesForNTSC(targetDurationSeconds: number): number {
// 29.97fps = 30000/1001fps
// Premiere互換性のため、30フレーム/秒として計算
return Math.round(targetDurationSeconds * 30)
}
// 例:60秒 = 1800フレーム(Premiere Proで1:00:00と表示)
const framesFor60s = calculateFramesForNTSC(60) // 1800
const framesFor30s = calculateFramesForNTSC(30) // 900
const framesFor90s = calculateFramesForNTSC(90) // 2700動的フレーム数計算の実装
様々なチャンク時間に対応できる汎用的な実装:
export function calculateOptimalFrameCount(fps: number, targetDuration: number): number {
if (Math.abs(fps - 29.97002997) < 0.001) {
// NTSC 29.97fps: Premiere互換のため30フレーム/秒
return Math.round(targetDuration * 30)
} else if (Math.abs(fps - 30) < 0.001) {
// 30fps: 正確に秒数 × 30フレーム
return Math.round(targetDuration * 30)
} else if (Math.abs(fps - 23.976023976) < 0.001) {
// NTSC Film 23.976fps: 24フレーム/秒として計算
return Math.round(targetDuration * 24)
} else if (Math.abs(fps - 24) < 0.001) {
// Film 24fps
return Math.round(targetDuration * 24)
} else {
// その他: 実際のFPS
return Math.round(targetDuration * fps)
}
}ffmpegでのフレーム単位分割実装
フレーム番号を直接指定する分割方法:
export async function cutVideoByFrames(
inputPath: string,
chunkIndex: number,
targetDuration: number,
outputPath: string,
fps: number,
): Promise<void> {
const framesPerChunk = calculateOptimalFrameCount(fps, targetDuration)
const startFrame = chunkIndex * framesPerChunk
const endFrame = startFrame + framesPerChunk
console.log(
`📹 Frame-based cut: ${startFrame}-${endFrame} (${framesPerChunk} frames @ ${fps.toFixed(2)}fps)`,
)
return new Promise((resolve, reject) => {
ffmpeg(inputPath)
.videoFilters([
`trim=start_frame=${startFrame}:end_frame=${endFrame}`,
'setpts=PTS-STARTPTS', // タイムスタンプをリセット
])
.audioFilters([
`atrim=start=${startFrame / fps}:end=${endFrame / fps}`,
'asetpts=PTS-STARTPTS', // 音声タイムスタンプもリセット
])
.outputOptions([
'-c:v libx264', // H.264エンコード
'-preset medium', // バランス重視
'-crf 18', // 高品質設定
'-pix_fmt yuv420p', // 互換性確保
'-r 30000/1001', // 正確なNTSCフレームレート
'-c:a aac', // AAC音声
'-ar 48000', // サンプリングレート
'-ac 2', // ステレオ
'-b:a 192k', // 音声ビットレート
'-map_metadata -1', // メタデータクリア
'-movflags +faststart', // Web配信最適化
'-y', // 上書き確認なし
])
.output(outputPath)
.on('end', () => {
console.log(`✅ Frame-accurate cut complete → ${outputPath}`)
resolve()
})
.on('error', (err) => {
console.error(`❌ Frame-based cut failed: ${err.message}`)
reject(err)
})
.run()
})
}fluent-ffmpegバグの回避策
-ss 0での問題を回避する実装:
// fluent-ffmpegの-ss 0バグを回避
if (startTime === 0) {
// 最初のチャンク: -ssを使わない
ffmpegCommand.outputOptions([`-t ${duration}`])
} else {
// 2番目以降: 通常通り-ssを使用
ffmpegCommand.inputOptions([`-ss ${startTime}`]).outputOptions([`-t ${duration}`])
}動画分析ワークフローへの統合
自動判定とフレーム分割の切り替え
NTSC動画を自動検出してフレーム分割に切り替える実装:
async function splitVideoIntelligently(
inputPath: string,
chunkDuration: number,
outputDir: string,
) {
// 動画のFPSを取得
const fps = await getVideoFPS(inputPath)
// NTSC 29.97fpsの場合はフレーム単位分割
const useFrameBasedCutting = Math.abs(fps - 29.97002997) < 0.001
if (useFrameBasedCutting) {
console.log('🎯 NTSC動画を検出:フレーム単位分割を使用')
const framesPerChunk = Math.round(chunkDuration * 30)
const timeDisplay = formatTimeDisplay(chunkDuration)
console.log(` ${framesPerChunk}フレーム = Premiere ${timeDisplay}`)
return await splitByFrames(inputPath, chunkDuration, outputDir, fps)
} else {
console.log('📺 標準動画:時間ベース分割を使用')
return await splitByTime(inputPath, chunkDuration, outputDir)
}
}LLMワークフローでの時間精度向上
分割されたチャンクをLLMに渡す際の時間情報の正確化:
interface VideoChunk {
chunkId: number
startFrame: number
endFrame: number
actualDurationSeconds: number
filePath: string
}
// LLMプロンプトでの時間情報
function createAnalysisPrompt(chunk: VideoChunk, fps: number): string {
const startTime = chunk.startFrame / fps
const endTime = chunk.endFrame / fps
return `
この動画チャンクを分析してください:
- 開始時刻: ${startTime.toFixed(2)}秒
- 終了時刻: ${endTime.toFixed(2)}秒
- 総時間: ${chunk.actualDurationSeconds.toFixed(2)}秒
- フレーム範囲: ${chunk.startFrame} - ${chunk.endFrame}
各分析セグメントの時間は10秒間隔で正確に区切られています。
`
}実装結果の検証
品質確認の実装
作成されたチャンクが期待通りの品質か確認する方法:
async function verifyChunkQuality(chunks: VideoChunk[]) {
console.log('🔍 チャンク品質検証を開始...')
for (const chunk of chunks) {
// 実際のフレーム数を確認
const actualFrameCount = await getVideoFrameCount(chunk.filePath)
const expectedFrames = chunk.endFrame - chunk.startFrame
// 時間精度を確認
const actualDuration = await getVideoDuration(chunk.filePath)
const expectedDuration = expectedFrames / 30 // NTSC基準
const frameAccuracy = Math.abs(actualFrameCount - expectedFrames)
const timeAccuracy = Math.abs(actualDuration - expectedDuration)
if (frameAccuracy === 0 && timeAccuracy < 0.1) {
console.log(`✅ チャンク${chunk.chunkId}: フレーム精度完璧`)
} else {
console.warn(`⚠️ チャンク${chunk.chunkId}: 精度に問題あり`)
console.warn(` フレーム差分: ${frameAccuracy}`)
console.warn(` 時間差分: ${timeAccuracy.toFixed(3)}秒`)
}
}
}Premiere Pro互換性の確認
作成したチャンクがプロ向けソフトで正しく認識されるかの確認:
# チャンクのフレーム数確認
ffprobe -v error -select_streams v:0 -count_packets \
-show_entries stream=nb_read_packets -of csv=p=0 chunk.mp4
# 期待値: 1800フレーム(60秒チャンクの場合)パフォーマンス最適化
並列処理での効率化
複数チャンクを同時処理する実装:
async function splitVideoParallel(
inputPath: string,
chunkDuration: number,
outputDir: string,
concurrency: number = 3,
): Promise<VideoChunk[]> {
const totalDuration = await getVideoDuration(inputPath)
const numChunks = Math.ceil(totalDuration / chunkDuration)
const fps = await getVideoFPS(inputPath)
console.log(`🚀 ${numChunks}個のチャンクを${concurrency}並列で処理開始`)
// チャンク作成タスクを生成
const tasks = Array.from(
{ length: numChunks },
(_, i) => () =>
cutVideoByFrames(
inputPath,
i,
chunkDuration,
path.join(outputDir, `chunk_${i.toString().padStart(4, '0')}.mp4`),
fps,
),
)
// 並列実行(concurrency制限付き)
const results = await Promise.allSettled(
tasks.map(
(task, i) =>
new Promise((resolve) => {
setTimeout(() => resolve(task()), i * 100) // 100ms間隔でスタート
}),
),
)
// 結果の集計
const successCount = results.filter((r) => r.status === 'fulfilled').length
console.log(`✅ ${successCount}/${numChunks}個のチャンク作成完了`)
return chunks
}トラブルシューティング
よくある問題と解決策
💡 問題1: 最初のチャンクだけ短くなる
// 解決策: startTimeが0の場合の特別処理
if (startTime === 0) {
// -ssオプションを使わない
ffmpegCommand.outputOptions([`-t ${duration}`])
} else {
ffmpegCommand.inputOptions([`-ss ${startTime}`])
}⚠️ 問題2: 音声と映像の同期ずれ
// 解決策: 音声フィルターも同時に適用
.audioFilters([
`atrim=start=${startFrame / fps}:end=${endFrame / fps}`,
'asetpts=PTS-STARTPTS'
])❌ 問題3: エンコード設定による品質低下
// 解決策: 高品質設定の使用
.outputOptions([
'-c:v libx264',
'-crf 18', // 高品質(0-51、低いほど高品質)
'-preset medium', // エンコード速度と品質のバランス
'-pix_fmt yuv420p' // 互換性確保
])まとめ
動画AI・LLM開発における動画チャンク分割では、時間ベースではなくフレーム単位でのアプローチが重要です。
重要なポイント
- NTSC 29.97fps動画では30フレーム/秒として計算
- fluent-ffmpegの
-ss 0バグに注意 - フレーム単位分割でPremiere Pro互換性を確保
- 音声同期も考慮したフィルター適用
実装で得られる効果
- ✅ 正確な時間表示(Premiere Proで1:00:00)
- ✅ LLM分析での時間情報精度向上
- ✅ プロ向け編集ソフトとの完全互換性
- ✅ 長時間動画での累積誤差解消
この手法により、動画AI・LLMアプリケーションの分析精度と編集ワークフローとの互換性を両立できます。
参考資料
お問い合わせはこちらから
ご希望に応じて職務経歴書や過去のポートフォリオを提出可能ですので、必要な方はお申し付けください。
また内容とによっては返信ができない場合や、お時間をいただく場合がございます。あらかじめご了承ください。
