新添加字幕的接口。

This commit is contained in:
Hommy
2025-09-24 14:47:27 +08:00
parent 1e3a67ea53
commit b609a37986
6 changed files with 701 additions and 1 deletions
+228
View File
@@ -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. **边框和阴影**: 支持字幕边框和阴影效果
+2
View File
@@ -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")
+34
View File
@@ -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:
"""
+46
View File
@@ -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列表")
+2 -1
View File
@@ -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"]
+389
View File
@@ -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: 文本IDmaterial_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)