mirror of
https://github.com/Hommy-master/capcut-mate.git
synced 2026-08-30 17:48:47 +08:00
新添加字幕的接口。
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
# add_captions 接口文档
|
||||
|
||||
## 接口描述
|
||||
向剪映草稿批量添加字幕,支持丰富的字幕样式设置,包括关键词高亮、字体样式、边框效果、对齐方式、透明度和动画效果等。
|
||||
|
||||
## 接口信息
|
||||
- **方法**: POST
|
||||
- **路径**: `/v1/add_captions`
|
||||
- **Content-Type**: `application/json`
|
||||
|
||||
## 请求参数
|
||||
|
||||
### 请求体
|
||||
```json
|
||||
{
|
||||
"draft_url": "https://ts.fyshark.com/#/cozeToJianyin?drafId=...",
|
||||
"captions": "[{\"start\":0,\"end\":10000000,\"text\":\"你好,剪映\",\"keyword\":\"好\",\"keyword_color\":\"#457616\",\"keyword_font_size\":15,\"font_size\":15}]",
|
||||
"text_color": "#ffffff",
|
||||
"border_color": null,
|
||||
"alignment": 1,
|
||||
"alpha": 1.0,
|
||||
"font": null,
|
||||
"font_size": 15,
|
||||
"letter_spacing": null,
|
||||
"line_spacing": null,
|
||||
"scale_x": 1.0,
|
||||
"scale_y": 1.0,
|
||||
"transform_x": 0,
|
||||
"transform_y": 0,
|
||||
"style_text": false
|
||||
}
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
| 字段名 | 类型 | 必填 | 默认值 | 描述 |
|
||||
|--------|------|------|--------|------|
|
||||
| draft_url | string | 是 | - | 草稿URL |
|
||||
| captions | string | 是 | - | 字幕信息列表的JSON字符串 |
|
||||
| text_color | string | 否 | "#ffffff" | 文本颜色(十六进制) |
|
||||
| border_color | string | 否 | null | 边框颜色(十六进制) |
|
||||
| alignment | integer | 否 | 1 | 文本对齐方式(0-5) |
|
||||
| alpha | number | 否 | 1.0 | 文本透明度(0.0-1.0) |
|
||||
| font | string | 否 | null | 字体名称 |
|
||||
| font_size | integer | 否 | 15 | 字体大小 |
|
||||
| letter_spacing | number | 否 | null | 字间距 |
|
||||
| line_spacing | number | 否 | null | 行间距 |
|
||||
| scale_x | number | 否 | 1.0 | 水平缩放 |
|
||||
| scale_y | number | 否 | 1.0 | 垂直缩放 |
|
||||
| transform_x | integer | 否 | 0 | 水平位移 |
|
||||
| transform_y | integer | 否 | 0 | 垂直位移 |
|
||||
| style_text | boolean | 否 | false | 是否使用样式文本 |
|
||||
|
||||
### captions 字段详细说明
|
||||
captions 是一个JSON字符串,包含字幕数组,每个字幕对象包含以下字段:
|
||||
|
||||
| 字段名 | 类型 | 必填 | 默认值 | 描述 |
|
||||
|--------|------|------|--------|------|
|
||||
| start | integer | 是 | - | 字幕开始时间(微秒) |
|
||||
| end | integer | 是 | - | 字幕结束时间(微秒) |
|
||||
| text | string | 是 | - | 字幕文本内容 |
|
||||
| keyword | string | 否 | null | 关键词(用\|分隔多个关键词) |
|
||||
| keyword_color | string | 否 | "#ff7100" | 关键词颜色 |
|
||||
| keyword_font_size | integer | 否 | 15 | 关键词字体大小 |
|
||||
| font_size | integer | 否 | 15 | 文本字体大小 |
|
||||
| in_animation | string | 否 | null | 入场动画 |
|
||||
| out_animation | string | 否 | null | 出场动画 |
|
||||
| loop_animation | string | 否 | null | 循环动画 |
|
||||
| in_animation_duration | integer | 否 | null | 入场动画时长 |
|
||||
| out_animation_duration | integer | 否 | null | 出场动画时长 |
|
||||
| loop_animation_duration | integer | 否 | null | 循环动画时长 |
|
||||
|
||||
### 对齐方式说明
|
||||
|
||||
| 值 | 说明 |
|
||||
|---|------|
|
||||
| 0 | 左对齐 |
|
||||
| 1 | 居中对齐 |
|
||||
| 2 | 右对齐 |
|
||||
| 3 | 垂直居中 |
|
||||
| 4 | 垂直左对齐 |
|
||||
| 5 | 垂直右对齐 |
|
||||
|
||||
## 响应结果
|
||||
|
||||
### 成功响应
|
||||
```json
|
||||
{
|
||||
"draft_url": "https://ts.fyshark.com/#/cozeToJianyin?drafId=...",
|
||||
"track_id": "text_track_123",
|
||||
"text_ids": ["text_001", "text_002"],
|
||||
"segment_ids": ["seg_001", "seg_002"]
|
||||
}
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段名 | 类型 | 描述 |
|
||||
|--------|------|------|
|
||||
| draft_url | string | 草稿URL |
|
||||
| track_id | string | 字幕轨道ID |
|
||||
| text_ids | array | 字幕ID列表 |
|
||||
| segment_ids | array | 字幕片段ID列表 |
|
||||
|
||||
### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 2018,
|
||||
"message": "无效的字幕信息,请检查captions字段值是否正确"
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### cURL 示例
|
||||
```bash
|
||||
curl -X POST "http://localhost:8000/v1/add_captions" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"draft_url": "https://ts.fyshark.com/#/cozeToJianyin?drafId=example123",
|
||||
"captions": "[{\"start\":0,\"end\":5000000,\"text\":\"你好,剪映\",\"keyword\":\"好\",\"keyword_color\":\"#ff0000\"}]",
|
||||
"text_color": "#ffffff",
|
||||
"alignment": 1,
|
||||
"alpha": 1.0,
|
||||
"font_size": 20
|
||||
}'
|
||||
```
|
||||
|
||||
### Python 示例
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
url = "http://localhost:8000/v1/add_captions"
|
||||
captions_data = [
|
||||
{
|
||||
"start": 0,
|
||||
"end": 5000000,
|
||||
"text": "你好,剪映",
|
||||
"keyword": "好",
|
||||
"keyword_color": "#ff0000",
|
||||
"font_size": 20
|
||||
},
|
||||
{
|
||||
"start": 5000000,
|
||||
"end": 10000000,
|
||||
"text": "欢迎使用字幕功能",
|
||||
"keyword": "字幕",
|
||||
"keyword_color": "#00ff00",
|
||||
"font_size": 18
|
||||
}
|
||||
]
|
||||
|
||||
payload = {
|
||||
"draft_url": "https://ts.fyshark.com/#/cozeToJianyin?drafId=example123",
|
||||
"captions": json.dumps(captions_data),
|
||||
"text_color": "#ffffff",
|
||||
"alignment": 1,
|
||||
"alpha": 1.0,
|
||||
"font_size": 16,
|
||||
"transform_y": -100
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload)
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
### JavaScript 示例
|
||||
```javascript
|
||||
const url = "http://localhost:8000/v1/add_captions";
|
||||
const captionsData = [
|
||||
{
|
||||
start: 0,
|
||||
end: 5000000,
|
||||
text: "你好,剪映",
|
||||
keyword: "好",
|
||||
keyword_color: "#ff0000",
|
||||
font_size: 20
|
||||
}
|
||||
];
|
||||
|
||||
const payload = {
|
||||
draft_url: "https://ts.fyshark.com/#/cozeToJianyin?drafId=example123",
|
||||
captions: JSON.stringify(captionsData),
|
||||
text_color: "#ffffff",
|
||||
alignment: 1,
|
||||
alpha: 1.0,
|
||||
font_size: 16
|
||||
};
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => console.log(data));
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **时间单位**: 所有时间参数使用微秒为单位(1秒 = 1,000,000微秒)
|
||||
2. **字幕时长**: end 时间必须大于 start 时间
|
||||
3. **颜色格式**: 颜色值使用十六进制格式,如 "#ffffff"、"#ff0000"
|
||||
4. **关键词高亮**: 暂未完全实现,目前为预留功能
|
||||
5. **动画效果**: 暂未完全实现,目前为预留功能
|
||||
6. **字体支持**: 字体名称需要系统支持或使用默认字体
|
||||
7. **对齐方式**: 目前仅支持基础对齐方式(0-2),高级对齐方式(3-5)为预留功能
|
||||
8. **坐标系统**: transform_x 和 transform_y 使用像素值,会自动转换为草稿相对坐标
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| 错误码 | 错误信息 | 说明 |
|
||||
|--------|----------|------|
|
||||
| 2001 | 无效的草稿URL | 草稿URL格式错误或草稿不存在 |
|
||||
| 2018 | 无效的字幕信息 | captions字段格式错误或包含无效数据 |
|
||||
| 2019 | 字幕添加失败 | 添加字幕过程中发生错误 |
|
||||
|
||||
## 高级功能(待实现)
|
||||
|
||||
1. **关键词高亮**: 支持对指定关键词设置不同颜色和字体大小
|
||||
2. **入场/出场动画**: 支持字幕的动画效果
|
||||
3. **循环动画**: 支持字幕的循环动画效果
|
||||
4. **样式文本**: 支持富文本格式的字幕样式
|
||||
5. **字体选择**: 支持自定义字体文件
|
||||
6. **边框和阴影**: 支持字幕边框和阴影效果
|
||||
@@ -29,6 +29,8 @@ class CustomError(Enum):
|
||||
SEGMENT_NOT_FOUND = (2015, "片段未找到,请检查segment_id是否正确", "Segment not found, please check if the segment_id is correct.")
|
||||
INVALID_SEGMENT_TYPE = (2016, "无效的片段类型,该片段不支持关键帧", "Invalid segment type, this segment does not support keyframes.")
|
||||
INVALID_KEYFRAME_PROPERTY = (2017, "无效的关键帧属性类型", "Invalid keyframe property type.")
|
||||
INVALID_CAPTION_INFO = (2018, "无效的字幕信息,请检查captions字段值是否正确", "Invalid caption information, please check if the value of the captions field is correct.")
|
||||
CAPTION_ADD_FAILED = (2019, "字幕添加失败", "Caption addition failed")
|
||||
|
||||
# ===== 系统错误码 (9000-9999) =====
|
||||
INTERNAL_SERVER_ERROR = (9998, "系统内部错误", "Internal server error")
|
||||
|
||||
@@ -5,6 +5,7 @@ from src.schemas.add_audios import AddAudiosResponse
|
||||
from src.schemas.add_images import AddImagesResponse
|
||||
from src.schemas.add_sticker import AddStickerResponse
|
||||
from src.schemas.add_keyframes import AddKeyframesResponse
|
||||
from src.schemas.add_captions import AddCaptionsResponse
|
||||
from src.schemas.save_draft import SaveDraftResponse
|
||||
from src.schemas.create_draft import CreateDraftResponse
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
@@ -14,6 +15,7 @@ from src.schemas.add_audios import AddAudiosRequest, AddAudiosResponse
|
||||
from src.schemas.add_images import AddImagesRequest, AddImagesResponse
|
||||
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.save_draft import SaveDraftRequest, SaveDraftResponse
|
||||
from src.schemas.gen_video import GenVideoRequest, GenVideoResponse
|
||||
from src.schemas.get_draft import GetDraftRequest, GetDraftResponse
|
||||
@@ -151,6 +153,38 @@ def add_keyframes(akr: AddKeyframesRequest) -> AddKeyframesResponse:
|
||||
affected_segments=affected_segments
|
||||
)
|
||||
|
||||
@router.post(path="/add_captions", response_model=AddCaptionsResponse)
|
||||
def add_captions(acr: AddCaptionsRequest) -> AddCaptionsResponse:
|
||||
"""
|
||||
向剪映草稿批量添加字幕 (v1版本)
|
||||
"""
|
||||
|
||||
# 调用service层处理业务逻辑
|
||||
draft_url, track_id, text_ids, segment_ids = service.add_captions(
|
||||
draft_url=acr.draft_url,
|
||||
captions=acr.captions,
|
||||
text_color=acr.text_color,
|
||||
border_color=acr.border_color,
|
||||
alignment=acr.alignment,
|
||||
alpha=acr.alpha,
|
||||
font=acr.font,
|
||||
font_size=acr.font_size,
|
||||
letter_spacing=acr.letter_spacing,
|
||||
line_spacing=acr.line_spacing,
|
||||
scale_x=acr.scale_x,
|
||||
scale_y=acr.scale_y,
|
||||
transform_x=acr.transform_x,
|
||||
transform_y=acr.transform_y,
|
||||
style_text=acr.style_text
|
||||
)
|
||||
|
||||
return AddCaptionsResponse(
|
||||
draft_url=draft_url,
|
||||
track_id=track_id,
|
||||
text_ids=text_ids,
|
||||
segment_ids=segment_ids
|
||||
)
|
||||
|
||||
@router.get(path="/get_draft", response_model=GetDraftResponse)
|
||||
def get_draft(params: Annotated[GetDraftRequest, Depends()]) -> GetDraftResponse:
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class AddCaptionsRequest(BaseModel):
|
||||
"""批量添加字幕请求参数"""
|
||||
draft_url: str = Field(default="", description="草稿URL")
|
||||
captions: str = Field(default="", description="字幕信息列表, 用JSON字符串表示")
|
||||
text_color: str = Field(default="#ffffff", description="文本颜色(十六进制)")
|
||||
border_color: Optional[str] = Field(default=None, description="边框颜色(十六进制)")
|
||||
alignment: int = Field(default=1, ge=0, le=5, description="文本对齐方式(0-5)")
|
||||
alpha: float = Field(default=1.0, ge=0.0, le=1.0, description="文本透明度(0.0-1.0)")
|
||||
font: Optional[str] = Field(default=None, description="字体名称")
|
||||
font_size: int = Field(default=15, ge=1, description="字体大小")
|
||||
letter_spacing: Optional[float] = Field(default=None, description="字间距")
|
||||
line_spacing: Optional[float] = Field(default=None, description="行间距")
|
||||
scale_x: float = Field(default=1.0, description="水平缩放")
|
||||
scale_y: float = Field(default=1.0, description="垂直缩放")
|
||||
transform_x: int = Field(default=0, description="水平位移")
|
||||
transform_y: int = Field(default=0, description="垂直位移")
|
||||
style_text: bool = Field(default=False, description="是否使用样式文本")
|
||||
|
||||
|
||||
class CaptionItem(BaseModel):
|
||||
"""单个字幕信息"""
|
||||
start: int = Field(..., description="字幕开始时间(微秒)")
|
||||
end: int = Field(..., description="字幕结束时间(微秒)")
|
||||
text: str = Field(..., description="字幕文本内容")
|
||||
keyword: Optional[str] = Field(default=None, description="关键词(用|分隔多个关键词)")
|
||||
keyword_color: str = Field(default="#ff7100", description="关键词颜色")
|
||||
keyword_font_size: int = Field(default=15, ge=1, description="关键词字体大小")
|
||||
font_size: int = Field(default=15, ge=1, description="文本字体大小")
|
||||
in_animation: Optional[str] = Field(default=None, description="入场动画")
|
||||
out_animation: Optional[str] = Field(default=None, description="出场动画")
|
||||
loop_animation: Optional[str] = Field(default=None, description="循环动画")
|
||||
in_animation_duration: Optional[int] = Field(default=None, description="入场动画时长")
|
||||
out_animation_duration: Optional[int] = Field(default=None, description="出场动画时长")
|
||||
loop_animation_duration: Optional[int] = Field(default=None, description="循环动画时长")
|
||||
|
||||
|
||||
class AddCaptionsResponse(BaseModel):
|
||||
"""添加字幕响应参数"""
|
||||
draft_url: str = Field(default="", description="草稿URL")
|
||||
track_id: str = Field(default="", description="字幕轨道ID")
|
||||
text_ids: List[str] = Field(default=[], description="字幕ID列表")
|
||||
segment_ids: List[str] = Field(default=[], description="字幕片段ID列表")
|
||||
@@ -4,8 +4,9 @@ from .add_audios import add_audios
|
||||
from .add_images import add_images
|
||||
from .add_sticker import add_sticker
|
||||
from .add_keyframes import add_keyframes
|
||||
from .add_captions import add_captions
|
||||
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", "save_draft", "gen_video", "get_draft"]
|
||||
__all__ = ["create_draft", "add_videos", "add_audios", "add_images", "add_sticker", "add_keyframes", "add_captions", "save_draft", "gen_video", "get_draft"]
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
import json
|
||||
from typing import List, Dict, Any, Tuple, Optional, Literal
|
||||
|
||||
from src.utils.logger import logger
|
||||
from src.pyJianYingDraft import ScriptFile, TrackType, TextSegment, TextStyle, ClipSettings, Timerange
|
||||
from src.utils.draft_cache import DRAFT_CACHE
|
||||
from exceptions import CustomException, CustomError
|
||||
from src.utils import helper
|
||||
|
||||
|
||||
def add_captions(
|
||||
draft_url: str,
|
||||
captions: str,
|
||||
text_color: str = "#ffffff",
|
||||
border_color: Optional[str] = None,
|
||||
alignment: int = 1,
|
||||
alpha: float = 1.0,
|
||||
font: Optional[str] = None,
|
||||
font_size: int = 15,
|
||||
letter_spacing: Optional[float] = None,
|
||||
line_spacing: Optional[float] = None,
|
||||
scale_x: float = 1.0,
|
||||
scale_y: float = 1.0,
|
||||
transform_x: int = 0,
|
||||
transform_y: int = 0,
|
||||
style_text: bool = False
|
||||
) -> Tuple[str, str, List[str], List[str]]:
|
||||
"""
|
||||
批量添加字幕到剪映草稿的业务逻辑
|
||||
|
||||
Args:
|
||||
draft_url: 草稿URL
|
||||
captions: 字幕信息列表的JSON字符串,格式如下:
|
||||
[
|
||||
{
|
||||
"start": 0, # 字幕开始时间(微秒)
|
||||
"end": 10000000, # 字幕结束时间(微秒)
|
||||
"text": "你好,剪映", # 字幕文本内容
|
||||
"keyword": "好", # 关键词(用|分隔多个关键词),可选参数
|
||||
"keyword_color": "#457616", # 关键词颜色,可选参数
|
||||
"keyword_font_size": 15, # 关键词字体大小,可选参数
|
||||
"font_size": 15, # 文本字体大小,可选参数
|
||||
"in_animation": None, # 入场动画,可选参数
|
||||
"out_animation": None, # 出场动画,可选参数
|
||||
"loop_animation": None, # 循环动画,可选参数
|
||||
"in_animation_duration": None, # 入场动画时长,可选参数
|
||||
"out_animation_duration": None, # 出场动画时长,可选参数
|
||||
"loop_animation_duration": None # 循环动画时长,可选参数
|
||||
}
|
||||
]
|
||||
text_color: 文本颜色(十六进制),默认"#ffffff"
|
||||
border_color: 边框颜色(十六进制),默认None
|
||||
alignment: 文本对齐方式(0-5),默认1
|
||||
alpha: 文本透明度(0.0-1.0),默认1.0
|
||||
font: 字体名称,默认None
|
||||
font_size: 字体大小,默认15
|
||||
letter_spacing: 字间距,默认None
|
||||
line_spacing: 行间距,默认None
|
||||
scale_x: 水平缩放,默认1.0
|
||||
scale_y: 垂直缩放,默认1.0
|
||||
transform_x: 水平位移,默认0
|
||||
transform_y: 垂直位移,默认0
|
||||
style_text: 是否使用样式文本,默认False
|
||||
|
||||
Returns:
|
||||
draft_url: 草稿URL
|
||||
track_id: 字幕轨道ID
|
||||
text_ids: 字幕ID列表
|
||||
segment_ids: 字幕片段ID列表
|
||||
|
||||
Raises:
|
||||
CustomException: 字幕添加失败
|
||||
"""
|
||||
logger.info(f"add_captions started, draft_url: {draft_url}, captions count: {len(json.loads(captions) if captions else [])}")
|
||||
|
||||
# 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. 解析字幕信息
|
||||
caption_items = parse_captions_data(json_str=captions)
|
||||
if len(caption_items) == 0:
|
||||
logger.info(f"No caption info provided, draft_id: {draft_id}")
|
||||
raise CustomException(CustomError.INVALID_CAPTION_INFO)
|
||||
|
||||
logger.info(f"Parsed {len(caption_items)} caption items")
|
||||
|
||||
# 3. 从缓存中获取草稿
|
||||
script: ScriptFile = DRAFT_CACHE[draft_id]
|
||||
|
||||
# 4. 添加字幕轨道
|
||||
track_name = f"caption_track_{helper.gen_unique_id()}"
|
||||
script.add_track(track_type=TrackType.text, track_name=track_name)
|
||||
logger.info(f"Added caption track: {track_name}")
|
||||
|
||||
# 5. 遍历字幕信息,添加字幕到草稿中的指定轨道,收集片段ID
|
||||
segment_ids = []
|
||||
text_ids = []
|
||||
for i, caption in enumerate(caption_items):
|
||||
try:
|
||||
logger.info(f"Processing caption {i+1}/{len(caption_items)}, text: {caption['text'][:20]}...")
|
||||
|
||||
segment_id, text_id = add_caption_to_draft(
|
||||
script, track_name,
|
||||
caption=caption,
|
||||
text_color=text_color,
|
||||
border_color=border_color,
|
||||
alignment=alignment,
|
||||
alpha=alpha,
|
||||
font=font,
|
||||
font_size=font_size,
|
||||
letter_spacing=letter_spacing,
|
||||
line_spacing=line_spacing,
|
||||
scale_x=scale_x,
|
||||
scale_y=scale_y,
|
||||
transform_x=transform_x,
|
||||
transform_y=transform_y,
|
||||
style_text=style_text
|
||||
)
|
||||
segment_ids.append(segment_id)
|
||||
text_ids.append(text_id)
|
||||
logger.info(f"Added caption {i+1}/{len(caption_items)}, segment_id: {segment_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add caption {i+1}/{len(caption_items)}, error: {str(e)}")
|
||||
raise
|
||||
|
||||
# 6. 保存草稿
|
||||
script.save()
|
||||
logger.info(f"Draft saved successfully")
|
||||
|
||||
# 7. 获取当前字幕轨道ID
|
||||
track_id = ""
|
||||
for key in script.tracks.keys():
|
||||
if script.tracks[key].name == track_name:
|
||||
track_id = script.tracks[key].track_id
|
||||
break
|
||||
logger.info(f"Caption track created, draft_id: {draft_id}, track_id: {track_id}")
|
||||
|
||||
logger.info(f"add_captions completed successfully - draft_id: {draft_id}, track_id: {track_id}, captions_added: {len(caption_items)}")
|
||||
|
||||
return draft_url, track_id, text_ids, segment_ids
|
||||
|
||||
|
||||
def add_caption_to_draft(
|
||||
script: ScriptFile,
|
||||
track_name: str,
|
||||
caption: dict,
|
||||
text_color: str = "#ffffff",
|
||||
border_color: Optional[str] = None,
|
||||
alignment: int = 1,
|
||||
alpha: float = 1.0,
|
||||
font: Optional[str] = None,
|
||||
font_size: int = 15,
|
||||
letter_spacing: Optional[float] = None,
|
||||
line_spacing: Optional[float] = None,
|
||||
scale_x: float = 1.0,
|
||||
scale_y: float = 1.0,
|
||||
transform_x: int = 0,
|
||||
transform_y: int = 0,
|
||||
style_text: bool = False
|
||||
) -> Tuple[str, str]:
|
||||
"""
|
||||
向剪映草稿中添加单个字幕
|
||||
|
||||
Args:
|
||||
script: 草稿文件对象
|
||||
track_name: 字幕轨道名称
|
||||
caption: 字幕信息字典,包含以下字段:
|
||||
start: 字幕开始时间(微秒)
|
||||
end: 字幕结束时间(微秒)
|
||||
text: 字幕文本内容
|
||||
keyword: 关键词(用|分隔多个关键词),可选
|
||||
keyword_color: 关键词颜色,可选
|
||||
keyword_font_size: 关键词字体大小,可选
|
||||
font_size: 文本字体大小,可选
|
||||
in_animation: 入场动画,可选
|
||||
out_animation: 出场动画,可选
|
||||
loop_animation: 循环动画,可选
|
||||
in_animation_duration: 入场动画时长,可选
|
||||
out_animation_duration: 出场动画时长,可选
|
||||
loop_animation_duration: 循环动画时长,可选
|
||||
其他参数:字幕样式设置
|
||||
|
||||
Returns:
|
||||
segment_id: 片段ID
|
||||
text_id: 文本ID(material_id)
|
||||
|
||||
Raises:
|
||||
CustomException: 添加字幕失败
|
||||
"""
|
||||
try:
|
||||
# 1. 创建时间范围
|
||||
caption_duration = caption['end'] - caption['start']
|
||||
timerange = Timerange(start=caption['start'], duration=caption_duration)
|
||||
|
||||
# 2. 解析颜色
|
||||
rgb_color = hex_to_rgb(text_color)
|
||||
|
||||
# 3. 创建文本样式
|
||||
align_value: Literal[0, 1, 2] = 0
|
||||
if alignment == 1:
|
||||
align_value = 1
|
||||
elif alignment == 2:
|
||||
align_value = 2
|
||||
|
||||
text_style = TextStyle(
|
||||
size=float(caption.get('font_size', font_size)),
|
||||
color=rgb_color,
|
||||
alpha=alpha,
|
||||
align=align_value,
|
||||
letter_spacing=int(letter_spacing) if letter_spacing is not None else 0,
|
||||
line_spacing=int(line_spacing) if line_spacing is not None else 0,
|
||||
auto_wrapping=True # 字幕默认开启自动换行
|
||||
)
|
||||
|
||||
# 4. 创建图像调节设置
|
||||
clip_settings = ClipSettings(
|
||||
scale_x=scale_x,
|
||||
scale_y=scale_y,
|
||||
transform_x=float(transform_x) / script.width * 2, # 转换为半画布宽度单位
|
||||
transform_y=float(transform_y) / script.height * 2 # 转换为半画布高度单位
|
||||
)
|
||||
|
||||
# 5. 创建文本片段
|
||||
text_segment = TextSegment(
|
||||
text=caption['text'],
|
||||
timerange=timerange,
|
||||
style=text_style,
|
||||
clip_settings=clip_settings
|
||||
)
|
||||
|
||||
logger.info(f"Created text segment, material_id: {text_segment.material_id}")
|
||||
logger.info(f"Text segment details - start: {caption['start']}, duration: {caption_duration}, text: {caption['text'][:50]}")
|
||||
|
||||
# 6. TODO: 处理关键词高亮(这需要更复杂的实现)
|
||||
if caption.get('keyword'):
|
||||
logger.info(f"Keyword highlighting specified but not implemented yet: {caption['keyword']}")
|
||||
|
||||
# 7. TODO: 处理动画效果(需要导入相应的动画类型)
|
||||
if caption.get('in_animation'):
|
||||
logger.info(f"In animation specified but not implemented yet: {caption['in_animation']}")
|
||||
if caption.get('out_animation'):
|
||||
logger.info(f"Out animation specified but not implemented yet: {caption['out_animation']}")
|
||||
if caption.get('loop_animation'):
|
||||
logger.info(f"Loop animation specified but not implemented yet: {caption['loop_animation']}")
|
||||
|
||||
# 8. 向指定轨道添加片段
|
||||
script.add_segment(text_segment, track_name)
|
||||
|
||||
return text_segment.segment_id, text_segment.material_id
|
||||
|
||||
except CustomException:
|
||||
logger.error(f"Add caption to draft failed, caption: {caption}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Add caption to draft failed, error: {str(e)}")
|
||||
raise CustomException(CustomError.CAPTION_ADD_FAILED)
|
||||
|
||||
|
||||
def parse_captions_data(json_str: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
解析字幕数据的JSON字符串,处理可选字段的默认值
|
||||
|
||||
Args:
|
||||
json_str: 包含字幕数据的JSON字符串,格式如下:
|
||||
[
|
||||
{
|
||||
"start": 0, # [必选] 字幕开始时间(微秒)
|
||||
"end": 10000000, # [必选] 字幕结束时间(微秒)
|
||||
"text": "你好,剪映", # [必选] 字幕文本内容
|
||||
"keyword": "好", # [可选] 关键词(用|分隔多个关键词)
|
||||
"keyword_color": "#457616", # [可选] 关键词颜色,默认"#ff7100"
|
||||
"keyword_font_size": 15, # [可选] 关键词字体大小,默认15
|
||||
"font_size": 15, # [可选] 文本字体大小,默认15
|
||||
"in_animation": None, # [可选] 入场动画,默认None
|
||||
"out_animation": None, # [可选] 出场动画,默认None
|
||||
"loop_animation": None, # [可选] 循环动画,默认None
|
||||
"in_animation_duration": None, # [可选] 入场动画时长,默认None
|
||||
"out_animation_duration": None, # [可选] 出场动画时长,默认None
|
||||
"loop_animation_duration": None # [可选] 循环动画时长,默认None
|
||||
}
|
||||
]
|
||||
|
||||
Returns:
|
||||
包含字幕对象的数组,每个对象都处理了默认值
|
||||
|
||||
Raises:
|
||||
CustomException: 当JSON格式错误或缺少必选字段时抛出
|
||||
"""
|
||||
try:
|
||||
# 解析JSON字符串
|
||||
data = json.loads(json_str)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"JSON parse error: {e.msg}")
|
||||
raise CustomException(CustomError.INVALID_CAPTION_INFO, f"JSON parse error: {e.msg}")
|
||||
|
||||
# 确保输入是列表
|
||||
if not isinstance(data, list):
|
||||
logger.error("captions should be a list")
|
||||
raise CustomException(CustomError.INVALID_CAPTION_INFO, "captions should be a list")
|
||||
|
||||
result = []
|
||||
|
||||
for i, item in enumerate(data):
|
||||
if not isinstance(item, dict):
|
||||
logger.error(f"the {i}th item should be a dict")
|
||||
raise CustomException(CustomError.INVALID_CAPTION_INFO, f"the {i}th item should be a dict")
|
||||
|
||||
# 检查必选字段
|
||||
required_fields = ["start", "end", "text"]
|
||||
missing_fields = [field for field in required_fields if field not in item]
|
||||
|
||||
if missing_fields:
|
||||
logger.error(f"the {i}th item is missing required fields: {', '.join(missing_fields)}")
|
||||
raise CustomException(CustomError.INVALID_CAPTION_INFO, f"the {i}th item is missing required fields: {', '.join(missing_fields)}")
|
||||
|
||||
# 创建处理后的对象,设置默认值
|
||||
processed_item = {
|
||||
"start": item["start"],
|
||||
"end": item["end"],
|
||||
"text": item["text"],
|
||||
"keyword": item.get("keyword", None),
|
||||
"keyword_color": item.get("keyword_color", "#ff7100"),
|
||||
"keyword_font_size": item.get("keyword_font_size", 15),
|
||||
"font_size": item.get("font_size", 15),
|
||||
"in_animation": item.get("in_animation", None),
|
||||
"out_animation": item.get("out_animation", None),
|
||||
"loop_animation": item.get("loop_animation", None),
|
||||
"in_animation_duration": item.get("in_animation_duration", None),
|
||||
"out_animation_duration": item.get("out_animation_duration", None),
|
||||
"loop_animation_duration": item.get("loop_animation_duration", None)
|
||||
}
|
||||
|
||||
# 验证数值类型和范围
|
||||
if not isinstance(processed_item["start"], (int, float)) or processed_item["start"] < 0:
|
||||
logger.error(f"the {i}th item has invalid start time: {processed_item['start']}")
|
||||
raise CustomException(CustomError.INVALID_CAPTION_INFO, f"the {i}th item has invalid start time")
|
||||
|
||||
if not isinstance(processed_item["end"], (int, float)) or processed_item["end"] <= processed_item["start"]:
|
||||
logger.error(f"the {i}th item has invalid end time: {processed_item['end']}")
|
||||
raise CustomException(CustomError.INVALID_CAPTION_INFO, f"the {i}th item has invalid end time")
|
||||
|
||||
if not isinstance(processed_item["text"], str) or len(processed_item["text"].strip()) == 0:
|
||||
logger.error(f"the {i}th item has invalid text: {processed_item['text']}")
|
||||
raise CustomException(CustomError.INVALID_CAPTION_INFO, f"the {i}th item has invalid text")
|
||||
|
||||
# 验证字体大小
|
||||
if not isinstance(processed_item["font_size"], (int, float)) or processed_item["font_size"] <= 0:
|
||||
processed_item["font_size"] = 15
|
||||
if not isinstance(processed_item["keyword_font_size"], (int, float)) or processed_item["keyword_font_size"] <= 0:
|
||||
processed_item["keyword_font_size"] = 15
|
||||
|
||||
result.append(processed_item)
|
||||
|
||||
logger.info(f"Successfully parsed {len(result)} caption items")
|
||||
return result
|
||||
|
||||
|
||||
def hex_to_rgb(hex_color: str) -> tuple:
|
||||
"""
|
||||
将十六进制颜色值转换为RGB三元组(0-1范围)
|
||||
|
||||
Args:
|
||||
hex_color: 十六进制颜色值,如"#ffffff"或"ffffff"
|
||||
|
||||
Returns:
|
||||
RGB三元组,取值范围为[0, 1]
|
||||
"""
|
||||
# 移除#号(如果存在)
|
||||
hex_color = hex_color.lstrip('#')
|
||||
|
||||
# 确保是6位十六进制
|
||||
if len(hex_color) != 6:
|
||||
logger.warning(f"Invalid hex color format: {hex_color}, using white as default")
|
||||
return (1.0, 1.0, 1.0)
|
||||
|
||||
try:
|
||||
# 转换为RGB值(0-255)
|
||||
r = int(hex_color[0:2], 16)
|
||||
g = int(hex_color[2:4], 16)
|
||||
b = int(hex_color[4:6], 16)
|
||||
|
||||
# 转换为0-1范围
|
||||
return (r / 255.0, g / 255.0, b / 255.0)
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid hex color format: {hex_color}, using white as default")
|
||||
return (1.0, 1.0, 1.0)
|
||||
Reference in New Issue
Block a user