mirror of
https://github.com/Hommy-master/capcut-mate.git
synced 2026-08-28 23:27:50 +08:00
添加文字竖排的功能。
This commit is contained in:
+15
-2
@@ -27,7 +27,7 @@ Batch-add captions to an existing CapCut/Jianying draft. Supports text color, bo
|
||||
| captions | string | ✅ | - | Caption list as a **JSON string** (not a raw JSON array) |
|
||||
| text_color | string | ❌ | `"#ffffff"` | Normal text color (hex) |
|
||||
| border_color | string | ❌ | `null` | Normal text stroke color (hex); `null` means no stroke |
|
||||
| alignment | integer | ❌ | `1` | Alignment: `0` left, `1` center, `2` right (`3`-`5` reserved) |
|
||||
| alignment | integer | ❌ | `1` | Alignment: `0` left, `1` center, `2` right (horizontal); `3` vertical center, `4` vertical left, `5` vertical right (vertical typesetting) |
|
||||
| alpha | number | ❌ | `1.0` | Opacity in `[0.0, 1.0]` |
|
||||
| font | string | ❌ | `null` | Font name from Supported Fonts below (enum/display/alias also ok); `null` uses default |
|
||||
| font_size | integer | ❌ | `15` | Default font size when a caption item omits `font_size`; must be `>= 1` |
|
||||
@@ -91,6 +91,19 @@ Default shadow when enabled without `*_shadow_info`:
|
||||
}
|
||||
```
|
||||
|
||||
### Notes on alignment
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| 0 | Left (horizontal) |
|
||||
| 1 | Center (horizontal) |
|
||||
| 2 | Right (horizontal) |
|
||||
| 3 | Vertical center (vertical typesetting + center) |
|
||||
| 4 | Vertical left (vertical typesetting + left) |
|
||||
| 5 | Vertical right (vertical typesetting + right) |
|
||||
|
||||
`alignment` controls in-box typesetting and line alignment, not canvas position. Use `transform_x` / `transform_y` to move captions on screen. Values `3`/`4`/`5` set the draft material `typesetting` to vertical.
|
||||
|
||||
### Notes on text_effect vs shadow
|
||||
|
||||
If `text_effect` resolves to a valid effect, the API resets `text_color` to `#ffffff`, `border_color` to `null`, `has_shadow` to `false`, and disables keyword shadow. Omit/leave `text_effect` null when you need custom colors or shadows.
|
||||
@@ -1314,7 +1327,7 @@ VHS
|
||||
|
||||
"text_color": "#ffffff", // [Optional] normal text color
|
||||
"border_color": "#333333", // [Optional] normal stroke color
|
||||
"alignment": 1, // [Optional] 0 left / 1 center / 2 right
|
||||
"alignment": 1, // [Optional] 0 left / 1 center / 2 right (horizontal); 3/4/5 vertical center/left/right
|
||||
"alpha": 1.0, // [Optional] opacity [0,1]
|
||||
"font": "得意黑", // [Optional] font name
|
||||
"font_size": 15, // [Optional] top-level default font size
|
||||
|
||||
+10
-8
@@ -27,7 +27,7 @@ POST /openapi/capcut-mate/v1/add_captions
|
||||
| captions | string | ✅ | - | 字幕信息列表的 **JSON 字符串**(不是 JSON 数组本身) |
|
||||
| text_color | string | ❌ | `"#ffffff"` | 普通字幕文本颜色,十六进制,如 `#ffffff` |
|
||||
| border_color | string | ❌ | `null` | 普通字幕描边颜色,十六进制;`null` 表示无描边 |
|
||||
| alignment | integer | ❌ | `1` | 文本对齐方式:`0` 左对齐,`1` 居中,`2` 右对齐(`3`-`5` 为预留) |
|
||||
| alignment | integer | ❌ | `1` | 文本对齐方式:`0` 左对齐,`1` 居中,`2` 右对齐(横排);`3` 垂直居中,`4` 垂直左对齐,`5` 垂直右对齐(竖排) |
|
||||
| alpha | number | ❌ | `1.0` | 文本透明度,取值范围 `[0.0, 1.0]`,`1.0` 为不透明 |
|
||||
| font | string | ❌ | `null` | 字体名称,须为下方「支持的字体」中的展示名(也支持枚举名/别名);`null` 使用默认字体 |
|
||||
| font_size | integer | ❌ | `15` | 接口级默认字号;当 caption 项未指定 `font_size` 时生效,须 `>= 1` |
|
||||
@@ -97,12 +97,14 @@ POST /openapi/capcut-mate/v1/add_captions
|
||||
|
||||
| 值 | 说明 |
|
||||
|---|------|
|
||||
| 0 | 左对齐 |
|
||||
| 1 | 居中对齐 |
|
||||
| 2 | 右对齐 |
|
||||
| 3 | 垂直居中(预留) |
|
||||
| 4 | 垂直左对齐(预留) |
|
||||
| 5 | 垂直右对齐(预留) |
|
||||
| 0 | 左对齐(横排) |
|
||||
| 1 | 居中对齐(横排) |
|
||||
| 2 | 右对齐(横排) |
|
||||
| 3 | 垂直居中(竖排 + 居中) |
|
||||
| 4 | 垂直左对齐(竖排 + 左对齐) |
|
||||
| 5 | 垂直右对齐(竖排 + 右对齐) |
|
||||
|
||||
`alignment` 控制文本框内的排版方向与行对齐,不会改变字幕在画布上的位置。画面位置请使用 `transform_x` / `transform_y`。`3`/`4`/`5` 会将草稿素材的 `typesetting` 设为竖排。
|
||||
|
||||
#### 花字与阴影的关系
|
||||
|
||||
@@ -1327,7 +1329,7 @@ VHS
|
||||
|
||||
"text_color": "#ffffff", // 【可选】普通文本颜色
|
||||
"border_color": "#333333", // 【可选】普通文本描边颜色
|
||||
"alignment": 1, // 【可选】对齐:0左/1中/2右
|
||||
"alignment": 1, // 【可选】对齐:0左/1中/2右(横排),3垂直居中/4垂直左/5垂直右(竖排)
|
||||
"alpha": 1.0, // 【可选】透明度 [0,1]
|
||||
"font": "得意黑", // 【可选】字体名称
|
||||
"font_size": 15, // 【可选】接口级默认字号
|
||||
|
||||
+1
-1
@@ -12183,7 +12183,7 @@ paths:
|
||||
properties:
|
||||
alignment:
|
||||
default: 1
|
||||
description: 文本对齐方式(0-5)
|
||||
description: 文本对齐方式:0左/1中/2右(横排),3垂直居中/4垂直左对齐/5垂直右对齐(竖排)
|
||||
type: integer
|
||||
alpha:
|
||||
default: 1
|
||||
|
||||
@@ -17,7 +17,12 @@ class AddCaptionsRequest(BaseModel):
|
||||
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)")
|
||||
alignment: int = Field(
|
||||
default=1,
|
||||
ge=0,
|
||||
le=5,
|
||||
description="文本对齐方式:0左/1中/2右(横排),3垂直居中/4垂直左对齐/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="字体大小")
|
||||
|
||||
@@ -104,6 +104,28 @@ def resolve_font_type(font_name: str) -> Optional[FontType]:
|
||||
return None
|
||||
|
||||
|
||||
# alignment → (草稿 alignment 0/1/2, 是否竖排)
|
||||
# 0/1/2 为横排左/中/右;3/4/5 为竖排居中/左/右(对齐值分别对应 1/0/2)
|
||||
CAPTION_ALIGNMENT_MAP: Dict[int, Tuple[Literal[0, 1, 2], bool]] = {
|
||||
0: (0, False),
|
||||
1: (1, False),
|
||||
2: (2, False),
|
||||
3: (1, True),
|
||||
4: (0, True),
|
||||
5: (2, True),
|
||||
}
|
||||
|
||||
|
||||
def resolve_caption_alignment(alignment: int) -> Tuple[Literal[0, 1, 2], bool]:
|
||||
"""将接口 alignment 映射为草稿字段 (align, vertical)。
|
||||
|
||||
0 左对齐、1 居中、2 右对齐(横排);
|
||||
3 垂直居中、4 垂直左对齐、5 垂直右对齐(竖排)。
|
||||
未识别的值回退为横排左对齐,与历史实现一致。
|
||||
"""
|
||||
return CAPTION_ALIGNMENT_MAP.get(alignment, (0, False))
|
||||
|
||||
|
||||
def add_captions(
|
||||
draft_url: str,
|
||||
captions: str,
|
||||
@@ -156,7 +178,7 @@ def add_captions(
|
||||
]
|
||||
text_color: 文本颜色(十六进制),默认"#ffffff"
|
||||
border_color: 边框颜色(十六进制),默认 None
|
||||
alignment: 文本对齐方式(0-5),默认 1
|
||||
alignment: 文本对齐方式,0左/1中/2右(横排),3垂直居中/4垂直左/5垂直右(竖排),默认 1
|
||||
alpha: 文本透明度(0.0-1.0),默认 1.0
|
||||
font: 字体名称,默认 None
|
||||
font_size: 字体大小,默认 15
|
||||
@@ -315,7 +337,7 @@ async def add_captions_async(
|
||||
captions: JSON 字符串,包含字幕信息列表,详见 add_captions 函数
|
||||
text_color: 文本颜色(十六进制),默认"#ffffff"
|
||||
border_color: 边框颜色(十六进制),默认 None
|
||||
alignment: 文本对齐方式(0-5),默认 1
|
||||
alignment: 文本对齐方式,0左/1中/2右(横排),3垂直居中/4垂直左/5垂直右(竖排),默认 1
|
||||
alpha: 文本透明度(0.0-1.0),默认 1.0
|
||||
font: 字体名称,默认 None
|
||||
font_size: 字体大小,默认 15
|
||||
@@ -447,7 +469,7 @@ def add_caption_to_draft(
|
||||
loop_animation_duration: 循环动画时长,可选
|
||||
text_color: 文本颜色(十六进制),默认"#ffffff"
|
||||
border_color: 边框颜色(十六进制),默认 None
|
||||
alignment: 文本对齐方式(0-5),默认 1
|
||||
alignment: 文本对齐方式,0左/1中/2右(横排),3垂直居中/4垂直左/5垂直右(竖排),默认 1
|
||||
alpha: 文本透明度(0.0-1.0),默认 1.0
|
||||
font: 字体名称,默认 None
|
||||
font_size: 字体大小,默认 15
|
||||
@@ -504,11 +526,7 @@ def add_caption_to_draft(
|
||||
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
|
||||
align_value, is_vertical = resolve_caption_alignment(alignment)
|
||||
|
||||
# 根据需求修改:只有当caption中明确指定了font_size时才使用,否则不设置默认值
|
||||
font_size_value = font_size
|
||||
@@ -521,6 +539,7 @@ def add_caption_to_draft(
|
||||
color=rgb_color,
|
||||
alpha=alpha,
|
||||
align=align_value,
|
||||
vertical=is_vertical,
|
||||
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, # 字幕默认开启自动换行
|
||||
@@ -528,7 +547,11 @@ def add_caption_to_draft(
|
||||
italic=italic,
|
||||
bold=bold
|
||||
)
|
||||
logger.info(f"Created text style, text_style.size: {text_style.size}, font_size from caption: {font_size}")
|
||||
logger.info(
|
||||
f"Created text style, text_style.size: {text_style.size}, "
|
||||
f"align: {text_style.align}, vertical: {text_style.vertical}, "
|
||||
f"font_size from caption: {font_size}"
|
||||
)
|
||||
|
||||
# 4. 创建文本描边(如果提供了border_color)
|
||||
text_border = None
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
"""add_captions alignment:0/1/2 横排保持不变,3/4/5 映射为竖排。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from src.pyJianYingDraft import ScriptFile, TrackType
|
||||
from src.schemas.add_captions import AddCaptionsRequest
|
||||
from src.service.add_captions import (
|
||||
add_caption_to_draft,
|
||||
resolve_caption_alignment,
|
||||
)
|
||||
|
||||
|
||||
def _make_script(track_name: str = "caption_track") -> ScriptFile:
|
||||
script = ScriptFile(width=1920, height=1080, fps=30, maintrack_adsorb=False)
|
||||
script.add_track(TrackType.text, track_name)
|
||||
return script
|
||||
|
||||
|
||||
def _add_and_export_material(alignment: int, *, caption_extra: dict | None = None, **kwargs):
|
||||
script = _make_script()
|
||||
caption = {"start": 0, "end": 1_000_000, "text": "你好世界"}
|
||||
if caption_extra:
|
||||
caption.update(caption_extra)
|
||||
_, text_id, _ = add_caption_to_draft(
|
||||
script,
|
||||
"caption_track",
|
||||
caption=caption,
|
||||
alignment=alignment,
|
||||
**kwargs,
|
||||
)
|
||||
material = next((item for item in script.materials.texts if item["id"] == text_id), None)
|
||||
assert material is not None
|
||||
return script, material
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"alignment, expected_align, expected_vertical",
|
||||
[
|
||||
(0, 0, False),
|
||||
(1, 1, False),
|
||||
(2, 2, False),
|
||||
(3, 1, True),
|
||||
(4, 0, True),
|
||||
(5, 2, True),
|
||||
],
|
||||
)
|
||||
def test_resolve_caption_alignment_mapping(alignment, expected_align, expected_vertical):
|
||||
assert resolve_caption_alignment(alignment) == (expected_align, expected_vertical)
|
||||
|
||||
|
||||
def test_resolve_caption_alignment_unknown_falls_back_to_horizontal_left():
|
||||
"""与历史实现一致:无法识别的值回退为横排左对齐。"""
|
||||
assert resolve_caption_alignment(-1) == (0, False)
|
||||
assert resolve_caption_alignment(6) == (0, False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"alignment, expected_align, expected_typesetting",
|
||||
[
|
||||
(0, 0, 0),
|
||||
(1, 1, 0),
|
||||
(2, 2, 0),
|
||||
(3, 1, 1),
|
||||
(4, 0, 1),
|
||||
(5, 2, 1),
|
||||
],
|
||||
)
|
||||
def test_add_caption_to_draft_writes_alignment_and_typesetting(
|
||||
alignment, expected_align, expected_typesetting
|
||||
):
|
||||
_, material = _add_and_export_material(alignment)
|
||||
assert material["alignment"] == expected_align
|
||||
assert material["typesetting"] == expected_typesetting
|
||||
|
||||
|
||||
def test_default_alignment_remains_center_horizontal():
|
||||
"""默认 alignment=1:居中横排,不影响既有默认行为。"""
|
||||
_, material = _add_and_export_material(1)
|
||||
assert material["alignment"] == 1
|
||||
assert material["typesetting"] == 0
|
||||
assert material["type"] == "subtitle"
|
||||
|
||||
|
||||
def test_horizontal_alignment_keeps_existing_style_fields():
|
||||
"""0/1/2 不应改变字号、颜色、自动换行等既有样式。"""
|
||||
_, material = _add_and_export_material(
|
||||
0,
|
||||
text_color="#ff0000",
|
||||
font_size=22,
|
||||
underline=True,
|
||||
italic=True,
|
||||
bold=True,
|
||||
)
|
||||
content = json.loads(material["content"])
|
||||
base_style = content["styles"][0]
|
||||
|
||||
assert material["alignment"] == 0
|
||||
assert material["typesetting"] == 0
|
||||
assert material["type"] == "subtitle"
|
||||
assert base_style["size"] == 22
|
||||
assert base_style["underline"] is True
|
||||
assert base_style["italic"] is True
|
||||
assert base_style["bold"] is True
|
||||
assert base_style["fill"]["content"]["solid"]["color"] == [1.0, 0.0, 0.0]
|
||||
|
||||
|
||||
def test_vertical_alignment_does_not_break_font_size_or_wrapping():
|
||||
"""3/4/5 只打开竖排,字号与自动换行保持原逻辑。"""
|
||||
_, material = _add_and_export_material(
|
||||
3,
|
||||
caption_extra={"font_size": 18},
|
||||
font_size=15,
|
||||
)
|
||||
content = json.loads(material["content"])
|
||||
base_style = content["styles"][0]
|
||||
|
||||
assert material["alignment"] == 1
|
||||
assert material["typesetting"] == 1
|
||||
assert material["type"] == "subtitle"
|
||||
assert base_style["size"] == 18.0
|
||||
|
||||
|
||||
def test_vertical_alignment_keeps_keyword_highlight():
|
||||
"""竖排不应打断关键词高亮分区。"""
|
||||
_, material = _add_and_export_material(
|
||||
5,
|
||||
caption_extra={"keyword": "世界", "keyword_color": "#ff7100", "keyword_font_size": 20},
|
||||
)
|
||||
content = json.loads(material["content"])
|
||||
|
||||
assert material["alignment"] == 2
|
||||
assert material["typesetting"] == 1
|
||||
assert len(content["styles"]) >= 2
|
||||
highlight = next(style for style in content["styles"] if style.get("size") == 20)
|
||||
assert highlight["range"] == [2, 4]
|
||||
|
||||
|
||||
def test_schema_accepts_alignment_0_to_5():
|
||||
captions = json.dumps([{"start": 0, "end": 1000, "text": "hi"}])
|
||||
for value in range(6):
|
||||
req = AddCaptionsRequest(draft_url="http://x?draft_id=1", captions=captions, alignment=value)
|
||||
assert req.alignment == value
|
||||
|
||||
|
||||
def test_schema_rejects_alignment_out_of_range():
|
||||
captions = json.dumps([{"start": 0, "end": 1000, "text": "hi"}])
|
||||
with pytest.raises(ValidationError):
|
||||
AddCaptionsRequest(draft_url="http://x?draft_id=1", captions=captions, alignment=6)
|
||||
with pytest.raises(ValidationError):
|
||||
AddCaptionsRequest(draft_url="http://x?draft_id=1", captions=captions, alignment=-1)
|
||||
Reference in New Issue
Block a user