AI视频编辑与后期制作完全教程

教程简介

本教程全面讲解AI视频编辑与后期制作的核心技术,涵盖AI视频剪辑工具(Runway/Premiere AI/DaVinci AI)、自动字幕生成与翻译、AI配音与语音合成、视频风格迁移与滤镜、智能裁剪与构图优化、物体移除与替换、AI转场与特效生成、人脸替换与Deepfake检测、视频摘要与精彩片段提取、批量处理工作流、开源FFmpeg+AI Pipeline等核心内容,帮助开发者构建AI视频编辑工具链。

AI视频编辑与后期制作完全教程

从零掌握AI视频编辑技术栈,构建智能视频处理工具链

一、前言

视频内容已成为互联网信息传播的主流形式。传统视频编辑依赖专业软件和人工操作,耗时且门槛高。随着AI技术的突破——从自动字幕生成到风格迁移、从智能裁剪到物体移除——视频编辑正在经历一场效率革命。

本教程系统讲解AI视频编辑与后期制作的核心技术,涵盖商业工具对比、开源方案实现、自动化流水线搭建等内容。无论你是短视频创作者、视频平台开发者,还是企业内容团队的技术负责人,都能从中找到可落地的解决方案。

二、AI视频编辑工具全景

2.1 商业工具对比

工具 核心AI能力 适用场景 价格 API
Runway Gen-3 文生视频、图生视频、视频编辑 创意视频、特效制作 $12/月起
Premiere Pro AI 自动剪辑、场景检测、音频清理 专业剪辑、影视制作 $23/月 有限
DaVinci Resolve AI 人脸追踪、速度扭曲、调色 调色、后期合成 免费+Studio $295
CapCut/剪映 自动字幕、智能抠图、AI特效 短视频、社交媒体 免费+Pro
Descript 文本驱动编辑、AI配音、填充词删除 播客、访谈、教程 $24/月
Synthesia AI数字人、文本转视频 企业培训、产品介绍 $22/月

2.2 工具选择决策树

需要AI生成全新视频?
├─ 是 → Runway Gen-3 / Pika / Sora
└─ 否 → 需要编辑已有视频?
    ├─ 短视频/社交媒体 → CapCut/剪映
    ├─ 专业影视后期 → DaVinci Resolve + Premiere
    ├─ 播客/访谈 → Descript
    ├─ 企业批量生产 → Synthesia + 自建Pipeline
    └─ 开发者集成 → FFmpeg + 开源AI模型

三、自动字幕生成与翻译

3.1 基于Whisper的语音转文字

OpenAI的Whisper是目前最强大的开源语音识别模型,支持99种语言,中文识别准确率极高。

import whisper
import json
import srt
from datetime import timedelta

class SubtitleGenerator:
    """基于Whisper的自动字幕生成器"""
    
    def __init__(self, model_size: str = "medium"):
        """
        初始化字幕生成器
        
        Args:
            model_size: 模型大小 (tiny/base/small/medium/large)
                       中文推荐 medium 或 large
        """
        print(f"加载Whisper模型: {model_size}")
        self.model = whisper.load_model(model_size)
        print("模型加载完成")
    
    def transcribe(self, audio_path: str, language: str = "zh",
                   task: str = "transcribe") -> dict:
        """
        转录音频/视频
        
        Args:
            audio_path: 音频或视频文件路径
            language: 语言代码 (zh/en/ja等)
            task: transcribe(转录) 或 translate(翻译成英文)
        
        Returns:
            包含时间戳的转录结果
        """
        result = self.model.transcribe(
            audio_path,
            language=language,
            task=task,
            verbose=False,
            word_timestamps=True  # 启用词级时间戳
        )
        return result
    
    def generate_srt(self, result: dict, output_path: str,
                     max_chars_per_line: int = 30):
        """生成SRT字幕文件"""
        subtitles = []
        
        for i, segment in enumerate(result['segments'], 1):
            text = segment['text'].strip()
            
            # 长文本分行
            lines = self._split_text(text, max_chars_per_line)
            
            start = timedelta(seconds=segment['start'])
            end = timedelta(seconds=segment['end'])
            
            subtitles.append(srt.Subtitle(
                index=i,
                start=start,
                end=end,
                content='\n'.join(lines)
            ))
        
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(srt.compose(subtitles))
        
        print(f"SRT字幕已生成: {output_path}")
        return output_path
    
    def generate_ass(self, result: dict, output_path: str,
                     font_size: int = 24, font_name: str = "Microsoft YaHei",
                     position: str = "bottom"):
        """生成ASS字幕文件(支持更丰富的样式)"""
        
        # ASS文件头
        ass_content = f"""[Script Info]
Title: AI Generated Subtitles
ScriptType: v4.00+
PlayResX: 1920
PlayResY: 1080

[V4+ Styles]
Format: Name,Fontname,Fontsize,PrimaryColour,SecondaryColour,OutlineColour,BackColour,Bold,Italic,Underline,StrikeOut,ScaleX,ScaleY,Spacing,Angle,BorderStyle,Outline,Shadow,Alignment,MarginL,MarginR,MarginV,Encoding
Style: Default,{font_name},{font_size},&H00FFFFFF,&H000000FF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,2,1,{2 if position == 'bottom' else 8},20,20,30,1

[Events]
Format: Layer,Start,End,Style,Name,MarginL,MarginR,MarginV,Effect,Text
"""
        
        for segment in result['segments']:
            start = self._seconds_to_ass_time(segment['start'])
            end = self._seconds_to_ass_time(segment['end'])
            text = segment['text'].strip().replace('\n', '\\N')
            
            ass_content += f"Dialogue: 0,{start},{end},Default,,0,0,0,,{text}\n"
        
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(ass_content)
        
        print(f"ASS字幕已生成: {output_path}")
        return output_path
    
    def _split_text(self, text: str, max_chars: int) -> list:
        """智能分行"""
        if len(text) <= max_chars:
            return [text]
        
        # 优先在标点处断行
        punctuations = ',。、;!?,;!?'
        lines = []
        current = ""
        
        for char in text:
            current += char
            if char in punctuations and len(current) >= max_chars // 2:
                lines.append(current)
                current = ""
        
        if current:
            if lines and len(current) < max_chars // 3:
                lines[-1] += current
            else:
                lines.append(current)
        
        return lines
    
    def _seconds_to_ass_time(self, seconds: float) -> str:
        """秒数转ASS时间格式"""
        h = int(seconds // 3600)
        m = int((seconds % 3600) // 60)
        s = seconds % 60
        return f"{h}:{m:02d}:{s:05.2f}"


# 使用示例
# generator = SubtitleGenerator(model_size="medium")
# result = generator.transcribe("input_video.mp4", language="zh")
# generator.generate_srt(result, "output.srt")
# generator.generate_ass(result, "output.ass", font_size=28)

3.2 字幕翻译

from openai import OpenAI

class SubtitleTranslator:
    """AI字幕翻译器"""
    
    def __init__(self, api_key: str, base_url: str = None,
                 model: str = "gpt-4"):
        self.client = OpenAI(api_key=api_key, base_url=base_url)
        self.model = model
    
    def translate_segments(self, segments: list, 
                          source_lang: str = "zh",
                          target_lang: str = "en",
                          batch_size: int = 20) -> list:
        """批量翻译字幕片段"""
        
        translated = []
        
        for i in range(0, len(segments), batch_size):
            batch = segments[i:i+batch_size]
            
            # 构建批量翻译请求
            texts = [seg['text'].strip() for seg in batch]
            
            prompt = f"""请将以下{source_lang}字幕翻译为{target_lang}。
要求:
1. 保持口语化风格
2. 保持原文的情感和语气
3. 专有名词保留原文
4. 输出JSON数组,每个元素包含原文和译文

原文:
{json.dumps(texts, ensure_ascii=False)}"""

            response = self.client.chat.completions.create(
                model=self.model,
                messages=[{"role": "user", "content": prompt}],
                response_format={"type": "json_object"},
                temperature=0.3
            )
            
            result = json.loads(response.choices[0].message.content)
            translations = result.get('translations', result.get('translated', []))
            
            for j, seg in enumerate(batch):
                new_seg = seg.copy()
                if j < len(translations):
                    if isinstance(translations[j], dict):
                        new_seg['translated_text'] = translations[j].get('translation', 
                                                                          translations[j].get('translated', ''))
                    else:
                        new_seg['translated_text'] = translations[j]
                new_seg['original_text'] = seg['text']
                translated.append(new_seg)
            
            print(f"  翻译进度: {min(i+batch_size, len(segments))}/{len(segments)}")
        
        return translated
    
    def generate_bilingual_srt(self, translated_segments: list,
                                output_path: str, 
                                primary: str = "translated"):
        """生成双语字幕"""
        import srt
        from datetime import timedelta
        
        subtitles = []
        
        for i, seg in enumerate(translated_segments, 1):
            if primary == "translated":
                text = f"{seg.get('translated_text', '')}\n{seg['text']}"
            else:
                text = f"{seg['text']}\n{seg.get('translated_text', '')}"
            
            subtitles.append(srt.Subtitle(
                index=i,
                start=timedelta(seconds=seg['start']),
                end=timedelta(seconds=seg['end']),
                text.strip()
            ))
        
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(srt.compose(subtitles))
        
        print(f"双语字幕已生成: {output_path}")


# 使用示例
# translator = SubtitleTranslator(api_key="your-key")
# translated = translator.translate_segments(result['segments'], "zh", "en")
# translator.generate_bilingual_srt(translated, "bilingual.srt")

3.3 字幕烧录(硬字幕)

import subprocess
import os

class SubtitleBurner:
    """字幕烧录工具 - 将字幕嵌入视频"""
    
    @staticmethod
    def burn_srt(video_path: str, srt_path: str, output_path: str,
                 font_size: int = 24, font_color: str = "white",
                 outline_color: str = "black", outline_width: int = 2):
        """烧录SRT字幕到视频"""
        
        # 字幕样式滤镜
        style = (
            f"FontSize={font_size},"
            f"PrimaryColour=&H00FFFFFF,"  # 白色
            f"OutlineColour=&H00000000,"  # 黑色描边
            f"Outline={outline_width},"
            f"Shadow=1,"
            f"MarginV=30"
        )
        
        cmd = [
            'ffmpeg', '-i', video_path,
            '-vf', f"subtitles={srt_path}:force_style='{style}'",
            '-c:a', 'copy',
            '-y', output_path
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise RuntimeError(f"字幕烧录失败: {result.stderr}")
        
        print(f"硬字幕视频已生成: {output_path}")
        return output_path
    
    @staticmethod
    def burn_ass(video_path: str, ass_path: str, output_path: str):
        """烧录ASS字幕到视频(支持更丰富的样式)"""
        
        cmd = [
            'ffmpeg', '-i', video_path,
            '-vf', f"ass={ass_path}",
            '-c:a', 'copy',
            '-y', output_path
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise RuntimeError(f"ASS字幕烧录失败: {result.stderr}")
        
        print(f"ASS字幕视频已生成: {output_path}")
        return output_path
    
    @staticmethod
    def soft_sub(video_path: str, srt_path: str, output_path: str,
                 language: str = "chi"):
        """封装软字幕(可开关)"""
        
        cmd = [
            'ffmpeg', '-i', video_path,
            '-i', srt_path,
            '-c', 'copy',
            '-c:s', 'mov_text',
            '-metadata:s:s:0', f'language={language}',
            '-y', output_path
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise RuntimeError(f"软字幕封装失败: {result.stderr}")
        
        print(f"软字幕视频已生成: {output_path}")
        return output_path

四、AI配音与语音合成

4.1 基于开源TTS的配音生成

import torch
import numpy as np
import soundfile as sf
from dataclasses import dataclass

@dataclass
class VoiceConfig:
    """语音配置"""
    speaker: str = "default"
    speed: float = 1.0
    pitch: float = 1.0
    emotion: str = "neutral"  # neutral/happy/sad/angry
    language: str = "zh"

class AIVoiceGenerator:
    """AI语音合成器"""
    
    def __init__(self, engine: str = "edge-tts"):
        """
        Args:
            engine: TTS引擎 (edge-tts/coqui/bark)
        """
        self.engine = engine
        
        if engine == "edge-tts":
            self._init_edge_tts()
        elif engine == "coqui":
            self._init_coqui()
    
    def _init_edge_tts(self):
        """初始化Edge TTS(微软免费TTS)"""
        try:
            import edge_tts
            self.edge_tts = edge_tts
        except ImportError:
            raise ImportError("请安装edge-tts: pip install edge-tts")
    
    def _init_coqui(self):
        """初始化Coqui TTS"""
        try:
            from TTS.api import TTS
            self.tts = TTS(model_name="tts_models/zh-CN/baker/tacotron2-DDC-GST",
                          progress_bar=False)
        except ImportError:
            raise ImportError("请安装TTS: pip install TTS")
    
    async def generate_edge_tts(self, text: str, output_path: str,
                                 voice: str = "zh-CN-XiaoxiaoNeural",
                                 rate: str = "+0%", pitch: str = "+0Hz"):
        """使用Edge TTS生成语音"""
        import edge_tts
        
        communicate = edge_tts.Communicate(text, voice, rate=rate, pitch=pitch)
        await communicate.save(output_path)
        print(f"语音已生成: {output_path}")
        return output_path
    
    def generate_coqui_tts(self, text: str, output_path: str,
                           speaker_wav: str = None):
        """使用Coqui TTS生成语音(支持声音克隆)"""
        if speaker_wav:
            self.tts.tts_to_file(text=text, speaker_wav=speaker_wav,
                                file_path=output_path)
        else:
            self.tts.tts_to_file(text=text, file_path=output_path)
        
        print(f"语音已生成: {output_path}")
        return output_path
    
    def list_edge_voices(self, language: str = "zh"):
        """列出可用的Edge TTS语音"""
        import asyncio
        
        async def _list():
            voices = await self.edge_tts.list_voices()
            return [v for v in voices if v['Locale'].startswith(language)]
        
        return asyncio.run(_list())


class VoiceOverGenerator:
    """视频配音生成器 - 自动为视频生成配音"""
    
    def __init__(self, tts_engine: AIVoiceGenerator):
        self.tts = tts_engine
    
    def generate_voiceover_from_script(self, script: list, 
                                        output_dir: str,
                                        voice: str = "zh-CN-YunxiNeural"):
        """
        从脚本生成配音
        
        Args:
            script: [{"text": "旁白内容", "start": 0.0, "end": 5.0}, ...]
            output_dir: 输出目录
            voice: 语音角色
        """
        import asyncio
        import os
        
        os.makedirs(output_dir, exist_ok=True)
        audio_files = []
        
        for i, segment in enumerate(script):
            output_path = os.path.join(output_dir, f"voiceover_{i:03d}.mp3")
            
            # 根据时长调整语速
            duration = segment['end'] - segment['start']
            char_count = len(segment['text'])
            
            # 估算语速调整(中文约4字/秒为正常语速)
            normal_duration = char_count / 4.0
            rate = int((normal_duration / duration - 1) * 100)
            rate_str = f"+{rate}%" if rate >= 0 else f"{rate}%"
            
            asyncio.run(self.tts.generate_edge_tts(
                segment['text'], output_path,
                voice=voice, rate=rate_str
            ))
            
            audio_files.append({
                'path': output_path,
                'start': segment['start'],
                'end': segment['end']
            })
        
        return audio_files
    
    def merge_voiceover_with_video(self, video_path: str, 
                                    voiceover_files: list,
                                    output_path: str,
                                    bgm_path: str = None,
                                    bgm_volume: float = 0.15):
        """将配音合并到视频"""
        import subprocess
        import tempfile
        
        # 先生成配音时间线音频
        timeline_audio = self._create_audio_timeline(
            voiceover_files, video_path
        )
        
        if bgm_path:
            # 混合背景音乐
            mixed_audio = tempfile.mktemp(suffix='.wav')
            cmd = [
                'ffmpeg',
                '-i', timeline_audio,
                '-i', bgm_path,
                '-filter_complex',
                f'[1:a]volume={bgm_volume}[bgm];[0:a][bgm]amix=inputs=2:duration=first',
                '-y', mixed_audio
            ]
            subprocess.run(cmd, capture_output=True)
            timeline_audio = mixed_audio
        
        # 合并到视频
        cmd = [
            'ffmpeg', '-i', video_path,
            '-i', timeline_audio,
            '-c:v', 'copy',
            '-map', '0:v:0',
            '-map', '1:a:0',
            '-shortest',
            '-y', output_path
        ]
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise RuntimeError(f"合并失败: {result.stderr}")
        
        print(f"配音视频已生成: {output_path}")
        return output_path
    
    def _create_audio_timeline(self, voiceover_files: list,
                                video_path: str) -> str:
        """创建音频时间线"""
        import subprocess
        import tempfile
        
        # 获取视频时长
        probe = subprocess.run(
            ['ffprobe', '-v', 'error', '-show_entries', 
             'format=duration', '-of', 'csv=p=0', video_path],
            capture_output=True, text=True
        )
        duration = float(probe.stdout.strip())
        
        # 使用FFmpeg的amerge/amix创建时间线
        inputs = []
        filter_parts = []
        
        for i, vf in enumerate(voiceover_files):
            inputs.extend(['-i', vf['path']])
            delay_ms = int(vf['start'] * 1000)
            filter_parts.append(
                f"[{i}:a]adelay={delay_ms}|{delay_ms}[a{i}]"
            )
        
        if not filter_parts:
            raise ValueError("没有配音文件")
        
        mix_inputs = ''.join(f'[a{i}]' for i in range(len(voiceover_files)))
        filter_parts.append(
            f"{mix_inputs}amix=inputs={len(voiceover_files)}:duration=longest[out]"
        )
        
        output_path = tempfile.mktemp(suffix='.wav')
        
        cmd = ['ffmpeg'] + inputs + [
            '-filter_complex', ';'.join(filter_parts),
            '-map', '[out]',
            '-t', str(duration),
            '-y', output_path
        ]
        
        subprocess.run(cmd, capture_output=True)
        return output_path

五、视频风格迁移与AI滤镜

5.1 基于神经网络的风格迁移

import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision.models as models
from PIL import Image
import cv2
import numpy as np

class VideoStyleTransfer:
    """视频风格迁移处理器"""
    
    def __init__(self, style_image_path: str, model: str = "vgg19"):
        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        
        # 加载预训练模型
        if model == "vgg19":
            self.model = models.vgg19(pretrained=True).features.to(self.device).eval()
        
        # 加载风格图片
        self.style_image = self._load_image(style_image_path)
        self.style_features = self._extract_features(self.style_image)
        
        # 图像变换
        self.transform = transforms.Compose([
            transforms.ToTensor(),
            transforms.Normalize(mean=[0.485, 0.456, 0.406],
                               std=[0.229, 0.224, 0.225])
        ])
    
    def _load_image(self, path: str, max_size: int = 512) -> torch.Tensor:
        """加载并预处理图片"""
        img = Image.open(path).convert('RGB')
        
        # 保持比例缩放
        ratio = max_size / max(img.size)
        if ratio < 1:
            new_size = tuple(int(s * ratio) for s in img.size)
            img = img.resize(new_size, Image.LANCZOS)
        
        tensor = self.transform(img).unsqueeze(0)
        return tensor.to(self.device)
    
    def _extract_features(self, x: torch.Tensor) -> list:
        """提取特征层"""
        features = []
        layer_names = ['relu1_1', 'relu2_1', 'relu3_1', 'relu4_1', 'relu5_1']
        layer_indices = [1, 6, 11, 20, 29]  # VGG19中的层索引
        
        for i, layer in enumerate(self.model):
            x = layer(x)
            if i in layer_indices:
                features.append(x)
        
        return features
    
    def _gram_matrix(self, tensor: torch.Tensor) -> torch.Tensor:
        """计算Gram矩阵(风格表示)"""
        b, c, h, w = tensor.size()
        features = tensor.view(b, c, h * w)
        gram = torch.bmm(features, features.transpose(1, 2))
        return gram / (c * h * w)
    
    def apply_to_frame(self, frame: np.ndarray, 
                       num_steps: int = 100,
                       style_weight: float = 1e6,
                       content_weight: int = 1) -> np.ndarray:
        """对单帧应用风格迁移"""
        
        # 转换帧格式
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        frame_pil = Image.fromarray(frame_rgb)
        
        # 保持原始尺寸
        original_size = frame_pil.size
        
        # 缩放到处理尺寸
        max_dim = 512
        ratio = max_dim / max(original_size)
        if ratio < 1:
            process_size = tuple(int(s * ratio) for s in original_size)
            frame_pil = frame_pil.resize(process_size, Image.LANCZOS)
        
        content_tensor = self.transform(frame_pil).unsqueeze(0).to(self.device)
        target = content_tensor.clone().requires_grad_(True)
        
        content_features = self._extract_features(content_tensor)
        
        # 优化器
        optimizer = torch.optim.Adam([target], lr=0.01)
        
        for step in range(num_steps):
            target_features = self._extract_features(target)
            
            content_loss = 0
            style_loss = 0
            
            for cf, tf, sf in zip(content_features, target_features, 
                                   self.style_features):
                content_loss += torch.mean((tf - cf) ** 2)
                
                target_gram = self._gram_matrix(tf)
                style_gram = self._gram_matrix(sf)
                style_loss += torch.mean((target_gram - style_gram) ** 2)
            
            total_loss = content_weight * content_loss + style_weight * style_loss
            
            optimizer.zero_grad()
            total_loss.backward()
            optimizer.step()
        
        # 转换回图片
        result = target.detach().squeeze(0).cpu()
        result = result * torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1)
        result = result + torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1)
        result = result.clamp(0, 1)
        
        result_np = (result.permute(1, 2, 0).numpy() * 255).astype(np.uint8)
        result_pil = Image.fromarray(result_np)
        
        # 恢复原始尺寸
        result_pil = result_pil.resize(original_size, Image.LANCZOS)
        
        return cv2.cvtColor(np.array(result_pil), cv2.COLOR_RGB2BGR)
    
    def process_video(self, input_path: str, output_path: str,
                      num_steps: int = 50, fps: float = None):
        """处理整个视频"""
        cap = cv2.VideoCapture(input_path)
        
        if not cap.isOpened():
            raise RuntimeError(f"无法打开视频: {input_path}")
        
        # 获取视频参数
        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        original_fps = cap.get(cv2.CAP_PROP_FPS)
        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        
        if fps is None:
            fps = original_fps
        
        # 创建视频写入器
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
        
        frame_count = 0
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            
            # 应用风格迁移
            styled_frame = self.apply_to_frame(frame, num_steps=num_steps)
            writer.write(styled_frame)
            
            frame_count += 1
            if frame_count % 10 == 0:
                print(f"处理进度: {frame_count}/{total_frames} "
                      f"({frame_count/total_frames*100:.1f}%)")
        
        cap.release()
        writer.release()
        print(f"风格迁移视频已生成: {output_path}")


# 使用示例
# transfer = VideoStyleTransfer("starry_night.jpg")
# transfer.process_video("input.mp4", "styled_output.mp4", num_steps=30)

5.2 快速滤镜方案(基于预训练模型)

import cv2
import numpy as np

class QuickFilters:
    """快速AI风格滤镜(无需训练)"""
    
    @staticmethod
    def apply_pencil_sketch(frame: np.ndarray) -> np.ndarray:
        """素描风格"""
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        inv = 255 - gray
        blur = cv2.GaussianBlur(inv, (21, 21), 0)
        sketch = cv2.divide(gray, 255 - blur, scale=256)
        return cv2.cvtColor(sketch, cv2.COLOR_GRAY2BGR)
    
    @staticmethod
    def apply_oil_painting(frame: np.ndarray, 
                           size: int = 5, dynRatio: int = 1) -> np.ndarray:
        """油画风格"""
        return cv2.xphoto.oilPainting(frame, size, dynRatio)
    
    @staticmethod
    def apply_cartoon(frame: np.ndarray) -> np.ndarray:
        """卡通风格"""
        # 边缘检测
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        gray = cv2.medianBlur(gray, 5)
        edges = cv2.adaptiveThreshold(
            gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
            cv2.THRESH_BINARY, 9, 9
        )
        
        # 颜色量化
        color = cv2.bilateralFilter(frame, 9, 300, 300)
        
        # 合并
        cartoon = cv2.bitwise_and(color, color, mask=edges)
        return cartoon
    
    @staticmethod
    def apply_cinematic(frame: np.ndarray) -> np.ndarray:
        """电影色调"""
        # 调整对比度和饱和度
        lab = cv2.cvtColor(frame, cv2.COLOR_BGR2LAB)
        l, a, b = cv2.split(lab)
        
        # CLAHE增强对比度
        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
        l = clahe.apply(l)
        
        # 调整色温(偏暖)
        b = cv2.add(b, 10)
        
        merged = cv2.merge([l, a, b])
        result = cv2.cvtColor(merged, cv2.COLOR_LAB2BGR)
        
        # 添加暗角
        rows, cols = result.shape[:2]
        kernel_x = cv2.getGaussianKernel(cols, cols * 0.6)
        kernel_y = cv2.getGaussianKernel(rows, rows * 0.6)
        kernel = kernel_y * kernel_x.T
        mask = kernel / kernel.max()
        vignette = np.copy(result)
        for i in range(3):
            vignette[:, :, i] = vignette[:, :, i] * mask
        
        return vignette
    
    @staticmethod
    def apply_color_grading(frame: np.ndarray, 
                            shadows: tuple = (30, 20, 10),
                            highlights: tuple = (240, 235, 250)) -> np.ndarray:
        """专业调色"""
        result = frame.astype(np.float32)
        
        # 分离暗部和亮部
        for c in range(3):
            channel = result[:, :, c]
            shadow_mask = channel < 128
            highlight_mask = channel >= 128
            
            # 暗部偏色
            result[:, :, c] = np.where(
                shadow_mask,
                channel * (shadows[c] / 128.0),
                channel
            )
            
            # 亮部偏色
            result[:, :, c] = np.where(
                highlight_mask,
                channel + (highlights[c] - 255) * (channel - 128) / 127.0,
                result[:, :, c]
            )
        
        return np.clip(result, 0, 255).astype(np.uint8)


class VideoFilterProcessor:
    """视频滤镜批量处理器"""
    
    def __init__(self):
        self.filters = QuickFilters()
    
    def apply_filter_to_video(self, input_path: str, output_path: str,
                               filter_name: str = "cinematic",
                               fps: float = None):
        """对视频应用滤镜"""
        
        filter_func = getattr(self.filters, f"apply_{filter_name}", None)
        if filter_func is None:
            raise ValueError(f"未知滤镜: {filter_name}")
        
        cap = cv2.VideoCapture(input_path)
        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        original_fps = cap.get(cv2.CAP_PROP_FPS)
        total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        
        if fps is None:
            fps = original_fps
        
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
        
        count = 0
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            
            filtered = filter_func(frame)
            writer.write(filtered)
            
            count += 1
            if count % 30 == 0:
                print(f"进度: {count}/{total}")
        
        cap.release()
        writer.release()
        print(f"滤镜视频已保存: {output_path}")

六、智能裁剪与构图优化

6.1 基于人脸检测的智能裁剪

import cv2
import numpy as np
from dataclasses import dataclass
from typing import Optional

@dataclass
class CropRegion:
    """裁剪区域"""
    x: int
    y: int
    width: int
    height: int
    confidence: float = 1.0

class SmartCropper:
    """智能裁剪器 - 基于内容感知的自动裁剪"""
    
    def __init__(self):
        # 加载人脸检测器
        self.face_cascade = cv2.CascadeClassifier(
            cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
        )
        
        # 尝试加载DNN人脸检测器(更准确)
        try:
            self.face_net = cv2.dnn.readNetFromCaffe(
                "deploy.prototxt",
                "res10_300x300_ssd_iter_140000.caffemodel"
            )
            self.use_dnn = True
        except:
            self.use_dnn = False
            print("DNN模型不可用,使用Haar级联检测器")
    
    def detect_faces(self, frame: np.ndarray) -> list:
        """检测人脸位置"""
        if self.use_dnn:
            return self._detect_faces_dnn(frame)
        return self._detect_faces_haar(frame)
    
    def _detect_faces_haar(self, frame: np.ndarray) -> list:
        """Haar级联人脸检测"""
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        faces = self.face_cascade.detectMultiScale(
            gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)
        )
        return [(x, y, w, h) for (x, y, w, h) in faces]
    
    def _detect_faces_dnn(self, frame: np.ndarray) -> list:
        """DNN人脸检测(更准确)"""
        h, w = frame.shape[:2]
        blob = cv2.dnn.blobFromImage(
            cv2.resize(frame, (300, 300)), 1.0,
            (300, 300), (104.0, 177.0, 123.0)
        )
        self.face_net.setInput(blob)
        detections = self.face_net.forward()
        
        faces = []
        for i in range(detections.shape[2]):
            confidence = detections[0, 0, i, 2]
            if confidence > 0.5:
                box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
                x1, y1, x2, y2 = box.astype(int)
                faces.append((x1, y1, x2 - x1, y2 - y1))
        
        return faces
    
    def auto_crop_for_portrait(self, frame: np.ndarray, 
                                target_ratio: float = 9/16) -> np.ndarray:
        """人像智能裁剪(适配竖屏)"""
        h, w = frame.shape[:2]
        faces = self.detect_faces(frame)
        
        if not faces:
            # 无人脸时居中裁剪
            return self._center_crop(frame, target_ratio)
        
        # 找到最大的人脸
        largest_face = max(faces, key=lambda f: f[2] * f[3])
        fx, fy, fw, fh = largest_face
        
        # 计算人脸中心
        face_cx = fx + fw // 2
        face_cy = fy + fh // 2
        
        # 计算裁剪区域(确保人脸在上方1/3处)
        target_w = int(h * target_ratio)
        if target_w > w:
            target_w = w
        target_h = int(target_w / target_ratio)
        
        # 人脸应在垂直方向的1/3处
        crop_y = face_cy - target_h // 3
        crop_x = face_cx - target_w // 2
        
        # 边界修正
        crop_x = max(0, min(crop_x, w - target_w))
        crop_y = max(0, min(crop_y, h - target_h))
        
        return frame[crop_y:crop_y+target_h, crop_x:crop_x+target_w]
    
    def auto_crop_for_landscape(self, frame: np.ndarray,
                                 target_ratio: float = 16/9) -> np.ndarray:
        """风景智能裁剪(适配横屏)"""
        h, w = frame.shape[:2]
        
        # 使用显著性检测找到重要区域
        saliency = cv2.saliency.StaticSaliencySpectralResidual_create()
        success, saliency_map = saliency.computeSaliency(frame)
        
        if success:
            saliency_map = (saliency_map * 255).astype(np.uint8)
            
            # 找到显著性中心
            moments = cv2.moments(saliency_map)
            if moments['m00'] > 0:
                cx = int(moments['m10'] / moments['m00'])
                cy = int(moments['m01'] / moments['m00'])
            else:
                cx, cy = w // 2, h // 2
        else:
            cx, cy = w // 2, h // 2
        
        # 计算裁剪区域
        target_h = h
        target_w = int(target_h * target_ratio)
        
        if target_w > w:
            target_w = w
            target_h = int(target_w / target_ratio)
        
        crop_x = cx - target_w // 2
        crop_y = cy - target_h // 2
        
        crop_x = max(0, min(crop_x, w - target_w))
        crop_y = max(0, min(crop_y, h - target_h))
        
        return frame[crop_y:crop_y+target_h, crop_x:crop_x+target_w]
    
    def _center_crop(self, frame: np.ndarray, 
                     target_ratio: float) -> np.ndarray:
        """居中裁剪"""
        h, w = frame.shape[:2]
        current_ratio = w / h
        
        if current_ratio > target_ratio:
            new_w = int(h * target_ratio)
            x = (w - new_w) // 2
            return frame[:, x:x+new_w]
        else:
            new_h = int(w / target_ratio)
            y = (h - new_h) // 2
            return frame[y:y+new_h, :]
    
    def smart_crop_video(self, input_path: str, output_path: str,
                         target_ratio: float = 9/16,
                         content_type: str = "portrait"):
        """智能裁剪视频"""
        cap = cv2.VideoCapture(input_path)
        
        # 先分析第一帧确定裁剪参数
        ret, first_frame = cap.read()
        if not ret:
            raise RuntimeError("无法读取视频")
        
        if content_type == "portrait":
            cropped = self.auto_crop_for_portrait(first_frame, target_ratio)
        else:
            cropped = self.auto_crop_for_landscape(first_frame, target_ratio)
        
        crop_h, crop_w = cropped.shape[:2]
        
        # 重置视频
        cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
        
        fps = cap.get(cv2.CAP_PROP_FPS)
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        writer = cv2.VideoWriter(output_path, fourcc, fps, (crop_w, crop_h))
        
        count = 0
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            
            if content_type == "portrait":
                cropped = self.auto_crop_for_portrait(frame, target_ratio)
            else:
                cropped = self.auto_crop_for_landscape(frame, target_ratio)
            
            # 确保尺寸一致
            cropped = cv2.resize(cropped, (crop_w, crop_h))
            writer.write(cropped)
            
            count += 1
            if count % 30 == 0:
                print(f"裁剪进度: {count} 帧")
        
        cap.release()
        writer.release()
        print(f"智能裁剪视频已生成: {output_path}")

七、物体移除与替换

7.1 基于图像修复的物体移除

import cv2
import numpy as np
import torch
from PIL import Image

class ObjectRemover:
    """AI物体移除器"""
    
    def __init__(self, method: str = "opencv"):
        """
        Args:
            method: 修复方法 (opencv/lama/sd)
        """
        self.method = method
        
        if method == "lama":
            self._init_lama()
        elif method == "sd":
            self._init_sd_inpaint()
    
    def _init_lama(self):
        """初始化LaMa修复模型"""
        try:
            # 需要下载LaMa模型
            print("LaMa模型需要单独下载,请参考: https://github.com/advimman/lama")
        except Exception as e:
            print(f"LaMa初始化失败: {e}")
    
    def _init_sd_inpaint(self):
        """初始化Stable Diffusion修复"""
        try:
            from diffusers import StableDiffusionInpaintPipeline
            self.sd_pipe = StableDiffusionInpaintPipeline.from_pretrained(
                "runwayml/stable-diffusion-inpainting",
                torch_dtype=torch.float16
            ).to("cuda" if torch.cuda.is_available() else "cpu")
        except ImportError:
            raise ImportError("请安装diffusers: pip install diffusers")
    
    def remove_with_mask(self, image: np.ndarray, 
                         mask: np.ndarray) -> np.ndarray:
        """使用掩码移除物体"""
        if self.method == "opencv":
            return self._opencv_inpaint(image, mask)
        elif self.method == "lama":
            return self._lama_inpaint(image, mask)
        elif self.method == "sd":
            return self._sd_inpaint(image, mask, "")
        return image
    
    def _opencv_inpaint(self, image: np.ndarray, 
                        mask: np.ndarray) -> np.ndarray:
        """OpenCV传统修复(快速但效果一般)"""
        # 使用NS方法
        result_ns = cv2.inpaint(image, mask, 3, cv2.INPAINT_NS)
        # 使用Telea方法
        result_telea = cv2.inpaint(image, mask, 3, cv2.INPAINT_TELEA)
        
        # 混合两种结果
        alpha = 0.5
        result = cv2.addWeighted(result_ns, alpha, result_telea, 1-alpha, 0)
        return result
    
    def _sd_inpaint(self, image: np.ndarray, mask: np.ndarray,
                    prompt: str = "clean background") -> np.ndarray:
        """Stable Diffusion修复(效果最好但较慢)"""
        image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
        mask_pil = Image.fromarray(mask)
        
        result = self.sd_pipe(
            prompt=prompt,
            image=image_pil,
            mask_image=mask_pil,
            num_inference_steps=20
        ).images[0]
        
        return cv2.cvtColor(np.array(result), cv2.COLOR_RGB2BGR)
    
    def auto_detect_and_remove(self, image: np.ndarray, 
                                object_class: str = "person") -> np.ndarray:
        """自动检测并移除指定类型的物体"""
        # 使用YOLO检测物体
        try:
            from ultralytics import YOLO
            model = YOLO('yolov8n.pt')
            results = model(image)
        except ImportError:
            print("请安装ultralytics: pip install ultralytics")
            return image
        
        mask = np.zeros(image.shape[:2], dtype=np.uint8)
        
        for result in results:
            for box in result.boxes:
                cls = result.names[int(box.cls)]
                if cls == object_class:
                    x1, y1, x2, y2 = box.xyxy[0].int().tolist()
                    # 扩展掩码区域
                    margin = 10
                    cv2.rectangle(mask, 
                                 (x1-margin, y1-margin),
                                 (x2+margin, y2+margin),
                                 255, -1)
        
        if mask.sum() == 0:
            print(f"未检测到 {object_class}")
            return image
        
        return self.remove_with_mask(image, mask)
    
    def process_video(self, input_path: str, output_path: str,
                      mask_video_path: str = None,
                      object_class: str = None):
        """处理视频中的物体移除"""
        cap = cv2.VideoCapture(input_path)
        
        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        fps = cap.get(cv2.CAP_PROP_FPS)
        
        if mask_video_path:
            mask_cap = cv2.VideoCapture(mask_video_path)
        
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
        
        count = 0
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            
            if mask_video_path:
                ret_m, mask_frame = mask_cap.read()
                if ret_m:
                    mask = cv2.cvtColor(mask_frame, cv2.COLOR_BGR2GRAY)
                    result = self.remove_with_mask(frame, mask)
                else:
                    result = frame
            elif object_class:
                result = self.auto_detect_and_remove(frame, object_class)
            else:
                result = frame
            
            writer.write(result)
            count += 1
            if count % 30 == 0:
                print(f"物体移除进度: {count} 帧")
        
        cap.release()
        if mask_video_path:
            mask_cap.release()
        writer.release()
        print(f"物体移除视频已生成: {output_path}")

八、AI转场与特效生成

8.1 智能转场效果

import cv2
import numpy as np

class AITransitions:
    """AI转场效果库"""
    
    @staticmethod
    def crossfade(frame1: np.ndarray, frame2: np.ndarray, 
                  progress: float) -> np.ndarray:
        """交叉淡入淡出"""
        return cv2.addWeighted(frame1, 1 - progress, frame2, progress, 0)
    
    @staticmethod
    def morph_transition(frame1: np.ndarray, frame2: np.ndarray,
                         progress: float) -> np.ndarray:
        """基于光流的形变转场"""
        # 计算光流
        gray1 = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY)
        gray2 = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY)
        
        flow = cv2.calcOpticalFlowFarneback(
            gray1, gray2, None, 0.5, 3, 15, 3, 5, 1.2, 0
        )
        
        h, w = frame1.shape[:2]
        flow_map = np.column_stack([
            np.repeat(np.arange(w), h),
            np.tile(np.arange(h), w)
        ]).reshape(h, w, 2).astype(np.float32)
        
        # 根据进度插值光流
        flow_map += flow * progress
        
        # 应用形变
        warped1 = cv2.remap(frame1, flow_map[:, :, 0], flow_map[:, :, 1],
                           cv2.INTER_LINEAR)
        warped2 = cv2.remap(frame2, 
                           flow_map[:, :, 0] - flow[:, :, 0] * (1 - progress),
                           flow_map[:, :, 1] - flow[:, :, 1] * (1 - progress),
                           cv2.INTER_LINEAR)
        
        return cv2.addWeighted(warped1, 1 - progress, warped2, progress, 0)
    
    @staticmethod
    def zoom_transition(frame1: np.ndarray, frame2: np.ndarray,
                        progress: float) -> np.ndarray:
        """缩放转场"""
        h, w = frame1.shape[:2]
        
        # frame1缩小
        scale1 = 1 + progress * 0.5
        center = (w // 2, h // 2)
        M1 = cv2.getRotationMatrix2D(center, 0, 1/scale1)
        zoomed1 = cv2.warpAffine(frame1, M1, (w, h))
        
        # frame2从小变大
        scale2 = 0.5 + progress * 0.5
        M2 = cv2.getRotationMatrix2D(center, 0, scale2)
        zoomed2 = cv2.warpAffine(frame2, M2, (w, h))
        
        # 混合
        alpha = min(1.0, progress * 2)
        return cv2.addWeighted(zoomed1, 1 - alpha, zoomed2, alpha, 0)
    
    @staticmethod
    def slide_transition(frame1: np.ndarray, frame2: np.ndarray,
                         progress: float, direction: str = "left") -> np.ndarray:
        """滑动转场"""
        h, w = frame1.shape[:2]
        result = np.zeros_like(frame1)
        
        if direction == "left":
            offset = int(w * progress)
            if offset < w:
                result[:, :w-offset] = frame1[:, offset:]
            if offset > 0:
                result[:, w-offset:] = frame2[:, :offset]
        elif direction == "right":
            offset = int(w * (1 - progress))
            if offset > 0:
                result[:, :offset] = frame2[:, w-offset:]
            if offset < w:
                result[:, offset:] = frame1[:, :w-offset]
        elif direction == "up":
            offset = int(h * progress)
            if offset < h:
                result[:h-offset, :] = frame1[offset:, :]
            if offset > 0:
                result[h-offset:, :] = frame2[:offset, :]
        
        return result
    
    @staticmethod
    def glitch_transition(frame1: np.ndarray, frame2: np.ndarray,
                          progress: float) -> np.ndarray:
        """故障风格转场"""
        h, w = frame1.shape[:2]
        
        if progress < 0.5:
            # 前半段:frame1 + 故障效果
            alpha = progress * 2
            result = frame1.copy()
            
            # RGB通道偏移
            offset = int(alpha * 20)
            result[:, offset:, 0] = frame1[:, :w-offset, 0]  # R偏移
            result[:, :w-offset, 2] = frame1[:, offset:, 2]  # B偏移
            
            # 随机水平条纹
            num_stripes = int(alpha * 10)
            for _ in range(num_stripes):
                y = np.random.randint(0, h)
                stripe_h = np.random.randint(2, 10)
                shift = np.random.randint(-30, 30)
                result[y:y+stripe_h] = np.roll(result[y:y+stripe_h], shift, axis=1)
        else:
            # 后半段:过渡到frame2
            alpha = (progress - 0.5) * 2
            result = cv2.addWeighted(frame1, 1 - alpha, frame2, alpha, 0)
            
            # 逐渐减少故障效果
            glitch_alpha = 1 - alpha
            offset = int(glitch_alpha * 15)
            if offset > 0:
                result[:, offset:, 0] = result[:, :w-offset, 0]
        
        return result


class TransitionApplicator:
    """转场应用器 - 为视频片段添加转场"""
    
    def __init__(self):
        self.transitions = AITransitions()
    
    def apply_transitions(self, video_clips: list, output_path: str,
                          transition_type: str = "crossfade",
                          transition_duration: float = 1.0):
        """
        为多个视频片段添加转场
        
        Args:
            video_clips: 视频文件路径列表
            output_path: 输出路径
            transition_type: 转场类型
            transition_duration: 转场时长(秒)
        """
        if not video_clips:
            raise ValueError("没有视频片段")
        
        # 读取所有视频的参数
        caps = [cv2.VideoCapture(p) for p in video_clips]
        
        width = int(caps[0].get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(caps[0].get(cv2.CAP_PROP_FRAME_HEIGHT))
        fps = caps[0].get(cv2.CAP_PROP_FPS)
        
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
        
        transition_frames = int(transition_duration * fps)
        transition_func = getattr(self.transitions, transition_type,
                                  self.transitions.crossfade)
        
        for clip_idx, cap in enumerate(caps):
            # 读取当前片段的所有帧
            frames = []
            while True:
                ret, frame = cap.read()
                if not ret:
                    break
                frame = cv2.resize(frame, (width, height))
                frames.append(frame)
            
            if clip_idx == 0:
                # 第一个片段:写入除最后transition_frames帧外的所有帧
                for frame in frames[:-transition_frames]:
                    writer.write(frame)
            
            if clip_idx < len(caps) - 1:
                # 读取下一个片段的前transition_frames帧
                next_cap = caps[clip_idx + 1]
                next_frames = []
                for _ in range(transition_frames):
                    ret, frame = next_cap.read()
                    if ret:
                        frame = cv2.resize(frame, (width, height))
                        next_frames.append(frame)
                
                # 生成转场帧
                last_frames = frames[-transition_frames:]
                for i in range(transition_frames):
                    progress = i / transition_frames
                    
                    f1 = last_frames[min(i, len(last_frames)-1)]
                    f2 = next_frames[min(i, len(next_frames)-1)]
                    
                    blended = transition_func(f1, f2, progress)
                    writer.write(blended)
                
                # 将next_frames中未使用的帧留给下一个循环处理
                # (这里简化处理,实际需要更复杂的缓冲管理)
            else:
                # 最后一个片段:写入所有剩余帧
                for frame in frames:
                    writer.write(frame)
            
            cap.release()
        
        writer.release()
        print(f"转场视频已生成: {output_path}")

九、视频摘要与精彩片段提取

9.1 基于AI的视频摘要

import cv2
import numpy as np
from dataclasses import dataclass
from typing import List

@dataclass
class VideoSegment:
    """视频片段"""
    start_time: float
    end_time: float
    score: float
    description: str = ""
    has_face: bool = False
    motion_level: float = 0.0
    audio_energy: float = 0.0

class VideoSummarizer:
    """AI视频摘要生成器"""
    
    def __init__(self):
        self.face_cascade = cv2.CascadeClassifier(
            cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
        )
    
    def analyze_video(self, video_path: str, 
                      sample_interval: float = 1.0) -> List[VideoSegment]:
        """分析视频,提取关键特征"""
        cap = cv2.VideoCapture(video_path)
        fps = cap.get(cv2.CAP_PROP_FPS)
        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        duration = total_frames / fps
        
        segments = []
        prev_frame = None
        sample_frames = int(fps * sample_interval)
        
        frame_idx = 0
        while True:
            ret, frame = cap.read()
            if not ret:
                break
            
            if frame_idx % sample_frames == 0:
                current_time = frame_idx / fps
                
                # 计算运动量
                motion = 0.0
                if prev_frame is not None:
                    diff = cv2.absdiff(
                        cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY),
                        cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
                    )
                    motion = np.mean(diff) / 255.0
                
                # 检测人脸
                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                faces = self.face_cascade.detectMultiScale(
                    gray, 1.3, 5, minSize=(30, 30)
                )
                has_face = len(faces) > 0
                
                # 计算视觉显著性
                saliency_score = self._compute_saliency(frame)
                
                # 综合评分
                score = (
                    motion * 0.3 +
                    (1.0 if has_face else 0.0) * 0.3 +
                    saliency_score * 0.4
                )
                
                segments.append(VideoSegment(
                    start_time=current_time,
                    end_time=current_time + sample_interval,
                    score=score,
                    has_face=has_face,
                    motion_level=motion
                ))
                
                prev_frame = frame.copy()
            
            frame_idx += 1
        
        cap.release()
        return segments
    
    def _compute_saliency(self, frame: np.ndarray) -> float:
        """计算视觉显著性"""
        try:
            saliency = cv2.saliency.StaticSaliencySpectralResidual_create()
            success, saliency_map = saliency.computeSaliency(frame)
            if success:
                return float(np.mean(saliency_map))
        except:
            pass
        
        # 备用方案:基于边缘密度
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        edges = cv2.Canny(gray, 50, 150)
        return np.mean(edges) / 255.0
    
    def extract_highlights(self, segments: List[VideoSegment],
                           target_duration: float = 60.0,
                           min_gap: float = 5.0) -> List[VideoSegment]:
        """提取精彩片段"""
        # 按分数排序
        sorted_segs = sorted(segments, key=lambda s: s.score, reverse=True)
        
        highlights = []
        total_duration = 0.0
        used_times = set()
        
        for seg in sorted_segs:
            if total_duration >= target_duration:
                break
            
            # 检查是否与已选片段重叠
            start = seg.start_time
            if any(abs(start - t) < min_gap for t in used_times):
                continue
            
            highlights.append(seg)
            used_times.add(start)
            total_duration += seg.end_time - seg.start_time
        
        # 按时间排序
        highlights.sort(key=lambda s: s.start_time)
        return highlights
    
    def generate_summary_video(self, input_path: str, output_path: str,
                                highlight_segments: List[VideoSegment],
                                transition_frames: int = 15):
        """生成摘要视频"""
        cap = cv2.VideoCapture(input_path)
        fps = cap.get(cv2.CAP_PROP_FPS)
        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
        
        for seg in highlight_segments:
            start_frame = int(seg.start_time * fps)
            end_frame = int(seg.end_time * fps)
            
            cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
            
            for f_idx in range(start_frame, end_frame):
                ret, frame = cap.read()
                if not ret:
                    break
                
                # 添加淡入效果
                if f_idx - start_frame < transition_frames:
                    alpha = (f_idx - start_frame) / transition_frames
                    frame = (frame * alpha).astype(np.uint8)
                
                # 添加淡出效果
                if end_frame - f_idx < transition_frames:
                    alpha = (end_frame - f_idx) / transition_frames
                    frame = (frame * alpha).astype(np.uint8)
                
                writer.write(frame)
        
        cap.release()
        writer.release()
        print(f"摘要视频已生成: {output_path}")
    
    def add_timestamp_overlay(self, frame: np.ndarray, 
                               time_seconds: float) -> np.ndarray:
        """添加时间戳水印"""
        minutes = int(time_seconds // 60)
        seconds = int(time_seconds % 60)
        time_str = f"{minutes:02d}:{seconds:02d}"
        
        cv2.putText(frame, time_str, (20, 40),
                    cv2.FONT_HERSHEY_SIMPLEX, 1.0,
                    (255, 255, 255), 2, cv2.LINE_AA)
        return frame

十、批量处理与自动化工作流

10.1 视频批量处理框架

import os
import json
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass, field
from typing import Callable, Optional

@dataclass
class ProcessingTask:
    """处理任务"""
    input_path: str
    output_path: str
    operations: list = field(default_factory=list)
    status: str = "pending"
    error: str = ""

@dataclass
class ProcessingPipeline:
    """处理流水线配置"""
    name: str
    steps: list = field(default_factory=list)
    parallel: bool = False
    max_workers: int = 4

class VideoBatchProcessor:
    """视频批量处理器"""
    
    def __init__(self, max_workers: int = 4):
        self.max_workers = max_workers
        self.operations = {}
        self._register_default_operations()
    
    def _register_default_operations(self):
        """注册默认操作"""
        self.operations['subtitle'] = self._op_add_subtitle
        self.operations['filter'] = self._op_apply_filter
        self.operations['crop'] = self._op_smart_crop
        self.operations['resize'] = self._op_resize
        self.operations['watermark'] = self._op_add_watermark
        self.operations['trim'] = self._op_trim
        self.operations['speed'] = self._op_change_speed
        self.operations['extract_audio'] = self._op_extract_audio
    
    def register_operation(self, name: str, func: Callable):
        """注册自定义操作"""
        self.operations[name] = func
    
    def process_batch(self, tasks: list, pipeline: ProcessingPipeline) -> dict:
        """批量处理视频"""
        results = {
            'total': len(tasks),
            'success': 0,
            'failed': 0,
            'details': []
        }
        
        if pipeline.parallel:
            with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
                futures = {}
                for task in tasks:
                    future = executor.submit(
                        self._process_single, task, pipeline
                    )
                    futures[future] = task
                
                for future in as_completed(futures):
                    task = futures[future]
                    try:
                        result = future.result()
                        results['success'] += 1
                        results['details'].append({
                            'input': task.input_path,
                            'status': 'success',
                            'output': task.output_path
                        })
                    except Exception as e:
                        results['failed'] += 1
                        results['details'].append({
                            'input': task.input_path,
                            'status': 'failed',
                            'error': str(e)
                        })
                    print(f"  [{'✓' if results['details'][-1]['status'] == 'success' else '✗'}] "
                          f"{os.path.basename(task.input_path)}")
        else:
            for task in tasks:
                try:
                    self._process_single(task, pipeline)
                    results['success'] += 1
                    results['details'].append({
                        'input': task.input_path,
                        'status': 'success',
                        'output': task.output_path
                    })
                    print(f"  [✓] {os.path.basename(task.input_path)}")
                except Exception as e:
                    results['failed'] += 1
                    results['details'].append({
                        'input': task.input_path,
                        'status': 'failed',
                        'error': str(e)
                    })
                    print(f"  [✗] {os.path.basename(task.input_path)}: {e}")
        
        return results
    
    def _process_single(self, task: ProcessingTask, 
                        pipeline: ProcessingPipeline):
        """处理单个视频"""
        current_input = task.input_path
        
        for step in pipeline.steps:
            op_name = step.get('operation')
            op_params = step.get('params', {})
            
            if op_name not in self.operations:
                raise ValueError(f"未知操作: {op_name}")
            
            # 中间步骤使用临时文件
            if step != pipeline.steps[-1]:
                temp_output = current_input + f".temp_{op_name}.mp4"
            else:
                temp_output = task.output_path
            
            self.operations[op_name](current_input, temp_output, **op_params)
            
            # 清理临时文件
            if current_input != task.input_path and os.path.exists(current_input):
                os.remove(current_input)
            
            current_input = temp_output
        
        task.status = "completed"
    
    # ===== 内置操作实现 =====
    
    def _op_add_subtitle(self, input_path: str, output_path: str,
                         srt_path: str = "", **kwargs):
        """添加字幕操作"""
        import subprocess
        cmd = [
            'ffmpeg', '-i', input_path,
            '-vf', f"subtitles={srt_path}",
            '-c:a', 'copy', '-y', output_path
        ]
        subprocess.run(cmd, capture_output=True, check=True)
    
    def _op_apply_filter(self, input_path: str, output_path: str,
                         filter_name: str = "cinematic", **kwargs):
        """应用滤镜操作"""
        processor = VideoFilterProcessor()
        processor.apply_filter_to_video(input_path, output_path, filter_name)
    
    def _op_smart_crop(self, input_path: str, output_path: str,
                       ratio: float = 9/16, **kwargs):
        """智能裁剪操作"""
        cropper = SmartCropper()
        cropper.smart_crop_video(input_path, output_path, ratio)
    
    def _op_resize(self, input_path: str, output_path: str,
                   width: int = 1920, height: int = 1080, **kwargs):
        """调整尺寸操作"""
        import subprocess
        cmd = [
            'ffmpeg', '-i', input_path,
            '-vf', f'scale={width}:{height}:force_original_aspect_ratio=decrease,'
                   f'pad={width}:{height}:(ow-iw)/2:(oh-ih)/2',
            '-c:a', 'copy', '-y', output_path
        ]
        subprocess.run(cmd, capture_output=True, check=True)
    
    def _op_add_watermark(self, input_path: str, output_path: str,
                          text: str = "© 2024", position: str = "bottom-right",
                          **kwargs):
        """添加水印操作"""
        import subprocess
        
        positions = {
            'top-left': 'x=20:y=20',
            'top-right': 'x=w-tw-20:y=20',
            'bottom-left': 'x=20:y=h-th-20',
            'bottom-right': 'x=w-tw-20:y=h-th-20',
            'center': 'x=(w-tw)/2:y=(h-th)/2'
        }
        
        pos = positions.get(position, positions['bottom-right'])
        
        cmd = [
            'ffmpeg', '-i', input_path,
            '-vf', f"drawtext=text='{text}':fontsize=24:fontcolor=white@"
                   f"0.5:{pos}:shadowcolor=black@0.5:shadowx=2:shadowy=2",
            '-c:a', 'copy', '-y', output_path
        ]
        subprocess.run(cmd, capture_output=True, check=True)
    
    def _op_trim(self, input_path: str, output_path: str,
                 start: float = 0, end: float = None, **kwargs):
        """裁剪时长操作"""
        import subprocess
        cmd = ['ffmpeg', '-ss', str(start), '-i', input_path]
        if end:
            cmd.extend(['-to', str(end)])
        cmd.extend(['-c', 'copy', '-y', output_path])
        subprocess.run(cmd, capture_output=True, check=True)
    
    def _op_change_speed(self, input_path: str, output_path: str,
                         speed: float = 1.0, **kwargs):
        """变速操作"""
        import subprocess
        video_filter = f"setpts={1/speed}*PTS"
        audio_filter = f"atempo={min(2.0, max(0.5, speed))}"
        
        # 对于极端速度,链式使用atempo
        if speed > 2.0:
            factors = []
            remaining = speed
            while remaining > 2.0:
                factors.append(2.0)
                remaining /= 2.0
            factors.append(remaining)
            audio_filter = ','.join(f'atempo={f}' for f in factors)
        elif speed < 0.5:
            factors = []
            remaining = speed
            while remaining < 0.5:
                factors.append(0.5)
                remaining /= 0.5
            factors.append(remaining)
            audio_filter = ','.join(f'atempo={f}' for f in factors)
        
        cmd = [
            'ffmpeg', '-i', input_path,
            '-filter_complex', f'[0:v]{video_filter}[v];[0:a]{audio_filter}[a]',
            '-map', '[v]', '-map', '[a]',
            '-y', output_path
        ]
        subprocess.run(cmd, capture_output=True, check=True)
    
    def _op_extract_audio(self, input_path: str, output_path: str,
                          format: str = "mp3", **kwargs):
        """提取音频操作"""
        import subprocess
        cmd = [
            'ffmpeg', '-i', input_path,
            '-vn', '-acodec', 'libmp3lame' if format == 'mp3' else 'copy',
            '-y', output_path
        ]
        subprocess.run(cmd, capture_output=True, check=True)


# 使用示例
def demo_batch_processing():
    """批量处理示例"""
    
    processor = VideoBatchProcessor(max_workers=4)
    
    # 定义处理流水线
    pipeline = ProcessingPipeline(
        name="social_media_pipeline",
        steps=[
            {'operation': 'crop', 'params': {'ratio': 9/16}},
            {'operation': 'filter', 'params': {'filter_name': 'cinematic'}},
            {'operation': 'resize', 'params': {'width': 1080, 'height': 1920}},
            {'operation': 'watermark', 'params': {'text': '@MyChannel', 'position': 'top-right'}},
        ],
        parallel=True,
        max_workers=4
    )
    
    # 创建任务列表
    video_dir = "./raw_videos"
    output_dir = "./processed_videos"
    os.makedirs(output_dir, exist_ok=True)
    
    tasks = []
    for filename in os.listdir(video_dir):
        if filename.endswith(('.mp4', '.avi', '.mov')):
            tasks.append(ProcessingTask(
                input_path=os.path.join(video_dir, filename),
                output_path=os.path.join(output_dir, f"processed_{filename}"),
            ))
    
    # 执行批量处理
    print(f"开始批量处理 {len(tasks)} 个视频...")
    results = processor.process_batch(tasks, pipeline)
    
    print(f"\n处理完成:")
    print(f"  成功: {results['success']}")
    print(f"  失败: {results['failed']}")
    print(f"  总计: {results['total']}")

# demo_batch_processing()

十一、FFmpeg + AI Pipeline 实战组合

11.1 完整的视频处理流水线

"""
FFmpeg + AI 完整视频处理流水线
将多个AI能力串联为自动化工作流
"""

import os
import subprocess
import json
from datetime import datetime

class VideoProductionPipeline:
    """视频生产流水线"""
    
    def __init__(self, config: dict):
        self.config = config
        self.temp_dir = config.get('temp_dir', './temp')
        os.makedirs(self.temp_dir, exist_ok=True)
    
    def produce(self, input_video: str, script: str = None,
                output_path: str = None) -> dict:
        """
        完整视频制作流水线
        
        Args:
            input_video: 原始视频路径
            script: 配音脚本(可选)
            output_path: 最终输出路径
        
        Returns:
            各阶段产出文件路径
        """
        results = {'input': input_video}
        
        # Step 1: 智能裁剪
        print("[1/7] 智能裁剪...")
        cropped = self._step_smart_crop(input_video)
        results['cropped'] = cropped
        
        # Step 2: 风格滤镜
        print("[2/7] 应用风格滤镜...")
        filtered = self._step_apply_filter(cropped)
        results['filtered'] = filtered
        
        # Step 3: 自动字幕
        print("[3/7] 生成自动字幕...")
        srt_path = self._step_generate_subtitles(filtered)
        results['subtitles'] = srt_path
        
        # Step 4: 字幕烧录
        print("[4/7] 烧录字幕...")
        subtitled = self._step_burn_subtitles(filtered, srt_path)
        results['subtitled'] = subtitled
        
        # Step 5: AI配音(如果有脚本)
        if script:
            print("[5/7] 生成AI配音...")
            voiced = self._step_add_voiceover(subtitled, script)
            results['voiced'] = voiced
        else:
            voiced = subtitled
            print("[5/7] 跳过配音(无脚本)")
        
        # Step 6: 提取精彩片段
        print("[6/7] 提取精彩片段...")
        highlights = self._step_extract_highlights(voiced)
        results['highlights'] = highlights
        
        # Step 7: 最终输出
        print("[7/7] 生成最终输出...")
        final = self._step_final_output(voiced, output_path)
        results['final'] = final
        
        print("\n✓ 视频制作完成!")
        for key, path in results.items():
            if path and os.path.exists(str(path)):
                size = os.path.getsize(path) / 1024 / 1024
                print(f"  {key}: {path} ({size:.1f}MB)")
        
        return results
    
    def _step_smart_crop(self, input_path: str) -> str:
        """智能裁剪步骤"""
        output = os.path.join(self.temp_dir, "01_cropped.mp4")
        target = self.config.get('target_ratio', 16/9)
        
        cropper = SmartCropper()
        cropper.smart_crop_video(input_path, output, target,
                                self.config.get('content_type', 'landscape'))
        return output
    
    def _step_apply_filter(self, input_path: str) -> str:
        """滤镜步骤"""
        output = os.path.join(self.temp_dir, "02_filtered.mp4")
        filter_name = self.config.get('filter', 'cinematic')
        
        processor = VideoFilterProcessor()
        processor.apply_filter_to_video(input_path, output, filter_name)
        return output
    
    def _step_generate_subtitles(self, input_path: str) -> str:
        """字幕生成步骤"""
        srt_path = os.path.join(self.temp_dir, "03_subtitles.srt")
        
        generator = SubtitleGenerator(
            model_size=self.config.get('whisper_model', 'medium')
        )
        result = generator.transcribe(input_path, language='zh')
        generator.generate_srt(result, srt_path)
        return srt_path
    
    def _step_burn_subtitles(self, input_path: str, srt_path: str) -> str:
        """字幕烧录步骤"""
        output = os.path.join(self.temp_dir, "04_subtitled.mp4")
        
        SubtitleBurner.burn_srt(
            input_path, srt_path, output,
            font_size=self.config.get('subtitle_font_size', 24)
        )
        return output
    
    def _step_add_voiceover(self, input_path: str, script: str) -> str:
        """配音步骤"""
        output = os.path.join(self.temp_dir, "05_voiceover.mp4")
        
        tts = AIVoiceGenerator(engine='edge-tts')
        vo_gen = VoiceOverGenerator(tts)
        
        # 解析脚本
        script_data = json.loads(script) if isinstance(script, str) else script
        
        voiceover_files = vo_gen.generate_voiceover_from_script(
            script_data, 
            os.path.join(self.temp_dir, 'voiceovers'),
            voice=self.config.get('voice', 'zh-CN-YunxiNeural')
        )
        
        vo_gen.merge_voiceover_with_video(
            input_path, voiceover_files, output,
            bgm_path=self.config.get('bgm_path'),
            bgm_volume=self.config.get('bgm_volume', 0.15)
        )
        return output
    
    def _step_extract_highlights(self, input_path: str) -> str:
        """精彩片段提取步骤"""
        output = os.path.join(self.temp_dir, "06_highlights.mp4")
        
        summarizer = VideoSummarizer()
        segments = summarizer.analyze_video(input_path, sample_interval=2.0)
        highlights = summarizer.extract_highlights(
            segments,
            target_duration=self.config.get('highlight_duration', 60)
        )
        summarizer.generate_summary_video(input_path, output, highlights)
        return output
    
    def _step_final_output(self, input_path: str, output_path: str = None) -> str:
        """最终输出步骤"""
        if output_path is None:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            output_path = f"./output/final_{timestamp}.mp4"
        
        os.makedirs(os.path.dirname(output_path), exist_ok=True)
        
        # 最终编码优化
        cmd = [
            'ffmpeg', '-i', input_path,
            '-c:v', 'libx264',
            '-preset', 'medium',
            '-crf', '23',
            '-c:a', 'aac',
            '-b:a', '128k',
            '-movflags', '+faststart',  # 优化网络播放
            '-y', output_path
        ]
        
        subprocess.run(cmd, capture_output=True, check=True)
        return output_path


# 使用示例
def demo_production_pipeline():
    """演示完整制作流水线"""
    
    config = {
        'target_ratio': 16/9,
        'content_type': 'landscape',
        'filter': 'cinematic',
        'whisper_model': 'medium',
        'subtitle_font_size': 24,
        'voice': 'zh-CN-YunxiNeural',
        'highlight_duration': 120,
        'temp_dir': './pipeline_temp',
    }
    
    # 配音脚本(可选)
    script = [
        {"text": "欢迎观看本期视频", "start": 0.0, "end": 3.0},
        {"text": "今天我们来聊聊AI技术的最新进展", "start": 3.0, "end": 7.0},
        {"text": "首先让我们看看大语言模型的发展", "start": 7.0, "end": 11.0},
    ]
    
    pipeline = VideoProductionPipeline(config)
    results = pipeline.produce(
        input_video="raw_footage.mp4",
        script=script,
        output_path="./output/final_video.mp4"
    )

# demo_production_pipeline()

十二、最佳实践与进阶建议

12.1 性能优化

  1. GPU加速:使用CUDA加速视频编解码
# FFmpeg NVIDIA GPU加速
cmd = ['ffmpeg', '-hwaccel', 'cuda', '-i', input_path, ...]

# PyTorch模型GPU推理
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = model.to(device)
  1. 批量帧处理:避免逐帧I/O,使用批量推理
# 批量处理而非逐帧
batch_size = 16
for i in range(0, len(frames), batch_size):
    batch = frames[i:i+batch_size]
    results = model(batch)  # 批量推理
  1. 流水线并行:使用多线程/多进程并行处理不同阶段
import multiprocessing as mp

def parallel_pipeline(input_files, num_workers=4):
    with mp.Pool(num_workers) as pool:
        results = pool.map(process_single_video, input_files)
    return results
  1. 内存管理:大视频分段处理,及时释放资源
import gc

def process_large_video(video_path, chunk_duration=60):
    """分段处理大视频"""
    # ... 按chunk_duration秒分段处理
    gc.collect()  # 每段处理后释放内存

12.2 常见问题

Q: Whisper中文识别准确率不高? A: 使用mediumlarge模型,并指定language="zh"

model = whisper.load_model("large")
result = model.transcribe("audio.mp3", language="zh")

Q: FFmpeg字幕烧录中文乱码? A: 确保字幕文件编码为UTF-8,并指定字体:

ffmpeg -i input.mp4 -vf "subtitles=sub.srt:force_style='FontName=Microsoft YaHei'" output.mp4

Q: 视频风格迁移速度太慢? A: 降低num_steps参数,或使用快速滤镜替代:

# 原始:100步优化
transfer.apply_to_frame(frame, num_steps=100)
# 快速:30步(质量略降但速度快3倍)
transfer.apply_to_frame(frame, num_steps=30)

Q: 批量处理时内存溢出? A: 减少并行数,或使用流式处理:

# 降低并行度
pipeline = ProcessingPipeline(max_workers=2)
# 或改用串行处理
pipeline = ProcessingPipeline(parallel=False)

十三、总结

AI视频编辑与后期制作技术正在快速成熟,从"辅助工具"进化为"自动化流水线"。核心要点:

  1. 字幕是基础:Whisper + SRT/ASS是最成熟的AI视频应用,投入产出比最高
  2. 滤镜要快:实时滤镜用OpenCV,高质量风格迁移用神经网络(但慢)
  3. 智能裁剪:人脸检测 + 显著性检测是跨平台适配的关键
  4. 物体移除:OpenCV修复最快,Stable Diffusion效果最好,LaMa是平衡方案
  5. 批量为王:构建可复用的处理流水线,一次配置,批量执行
  6. FFmpeg为骨:所有视频操作最终都通过FFmpeg执行,它是不可替代的基础设施

掌握这套技术栈,你可以为团队构建一个"输入原始素材,输出成品视频"的全自动视频工厂,将传统需要数小时的后期制作压缩到分钟级别。


本教程配套完整代码已开源,欢迎Star和PR。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录