<title>剪影草稿导出功能</title>

# 剪影草稿导出功能

## 功能概述

视频工作流支持将时间轴导出为剪影（JianyingPro）草稿文件，方便用户在剪映中进一步编辑。

## 主要特性

### 1. 时间轴渲染组件支持

> 时间轴渲染组件的详细说明请参考 时间轴渲染组件文档

导出时的处理特性：

- **不连续视频处理**：即使某些镜头没有视频，也能正确导出所有镜头
- **占位符机制**：对于没有视频的片段位，自动创建占位符片段（不可见、静音），确保时间轴连续

### 2. 多轨道支持

- **视频轨道**：主轨道，包含所有视频片段
- **音频轨道**：独立的音频轨道，包含所有音频片段
- **轨道同步**：视频和音频在各自轨道上按镜头顺序排列

### 3. 兼容性

- **新版本**：支持时间轴渲染组件，可处理不连续的视频和音频
- **旧版本**：兼容旧的时间轴格式（经典模式），按顺序连续排列视频

## 数据结构

### 前端发送的数据格式

```JavaScript
{
  draft_path: "C:\\Users\\...\\JianyingPro\\User Data\\Projects\\com.lveditor.draft",
  video_clips: [
    {
      url: "视频URL",
      name: "视频名称",
      duration: 10.5,
      startTime: 0,
      endTime: 10.5,
      pillarId: "scriptId_shotNumber"  // 所属片段位ID
    }
  ],
  audio_clips: [
    {
      url: "音频URL",
      name: "音频名称",
      duration: 8.0,
      startTime: 0,
      endTime: 8.0,
      pillarId: "scriptId_shotNumber"  // 所属片段位ID
    }
  ],
  pillars: [
    {
      id: "scriptId_shotNumber",
      scriptId: 123,
      shotNumber: 1,
      defaultDuration: 15,
      videoClipIds: ["clip_id_1"],
      audioClipIds: ["clip_id_2"]
    }
  ],
  workflow_name: "工作流名称",
  ratio: "9:16"  // 画布比例：9:16/16:9/1:1/3:4/4:3，决定草稿画布尺寸（竖屏/横屏）
}
```

### 后端处理逻辑

#### 时间轴渲染组件模式（有pillars数据）

1. **按片段位顺序处理**：根据 `scriptId` 和 `shotNumber` 排序
2. **处理每个片段位**：

   - 收集该片段位内的所有视频片段（通过 `pillarId` 匹配）
   - **如果片段位没有视频**：创建占位符片段（使用任意已下载的视频素材，设置为不可见、静音、时长为默认时长）
   - 收集该片段位内的所有音频片段（通过 `pillarId` 匹配）
   - 计算片段位实际时长 = max(默认时长, 视频总时长, 音频总时长)
3. **时间轴累加**：每个片段位结束后，时间轴向前推进该片段的实际时长

#### 经典模式（无pillars数据）

1. **视频轨道**：按 `video_clips` 数组顺序连续排列
2. **音频轨道**：按 `audio_clips` 数组顺序连续排列
3. **兼容旧版本**：支持旧的 `timeline_clips` 字段

## 使用流程

### 1. 前端导出

在视频工作流页面，点击"导出到剪影"按钮：

1. 弹出导出对话框，输入剪影草稿路径前缀（如：`C:\Users\Administrator\AppData\Local\JianyingPro\User Data\Projects\com.lveditor.draft`）

   - 系统会从 Cookie 中读取上次保存的路径（有效期 90 天），自动填充到输入框
   - 对话框中包含获取剪影草稿路径的图文说明
2. 点击"开始导出"按钮
3. 系统自动保存路径到 Cookie（`jianying_draft_path`），有效期 3 个月
4. 系统收集时间轴数据（视频片段、音频片段、片段位信息）
5. 发送到后端 `/api/export_timeline_draft` 接口

### 2. 后端处理

1. **下载媒体文件**：

   - 从URL下载视频和音频文件
   - 支持本地上传文件复用
   - 使用缓存避免重复下载相同素材
2. **生成剪影草稿**：

   - 根据前端 `ratio` 设置画布尺寸（查 `JIANYING_RATIO_RESOLUTION` 映射，写入 `canvas_config`）
   - 用 ffprobe 预取每个已下载文件的真实时长与视频尺寸（`asyncio.to_thread` + 信号量限流，不阻塞事件循环）
   - 创建多轨道草稿库实例，根据时间轴渲染组件或经典模式添加视频和音频片段
   - 视频素材声明真实尺寸，`clip.scale` 保持 1.0（剪映默认适配基准，素材自动等比适应画布、保持长宽比、不裁切）；音视频用真实时长，超出部分完整保留
   - 生成草稿文件（`draft_content.json` 和 `draft_meta_info.json`）
   - 复制媒体文件到草稿的 `Resources/local` 目录
3. **打包下载**：

   - 创建包含草稿文件夹和导入指南的ZIP压缩包
   - 根据部署形态（本地部署/云端部署）决定下载方式：
   
     - **本地部署**：返回本地服务器的下载链接
     - **云端部署**：先上传到对象存储服务，返回 CDN 下载链接

### 3. 导入剪影

1. 下载ZIP压缩包并解压
2. 将草稿文件夹复制到剪影草稿路径
3. 打开剪映，在草稿列表中找到导入的草稿

## 技术细节

### 时间单位

- **前端**：秒（浮点数）
- **后端**：微秒（整数），通过 `seconds_to_microseconds()` 转换

### 画布比例与素材适配

- **画布尺寸**：由前端 `ratio` 决定，经 `JIANYING_RATIO_RESOLUTION`（`config/constant.py`）映射为像素尺寸（如 9:16 → 1080×1920），写入 `canvas_config.width/height/ratio` 与 `draft_meta_info.resolution`。
- **素材尺寸与适配**：视频素材的 `width/height` 用 ffprobe 探测的真实值（非画布尺寸）。`clip.scale` 保持 `1.0`——这是剪映的**默认适配基准（导入素材时剪映自动等比适应画布，保持长宽比、不裁切、不变形）。切勿**按"画布/素材尺寸比例"计算后写入 scale，否则剪映会在适配基准上叠加缩放，导致画面过度放大、双向溢出只看到中间。竖屏素材进竖屏画布（比例一致）会铺满无黑边；横屏素材进竖屏画布则完整显示、上下留黑边。
- **超出不切除**：音视频用 ffprobe 真实时长（前端用浏览器元素取时长不可靠、失败回退默认值），在未手动裁剪时（`startTime==0 && endTime>=duration`）用真实时长覆盖前端预估；`material.duration` 存文件完整时长，草稿顶层 `duration` 自动延长以完整容纳最长素材。

### 素材路径

- **草稿中的路径**：使用用户提供的 `draft_path` 作为前缀
- **实际文件位置**：`草稿文件夹/Resources/local/文件名`

### 文件命名

- **视频**：`video_{序号}_{随机ID}.{扩展名}`
- **音频**：`audio_{序号}_{随机ID}.{扩展名}`

## 问题修复记录

### 2025-01-07 修复

**问题1：视频不连续时，后续镜头丢失**

- **原因**：旧版本只处理成功下载的视频，如果某个镜头没有视频，会导致时间轴错位
- **解决**：引入时间轴渲染组件和占位符机制

  - 按片段位顺序处理，为没有视频的片段位创建占位符片段
  - 占位符使用已有视频素材，但设置为不可见（`visible=false`）、静音（`volume=0`）
  - 占位符时长等于片段位的默认时长，确保时间轴连续

**问题2：时间轴中的音频没有导出**

- **原因**：旧版本只处理视频轨道，没有处理音频轨道
- **解决**：

  - 前端：导出时包含 `audio_clips` 数据
  - 后端：创建独立的音频轨道，下载并添加音频片段

**问题3：时间轴刻度尺显示不正确**

- **原因**：刻度尺没有与轨道标签对齐，导致0秒刻度与视频片段位置不匹配
- **解决**：

  - 刻度尺DOM结构：添加60px左侧占位，与轨道标签宽度一致
  - 轨道padding：移除左右padding（改为只有上下padding），让片段从真正的0位置开始
  - 刻度线样式：添加主刻度线（12px高）和次刻度线（6px高），提升可读性
  - 刻度线层级：设置 `z-index: 10`，确保刻度线显示在视频片段上方

**问题4：清空时间轴功能不完整**

- **原因**：只清空了视频片段，没有清空音频片段和片段位引用
- **解决**：

  - 同时清空 `clips`、`audioClips`、`selectedClipId`、`selectedAudioClipId`
  - 清空所有片段位中的 `videoClipIds` 和 `audioClipIds` 引用

## API 接口

### POST /api/export_timeline_draft

**请求参数**：

```JSON
{
  "draft_path": "剪影草稿路径前缀",
  "video_clips": [...],
  "audio_clips": [...],
  "pillars": [...],
  "workflow_name": "工作流名称",
  "ratio": "画布比例（可选，如 9:16 / 16:9，默认 16:9）"
}
```

**响应**：

```JSON
{
  "success": true,
  "download_url": "下载链接",
  "zip_filename": "压缩包文件名"
}
```

## 相关文件

- **前端**：`web/js/timeline.js` - 时间轴导出函数
- **后端**：`server.py` - `/api/export_timeline_draft` 接口
- **剪影库**：`jianying/src/` - 草稿生成核心代码

### 2026-06-14 修复

**问题1：竖屏设置下导出的草稿画布/首个素材仍为横屏**

- **原因**：前端 `state.ratio`（竖屏 9:16）从未传给后端导出接口；后端用硬编码 1920×1080 初始化剪映库，`canvas_config` 永远是横屏。次要：视频素材 material 的宽高用了画布尺寸而非真实文件尺寸，`clip.scale` 固定 1.0 无画布适配。
- **解决**：

  - 前端导出请求体增加 `ratio` 字段
  - 新增画布比例映射常量 `JIANYING_RATIO_RESOLUTION`（`config/constant.py`），后端按 `ratio` 设置画布 width/height 并写入 `canvas_config.ratio`
  - 视频素材用 ffprobe 真实尺寸，`clip.scale` 保持 `1.0`（剪映默认适配基准，素材自动等比适应画布、保持长宽比、不裁切）

**问题2：音频/视频超出预设时长被切除**

- **原因**：`source_timerange.duration` 钉死在前端预估时长上（前端用浏览器 `<audio>`/`<video>` 取时长，失败回退默认值，不可靠），剪映只播放这段，超出被切。
- **解决**：

  - 新增 ffprobe 安全探测工具（`jianying/src/media_utils.py`）：单次 ffprobe 安全取时长/尺寸，失败返回零值不抛异常
  - 后端导出时用 `asyncio.to_thread` 预取真实时长（按 file_path 去重、信号量限流），在「未手动裁剪」时（`startTime==0 && endTime>=duration`）用真实时长覆盖前端预估
  - `material.duration` 存文件完整时长，`segment` 用使用片段时长；`total_duration` 自动延长以完整容纳长素材

**后续修正：clip.scale 的剪映适配语义**

- **现象**：首版按 `max(画布宽/素材宽, 画布高/素材高)` 计算 cover scale 写入 `clip.scale`，但导出后画面长宽双向溢出、只看到中间。
- **根因**：剪映的 `clip.scale=1.0` 是**默认适配基准**（导入素材时剪映已自动等比适应画布），scale 字段是相对该基准的额外缩放。把 cover 比例写入会被叠加，导致过度放大。（与主流开源剪映草稿库"添加视频时默认 scale=1.0、不额外计算 scale"的做法一致。）
- **解决**：`clip.scale` 恢复 `1.0`，依赖剪映默认适配（保持长宽比、不裁切、不变形）。

### 2026-06-06 修复

**问题：导出剪影草稿时视频下载 403 Forbidden**

- **原因**：视频生成后，CDN URL 带有签名 token，存入 `clip.url`。导出草稿时后端直接下载该 URL，但下载链接带时效性签名，签名过期后 CDN 返回 403。
- **解决**：

  - 使用 CDN 签名刷新工具（`utils/` 目录），在下载视频/音频前检测 CDN URL 并重新生成签名
  - 仅对匹配已配置 CDN 域名的 URL 进行刷新，非 CDN URL 原样返回
  - 视频和音频下载流程均增加了此步骤

## 注意事项

1. **路径格式**：Windows路径使用反斜杠（`\`），需要在输入时正确转义
2. **文件大小**：大量视频和音频会导致下载时间较长，建议优化素材大小
3. **临时文件清理**：系统会自动清理下载的临时文件

   - 本地部署：草稿压缩包会保留在服务器上
   - 云端部署：上传到对象存储服务后会删除本地压缩包以节省空间
4. **兼容性**：支持新旧两种数据格式，确保向后兼容
5. **云存储配置**：云端部署需要正确配置 `file_storage` 的对象存储服务参数
6. **CDN 签名刷新**：导出草稿时会自动刷新 CDN URL 签名（下载链接带时效性签名），避免因签名过期导致下载失败
