上一篇方案的架构方向正确,但深入调研后发现 5 个硬伤 + 8 个盲区,本篇逐一修复。


🚨 一、硬伤修复(不做会死)

硬伤 1:Unity Recorder 在 batchmode 下无法工作

问题: 原方案 P0 阶段用 -batchmode -nographics 做批量渲染,但 Unity Recorder 官方明确说明:

Recorder 在 -batchmode 模式下永远不会启动录制,因为图形管线不会被初始化,帧捕获机制无法生效。

这是底层架构限制,没有官方 workaround。

影响: 原方案第十节"批量渲染方案"中的 headless 渲染路线完全不可行

修复方案:三选一

方案 原理 优点 缺点
A. 虚拟显示 + Editor 窗口模式 Linux 上用 Xvfb 创建虚拟显示器,Unity 以窗口模式运行(非 batchmode),Recorder 正常工作 最接近原方案,代码改动最小 仅限 Linux,需配置虚拟显示
B. Render Texture + 自定义帧捕获 不用 Recorder,改用 Camera → RenderTexture → 逐帧读取像素 → FFmpeg 编码 跨平台,可脱离 Recorder,可在 batchmode 下工作 需要自己写帧捕获管线,音视频需后期合成
C. Unity Player(非 Editor)+ 自定义录制 打包成 Standalone Player,内置录制逻辑,用 -batchmode -screen-width 1920 -screen-height 1080 运行 最稳定,性能最好 打包耗时,调试不便,Renderer 不可用需替换

推荐:方案 B(Render Texture + FFmpeg)

/// <summary>
/// 替代 Unity Recorder 的自定义帧捕获方案
/// 支持 batchmode 下运行
/// </summary>
public class CustomFrameCapture : MonoBehaviour
{
    [SerializeField] private Camera renderCamera;
    [SerializeField] private int width = 1920;
    [SerializeField] private int height = 1080;
    [SerializeField] private int frameRate = 30;
    
    private RenderTexture renderTexture;
    private Texture2D readbackTexture;
    private bool isCapturing;
    private int frameIndex;
    private string outputFolder;
    private Process ffmpegProcess;
    
    // 音频捕获
    private AudioSource audioSource;
    private List<float> audioSamples = new();
    
    public void StartCapture(string outputPath)
    {
        outputFolder = outputPath;
        System.IO.Directory.CreateDirectory(outputFolder);
        
        // 创建 Render Texture
        renderTexture = new RenderTexture(width, height, 24, RenderTextureFormat.ARGB32);
        renderCamera.targetTexture = renderTexture;
        readbackTexture = new Texture2D(width, height, TextureFormat.RGB24, false);
        
        // 启动 FFmpeg 管道(接收原始帧数据,输出 MP4)
        StartFFmpeg(outputPath + "/output.mp4");
        
        isCapturing = true;
        frameIndex = 0;
        
        // 固定帧率
        Time.captureDeltaTime = 1f / frameRate;
    }
    
    private void StartFFmpeg(string outputPath)
    {
        var startInfo = new ProcessStartInfo
        {
            FileName = "ffmpeg",
            Arguments = $"-y -f rawvideo -pix_fmt rgb24 " +
                       $"-s {width}x{height} -r {frameRate} " +
                       $"-i - -i {outputFolder}/audio.wav " +  // 后期合成音频
                       $"-c:v libx264 -preset fast -crf 18 " +
                       $"-c:a aac -b:a 192k " +
                       $"-pix_fmt yuv420p {outputPath}",
            UseShellExecute = false,
            RedirectStandardInput = true,
            CreateNoWindow = true
        };
        ffmpegProcess = Process.Start(startInfo);
    }
    
    private void LateUpdate()
    {
        if (!isCapturing) return;
        
        // 从 Render Texture 读取像素
        RenderTexture.active = renderTexture;
        readbackTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
        readbackTexture.Apply();
        RenderTexture.active = null;
        
        // 写入 FFmpeg 管道
        byte[] frameData = readbackTexture.GetRawTextureData();
        ffmpegProcess.StandardInput.BaseStream.Write(frameData, 0, frameData.Length);
        
        frameIndex++;
    }
    
    public void StopCapture()
    {
        isCapturing = false;
        Time.captureDeltaTime = 0;
        renderCamera.targetTexture = null;
        
        // 关闭 FFmpeg
        ffmpegProcess.StandardInput.Close();
        ffmpegProcess.WaitForExit(30000);
        
        Debug.Log($"录制完成:{frameIndex} 帧,输出:{outputFolder}/output.mp4");
    }
}

批量渲染修正脚本:

#!/bin/bash
# render_episode.sh — 修正后的批量渲染(不再使用 -batchmode -nographics)
# 使用虚拟显示 + 窗口模式

# Linux: 使用 Xvfb 虚拟显示
Xvfb :99 -screen 0 1920x1080x24 &
export DISPLAY=:99

for i in $(seq -w 1 10); do
  /path/to/Unity \
    -projectPath /path/to/AutoAnimeProject \
    -executeMethod BatchRenderer.Render \
    -episodeId "E${i}" \
    -logFile "Output/E${i}/render.log" \
    -screen-width 1920 \
    -screen-height 1080 \
    -quit
  
  echo "Episode E${i} rendered."
done

# 清理虚拟显示
kill %1
# Windows: 无需虚拟显示,直接窗口模式
# render_episode.ps1
for ($i=1; $i -le 10; $i++) {
    $id = "{0:D2}" -f $i
    & "C:\Program Files\Unity\Hub\Editor\2022.3.X\Editor\Unity.exe" `
        -projectPath "C:\Projects\AutoAnimeProject" `
        -executeMethod BatchRenderer.Render `
        -episodeId "E${id}" `
        -logFile "Output\E${id}\render.log" `
        -screen-width 1920 `
        -screen-height 1080 `
        -quit
    Write-Host "Episode E${id} rendered."
}

硬伤 2:Unity MCP 单连接限制——OpenClaw 和 Hermes 不能同时连

问题: Unity MCP 同一时刻只接受一个 TCP 客户端连接。原方案中 OpenClaw 和 Hermes 都需要通过 MCP 操作 Unity,但无法同时连接。

影响: 双 Agent 协作时存在连接冲突风险。

修复方案:OpenClaw 作为唯一 MCP 客户端,Hermes 通过 ACP 委托

用户 → OpenClaw(唯一 MCP 客户端)
         │
         ├── 简单任务 → OpenClaw 直接操作 Unity
         │
         └── 需要创意 → 通过 ACP 委托 Hermes
                         │
                         ├── Hermes 生成剧本/分镜
                         ├── 返回 JSON 给 OpenClaw
                         └── OpenClaw 拿着 JSON 操作 Unity
                         │
                         (Hermes 从不直接连 Unity)

关键架构修正:

原方案(有冲突):
  Hermes ←MCP→ Unity
  OpenClaw ←MCP→ Unity  ← 冲突!

修正方案(无冲突):
  OpenClaw ←MCP→ Unity(唯一连接)
  OpenClaw ←ACP→ Hermes(Agent 间通信)
  Hermes 不直连 Unity

ACP 协议配置(OpenClaw ↔ Hermes 官方协作方式):

# 1. 启动 Hermes ACP 模式
hermes acp

# 2. 在 OpenClaw 中配置 ACP 连接
# 编辑 ~/.openclaw/config.yaml
agents:
  hermes:
    type: acp
    command: "hermes acp"
    timeout: 300  # 超时秒数

# 3. OpenClaw 自动管理 Hermes 子进程
# 当需要创意内容时,OpenClaw 通过 ACP 向 Hermes 发送任务
# Hermes 完成后返回结果,OpenClaw 再操作 Unity

OpenClaw Skill 修正(使用 ACP):

# 在 auto-anime Skill 中修正调用方式

### 2. 调用 Hermes 生成剧本和分镜
通过 ACP 协议委托 Hermes:
```bash
# 方式 A:通过 OpenClaw 的 ACP agent 路由
# 在 OpenClaw 对话中直接说:
@hermes 请根据以下创意生成10集动画剧本和分镜JSON:{用户输入}

# 方式 B:通过 shell 调用 Hermes Coordinator
~/.hermes/hermes-agent/venv/bin/python3 \
  scripts/hermes_coordinator.py \
  ask "生成动画剧本:{用户输入}" \
  --timeout 300

重要:Hermes 只负责创意生成,不操作 Unity

所有 Unity 操作统一由 OpenClaw 通过 MCP 执行


---

### 硬伤 3:MCP 每次调用有 1–3 秒编译延迟

**问题:** Unity MCP 的 `execute_editor_code` 每次调用都要经过 AssemblyBuilder 编译,耗时 1–3 秒。一个 10 集项目可能需要几百次调用,累计延迟可达分钟级。

**影响:** 交互体验差,自动化流程变慢。

**修复方案:预编译 + 批量指令**

```csharp
/// <summary>
/// 预编译的 Unity 操作桥接器
/// 启动时一次性编译,后续通过轻量 JSON 指令调用
/// 避免 MCP 每次调用都触发 AssemblyBuilder
/// </summary>
public class AnimationBridge : MonoBehaviour
{
    private static AnimationBridge instance;
    public static AnimationBridge Instance => instance;
    
    [SerializeField] private AutoDirector director;
    [SerializeField] private AssetCatalog catalog;
    [SerializeField] private CustomFrameCapture recorder;
    
    private HttpListener httpListener;
    
    private void Awake()
    {
        instance = this;
    }
    
    /// <summary>
    /// 启动 HTTP 监听(替代 MCP 的高频调用)
    /// MCP 只在启动时调用一次 StartServer()
    /// 后续所有操作通过 HTTP JSON 指令完成,无编译开销
    /// </summary>
    public void StartServer(int port = 9090)
    {
        httpListener = new HttpListener();
        httpListener.Prefixes.Add($"http://127.0.0.1:{port}/");
        httpListener.Start();
        httpListener.BeginGetContext(OnRequest, null);
        Debug.Log($"AnimationBridge started on port {port}");
    }
    
    private void OnRequest(IAsyncResult result)
    {
        var context = httpListener.EndGetContext(result);
        httpListener.BeginGetContext(OnRequest, null); // 继续监听
        
        using var reader = new StreamReader(context.Request.InputStream);
        string body = reader.ReadToEnd();
        
        string response = HandleCommand(body);
        
        var buffer = Encoding.UTF8.GetBytes(response);
        context.Response.ContentType = "application/json";
        context.Response.ContentLength64 = buffer.Length;
        context.Response.OutputStream.Write(buffer, 0, buffer.Length);
        context.Response.Close();
    }
    
    private string HandleCommand(string json)
    {
        try
        {
            var cmd = JsonUtility.FromJson<BridgeCommand>(json);
            
            switch (cmd.action)
            {
                case "build_timeline":
                    director.BuildTimelineFromJson(cmd.data);
                    return "{\"status\":\"ok\",\"message\":\"Timeline built\"}";
                    
                case "start_recording":
                    recorder.StartCapture(cmd.outputPath);
                    return "{\"status\":\"ok\",\"message\":\"Recording started\"}";
                    
                case "stop_recording":
                    recorder.StopCapture();
                    return "{\"status\":\"ok\",\"message\":\"Recording stopped\"}";
                    
                case "get_catalog":
                    return JsonUtility.ToJson(catalog);
                    
                case "validate_assets":
                    return ValidateAssets(cmd.data);
                    
                case "preview_shot":
                    return PreviewShot(cmd.data);
                    
                case "health_check":
                    return "{\"status\":\"ok\",\"unity_version\":\"" + Application.unityVersion + "\"}";
                    
                default:
                    return "{\"status\":\"error\",\"message\":\"Unknown action: " + cmd.action + "\"}";
            }
        }
        catch (Exception e)
        {
            return $"{{\"status\":\"error\",\"message\":\"{e.Message}\"}}";
        }
    }
    
    [System.Serializable]
    private class BridgeCommand
    {
        public string action;
        public string data;
        public string outputPath;
    }
}

修正后的操作流:

1. MCP 调用一次:启动 AnimationBridge HTTP 服务(~3 秒编译)
2. 后续所有操作通过 HTTP POST(毫秒级响应):
   - POST http://127.0.0.1:9090  {"action":"build_timeline","data":"{...}"}
   - POST http://127.0.0.1:9090  {"action":"start_recording","outputPath":"./Output/E01"}
   - POST http://127.0.0.1:9090  {"action":"stop_recording"}
3. MCP 仅用于:启动/关闭 Unity 项目、安装包等低频操作

硬伤 4:URP 2D 项目不能用 Targeted Camera 录制

问题: Unity Recorder 在 URP 2D 渲染器下无法使用 Targeted Camera 模式录制,因为 URP 2D 缺少 Recorder 所需的捕获通道。

影响: 原方案使用 Cinemachine + Targeted Camera 的录制路线在 2D 项目中不可行。

修复方案:使用 Game View 录制 + Render Texture 方案

/// <summary>
/// 2D 项目的录制配置
/// 不使用 Targeted Camera,改用 Game View 录制
/// </summary>
public class RecorderConfig2D : MonoBehaviour
{
    // Cinachine 仍然可用于运镜效果
    // 但录制源改为 Game View 或 Render Texture
    
    [SerializeField] private Camera mainCamera;
    [SerializeField] private CustomFrameCapture frameCapture;
    
    /// <summary>
    /// 配置 2D 项目的录制管线
    /// </summary>
    public void ConfigureFor2D()
    {
        // 1. 确保使用正交相机(2D 项目)
        mainCamera.orthographic = true;
        mainCamera.orthographicSize = 5.4f; // 1080p 下约 5.4 单位半高
        
        // 2. 使用 Render Texture 方案(替代 Recorder)
        // CustomFrameCapture 已在硬伤1中实现
        
        // 3. 如仍要用 Unity Recorder,改为 Game View 录制
        // var settings = ScriptableObject.CreateInstance<MP4RecorderSettings>();
        // settings.RecorderInputSettings.InputType = ImageInputType.GameView;
    }
}

硬伤 5:Timeline 编程创建后 UI 不会自动刷新

问题: 通过 TimelineAsset.CreateTrack<>() 等 API 编程创建 Timeline 后,编辑器的 Timeline 窗口不会自动更新,看起来像是没有创建成功。

影响: 调试困难,预览时可能看到空白 Timeline。

修复方案:

/// <summary>
/// Timeline 创建后强制刷新编辑器
/// </summary>
public static class TimelineRefreshHelper
{
#if UNITY_EDITOR
    public static void RefreshTimelineWindow()
    {
        // 强制刷新 Timeline 编辑器窗口
        UnityEditor.Timeline.TimelineEditor.Refresh(
            UnityEditor.Timeline.RefreshReason.ContentsAddedToTimeline |
            UnityEditor.Timeline.RefreshReason.SceneNeedsUpdate
        );
        
        // 如果上面的方法不生效,切换选中对象强制刷新
        var go = UnityEditor.Selection.activeGameObject;
        UnityEditor.Selection.activeGameObject = null;
        UnityEditor.Selection.activeGameObject = go;
    }
#endif
}

🔍 二、8 大盲区补充

盲区 1:TTS 配音集成——音频不能只靠自备

问题: 原方案假设所有配音都自备,但 10 集×每集 5-10 句台词 = 50-100 条配音,手工录制不现实。

补充方案:集成 TTS 自动配音

优先级:
1. 自备配音(有 → 直接用)
2. TTS 生成(无 → 自动生成)
3. TTS + 音色克隆(有 1 条样本 → 克隆音色)

推荐 TTS 方案:

方案 质量 成本 适合
CosyVoice 2(阿里开源) ⭐⭐⭐⭐ 免费/自部署 中文首选
Fish Speech(开源) ⭐⭐⭐⭐ 免费/自部署 中英文
MiniMax T2A(API) ⭐⭐⭐⭐⭐ ¥0.1-0.5/条 最高质量
Edge TTS(免费) ⭐⭐⭐ 免费 快速验证

TTS 集成脚本:

# tts_bridge.py — OpenClaw/Hermes 调用的 TTS 桥接服务
from flask import Flask, request, jsonify
import subprocess
import os
import hashlib

app = Flask(__name__)

TTS_ENGINE = "cosyvoice"  # cosyvoice / fish_speech / minimax / edge
OUTPUT_DIR = "./Audio/Voice/"

@app.route("/tts", methods=["POST"])
def generate_voice():
    data = request.json
    text = data.get("text", "")
    speaker = data.get("speaker", "default")
    emotion = data.get("emotion", "neutral")
    
    if not text:
        return jsonify({"error": "text is required"}), 400
    
    # 生成唯一文件名
    filename = hashlib.md5(f"{speaker}_{text}".encode()).hexdigest()[:12]
    output_path = os.path.join(OUTPUT_DIR, f"{speaker}_{filename}.wav")
    
    if os.path.exists(output_path):
        # 缓存命中
        duration = get_audio_duration(output_path)
        return jsonify({
            "clipName": f"{speaker}_{filename}",
            "path": output_path,
            "duration": duration,
            "cached": True
        })
    
    # 调用 TTS 引擎
    if TTS_ENGINE == "cosyvoice":
        cmd = f"python cosyvoice_infer.py --text '{text}' --speaker {speaker} --output {output_path}"
    elif TTS_ENGINE == "edge":
        cmd = f"edge-tts --voice zh-CN-YunxiNeural --text '{text}' --write-media {output_path}"
    elif TTS_ENGINE == "minimax":
        cmd = f"python minimax_tts.py --text '{text}' --speaker {speaker} --output {output_path}"
    
    subprocess.run(cmd, shell=True, check=True)
    
    duration = get_audio_duration(output_path)
    
    return jsonify({
        "clipName": f"{speaker}_{filename}",
        "path": output_path,
        "duration": duration,
        "cached": False
    })

def get_audio_duration(path):
    """获取音频时长"""
    import wave
    with wave.open(path, 'rb') as f:
        frames = f.getnframes()
        rate = f.getframerate()
        return frames / float(rate)

if __name__ == "__main__":
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    app.run(host="0.0.0.0", port=9091)

Hermes Skill 修正:分镜生成时自动调 TTS 获取时长

### 分镜拆解步骤修正

3. 分镜拆解时:
   - 对每句台词调用 TTS 生成配音
   - 用返回的 duration 作为 dialogue.duration
   - 不再依赖用户手动标注时长
   - TTS 缓存:相同文本+角色不会重复生成

盲区 2:口型同步(Lip Sync)

问题: 有配音但没有口型同步,角色说话时嘴巴不动,观感极差。

补充方案:

方案 原理 难度 效果
A. 预制口型帧 Spine/2D Animation 预制 talk 动画,配音播放时自动循环 中等
B. 音素驱动 分析配音音频提取音素(phoneme),映射到口型帧 ⭐⭐⭐
C. OVRLipSync Unity 插件,实时分析音频驱动口型 ⭐⭐

推荐方案 A+B 混合:

/// <summary>
/// 简易口型同步:播放配音时自动播放说话动画
/// </summary>
public class LipSyncController : MonoBehaviour
{
    [SerializeField] private Animator characterAnimator;
    [SerializeField] private AudioSource audioSource;
    
    private int talkHash = Animator.StringToHash("Talk");
    private int mouthOpenHash = Animator.StringToHash("MouthOpen");
    
    private AudioClip currentClip;
    private float[] audioData;
    private int sampleRate;
    
    /// <summary>
    /// 播放配音 + 口型同步
    /// </summary>
    public void PlayDialogue(AudioClip clip)
    {
        currentClip = clip;
        audioSource.clip = clip;
        audioSource.Play();
        
        // 方案 A:直接触发 talk 动画
        characterAnimator.SetBool(talkHash, true);
        
        // 方案 B:实时分析音频振幅驱动嘴型
        StartCoroutine(LipSyncFromAudio());
    }
    
    private System.Collections.IEnumerator LipSyncFromAudio()
    {
        sampleRate = currentClip.frequency;
        audioData = new float[currentClip.samples * currentClip.channels];
        currentClip.GetData(audioData, 0);
        
        while (audioSource.isPlaying)
        {
            int currentSample = (int)(audioSource.time * sampleRate);
            float amplitude = GetAmplitudeAtSample(currentSample);
            
            // 映射振幅到嘴型开合度 0~1
            characterAnimator.SetFloat(mouthOpenHash, amplitude);
            
            yield return null;
        }
        
        characterAnimator.SetBool(talkHash, false);
        characterAnimator.SetFloat(mouthOpenHash, 0);
    }
    
    private float GetAmplitudeAtSample(int sampleIndex)
    {
        int windowSize = 1024;
        int start = Mathf.Max(0, sampleIndex - windowSize / 2);
        int end = Mathf.Min(audioData.Length, sampleIndex + windowSize / 2);
        
        float sum = 0;
        for (int i = start; i < end; i++)
        {
            sum += Mathf.Abs(audioData[i]);
        }
        
        return Mathf.Clamp01((sum / (end - start)) * 10f); // 放大映射
    }
}

盲区 3:素材校验管线

问题: 自备素材格式不规范(图层命名乱、PSB 分层不够、Spine 动画缺关键动画),导致自动化构建时崩溃。

补充方案:素材导入时自动校验

/// <summary>
/// 素材校验器:导入时自动检查格式规范
/// </summary>
public class AssetValidator
{
    public ValidationResult ValidateCharacter(string characterId, GameObject prefab)
    {
        var result = new ValidationResult { characterId = characterId };
        
        // 1. 检查是否有 Animator
        var animator = prefab.GetComponentInChildren<Animator>();
        if (animator == null)
        {
            result.errors.Add("缺少 Animator 组件");
        }
        
        // 2. 检查必需动画是否存在
        var requiredAnimations = new[] { "idle", "walk", "talk", "happy", "sad", "angry" };
        var runtimeAnimator = animator?.runtimeAnimatorController;
        if (runtimeAnimator != null)
        {
            foreach (var animName in requiredAnimations)
            {
                bool found = false;
                foreach (var clip in runtimeAnimator.animationClips)
                {
                    if (clip.name.ToLower().Contains(animName))
                    {
                        found = true;
                        break;
                    }
                }
                if (!found)
                {
                    result.warnings.Add($"缺少推荐动画: {animName}");
                }
            }
        }
        
        // 3. 检查 Sprite Skin(2D Animation)
        var spriteSkins = prefab.GetComponentsInChildren<SpriteSkin>();
        if (spriteSkins.Length == 0)
        {
            result.warnings.Add("未检测到 Sprite Skin,可能未绑定骨骼");
        }
        
        // 4. 检查 Spine(如果是 Spine 角色)
        var skeletonAnim = prefab.GetComponentInChildren<SkeletonAnimation>();
        if (skeletonAnim != null)
        {
            var skeletonData = skeletonAnim.Skeleton.Data;
            foreach (var animName in requiredAnimations)
            {
                if (skeletonData.FindAnimation(animName) == null)
                {
                    result.warnings.Add($"Spine 缺少动画: {animName}");
                }
            }
        }
        
        result.isValid = result.errors.Count == 0;
        return result;
    }
    
    public ValidationResult ValidateScene(string sceneId, Sprite bgSprite)
    {
        var result = new ValidationResult { sceneId = sceneId };
        
        if (bgSprite == null)
        {
            result.errors.Add("背景图 Sprite 为空");
        }
        else if (bgSprite.texture.width < 1920 || bgSprite.texture.height < 1080)
        {
            result.warnings.Add($"背景图分辨率不足:{bgSprite.texture.width}x{bgSprite.texture.height},建议 1920x1080+");
        }
        
        result.isValid = result.errors.Count == 0;
        return result;
    }
}

[System.Serializable]
public class ValidationResult
{
    public string characterId;
    public string sceneId;
    public bool isValid;
    public List<string> errors = new();
    public List<string> warnings = new();
    
    public string ToReport()
    {
        var sb = new StringBuilder();
        sb.AppendLine($"校验结果:{(isValid ? "✅ 通过" : "❌ 不通过")}");
        foreach (var e in errors) sb.AppendLine($"  🔴 错误:{e}");
        foreach (var w in warnings) sb.AppendLine($"  🟡 警告:{w}");
        return sb.ToString();
    }
}

集成到 AnimationBridge:

// 在 AnimationBridge.HandleCommand 中新增
case "validate_assets":
    var validator = new AssetValidator();
    var results = new List<ValidationResult>();
    foreach (var ch in catalog.characters)
    {
        results.Add(validator.ValidateCharacter(ch.id, ch.prefab));
    }
    return JsonUtility.ToJson(new { results });

盲区 4:视频后期合成管线

问题: 原方案只考虑了 Unity 内录制,忽略了:字幕烧录、音频混流、片头片尾、多集合并。

补充方案:FFmpeg 后期管线

# post_pipeline.py — 视频后期处理管线
import subprocess
import os

class PostPipeline:
    def __init__(self, ffmpeg_path="ffmpeg"):
        self.ffmpeg = ffmpeg_path
    
    def add_subtitles(self, video_path, srt_path, output_path):
        """烧录 SRT 字幕"""
        cmd = [
            self.ffmpeg, "-y",
            "-i", video_path,
            "-vf", f"subtitles={srt_path}:force_style='FontSize=24,PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,Outline=2'",
            "-c:a", "copy",
            output_path
        ]
        subprocess.run(cmd, check=True)
    
    def mix_audio(self, video_path, voice_path, bgm_path, output_path, 
                  voice_vol=1.0, bgm_vol=0.3):
        """混流:视频 + 配音 + BGM"""
        cmd = [
            self.ffmpeg, "-y",
            "-i", video_path,
            "-i", voice_path,
            "-i", bgm_path,
            "-filter_complex",
            f"[1:a]volume={voice_vol}[voice];"
            f"[2:a]volume={bgm_vol},afade=t=in:d=1,afade=t=out:st=28:d=2[bgm];"
            f"[voice][bgm]amix=inputs=2:duration=longest[aout]",
            "-map", "0:v", "-map", "[aout]",
            "-c:v", "copy", "-c:a", "aac", "-b:a", "192k",
            output_path
        ]
        subprocess.run(cmd, check=True)
    
    def concat_episodes(self, episode_paths, output_path):
        """合并多集"""
        list_file = "concat_list.txt"
        with open(list_file, "w") as f:
            for p in episode_paths:
                f.write(f"file '{p}'\n")
        
        cmd = [
            self.ffmpeg, "-y",
            "-f", "concat", "-safe", "0",
            "-i", list_file,
            "-c", "copy",
            output_path
        ]
        subprocess.run(cmd, check=True)
        os.remove(list_file)
    
    def add_intro_outro(self, video_path, intro_path, outro_path, output_path):
        """添加片头片尾"""
        concat_list = [intro_path, video_path, outro_path]
        self.concat_episodes(concat_list, output_path)

盲区 5:项目状态持久化与恢复

问题: 渲染到第 7 集时 Unity 崩溃了,所有进度丢失,无法从断点恢复。

补充方案:项目状态检查点

/// <summary>
/// 项目状态持久化:每集渲染前后保存检查点
/// </summary>
public class ProjectCheckpoint : MonoBehaviour
{
    private string checkpointDir = "Output/checkpoints/";
    
    [System.Serializable]
    public class ProjectState
    {
        public string projectId;
        public int totalEpisodes;
        public int completedEpisodes;
        public List<string> completedEpisodeIds = new();
        public List<string> failedEpisodeIds = new();
        public string lastError;
        public System.DateTime lastUpdate;
    }
    
    public void SaveCheckpoint(ProjectState state)
    {
        System.IO.Directory.CreateDirectory(checkpointDir);
        state.lastUpdate = System.DateTime.Now;
        string json = JsonUtility.ToJson(state, true);
        System.IO.File.WriteAllText(
            $"{checkpointDir}{state.projectId}.json", json);
    }
    
    public ProjectState LoadCheckpoint(string projectId)
    {
        string path = $"{checkpointDir}{projectId}.json";
        if (!System.IO.File.Exists(path))
            return null;
        
        string json = System.IO.File.ReadAllText(path);
        return JsonUtility.FromJson<ProjectState>(json);
    }
    
    /// <summary>
    /// 从断点恢复:跳过已完成的集数
    /// </summary>
    public List<string> GetRemainingEpisodes(string projectId, int totalEpisodes)
    {
        var state = LoadCheckpoint(projectId);
        var remaining = new List<string>();
        
        for (int i = 1; i <= totalEpisodes; i++)
        {
            string epId = $"E{i:D2}";
            if (state == null || !state.completedEpisodeIds.Contains(epId))
            {
                remaining.Add(epId);
            }
        }
        
        return remaining;
    }
}

盲区 6:字幕生成管线

问题: 原方案只提到字幕渲染,但没解决 SRT 文件生成。

补充方案:从分镜 JSON 自动生成 SRT

# subtitle_generator.py
import json

def generate_srt(episode_json_path, output_srt_path):
    """从分镜 JSON 生成 SRT 字幕文件"""
    with open(episode_json_path, 'r', encoding='utf-8') as f:
        episode = json.load(f)
    
    srt_lines = []
    index = 1
    
    for dialogue in episode.get("dialogues", []):
        if not dialogue.get("text"):
            continue
        
        start_time = format_srt_time(dialogue["startTime"])
        end_time = format_srt_time(dialogue["startTime"] + dialogue["duration"])
        speaker = dialogue.get("speaker", "")
        text = dialogue["text"]
        
        srt_lines.append(f"{index}")
        srt_lines.append(f"{start_time} --> {end_time}")
        srt_lines.append(f"【{speaker}{text}")
        srt_lines.append("")
        index += 1
    
    with open(output_srt_path, 'w', encoding='utf-8') as f:
        f.write('\n'.join(srt_lines))

def format_srt_time(seconds):
    """格式化为 SRT 时间格式 00:00:00,000"""
    h = int(seconds // 3600)
    m = int((seconds % 3600) // 60)
    s = int(seconds % 60)
    ms = int((seconds % 1) * 1000)
    return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"

盲区 7:多角色同屏的渲染层级管理

问题: 2D 动画中多角色同屏时,前后层级(sortOrder)和遮挡关系需要精确控制,否则会出现"远的人挡住近的人"。

补充方案:基于位置和角色优先级的自动排序

/// <summary>
/// 2D 角色自动排序:基于 Y 轴位置 + 角色优先级
/// </summary>
public class Auto2DSorter : MonoBehaviour
{
    /// <summary>
    /// 根据角色位置自动设置排序层级
    /// Y 值越小(越靠画面下方/越近)→ sortOrder 越大(渲染在前面)
    /// </summary>
    public void UpdateSortOrders(List<CharacterCueData> characters)
    {
        // 按 Y 坐标排序(Y 小的排后面 = 画面远处)
        var sorted = characters
            .OrderBy(c => c.positionFrom.y)
            .ToList();
        
        for (int i = 0; i < sorted.Count; i++)
        {
            sorted[i].sortOrder = i * 10; // 留 10 的间隔,方便插入道具
        }
    }
    
    /// <summary>
    /// 运行时动态更新排序
    /// </summary>
    public void UpdateRuntimeSortOrder(GameObject character, int sortOrder)
    {
        var renderers = character.GetComponentsInChildren<SpriteRenderer>();
        foreach (var r in renderers)
        {
            r.sortingOrder = sortOrder;
        }
        
        // Spine 角色
        var skeletonAnim = character.GetComponentInChildren<SkeletonAnimation>();
        if (skeletonAnim != null)
        {
            skeletonAnim.MeshRenderer.sortingOrder = sortOrder;
        }
    }
}

盲区 8:预览与审核机制

问题: 原方案直接录制输出,如果分镜有问题就要重新渲染,浪费时间。

补充方案:低分辨率快速预览 → 人工审核 → 正式渲染

完整流程修正:

1. Hermes 生成剧本+分镜 JSON
2. Unity 低分辨率快速预览(480p,2x 速度)
   → 截取关键帧截图(每镜头 1 张)
   → 生成预览 GIF
3. 用户在 IM 中审核
   → 通过 → 正式渲染(1080p)
   → 修改 → 回到步骤 1
/// <summary>
/// 快速预览模式:低分辨率 + 关键帧截图
/// </summary>
public class PreviewMode : MonoBehaviour
{
    [SerializeField] private AutoDirector director;
    
    /// <summary>
    /// 生成预览截图
    /// </summary>
    public List<string> GeneratePreviewShots(EpisodeData episode)
    {
        var screenshots = new List<string>();
        
        // 降低分辨率加速
        var originalSize = new Vector2Int(Screen.width, Screen.height);
        Screen.SetResolution(960, 540, false);
        
        foreach (var shot in episode.shots)
        {
            // 跳到镜头起始帧
            director.JumpToTime(shot.startTime);
            
            // 截图
            string path = $"Output/preview/shot_{shot.shotNumber:D3}.png";
            CaptureScreenshot(path);
            screenshots.Add(path);
        }
        
        // 恢复分辨率
        Screen.SetResolution(originalSize.x, originalSize.y, false);
        
        return screenshots;
    }
    
    /// <summary>
    /// 生成预览 GIF(用 FFmpeg)
    /// </summary>
    public string GeneratePreviewGif(string videoPath, int fps = 5)
    {
        string outputPath = videoPath.Replace(".mp4", "_preview.gif");
        var cmd = $"ffmpeg -y -i {videoPath} -vf \"fps={fps},scale=480:-1:flags=lanczos\" {outputPath}";
        System.Diagnostics.Process.Start("bash", $"-c \"{cmd}\"").WaitForExit();
        return outputPath;
    }
}

🏗️ 三、修正后的完整架构

┌─────────────────────────────────────────────────────────────────┐
│                        用户交互层                                │
│   Telegram / 飞书 / Discord / Web UI                            │
├─────────────────────────────────────────────────────────────────┤
│                    OpenClaw Agent                                │
│   · 消息路由 · Skill 匹配 · 记忆管理 · 心跳监控                │
│   · ACP 客户端(连 Hermes)· HTTP 客户端(连 Unity)           │
│   · 唯一 MCP 客户端(连 Unity)                                │
├──────────────────────┬──────────────────────────────────────────┤
│         ACP 协议     │(Hermes 从不直连 Unity)                 │
├──────────────────────▼──────────────────────────────────────────┤
│                    Hermes Agent                                  │
│   · 剧本生成 · 智能分镜 · Skill 自进化                          │
│   · TTS 调用(配音时长)· 风格学习                               │
├─────────────────────────────────────────────────────────────────┤
│            TTS 服务(CosyVoice / Edge TTS)                      │
├─────────────────────────────────────────────────────────────────┤
│         AnimationBridge HTTP 服务(替代高频 MCP 调用)           │
│   POST http://127.0.0.1:9090                                    │
│   · build_timeline · start_recording · validate_assets          │
│   · preview_shot · health_check · get_catalog                   │
├─────────────────────────────────────────────────────────────────┤
│                    Unity 3D 引擎                                 │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐     │
│   │2D Animation│ │ Timeline │ │ 2D IK   │ │Spine Runtime │     │
│   └──────────┘ └──────────┘ └──────────┘ └──────────────┘     │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐     │
│   │PSD Importer│ │Auto2DSort│ │LipSync  │ │CustomFrameCap│     │
│   └──────────┘ └──────────┘ └──────────┘ └──────────────┘     │
│   ┌──────────┐ ┌──────────┐ ┌──────────┐                      │
│   │AssetValid│ │PreviewMode│ │Checkpoint│                      │
│   └──────────┘ └──────────┘ └──────────┘                      │
├─────────────────────────────────────────────────────────────────┤
│                    FFmpeg 后期管线                                │
│   · 音视频混流 · 字幕烧录 · 片头片尾 · 多集合并                │
├─────────────────────────────────────────────────────────────────┤
│                    素材资产层(自备 + TTS 生成)                  │
│   角色 PSB/Spine · 场景 PNG · 道具 · BGM · SFX · 配音(WAV)     │
├─────────────────────────────────────────────────────────────────┤
│                    存储 & 基础设施                                │
│   检查点文件 · SRT 字幕 · 预览截图 · FFmpeg · Xvfb(Linux)      │
└─────────────────────────────────────────────────────────────────┘

📋 四、修正后的落地路线图

阶段 时间 目标 关键交付 修正点
P0 2 周 Unity 项目 + 手动出片 1 集手动 2D 动画 ✅ 用 CustomFrameCapture 而非 Recorder
P1 3 周 JSON 驱动 + AnimationBridge JSON → 自动构建 → 出片 ✅ HTTP Bridge 替代高频 MCP
P1.5 1 周 素材校验 + 预览 校验报告 + 预览截图 🆕 新增
P2 3 周 OpenClaw MCP + Skill IM 消息 → 自动出片 ✅ ACP 替代 HTTP 直连 Hermes
P3 2 周 Hermes 剧本/分镜 + TTS 一句话 → 10 集 🆕 TTS 配音集成
P3.5 1 周 口型同步 + 字幕 音画同步成片 🆕 LipSync + SRT
P4 2 周 批量渲染 + 检查点恢复 工业化产出 ✅ 不用 batchmode,用窗口模式
P5 2 周 FFmpeg 后期 + Skill 进化 完整成片交付 🆕 后期管线

总计:16 周(比原方案多 4 周,但每一周都在补真实存在的坑)


💰 五、修正后的成本估算

项目 月成本 备注
AI API(Hermes+OpenClaw) ¥200–400 剧本+分镜+调度
TTS(CosyVoice 自部署) ¥0 开源,自部署免费
TTS(MiniMax API 备选) ¥50–100 按需
服务器(部署 Hermes+TTS) ¥100–300 2核4G 腾讯云
FFmpeg 后期处理 ¥0 本地执行
合计 ¥300–800/月

素材自备 = 零图像生成成本。这仍然是最大的护城河。


✅ 六、能不能实现?坦率回答

能实现,但有前提

判断 说明
技术上完全可行 Unity 2D Animation + Timeline + Spine 都是成熟技术,编程构建 Timeline 有官方 API 支持
Agent 协作可行 OpenClaw + Hermes 已有 ACP 官方协作协议,不是猜测
最大风险不在技术 在于素材质量和规范程度——自备的素材如果 PSB 分层不够细、Spine 动画不全,自动化就会卡住

最大的 3 个风险

风险 概率 影响 缓解
素材不规范 🔴 高 自动化流程中断 P1.5 阶段加入素材校验管线,提前暴露问题
Timeline 编程构建踩坑 🟡 中 延期 1-2 周 P0 先手动做 1 集,摸清 Timeline 结构后再自动化
Agent 调试困难 🟡 中 排错时间翻倍 AnimationBridge 加详细日志 + health_check 接口

一句话结论

这个方向能做,但不要妄想 2 周出 MVP。给 4 个月,先把 P0(手动出片)→ P1(JSON 驱动出片)跑通,验证 Unity 自动化这条线真的走得通,再接 Agent。

第一步:今天打开 Unity,手动做 30 秒的 2D 骨骼动画。先证明"手动能做",再谈"自动"。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐