新增加添加遮罩的接口。

This commit is contained in:
Hommy
2025-09-24 17:33:40 +08:00
parent 87d2cef3f2
commit bd00c9564a
6 changed files with 466 additions and 1 deletions
+154
View File
@@ -0,0 +1,154 @@
# 添加遮罩接口文档
## 接口概述
向现有草稿中的指定片段添加遮罩效果。遮罩是视频编辑中的重要功能,通过遮罩可以控制图像的可见区域,创造出各种视觉效果。支持多种遮罩类型(线性、镜面、圆形、矩形、爱心、星形),每种遮罩都可以精确配置位置、大小、羽化、旋转等属性。
## 接口信息
- **请求方式**: POST
- **接口路径**: `/v1/add_masks`
- **Content-Type**: `application/json`
## 请求参数
### 请求体 (Body)
| 参数名 | 类型 | 必填 | 默认值 | 描述 |
|--------|------|------|--------|------|
| draft_url | string | 是 | "" | 目标草稿的完整URL |
| segment_ids | array | 是 | [] | 要应用遮罩的片段ID数组 |
| name | string | 否 | "线性" | 遮罩类型名称 |
| X | integer | 否 | 0 | 遮罩中心X坐标(像素) |
| Y | integer | 否 | 0 | 遮罩中心Y坐标(像素) |
| width | integer | 否 | 512 | 遮罩宽度(像素) |
| height | integer | 否 | 512 | 遮罩高度(像素) |
| feather | integer | 否 | 0 | 羽化程度(0-100 |
| rotation | integer | 否 | 0 | 旋转角度(度) |
| invert | boolean | 否 | false | 是否反转遮罩 |
| roundCorner | integer | 否 | 0 | 圆角半径(0-100 |
### 支持的遮罩类型
| 遮罩名称 | 描述 | 适用场景 |
|----------|------|----------|
| 线性 | 线性渐变遮罩 | 线性过渡效果、渐变显示隐藏 |
| 镜面 | 镜像对称遮罩 | 对称效果、镜像反射 |
| 圆形 | 圆形遮罩 | 聚光灯效果、圆形裁剪 |
| 矩形 | 矩形遮罩 | 窗口效果、矩形裁剪 |
| 爱心 | 爱心形状遮罩 | 浪漫场景、特殊形状裁剪 |
| 星形 | 星形遮罩 | 闪光效果、装饰性裁剪 |
### 遮罩配置详解
- **位置控制**: X、Y参数控制遮罩在画面中的位置
- **尺寸控制**: width、height控制遮罩的大小
- **羽化效果**: feather参数控制边缘的柔化程度,值越大边缘越柔和
- **旋转变换**: rotation参数控制遮罩的旋转角度
- **反转效果**: invert参数可以反转遮罩的显示/隐藏区域
- **圆角处理**: roundCorner参数为矩形遮罩添加圆角效果
## 请求示例
```bash
curl -X POST "http://localhost:8000/v1/add_masks" \
-H "Content-Type: application/json" \
-d '{
"draft_url": "https://ts.fyshark.com/#/cozeToJianyin?drafId=7427078525303048221",
"segment_ids": ["d62994b4-25fe-422a-a123-87ef05038558", "e73995c5-36ef-533b-b234-98fg16149669"],
"name": "圆形",
"X": 100,
"Y": 200,
"width": 300,
"height": 300,
"feather": 20,
"rotation": 0,
"invert": false,
"roundCorner": 0
}'
```
## 响应格式
### 成功响应
```json
{
"code": 0,
"message": "Success",
"data": {
"draft_url": "https://ts.fyshark.com/#/cozeToJianyin?drafId=7427078525303048221",
"masks_added": 2,
"affected_segments": ["d62994b4-25fe-422a-a123-87ef05038558", "e73995c5-36ef-533b-b234-98fg16149669"],
"mask_ids": ["mask_001", "mask_002"]
}
}
```
**响应字段说明**:
- `draft_url`: 草稿URL
- `masks_added`: 成功添加的遮罩数量
- `affected_segments`: 受影响的片段ID列表
- `mask_ids`: 遮罩ID列表
### 错误响应
```json
{
"code": 2023,
"message": "无效的遮罩信息,请检查遮罩参数是否正确"
}
```
## 错误码说明
| 错误码 | 错误信息 | 描述 |
|--------|----------|------|
| 2001 | 无效的草稿URL | 草稿URL格式错误或草稿不存在 |
| 2015 | 片段未找到 | 指定的segment_id不存在 |
| 2016 | 无效的片段类型 | 片段类型不支持添加遮罩 |
| 2023 | 无效的遮罩信息 | 遮罩参数格式错误或值无效 |
| 2024 | 遮罩添加失败 | 添加遮罩过程中发生错误 |
| 2025 | 遮罩类型未找到 | 指定的遮罩名称不存在 |
## 使用说明
1. **片段要求**: 只有视频片段(VideoSegment)支持添加遮罩
2. **遮罩限制**: 每个片段只能添加一个遮罩,重复添加会失败
3. **坐标系统**: X、Y坐标以像素为单位,原点位于素材中心
4. **参数范围**:
- feather: 0-100,羽化程度
- rotation: 0-360度,旋转角度
- roundCorner: 0-100,圆角半径(仅矩形遮罩有效)
5. **批量处理**: 支持同时为多个片段添加相同配置的遮罩
## 应用场景
### 创意视觉效果
- **聚光灯效果**: 使用圆形遮罩突出画面重点
- **窗口效果**: 使用矩形遮罩创建窗口视觉
- **浪漫场景**: 使用爱心遮罩营造浪漫氛围
- **装饰效果**: 使用星形遮罩增加装饰元素
### 画面过渡
- **渐变显示**: 使用线性遮罩实现渐进显示效果
- **对称效果**: 使用镜面遮罩创建对称视觉
- **柔和边缘**: 通过羽化参数实现柔和的边缘过渡
### 内容遮挡
- **隐私保护**: 遮挡敏感信息区域
- **焦点引导**: 突出重要内容,弱化无关区域
- **艺术创作**: 创造独特的视觉艺术效果
## 注意事项
- 遮罩效果只对视频片段有效,不支持音频、文本或贴纸片段
- 添加遮罩后会影响片段的视觉呈现,请确保遮罩配置符合预期
- 遮罩参数的单位和坐标系以素材自身为基准
- 建议在添加遮罩前预览效果,确保达到预期的视觉效果
- 遮罩的渲染性能可能影响导出速度,请根据需要合理使用
## 相关接口
- [创建草稿](./create_draft.md) - 创建新的剪映草稿
- [添加视频](./add_videos.md) - 向草稿添加视频内容
- [添加特效](./add_effects.md) - 向草稿添加视频特效
- [保存草稿](./save_draft.md) - 保存草稿更改
+3
View File
@@ -34,6 +34,9 @@ class CustomError(Enum):
INVALID_EFFECT_INFO = (2020, "无效的特效信息,请检查effect_infos字段值是否正确", "Invalid effect information, please check if the value of the effect_infos field is correct.")
EFFECT_ADD_FAILED = (2021, "特效添加失败", "Effect addition failed")
EFFECT_NOT_FOUND = (2022, "特效未找到,请检查特效名称是否正确", "Effect not found, please check if the effect name is correct.")
INVALID_MASK_INFO = (2023, "无效的遮罩信息,请检查遮罩参数是否正确", "Invalid mask information, please check if mask parameters are correct.")
MASK_ADD_FAILED = (2024, "遮罩添加失败", "Mask addition failed")
MASK_NOT_FOUND = (2025, "遮罩类型未找到,请检查遮罩名称是否正确", "Mask type not found, please check if the mask name is correct.")
# ===== 系统错误码 (9000-9999) =====
INTERNAL_SERVER_ERROR = (9998, "系统内部错误", "Internal server error")
+30
View File
@@ -7,6 +7,7 @@ from src.schemas.add_sticker import AddStickerResponse
from src.schemas.add_keyframes import AddKeyframesResponse
from src.schemas.add_captions import AddCaptionsResponse
from src.schemas.add_effects import AddEffectsResponse
from src.schemas.add_masks import AddMasksResponse
from src.schemas.save_draft import SaveDraftResponse
from src.schemas.create_draft import CreateDraftResponse
from fastapi import APIRouter, Request, Depends
@@ -18,6 +19,7 @@ from src.schemas.add_sticker import AddStickerRequest, AddStickerResponse
from src.schemas.add_keyframes import AddKeyframesRequest, AddKeyframesResponse
from src.schemas.add_captions import AddCaptionsRequest, AddCaptionsResponse
from src.schemas.add_effects import AddEffectsRequest, AddEffectsResponse
from src.schemas.add_masks import AddMasksRequest, AddMasksResponse
from src.schemas.save_draft import SaveDraftRequest, SaveDraftResponse
from src.schemas.gen_video import GenVideoRequest, GenVideoResponse
from src.schemas.get_draft import GetDraftRequest, GetDraftResponse
@@ -206,6 +208,34 @@ def add_effects(aer: AddEffectsRequest) -> AddEffectsResponse:
segment_ids=segment_ids
)
@router.post(path="/add_masks", response_model=AddMasksResponse)
def add_masks(amr: AddMasksRequest) -> AddMasksResponse:
"""
向剪映草稿添加遮罩 (v1版本)
"""
# 调用service层处理业务逻辑
draft_url, masks_added, affected_segments, mask_ids = service.add_masks(
draft_url=amr.draft_url,
segment_ids=amr.segment_ids,
name=amr.name,
X=amr.X,
Y=amr.Y,
width=amr.width,
height=amr.height,
feather=amr.feather,
rotation=amr.rotation,
invert=amr.invert,
roundCorner=amr.roundCorner
)
return AddMasksResponse(
draft_url=draft_url,
masks_added=masks_added,
affected_segments=affected_segments,
mask_ids=mask_ids
)
@router.get(path="/get_draft", response_model=GetDraftResponse)
def get_draft(params: Annotated[GetDraftRequest, Depends()]) -> GetDraftResponse:
"""
+25
View File
@@ -0,0 +1,25 @@
from pydantic import BaseModel, Field
from typing import List
class AddMasksRequest(BaseModel):
"""添加遮罩请求参数"""
draft_url: str = Field(default="", description="草稿URL")
segment_ids: List[str] = Field(default=[], description="要应用遮罩的片段ID数组")
name: str = Field(default="线性", description="遮罩类型名称")
X: int = Field(default=0, description="遮罩中心X坐标(像素)")
Y: int = Field(default=0, description="遮罩中心Y坐标(像素)")
width: int = Field(default=512, description="遮罩宽度(像素)")
height: int = Field(default=512, description="遮罩高度(像素)")
feather: int = Field(default=0, description="羽化程度(0-100")
rotation: int = Field(default=0, description="旋转角度(度)")
invert: bool = Field(default=False, description="是否反转遮罩")
roundCorner: int = Field(default=0, description="圆角半径(0-100")
class AddMasksResponse(BaseModel):
"""添加遮罩响应参数"""
draft_url: str = Field(default="", description="草稿URL")
masks_added: int = Field(default=0, description="成功添加的遮罩数量")
affected_segments: List[str] = Field(default=[], description="受影响的片段ID列表")
mask_ids: List[str] = Field(default=[], description="遮罩ID列表")
+2 -1
View File
@@ -6,8 +6,9 @@ from .add_sticker import add_sticker
from .add_keyframes import add_keyframes
from .add_captions import add_captions
from .add_effects import add_effects
from .add_masks import add_masks
from .save_draft import save_draft
from .gen_video import gen_video
from .get_draft import get_draft
__all__ = ["create_draft", "add_videos", "add_audios", "add_images", "add_sticker", "add_keyframes", "add_captions", "add_effects", "save_draft", "gen_video", "get_draft"]
__all__ = ["create_draft", "add_videos", "add_audios", "add_images", "add_sticker", "add_keyframes", "add_captions", "add_effects", "add_masks", "save_draft", "gen_video", "get_draft"]
+252
View File
@@ -0,0 +1,252 @@
from typing import List, Dict, Any, Tuple, Optional
from src.utils.logger import logger
from src.pyJianYingDraft import ScriptFile, MaskType
from src.pyJianYingDraft.video_segment import VideoSegment
from src.pyJianYingDraft.segment import VisualSegment
from src.utils.draft_cache import DRAFT_CACHE
from exceptions import CustomException, CustomError
from src.utils import helper
def add_masks(
draft_url: str,
segment_ids: List[str],
name: str = "线性",
X: int = 0,
Y: int = 0,
width: int = 512,
height: int = 512,
feather: int = 0,
rotation: int = 0,
invert: bool = False,
roundCorner: int = 0
) -> Tuple[str, int, List[str], List[str]]:
"""
向现有草稿中的指定片段添加遮罩效果的业务逻辑
Args:
draft_url: 草稿URL,必选参数
segment_ids: 要应用遮罩的片段ID数组,必选参数
name: 遮罩类型名称,默认值:"线性"。支持:"线性", "镜面", "圆形", "矩形", "爱心", "星形"
X: 遮罩中心X坐标(像素),默认值:0
Y: 遮罩中心Y坐标(像素),默认值:0
width: 遮罩宽度(像素),默认值:512
height: 遮罩高度(像素),默认值:512
feather: 羽化程度(0-100),默认值:0
rotation: 旋转角度(度),默认值:0
invert: 是否反转遮罩,默认值:false
roundCorner: 圆角半径(0-100),默认值:0
Returns:
Tuple[str, int, List[str], List[str]]: 返回元组包含以下信息
- draft_url: 草稿URL
- masks_added: 成功添加的遮罩数量
- affected_segments: 受影响的片段ID列表
- mask_ids: 遮罩ID列表
Raises:
CustomException: 遮罩添加失败
"""
logger.info(f"add_masks started, draft_url: {draft_url}, segment_ids: {segment_ids}, mask_name: {name}")
# 1. 提取草稿ID
draft_id = helper.get_url_param(draft_url, "draft_id")
if (not draft_id) or (draft_id not in DRAFT_CACHE):
logger.error(f"Invalid draft_url or draft not found in cache: {draft_url}")
raise CustomException(CustomError.INVALID_DRAFT_URL)
# 2. 验证片段ID列表
if not segment_ids or len(segment_ids) == 0:
logger.error("No segment_ids provided")
raise CustomException(CustomError.INVALID_MASK_INFO)
logger.info(f"Processing {len(segment_ids)} segments for mask addition")
# 3. 从缓存中获取草稿
script: ScriptFile = DRAFT_CACHE[draft_id]
# 4. 查找遮罩类型
mask_type = find_mask_type_by_name(name)
if mask_type is None:
logger.error(f"Mask type not found for name: {name}")
raise CustomException(CustomError.MASK_NOT_FOUND)
# 5. 遍历片段ID,为每个片段添加遮罩
masks_added = 0
affected_segments: List[str] = []
mask_ids: List[str] = []
for i, segment_id in enumerate(segment_ids):
try:
logger.info(f"Processing segment {i+1}/{len(segment_ids)}, segment_id: {segment_id}")
mask_id = add_mask_to_segment(
script=script,
segment_id=segment_id,
mask_type=mask_type,
center_x=X,
center_y=Y,
width=width,
height=height,
feather=feather,
rotation=rotation,
invert=invert,
round_corner=roundCorner
)
masks_added += 1
affected_segments.append(segment_id)
mask_ids.append(mask_id)
logger.info(f"Added mask to segment {i+1}/{len(segment_ids)}, mask_id: {mask_id}")
except Exception as e:
logger.error(f"Failed to add mask to segment {i+1}/{len(segment_ids)}, segment_id: {segment_id}, error: {str(e)}")
raise
# 6. 保存草稿
script.save()
logger.info(f"Draft saved successfully")
logger.info(f"add_masks completed successfully - draft_id: {draft_id}, masks_added: {masks_added}")
return draft_url, masks_added, affected_segments, mask_ids
def add_mask_to_segment(
script: ScriptFile,
segment_id: str,
mask_type: MaskType,
center_x: int = 0,
center_y: int = 0,
width: int = 512,
height: int = 512,
feather: int = 0,
rotation: int = 0,
invert: bool = False,
round_corner: int = 0
) -> str:
"""
向指定片段添加遮罩
Args:
script: 草稿文件对象
segment_id: 目标片段ID
mask_type: 遮罩类型
center_x: 遮罩中心X坐标(像素)
center_y: 遮罩中心Y坐标(像素)
width: 遮罩宽度(像素)
height: 遮罩高度(像素)
feather: 羽化程度(0-100
rotation: 旋转角度(度)
invert: 是否反转遮罩
round_corner: 圆角半径(0-100
Returns:
mask_id: 遮罩ID
Raises:
CustomException: 添加遮罩失败
"""
try:
# 1. 查找片段
segment = find_segment_by_id(script, segment_id)
if segment is None:
logger.error(f"Segment not found: {segment_id}")
raise CustomException(CustomError.SEGMENT_NOT_FOUND)
# 2. 验证片段类型(只有VideoSegment支持遮罩)
if not isinstance(segment, VideoSegment):
logger.error(f"Segment {segment_id} is not a video segment, cannot add mask")
raise CustomException(CustomError.INVALID_SEGMENT_TYPE)
# 3. 检查片段是否已有遮罩
if segment.mask is not None:
logger.error(f"Segment {segment_id} already has a mask")
raise CustomException(CustomError.MASK_ADD_FAILED)
# 4. 计算遮罩尺寸参数
# 根据 add_mask 方法的要求,size 是主要尺寸(以占素材高度的比例表示)
material_width, material_height = segment.material_size
size = height / material_height # 高度比例
rect_width = width / material_width if mask_type == MaskType.矩形 else None
logger.info(f"Adding mask to segment {segment_id}: type={mask_type.value.name}, center=({center_x}, {center_y}), size={size}")
logger.info(f"Mask details - width: {width}, height: {height}, feather: {feather}, rotation: {rotation}, invert: {invert}, round_corner: {round_corner}")
# 5. 添加遮罩到片段
segment.add_mask(
mask_type=mask_type,
center_x=float(center_x),
center_y=float(center_y),
size=size,
rotation=float(rotation),
feather=float(feather),
invert=invert,
rect_width=rect_width,
round_corner=float(round_corner)
)
mask_id = segment.mask.global_id if segment.mask is not None else ""
if not mask_id:
logger.error(f"Failed to create mask for segment {segment_id}")
raise CustomException(CustomError.MASK_ADD_FAILED)
logger.info(f"Successfully added mask to segment {segment_id}, mask_id: {mask_id}")
return mask_id
except CustomException:
logger.error(f"Add mask to segment failed, segment_id: {segment_id}")
raise
except Exception as e:
logger.error(f"Add mask to segment failed, error: {str(e)}")
raise CustomException(CustomError.MASK_ADD_FAILED)
def find_segment_by_id(script: ScriptFile, segment_id: str) -> Optional[VisualSegment]:
"""
通过segment_id在草稿中查找对应的片段
Args:
script: 草稿文件对象
segment_id: 片段ID
Returns:
找到的片段对象,如果未找到则返回None
"""
logger.info(f"Searching for segment with id: {segment_id}")
# 遍历所有轨道
for track_name, track in script.tracks.items():
logger.info(f"Searching in track: {track_name}, segments count: {len(track.segments)}")
# 遍历轨道中的所有片段
for segment in track.segments:
if segment.segment_id == segment_id:
logger.info(f"Found segment {segment_id} in track {track_name}")
return segment
logger.warning(f"Segment {segment_id} not found in any track")
return None
def find_mask_type_by_name(mask_name: str) -> Optional[MaskType]:
"""
根据遮罩名称查找对应的遮罩类型
Args:
mask_name: 遮罩名称
Returns:
对应的遮罩类型枚举,如果未找到则返回None
"""
logger.info(f"Searching for mask type with name: {mask_name}")
# 搜索MaskType中的遮罩
for mask_type in MaskType:
if mask_type.value.name == mask_name:
logger.info(f"Found mask type: {mask_name}")
return mask_type
logger.warning(f"Mask type not found for name: {mask_name}")
return None