添加文字竖排的功能。

This commit is contained in:
Hommy
2026-08-19 11:12:36 +08:00
parent 873282d966
commit 7c7efe860c
6 changed files with 219 additions and 21 deletions
+15 -2
View File
@@ -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
View File
@@ -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
View File
@@ -12183,7 +12183,7 @@ paths:
properties:
alignment:
default: 1
description: 文本对齐方式0-5
description: 文本对齐方式:0左/1中/2右(横排),3垂直居中/4垂直左对齐/5垂直右对齐(竖排
type: integer
alpha:
default: 1
+6 -1
View File
@@ -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="字体大小")
+32 -9
View File
@@ -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
+155
View File
@@ -0,0 +1,155 @@
"""add_captions alignment0/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)