Files
capcut-mate/scripts/gen_media_effect_docs.py
2026-08-06 11:12:59 +08:00

414 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Generate transition/animation lists for add_images / add_videos docs.
Lists belong in add_xxx docs (not xxx_infos). This script also strips any
leftover MEDIA_EFFECT_LIST blocks from imgs_infos / video_infos and leaves
a short pointer to the corresponding add_xxx document.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from src.pyJianYingDraft.metadata import (
IntroType,
OutroType,
GroupAnimationType,
TransitionType,
)
from src.service.add_images import map_video_animation_name_to_enum
from src.service.add_videos import find_transition_type_by_name
MARKER_START = "<!-- MEDIA_EFFECT_LIST_START -->"
MARKER_END = "<!-- MEDIA_EFFECT_LIST_END -->"
EXAMPLE_IN = "渐显"
EXAMPLE_OUT = "渐隐"
EXAMPLE_LOOP = "动感摇晃I"
EXAMPLE_TRANSITION = "叠化"
def _sorted_titles(enum_cls) -> list[str]:
return sorted({item.value.title for item in enum_cls}, key=lambda s: (s.casefold(), s))
def _sorted_transition_names() -> list[str]:
return sorted({item.value.name for item in TransitionType}, key=lambda s: (s.casefold(), s))
def _validate() -> tuple[list[str], list[str], list[str], list[str]]:
ins = _sorted_titles(IntroType)
outs = _sorted_titles(OutroType)
loops = _sorted_titles(GroupAnimationType)
transitions = _sorted_transition_names()
bad_in = [n for n in ins if map_video_animation_name_to_enum(n, "in") is None]
bad_out = [n for n in outs if map_video_animation_name_to_enum(n, "out") is None]
bad_loop = [n for n in loops if map_video_animation_name_to_enum(n, "group") is None]
bad_tr = [n for n in transitions if find_transition_type_by_name(n) is None]
if bad_in or bad_out or bad_loop or bad_tr:
raise SystemExit(
f"unresolvable: in={bad_in[:3]} out={bad_out[:3]} "
f"loop={bad_loop[:3]} tr={bad_tr[:3]}"
)
for name, kind in [(EXAMPLE_IN, "in"), (EXAMPLE_OUT, "out"), (EXAMPLE_LOOP, "group")]:
if map_video_animation_name_to_enum(name, kind) is None:
raise SystemExit(f"example unresolved: {name}")
if find_transition_type_by_name(EXAMPLE_TRANSITION) is None:
raise SystemExit(f"example transition unresolved: {EXAMPLE_TRANSITION}")
return ins, outs, loops, transitions
def build_images_zh(ins, outs, loops, transitions) -> str:
return "\n".join(
[
"### 支持的转场与动画名称",
"",
"下列名称可直接作为 `image_infos` 中对应字段的值(与剪映端展示名一致)。未匹配到时该效果不会生效。",
"",
f"#### 转场(transition,共 {len(transitions)} 种)",
"",
"```text",
*transitions,
"```",
"",
f"#### 入场动画(in_animation,共 {len(ins)} 种)",
"",
"```text",
*ins,
"```",
"",
f"#### 出场动画(out_animation,共 {len(outs)} 种)",
"",
"```text",
*outs,
"```",
"",
f"#### 循环动画(loop_animation,共 {len(loops)} 种)",
"",
"```text",
*loops,
"```",
"",
]
)
def build_images_en(ins, outs, loops, transitions) -> str:
return "\n".join(
[
"### Supported Transitions and Animations",
"",
"Use the names below directly as `image_infos` field values (same as CapCut/Jianying display names). Unmatched names are ignored.",
"",
f"#### Transitions (transition, {len(transitions)} total)",
"",
"```text",
*transitions,
"```",
"",
f"#### Intro animations (in_animation, {len(ins)} total)",
"",
"```text",
*ins,
"```",
"",
f"#### Outro animations (out_animation, {len(outs)} total)",
"",
"```text",
*outs,
"```",
"",
f"#### Loop animations (loop_animation, {len(loops)} total)",
"",
"```text",
*loops,
"```",
"",
]
)
def build_videos_zh(transitions) -> str:
return "\n".join(
[
"### 支持的转场名称(transition 可用值)",
"",
"下列名称可直接作为 `video_infos` 中 `transition` 的值(与剪映端展示名一致)。未匹配到时该转场不会生效。",
"",
f"当前共 **{len(transitions)}** 种转场:",
"",
"```text",
*transitions,
"```",
"",
]
)
def build_videos_en(transitions) -> str:
return "\n".join(
[
"### Supported Transition Names (`transition` values)",
"",
"Use the names below directly as `video_infos.transition` (same as CapCut/Jianying display names). Unmatched names are ignored.",
"",
f"Total: **{len(transitions)}** transitions:",
"",
"```text",
*transitions,
"```",
"",
]
)
def inject(path: Path, before: str, block: str) -> None:
text = path.read_text(encoding="utf-8")
wrapped = f"{MARKER_START}\n{block.rstrip()}\n{MARKER_END}\n\n"
if MARKER_START in text and MARKER_END in text:
start = text.index(MARKER_START)
end = text.index(MARKER_END) + len(MARKER_END)
after = text[end:].lstrip("\n")
text = text[:start] + wrapped.rstrip("\n") + "\n\n" + after
else:
if before not in text:
raise SystemExit(f"insert marker not found in {path}: {before}")
text = text.replace(before, wrapped + before, 1)
path.write_text(text, encoding="utf-8")
def strip_list_keep_pointer(path: Path, pointer: str) -> None:
"""Remove MEDIA_EFFECT_LIST from infos docs and insert a short pointer."""
text = path.read_text(encoding="utf-8")
if MARKER_START in text and MARKER_END in text:
start = text.index(MARKER_START)
end = text.index(MARKER_END) + len(MARKER_END)
after = text[end:].lstrip("\n")
text = text[:start] + pointer.rstrip() + "\n\n" + after
path.write_text(text, encoding="utf-8")
def update_add_images_hints(path: Path, lang: str) -> None:
text = path.read_text(encoding="utf-8")
if lang == "zh":
text = text.replace(
"| in_animation | string | ❌ | - | 入场动画名称(可选) |",
"| in_animation | string | ❌ | - | 入场动画名称,须为下方「入场动画」列表中的值 |",
)
text = text.replace(
"| out_animation | string | ❌ | - | 出场动画名称(可选) |",
"| out_animation | string | ❌ | - | 出场动画名称,须为下方「出场动画」列表中的值 |",
)
text = text.replace(
"| loop_animation | string | ❌ | - | 循环动画名称(可选) |",
"| loop_animation | string | ❌ | - | 循环动画名称,须为下方「循环动画」列表中的值 |",
)
text = text.replace(
"| transition | string | ❌ | - | 转场效果名称(可选) |",
"| transition | string | ❌ | - | 转场名称,须为下方「转场」列表中的值 |",
)
else:
text = text.replace(
"| in_animation | string | ❌ | - | Intro animation name (optional) |",
"| in_animation | string | ❌ | - | Intro animation name from Intro animations below |",
)
text = text.replace(
"| out_animation | string | ❌ | - | Outro animation name (optional) |",
"| out_animation | string | ❌ | - | Outro animation name from Outro animations below |",
)
text = text.replace(
"| loop_animation | string | ❌ | - | Loop animation name (optional) |",
"| loop_animation | string | ❌ | - | Loop animation name from Loop animations below |",
)
text = text.replace(
"| transition | string | ❌ | - | Transition effect name (optional) |",
"| transition | string | ❌ | - | Transition name from Transitions below |",
)
path.write_text(text, encoding="utf-8")
def update_add_videos_hints(path: Path, lang: str) -> None:
text = path.read_text(encoding="utf-8")
if lang == "zh":
text = text.replace(
"| transition | string | ❌ | - | 转场效果名称 |",
"| transition | string | ❌ | - | 转场名称,须为下方「支持的转场名称」列表中的值 |",
)
# Replace invalid transition examples
text = text.replace('\\"淡入淡出\\"', f'\\"{EXAMPLE_TRANSITION}\\"')
text = text.replace('"淡入淡出"', f'"{EXAMPLE_TRANSITION}"')
# Shorten vague transition section if present
text = re.sub(
r"#### 转场效果\n\n- \*\*transition\*\*: 转场效果名称\n- \*\*transition_duration\*\*: 转场持续时间\n"
r" - 最小值:100,000微秒(0\.1秒)\n"
r" - 最大值:2,500,000微秒(2\.5秒)\n"
r" - 推荐值:500,000微秒(0\.5秒)\n",
"#### 转场效果\n\n"
"- **transition**: 转场名称,须为下方「支持的转场名称」列表中的值\n"
"- **transition_duration**: 转场持续时间\n"
" - 最小值:100,000微秒(0.1秒)\n"
" - 最大值:2,500,000微秒(2.5秒)\n"
" - 推荐值:500,000微秒(0.5秒)\n",
text,
count=1,
)
else:
text = text.replace(
"| transition | string | ❌ | - | Transition effect name |",
"| transition | string | ❌ | - | Transition name from Supported Transition Names below |",
)
text = text.replace('\\"fade\\"', f'\\"{EXAMPLE_TRANSITION}\\"')
text = text.replace('"fade"', f'"{EXAMPLE_TRANSITION}"')
text = text.replace('\\"淡入淡出\\"', f'\\"{EXAMPLE_TRANSITION}\\"')
text = text.replace('"淡入淡出"', f'"{EXAMPLE_TRANSITION}"')
path.write_text(text, encoding="utf-8")
def update_infos_examples(path: Path) -> None:
"""Keep infos docs examples valid, without embedding full lists."""
text = path.read_text(encoding="utf-8")
text = text.replace('\\"淡入淡出\\"', f'\\"{EXAMPLE_TRANSITION}\\"')
text = text.replace('"淡入淡出"', f'"{EXAMPLE_TRANSITION}"')
text = text.replace('\\"fade\\"', f'\\"{EXAMPLE_TRANSITION}\\"')
text = text.replace('"fade"', f'"{EXAMPLE_TRANSITION}"')
text = text.replace('\\"cross_fade\\"', f'\\"{EXAMPLE_TRANSITION}\\"')
text = text.replace('"cross_fade"', f'"{EXAMPLE_TRANSITION}"')
text = text.replace('\\"fade_in\\"', f'\\"{EXAMPLE_IN}\\"')
text = text.replace('"fade_in"', f'"{EXAMPLE_IN}"')
text = text.replace('\\"fade_out\\"', f'\\"{EXAMPLE_OUT}\\"')
text = text.replace('"fade_out"', f'"{EXAMPLE_OUT}"')
text = text.replace('\\"bounce\\"', f'\\"{EXAMPLE_LOOP}\\"')
text = text.replace('"bounce"', f'"{EXAMPLE_LOOP}"')
path.write_text(text, encoding="utf-8")
def main() -> None:
docs = ROOT / "docs"
ins, outs, loops, transitions = _validate()
# --- add_images ---
inject(docs / "add_images.zh.md", "## 响应格式", build_images_zh(ins, outs, loops, transitions))
inject(docs / "add_images.md", "## Response Format", build_images_en(ins, outs, loops, transitions))
update_add_images_hints(docs / "add_images.zh.md", "zh")
update_add_images_hints(docs / "add_images.md", "en")
# --- add_videos ---
inject(docs / "add_videos.zh.md", "## 响应格式", build_videos_zh(transitions))
# EN may use "## Response Format" after transition section
inject(docs / "add_videos.md", "## Response Format", build_videos_en(transitions))
update_add_videos_hints(docs / "add_videos.zh.md", "zh")
update_add_videos_hints(docs / "add_videos.md", "en")
# --- strip from infos docs, leave pointers ---
strip_list_keep_pointer(
docs / "imgs_infos.zh.md",
"### 可用转场与动画名称\n\n"
"转场、入场/出场/循环动画的完整可用值清单,请参见 "
"[添加图片(add_images](./add_images.zh.md) 文档中的「支持的转场与动画名称」。",
)
strip_list_keep_pointer(
docs / "imgs_infos.md",
"### Available Transitions and Animations\n\n"
"For the full lists of valid `transition` / `in_animation` / `out_animation` / `loop_animation` values, "
"see [add_images](./add_images.md) → Supported Transitions and Animations.",
)
strip_list_keep_pointer(
docs / "video_infos.zh.md",
"### 可用转场名称\n\n"
"转场完整可用值清单,请参见 [添加视频(add_videos](./add_videos.zh.md) 文档中的「支持的转场名称」。"
"画面入场/出场/循环动画请参见 [添加图片(add_images](./add_images.zh.md)。",
)
strip_list_keep_pointer(
docs / "video_infos.md",
"### Available Transition Names\n\n"
"For the full list of valid `transition` values, see [add_videos](./add_videos.md) → Supported Transition Names. "
"For intro/outro/loop clip animations, see [add_images](./add_images.md).",
)
for name in ("imgs_infos.zh.md", "imgs_infos.md", "video_infos.zh.md", "video_infos.md"):
update_infos_examples(docs / name)
# Restore infos param hints that previously pointed to "下方列表"
for path, replacements in [
(
docs / "imgs_infos.zh.md",
[
(
"| in_animation | string |❌ | None |入场动画名称,须为下方「入场动画」列表中的值 |",
"| in_animation | string |❌ | None |入场动画名称,可用值见 [add_images](./add_images.zh.md) |",
),
(
"| out_animation | string |❌ | None |出场动画名称,须为下方「出场动画」列表中的值 |",
"| out_animation | string |❌ | None |出场动画名称,可用值见 [add_images](./add_images.zh.md) |",
),
(
"| loop_animation | string |❌ | None |循环动画名称,须为下方「循环动画」列表中的值 |",
"| loop_animation | string |❌ | None |循环动画名称,可用值见 [add_images](./add_images.zh.md) |",
),
(
"| transition | string |❌ | None |转场名称,须为下方「转场」列表中的值 |",
"| transition | string |❌ | None |转场名称,可用值见 [add_images](./add_images.zh.md) / [add_videos](./add_videos.zh.md) |",
),
],
),
(
docs / "imgs_infos.md",
[
(
"| in_animation | string |❌ | None | Intro animation name from Intro animations below |",
"| in_animation | string |❌ | None | Intro animation name; see [add_images](./add_images.md) |",
),
(
"| out_animation | string |❌ | None | Outro animation name from Outro animations below |",
"| out_animation | string |❌ | None | Outro animation name; see [add_images](./add_images.md) |",
),
(
"| loop_animation | string |❌ | None | Loop animation name from Loop animations below |",
"| loop_animation | string |❌ | None | Loop animation name; see [add_images](./add_images.md) |",
),
(
"| transition | string |❌ | None | Transition name from Transitions below |",
"| transition | string |❌ | None | Transition name; see [add_images](./add_images.md) / [add_videos](./add_videos.md) |",
),
],
),
(
docs / "video_infos.zh.md",
[
(
"| transition | string |❌ | None |转场名称,须为下方「转场」列表中的值 |",
"| transition | string |❌ | None |转场名称,可用值见 [add_videos](./add_videos.zh.md) |",
),
],
),
(
docs / "video_infos.md",
[
(
"| transition | string |❌ | None | Transition name from Transitions below |",
"| transition | string |❌ | None | Transition name; see [add_videos](./add_videos.md) |",
),
],
),
]:
text = path.read_text(encoding="utf-8")
for old, new in replacements:
text = text.replace(old, new)
path.write_text(text, encoding="utf-8")
print(
"moved media effect lists to add_images/add_videos; "
f"transitions={len(transitions)}, in={len(ins)}, out={len(outs)}, loop={len(loops)}"
)
if __name__ == "__main__":
main()