├── template ├── draft_biz_config.json ├── performance_opt_info.json ├── common_attachment │ ├── aigc_aigc_generate.json │ └── attachment_script_video.json ├── draft_agency_config.json ├── draft_settings ├── attachment_pc_common.json ├── attachment_editing.json ├── draft_meta_info.json ├── template.tmp ├── template-2.tmp ├── draft_info.json └── draft_info.json.bak ├── template_jianying ├── draft_biz_config.json ├── draft_agency_config.json ├── draft_cover.jpg ├── draft_settings ├── attachment_pc_common.json ├── draft_meta_info.json ├── template.tmp ├── draft_info.json ├── template-2.tmp ├── draft_info.json.bak └── .backup │ ├── 20250516114846_07a08f0b2c07e8e8f544c49e495344ea.save.bak │ └── 20250516114846_541eea452c3d8402ca82d1bb791a9eb6.save.bak ├── requirements.txt ├── requirements-mcp.txt ├── .gitignore ├── mcp_config.json ├── pattern └── README.md ├── .flake8 ├── pyJianYingDraft ├── exceptions.py ├── effect_segment.py ├── metadata │ ├── mask_meta.py │ ├── __init__.py │ ├── capcut_mask_meta.py │ ├── effect_meta.py │ └── capcut_audio_effect_meta.py ├── util.py ├── __init__.py ├── keyframe.py ├── time_util.py ├── draft_content_template.json ├── draft_folder.py ├── animation.py ├── track.py ├── segment.py └── audio_segment.py ├── rest_client_test.http ├── draft_cache.py ├── settings ├── __init__.py └── local.py ├── config.json.example ├── pyproject.toml ├── oss.py ├── create_draft.py ├── examples └── example_capcut_effect.py ├── util.py ├── add_sticker_impl.py ├── add_effect_impl.py ├── get_duration_impl.py ├── MCP_文档_中文.md ├── save_task_cache.py ├── add_subtitle_impl.py ├── MCP_Documentation_English.md ├── README-zh.md ├── add_audio_track.py ├── add_video_keyframe_impl.py ├── downloader.py ├── README.md └── test_mcp_client.py /template/draft_biz_config.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template_jianying/draft_biz_config.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /template/performance_opt_info.json: -------------------------------------------------------------------------------- 1 | {"manual_cancle_precombine_segs":null} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | imageio 2 | psutil 3 | flask 4 | requests 5 | oss2 6 | json5 -------------------------------------------------------------------------------- /requirements-mcp.txt: -------------------------------------------------------------------------------- 1 | # MCP相关依赖 2 | mcp>=1.0.0 3 | aiohttp>=3.8.0 4 | pydantic>=2.0.0 -------------------------------------------------------------------------------- /template_jianying/draft_agency_config.json: -------------------------------------------------------------------------------- 1 | {"marterials":null,"use_converter":false,"video_resolution":720} -------------------------------------------------------------------------------- /template_jianying/draft_cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sun-guannan/VectCutAPI/HEAD/template_jianying/draft_cover.jpg -------------------------------------------------------------------------------- /template/common_attachment/aigc_aigc_generate.json: -------------------------------------------------------------------------------- 1 | {"aigc_aigc_generate":{"aigc_generate_segment_list":[],"version":"1.0.0"}} 2 | -------------------------------------------------------------------------------- /template_jianying/draft_settings: -------------------------------------------------------------------------------- 1 | [General] 2 | draft_create_time=1747367326 3 | draft_last_edit_time=1747367328 4 | real_edit_keys=1 5 | real_edit_seconds=2 6 | -------------------------------------------------------------------------------- /template/draft_agency_config.json: -------------------------------------------------------------------------------- 1 | {"is_auto_agency_enabled":false,"is_auto_agency_popup":false,"is_single_agency_mode":false,"marterials":null,"use_converter":false,"video_resolution":720} -------------------------------------------------------------------------------- /template/draft_settings: -------------------------------------------------------------------------------- 1 | [General] 2 | cloud_last_modify_platform=mac 3 | draft_create_time=1751876007 4 | draft_last_edit_time=1751876105 5 | real_edit_keys=1 6 | real_edit_seconds=8 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | ignored 3 | 4 | build/ 5 | dist/ 6 | *.egg-info/ 7 | 8 | .vscode/ 9 | .idea/ 10 | .DS_Store 11 | 12 | config.json 13 | ./resource 14 | 15 | .venv/ 16 | venv/ 17 | tmp/ -------------------------------------------------------------------------------- /template/common_attachment/attachment_script_video.json: -------------------------------------------------------------------------------- 1 | {"script_video":{"attachment_valid":false,"language":"","overdub_recover":[],"overdub_sentence_ids":[],"parts":[],"sync_subtitle":false,"translate_segments":[],"translate_type":"","version":"1.0.0"}} 2 | -------------------------------------------------------------------------------- /template_jianying/attachment_pc_common.json: -------------------------------------------------------------------------------- 1 | {"ai_packaging_infos":[],"ai_packaging_report_info":{"caption_id_list":[],"task_id":"","text_style":"","tos_id":"","video_category":""},"commercial_music_category_ids":[],"pc_feature_flag":0,"recognize_tasks":[],"template_item_infos":[],"unlock_template_ids":[]} -------------------------------------------------------------------------------- /mcp_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "capcut-api": { 4 | "command": "python3.10", 5 | "args": ["mcp_server.py"], 6 | "cwd": "/Users/chuham/Downloads/CapCutAPI-dev", 7 | "env": { 8 | "PYTHONPATH": "/Users/chuham/Downloads/CapCutAPI-dev" 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /pattern/README.md: -------------------------------------------------------------------------------- 1 | # Pattern Gallery 2 | 3 | ## 001-words.py 4 | 5 | [source](001-words.py) 6 | 7 | [](https://www.youtube.com/watch?v=HLSHaJuNtBw) 8 | 9 | ## 002-relationship.py 10 | 11 | [source](002-relationship.py) 12 | 13 | [](https://www.youtube.com/watch?v=f2Q1OI_SQZo) 14 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = E301, E302, E303, E305, E306, E701, F403, F405 3 | exclude = 4 | .git, 5 | __pycache__, 6 | ignored, 7 | build, 8 | pyJianYingDraft/metadata/animation_meta.py, 9 | pyJianYingDraft/metadata/audio_effect_meta.py, 10 | pyJianYingDraft/metadata/filter_meta.py, 11 | pyJianYingDraft/metadata/video_effect_meta.py, 12 | pyJianYingDraft/metadata/transition_meta.py, 13 | pyJianYingDraft\metadata\font_meta.py 14 | max-line-length = 140 15 | -------------------------------------------------------------------------------- /pyJianYingDraft/exceptions.py: -------------------------------------------------------------------------------- 1 | """自定义异常类""" 2 | 3 | class TrackNotFound(NameError): 4 | """未找到满足条件的轨道""" 5 | class AmbiguousTrack(ValueError): 6 | """找到多个满足条件的轨道""" 7 | class SegmentOverlap(ValueError): 8 | """新片段与已有的轨道片段重叠""" 9 | 10 | class MaterialNotFound(NameError): 11 | """未找到满足条件的素材""" 12 | class AmbiguousMaterial(ValueError): 13 | """找到多个满足条件的素材""" 14 | 15 | class ExtensionFailed(ValueError): 16 | """替换素材时延伸片段失败""" 17 | 18 | class DraftNotFound(NameError): 19 | """未找到草稿""" 20 | class AutomationError(Exception): 21 | """自动化操作失败""" 22 | class ExportTimeout(Exception): 23 | """导出超时""" 24 | -------------------------------------------------------------------------------- /template/attachment_pc_common.json: -------------------------------------------------------------------------------- 1 | {"ai_packaging_infos":[],"ai_packaging_report_info":{"caption_id_list":[],"commercial_material":"","material_source":"","method":"","page_from":"","style":"","task_id":"","text_style":"","tos_id":"","video_category":""},"broll":{"ai_packaging_infos":[],"ai_packaging_report_info":{"caption_id_list":[],"commercial_material":"","material_source":"","method":"","page_from":"","style":"","task_id":"","text_style":"","tos_id":"","video_category":""}},"commercial_music_category_ids":[],"pc_feature_flag":0,"recognize_tasks":[],"reference_lines_config":{"horizontal_lines":[],"is_lock":false,"is_visible":false,"vertical_lines":[]},"safe_area_type":0,"template_item_infos":[],"unlock_template_ids":[]} -------------------------------------------------------------------------------- /rest_client_test.http: -------------------------------------------------------------------------------- 1 | ###添加文本 2 | POST http://localhost:9001/add_text 3 | Content-Type: application/json 4 | 5 | { 6 | "text": "你好,世界!", 7 | "start": 0, 8 | "end": 3, 9 | "font": "SourceHanSansCN_Regular", 10 | "font_color": "#FF0000", 11 | "font_size": 30.0 12 | } 13 | 14 | ###添加视频 15 | POST http://localhost:9001/add_video 16 | Content-Type: application/json 17 | 18 | { 19 | "video_url": "http://example.com/video.mp4", 20 | "start": 0, 21 | "end": 10, 22 | "width": 1080, 23 | "height": 1920 24 | } 25 | 26 | ###保存草稿 27 | POST http://localhost:9001/save_draft 28 | Content-Type: application/json 29 | 30 | { 31 | "draft_id": "dfd_cat_1752912902_0cae2e04", 32 | "draft_folder":"/Users/xxx/Movies/JianyingPro/User Data/Projects/com.lveditor.draft/" 33 | } -------------------------------------------------------------------------------- /template/attachment_editing.json: -------------------------------------------------------------------------------- 1 | {"editing_draft":{"ai_remove_filter_words":{"enter_source":"","right_id":""},"ai_shorts_info":{"report_params":"","type":0},"digital_human_template_to_video_info":{"has_upload_material":false,"template_type":0},"draft_used_recommend_function":"","edit_type":0,"eye_correct_enabled_multi_face_time":0,"has_adjusted_render_layer":false,"is_open_expand_player":false,"is_use_adjust":false,"is_use_edit_multi_camera":false,"is_use_lock_object":false,"is_use_loudness_unify":false,"is_use_retouch_face":false,"is_use_smart_adjust_color":false,"is_use_smart_motion":false,"is_use_text_to_audio":false,"material_edit_session":{"material_edit_info":[],"session_id":"","session_time":0},"profile_entrance_type":"","publish_enter_from":"","publish_type":"","single_function_type":0,"text_convert_case_types":[],"version":"1.0.0","video_recording_create_draft":""}} 2 | -------------------------------------------------------------------------------- /draft_cache.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import pyJianYingDraft as draft 3 | from typing import Dict 4 | 5 | # Modify global variable, use OrderedDict to implement LRU cache, limit the maximum number to 10000 6 | DRAFT_CACHE: Dict[str, 'draft.Script_file'] = OrderedDict() # Use Dict for type hinting 7 | MAX_CACHE_SIZE = 10000 8 | 9 | def update_cache(key: str, value: draft.Script_file) -> None: 10 | """Update LRU cache""" 11 | if key in DRAFT_CACHE: 12 | # If the key exists, delete the old item 13 | DRAFT_CACHE.pop(key) 14 | elif len(DRAFT_CACHE) >= MAX_CACHE_SIZE: 15 | print(f"{key}, Cache is full, deleting the least recently used item") 16 | # If the cache is full, delete the least recently used item (the first item) 17 | DRAFT_CACHE.popitem(last=False) 18 | # Add new item to the end (most recently used) 19 | DRAFT_CACHE[key] = value -------------------------------------------------------------------------------- /settings/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 配置包入口,导出所有配置 3 | """ 4 | 5 | # 从local模块导入所有配置,local模块已经导入了env和base模块的配置 6 | from .local import * 7 | 8 | __all__ = [ 9 | "IS_CAPCUT_ENV", 10 | "API_KEYS", 11 | "MODEL_CONFIG", 12 | "PURCHASE_LINKS", 13 | "LICENSE_CONFIG" 14 | ] 15 | 16 | # 提供一个获取平台信息的辅助函数 17 | def get_platform_info(): 18 | """ 19 | 获取平台信息,用于script_file.py中的dumps方法,cap_cut需要返回platform信息 20 | 21 | Returns: 22 | dict: 平台信息字典 23 | """ 24 | if not IS_CAPCUT_ENV: 25 | return None 26 | 27 | return { 28 | "app_id": 359289, 29 | "app_source": "cc", 30 | "app_version": "6.5.0", 31 | "device_id": "c4ca4238a0b923820dcc509a6f75849b", 32 | "hard_disk_id": "307563e0192a94465c0e927fbc482942", 33 | "mac_address": "c3371f2d4fb02791c067ce44d8fb4ed5", 34 | "os": "mac", 35 | "os_version": "15.5" 36 | } -------------------------------------------------------------------------------- /pyJianYingDraft/effect_segment.py: -------------------------------------------------------------------------------- 1 | """定义特效/滤镜片段类""" 2 | 3 | from typing import Union, Optional, List 4 | 5 | from .time_util import Timerange 6 | from .segment import Base_segment 7 | from .video_segment import Video_effect, Filter 8 | 9 | from .metadata import Video_scene_effect_type, Video_character_effect_type, Filter_type 10 | 11 | class Effect_segment(Base_segment): 12 | """放置在独立特效轨道上的特效片段""" 13 | 14 | effect_inst: Video_effect 15 | """相应的特效素材 16 | 17 | 在放入轨道时自动添加到素材列表中 18 | """ 19 | 20 | def __init__(self, effect_type: Union[Video_scene_effect_type, Video_character_effect_type], 21 | target_timerange: Timerange, params: Optional[List[Optional[float]]] = None): 22 | self.effect_inst = Video_effect(effect_type, params, apply_target_type=2) # 作用域为全局 23 | super().__init__(self.effect_inst.global_id, target_timerange) 24 | 25 | class Filter_segment(Base_segment): 26 | """放置在独立滤镜轨道上的滤镜片段""" 27 | 28 | material: Filter 29 | """相应的滤镜素材 30 | 31 | 在放入轨道时自动添加到素材列表中 32 | """ 33 | 34 | def __init__(self, meta: Filter_type, target_timerange: Timerange, intensity: float): 35 | self.material = Filter(meta.value, intensity) 36 | super().__init__(self.material.global_id, target_timerange) 37 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "is_capcut_env": true, // Whether to use CapCut environment (true) or JianYing environment (false) 3 | "draft_domain": "https://www.capcutapi.top", // Base domain for draft operations 4 | "port": 9001, // Port number for the local server 5 | "preview_router": "/draft/downloader", // Router path for preview functionality 6 | "is_upload_draft": false, // Whether to upload drafts to remote storage 7 | "oss_config": { // General OSS (Object Storage Service) configuration 8 | "bucket_name": "your-bucket-name", // OSS bucket name for general storage 9 | "access_key_id": "your-access-key-id", // Access key ID for OSS authentication 10 | "access_key_secret": "your-access-key-secret", // Access key secret for OSS authentication 11 | "endpoint": "https://your-endpoint.aliyuncs.com" // OSS service endpoint URL 12 | }, 13 | "mp4_oss_config": { // MP4-specific OSS configuration 14 | "bucket_name": "your-mp4-bucket-name", // OSS bucket name for MP4 files 15 | "access_key_id": "your-access-key-id", // Access key ID for MP4 OSS authentication 16 | "access_key_secret": "your-access-key-secret", // Access key secret for MP4 OSS authentication 17 | "region": "your-region", // Region for MP4 storage 18 | "endpoint": "http://your-custom-domain" // Custom domain endpoint for MP4 access 19 | } 20 | } -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "capcut-api" 7 | version = "1.0.0" 8 | description = "Open source CapCut API tool with MCP support" 9 | readme = "README.md" 10 | requires-python = ">=3.10" 11 | classifiers = [ 12 | "Development Status :: 4 - Beta", 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Operating System :: OS Independent", 16 | "Programming Language :: Python :: 3", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Programming Language :: Python :: 3.12", 20 | ] 21 | dependencies = [ 22 | "requests>=2.28.0", 23 | "Pillow>=9.0.0", 24 | "numpy>=1.21.0", 25 | "opencv-python>=4.6.0", 26 | "ffmpeg-python>=0.2.0", 27 | "pydantic>=2.0.0", 28 | "fastapi>=0.100.0", 29 | "uvicorn[standard]>=0.23.0", 30 | ] 31 | 32 | [project.optional-dependencies] 33 | mcp = [ 34 | "mcp>=1.0.0", 35 | "aiohttp>=3.8.0", 36 | "websockets>=11.0", 37 | "jsonrpc-base>=2.2.0", 38 | "jsonrpc-websocket>=3.1.0", 39 | "jsonrpc-async>=2.1.0", 40 | ] 41 | 42 | [project.urls] 43 | Homepage = "https://github.com/ashreo/CapCutAPI" 44 | Repository = "https://github.com/ashreo/CapCutAPI.git" 45 | Issues = "https://github.com/ashreo/CapCutAPI/issues" -------------------------------------------------------------------------------- /template_jianying/draft_meta_info.json: -------------------------------------------------------------------------------- 1 | {"cloud_package_completed_time":"","draft_cloud_capcut_purchase_info":"","draft_cloud_last_action_download":false,"draft_cloud_materials":[],"draft_cloud_purchase_info":"","draft_cloud_template_id":"","draft_cloud_tutorial_info":"","draft_cloud_videocut_purchase_info":"","draft_cover":"draft_cover.jpg","draft_deeplink_url":"","draft_enterprise_info":{"draft_enterprise_extra":"","draft_enterprise_id":"","draft_enterprise_name":"","enterprise_material":[]},"draft_fold_path":"/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft/5月16日","draft_id":"EC6E4680-CEF1-408B-8BA9-5E13124C61D2","draft_is_ai_packaging_used":false,"draft_is_ai_shorts":false,"draft_is_ai_translate":false,"draft_is_article_video_draft":false,"draft_is_from_deeplink":"false","draft_is_invisible":false,"draft_materials":[{"type":0,"value":[]},{"type":1,"value":[]},{"type":2,"value":[]},{"type":3,"value":[]},{"type":6,"value":[]},{"type":7,"value":[]},{"type":8,"value":[]}],"draft_materials_copied_info":[],"draft_name":"5月16日","draft_new_version":"","draft_removable_storage_device":"","draft_root_path":"/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft","draft_segment_extra_info":[],"draft_timeline_materials_size_":7867,"draft_type":"","tm_draft_cloud_completed":"","tm_draft_cloud_modified":0,"tm_draft_create":1747367326219300,"tm_draft_modified":1747367328691892,"tm_draft_removed":0,"tm_duration":0} -------------------------------------------------------------------------------- /pyJianYingDraft/metadata/mask_meta.py: -------------------------------------------------------------------------------- 1 | """视频蒙版元数据""" 2 | 3 | from .effect_meta import Effect_enum 4 | 5 | class Mask_meta: 6 | """蒙版元数据""" 7 | 8 | name: str 9 | """转场名称""" 10 | 11 | resource_type: str 12 | """资源类型, 与蒙版形状相关""" 13 | 14 | resource_id: str 15 | """资源ID""" 16 | effect_id: str 17 | """效果ID""" 18 | md5: str 19 | 20 | default_aspect_ratio: float 21 | """默认宽高比(宽高都是相对素材的比例)""" 22 | 23 | def __init__(self, name: str, resource_type: str, resource_id: str, effect_id: str, md5: str, default_aspect_ratio: float): 24 | self.name = name 25 | self.resource_type = resource_type 26 | self.resource_id = resource_id 27 | self.effect_id = effect_id 28 | self.md5 = md5 29 | 30 | self.default_aspect_ratio = default_aspect_ratio 31 | 32 | class Mask_type(Effect_enum): 33 | """蒙版类型""" 34 | 35 | 线性 = Mask_meta("线性", "line", "6791652175668843016", "636071", "1f467b8b9bb94cecc46d916219b7940a", 1.0) 36 | """默认遮挡下方部分""" 37 | 镜面 = Mask_meta("镜面", "mirror", "6791699060140020232", "636073", "b2c0516d1f737f4542fb9b2862907817", 1.0) 38 | """默认保留两线之间部分""" 39 | 圆形 = Mask_meta("圆形", "circle", "6791700663249146381", "636075", "9a55eae0e99ee6d1ecbc6defaf0501ec", 1.0) 40 | 矩形 = Mask_meta("矩形", "rectangle", "6791700809454195207", "636077", "ef361d96c456cd6077c76d737f98898d", 1.0) 41 | 爱心 = Mask_meta("爱心", "geometric_shape", "6794051276482023949", "636079", "0bf09fa1e3a32464fed4f71e49a8ab01", 1.115) 42 | 星形 = Mask_meta("星形", "geometric_shape", "6794051169434997255", "636081", "155612dee601d3f5422a3fbeabc7610c", 1.05) 43 | -------------------------------------------------------------------------------- /oss.py: -------------------------------------------------------------------------------- 1 | 2 | import oss2 3 | import os 4 | from settings.local import OSS_CONFIG, MP4_OSS_CONFIG 5 | 6 | def upload_to_oss(path): 7 | # Create OSS client 8 | auth = oss2.Auth(OSS_CONFIG['access_key_id'], OSS_CONFIG['access_key_secret']) 9 | bucket = oss2.Bucket(auth, OSS_CONFIG['endpoint'], OSS_CONFIG['bucket_name']) 10 | 11 | # Upload file 12 | object_name = os.path.basename(path) 13 | bucket.put_object_from_file(object_name, path) 14 | 15 | # Generate signed URL (valid for 24 hours) 16 | url = bucket.sign_url('GET', object_name, 24 * 60 * 60) 17 | 18 | # Clean up temporary file 19 | os.remove(path) 20 | 21 | return url 22 | 23 | def upload_mp4_to_oss(path): 24 | """Special method for uploading MP4 files, using custom domain and v4 signature""" 25 | # Directly use credentials from the configuration file 26 | auth = oss2.AuthV4(MP4_OSS_CONFIG['access_key_id'], MP4_OSS_CONFIG['access_key_secret']) 27 | 28 | # Create OSS client with custom domain 29 | bucket = oss2.Bucket( 30 | auth, 31 | MP4_OSS_CONFIG['endpoint'], 32 | MP4_OSS_CONFIG['bucket_name'], 33 | region=MP4_OSS_CONFIG['region'], 34 | is_cname=True 35 | ) 36 | 37 | # Upload file 38 | object_name = os.path.basename(path) 39 | bucket.put_object_from_file(object_name, path) 40 | 41 | # Generate pre-signed URL (valid for 24 hours), set slash_safe to True to avoid path escaping 42 | url = bucket.sign_url('GET', object_name, 24 * 60 * 60, slash_safe=True) 43 | 44 | # Clean up temporary file 45 | os.remove(path) 46 | 47 | return url -------------------------------------------------------------------------------- /template/draft_meta_info.json: -------------------------------------------------------------------------------- 1 | {"cloud_draft_cover":true,"cloud_draft_sync":true,"cloud_package_completed_time":"","draft_cloud_capcut_purchase_info":"","draft_cloud_last_action_download":false,"draft_cloud_package_type":"","draft_cloud_purchase_info":"","draft_cloud_template_id":"","draft_cloud_tutorial_info":"","draft_cloud_videocut_purchase_info":"","draft_cover":"draft_cover.jpg","draft_deeplink_url":"","draft_enterprise_info":{"draft_enterprise_extra":"","draft_enterprise_id":"","draft_enterprise_name":"","enterprise_material":[]},"draft_fold_path":"/Users/sunguannan/Movies/CapCut/User Data/Projects/com.lveditor.draft/0707","draft_id":"989869B1-B560-489C-9C6F-4B444F24BF36","draft_is_ae_produce":false,"draft_is_ai_packaging_used":false,"draft_is_ai_shorts":false,"draft_is_ai_translate":false,"draft_is_article_video_draft":false,"draft_is_cloud_temp_draft":false,"draft_is_from_deeplink":"false","draft_is_invisible":false,"draft_materials":[{"type":0,"value":[]},{"type":1,"value":[]},{"type":2,"value":[]},{"type":3,"value":[]},{"type":6,"value":[]},{"type":7,"value":[]},{"type":8,"value":[]}],"draft_materials_copied_info":[],"draft_name":"0707","draft_need_rename_folder":false,"draft_new_version":"","draft_removable_storage_device":"","draft_root_path":"/Users/sunguannan/Movies/CapCut/User Data/Projects/com.lveditor.draft","draft_segment_extra_info":[],"draft_timeline_materials_size_":2851,"draft_type":"","tm_draft_cloud_completed":"","tm_draft_cloud_entry_id":-1,"tm_draft_cloud_modified":0,"tm_draft_cloud_parent_entry_id":-1,"tm_draft_cloud_space_id":-1,"tm_draft_cloud_user_id":-1,"tm_draft_create":1751876007857286,"tm_draft_modified":1751876105604683,"tm_draft_removed":0,"tm_duration":0} -------------------------------------------------------------------------------- /create_draft.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import pyJianYingDraft as draft 3 | import time 4 | from draft_cache import DRAFT_CACHE, update_cache 5 | 6 | def create_draft(width=1080, height=1920): 7 | """ 8 | Create new CapCut draft 9 | :param width: Video width, default 1080 10 | :param height: Video height, default 1920 11 | :return: (draft_name, draft_path, draft_id, draft_url) 12 | """ 13 | # Generate timestamp and draft_id 14 | unix_time = int(time.time()) 15 | unique_id = uuid.uuid4().hex[:8] # Take the first 8 digits of UUID 16 | draft_id = f"dfd_cat_{unix_time}_{unique_id}" # Use Unix timestamp and UUID combination 17 | 18 | # Create CapCut draft with specified resolution 19 | script = draft.Script_file(width, height) 20 | 21 | # Store in global cache 22 | update_cache(draft_id, script) 23 | 24 | return script, draft_id 25 | 26 | def get_or_create_draft(draft_id=None, width=1080, height=1920): 27 | """ 28 | Get or create CapCut draft 29 | :param draft_id: Draft ID, if None or corresponding zip file not found, create new draft 30 | :param width: Video width, default 1080 31 | :param height: Video height, default 1920 32 | :return: (draft_name, draft_path, draft_id, draft_dir, script) 33 | """ 34 | global DRAFT_CACHE # Declare use of global variable 35 | 36 | if draft_id is not None and draft_id in DRAFT_CACHE: 37 | # Get existing draft information from cache 38 | print(f"Getting draft from cache: {draft_id}") 39 | # Update last access time 40 | update_cache(draft_id, DRAFT_CACHE[draft_id]) 41 | return draft_id, DRAFT_CACHE[draft_id] 42 | 43 | # Create new draft logic 44 | print("Creating new draft") 45 | script, generate_draft_id = create_draft( 46 | width=width, 47 | height=height, 48 | ) 49 | return generate_draft_id, script 50 | -------------------------------------------------------------------------------- /settings/local.py: -------------------------------------------------------------------------------- 1 | """ 2 | 本地配置模块,用于从本地配置文件中加载配置 3 | """ 4 | 5 | import os 6 | import json5 # 替换原来的json模块 7 | 8 | # 配置文件路径 9 | CONFIG_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json") 10 | 11 | # 默认配置 12 | IS_CAPCUT_ENV = True 13 | 14 | # 默认域名配置 15 | DRAFT_DOMAIN = "https://www.install-ai-guider.top" 16 | 17 | # 默认预览路由 18 | PREVIEW_ROUTER = "/draft/downloader" 19 | 20 | # 是否上传草稿文件 21 | IS_UPLOAD_DRAFT = False 22 | 23 | # 端口号 24 | PORT = 9000 25 | 26 | OSS_CONFIG = [] 27 | MP4_OSS_CONFIG=[] 28 | 29 | # 尝试加载本地配置文件 30 | if os.path.exists(CONFIG_FILE_PATH): 31 | try: 32 | with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f: 33 | # 使用json5.load替代json.load 34 | local_config = json5.load(f) 35 | 36 | # 更新是否是国际版 37 | if "is_capcut_env" in local_config: 38 | IS_CAPCUT_ENV = local_config["is_capcut_env"] 39 | 40 | # 更新域名配置 41 | if "draft_domain" in local_config: 42 | DRAFT_DOMAIN = local_config["draft_domain"] 43 | 44 | # 更新端口号配置 45 | if "port" in local_config: 46 | PORT = local_config["port"] 47 | 48 | # 更新预览路由 49 | if "preview_router" in local_config: 50 | PREVIEW_ROUTER = local_config["preview_router"] 51 | 52 | # 更新是否上传草稿文件 53 | if "is_upload_draft" in local_config: 54 | IS_UPLOAD_DRAFT = local_config["is_upload_draft"] 55 | 56 | # 更新OSS配置 57 | if "oss_config" in local_config: 58 | OSS_CONFIG = local_config["oss_config"] 59 | 60 | # 更新MP4 OSS配置 61 | if "mp4_oss_config" in local_config: 62 | MP4_OSS_CONFIG = local_config["mp4_oss_config"] 63 | 64 | except Exception as e: 65 | # 配置文件加载失败,使用默认配置 66 | pass -------------------------------------------------------------------------------- /pyJianYingDraft/util.py: -------------------------------------------------------------------------------- 1 | """辅助函数,主要与模板模式有关""" 2 | 3 | import inspect 4 | 5 | from typing import Union, Type 6 | from typing import List, Dict, Any 7 | 8 | JsonExportable = Union[int, float, bool, str, List["JsonExportable"], Dict[str, "JsonExportable"]] 9 | 10 | def provide_ctor_defaults(cls: Type) -> Dict[str, Any]: 11 | """为构造函数提供默认值,以绕开构造函数的参数限制""" 12 | 13 | signature = inspect.signature(cls.__init__) 14 | provided_defaults: Dict[str, Any] = {} 15 | 16 | for name, param in signature.parameters.items(): 17 | if name == 'self': continue 18 | if param.default is not inspect.Parameter.empty: continue 19 | 20 | if param.annotation is int or param.annotation is float: 21 | provided_defaults[name] = 0 22 | elif param.annotation is str: 23 | provided_defaults[name] = "" 24 | elif param.annotation is bool: 25 | provided_defaults[name] = False 26 | else: 27 | raise ValueError(f"Unsupported parameter type: {param.annotation}") 28 | 29 | return provided_defaults 30 | 31 | def assign_attr_with_json(obj: object, attrs: List[str], json_data: Dict[str, Any]): 32 | """根据json数据赋值给指定的对象属性 33 | 34 | 若有复杂类型,则尝试调用其`import_json`方法进行构造 35 | """ 36 | type_hints: Dict[str, Type] = {} 37 | for cls in obj.__class__.__mro__: 38 | if '__annotations__' in cls.__dict__: 39 | type_hints.update(cls.__annotations__) 40 | 41 | for attr in attrs: 42 | if hasattr(type_hints[attr], 'import_json'): 43 | obj.__setattr__(attr, type_hints[attr].import_json(json_data[attr])) 44 | else: 45 | obj.__setattr__(attr, type_hints[attr](json_data[attr])) 46 | 47 | def export_attr_to_json(obj: object, attrs: List[str]) -> Dict[str, JsonExportable]: 48 | """将对象属性导出为json数据 49 | 50 | 若有复杂类型,则尝试调用其`export_json`方法进行导出 51 | """ 52 | json_data: Dict[str, Any] = {} 53 | for attr in attrs: 54 | if hasattr(getattr(obj, attr), 'export_json'): 55 | json_data[attr] = getattr(obj, attr).export_json() 56 | else: 57 | json_data[attr] = getattr(obj, attr) 58 | return json_data 59 | -------------------------------------------------------------------------------- /pyJianYingDraft/metadata/__init__.py: -------------------------------------------------------------------------------- 1 | """记录各种特效/音效/滤镜等的元数据""" 2 | 3 | from .effect_meta import Effect_meta, Effect_param_instance 4 | 5 | from .font_meta import Font_type 6 | from .mask_meta import Mask_type, Mask_meta 7 | from .capcut_mask_meta import CapCut_Mask_type 8 | from .filter_meta import Filter_type 9 | from .transition_meta import Transition_type 10 | from .capcut_transition_meta import CapCut_Transition_type 11 | from .animation_meta import Intro_type, Outro_type, Group_animation_type 12 | from .capcut_animation_meta import CapCut_Intro_type, CapCut_Outro_type, CapCut_Group_animation_type 13 | from .animation_meta import Text_intro, Text_outro, Text_loop_anim 14 | from .capcut_text_animation_meta import CapCut_Text_intro, CapCut_Text_outro, CapCut_Text_loop_anim 15 | from .audio_effect_meta import Audio_scene_effect_type, Tone_effect_type, Speech_to_song_type 16 | from .capcut_audio_effect_meta import CapCut_Voice_filters_effect_type, CapCut_Voice_characters_effect_type, CapCut_Speech_to_song_effect_type 17 | from .video_effect_meta import Video_scene_effect_type, Video_character_effect_type 18 | from .capcut_effect_meta import CapCut_Video_scene_effect_type, CapCut_Video_character_effect_type 19 | 20 | __all__ = [ 21 | "Effect_meta", 22 | "Effect_param_instance", 23 | "Mask_type", 24 | "Mask_meta", 25 | "CapCut_Mask_type", 26 | "Filter_type", 27 | "Font_type", 28 | "Transition_type", 29 | "CapCut_Transition_type", 30 | "Intro_type", 31 | "Outro_type", 32 | "Group_animation_type", 33 | "CapCut_Intro_type", 34 | "CapCut_Outro_type", 35 | "CapCut_Group_animation_type", 36 | "Text_intro", 37 | "Text_outro", 38 | "Text_loop_anim", 39 | "CapCut_Text_intro", 40 | "CapCut_Text_outro", 41 | "CapCut_Text_loop_anim", 42 | "Audio_scene_effect_type", 43 | "Tone_effect_type", 44 | "Speech_to_song_type", 45 | "CapCut_Voice_filters_effect_type", 46 | "CapCut_Voice_characters_effect_type", 47 | "CapCut_Speech_to_song_effect_type", 48 | "Video_scene_effect_type", 49 | "Video_character_effect_type", 50 | "CapCut_Video_scene_effect_type", 51 | "CapCut_Video_character_effect_type" 52 | ] 53 | -------------------------------------------------------------------------------- /template_jianying/template.tmp: -------------------------------------------------------------------------------- 1 | {"canvas_config":{"height":0,"ratio":"original","width":0},"color_space":-1,"config":{"adjust_max_index":1,"attachment_info":[],"combination_max_index":1,"export_range":null,"extract_audio_last_index":1,"lyrics_recognition_id":"","lyrics_sync":true,"lyrics_taskinfo":[],"maintrack_adsorb":true,"material_save_mode":0,"multi_language_current":"none","multi_language_list":[],"multi_language_main":"none","multi_language_mode":"none","original_sound_last_index":1,"record_audio_last_index":1,"sticker_max_index":1,"subtitle_keywords_config":null,"subtitle_recognition_id":"","subtitle_sync":true,"subtitle_taskinfo":[],"system_font_list":[],"video_mute":false,"zoom_info_params":null},"cover":null,"create_time":0,"duration":0,"extra_info":null,"fps":30.0,"free_render_index_mode_on":false,"group_container":null,"id":"32F0961A-740A-4107-A2A1-02C5936204C8","keyframe_graph_list":[],"keyframes":{"adjusts":[],"audios":[],"effects":[],"filters":[],"handwrites":[],"stickers":[],"texts":[],"videos":[]},"last_modified_platform":{"app_id":0,"app_source":"","app_version":"","device_id":"","hard_disk_id":"","mac_address":"","os":"","os_version":""},"materials":{"ai_translates":[],"audio_balances":[],"audio_effects":[],"audio_fades":[],"audio_track_indexes":[],"audios":[],"beats":[],"canvases":[],"chromas":[],"color_curves":[],"digital_humans":[],"drafts":[],"effects":[],"flowers":[],"green_screens":[],"handwrites":[],"hsl":[],"images":[],"log_color_wheels":[],"loudnesses":[],"manual_deformations":[],"masks":[],"material_animations":[],"material_colors":[],"multi_language_refs":[],"placeholders":[],"plugin_effects":[],"primary_color_wheels":[],"realtime_denoises":[],"shapes":[],"smart_crops":[],"smart_relights":[],"sound_channel_mappings":[],"speeds":[],"stickers":[],"tail_leaders":[],"text_templates":[],"texts":[],"time_marks":[],"transitions":[],"video_effects":[],"video_trackings":[],"videos":[],"vocal_beautifys":[],"vocal_separations":[]},"mutable_config":null,"name":"","new_version":"75.0.0","platform":{"app_id":0,"app_source":"","app_version":"","device_id":"","hard_disk_id":"","mac_address":"","os":"","os_version":""},"relationships":[],"render_index_track_mode_on":false,"retouch_cover":null,"source":"default","static_cover_image_path":"","time_marks":null,"tracks":[],"update_time":0,"version":360000} -------------------------------------------------------------------------------- /template_jianying/draft_info.json: -------------------------------------------------------------------------------- 1 | {"canvas_config":{"height":1080,"ratio":"original","width":1920},"color_space":-1,"config":{"adjust_max_index":1,"attachment_info":[],"combination_max_index":1,"export_range":null,"extract_audio_last_index":1,"lyrics_recognition_id":"","lyrics_sync":true,"lyrics_taskinfo":[],"maintrack_adsorb":true,"material_save_mode":0,"multi_language_current":"none","multi_language_list":[],"multi_language_main":"none","multi_language_mode":"none","original_sound_last_index":1,"record_audio_last_index":1,"sticker_max_index":1,"subtitle_keywords_config":null,"subtitle_recognition_id":"","subtitle_sync":true,"subtitle_taskinfo":[],"system_font_list":[],"video_mute":false,"zoom_info_params":null},"cover":null,"create_time":0,"duration":0,"extra_info":null,"fps":30.0,"free_render_index_mode_on":false,"group_container":null,"id":"DF724EE8-CA13-430C-B9E1-643276C263DA","keyframe_graph_list":[],"keyframes":{"adjusts":[],"audios":[],"effects":[],"filters":[],"handwrites":[],"stickers":[],"texts":[],"videos":[]},"last_modified_platform":{"app_id":3704,"app_source":"lv","app_version":"5.9.0","device_id":"9c1d156ba20d5d2ef89a71f1a92596fd","hard_disk_id":"81f3587033961b2ad1d1ef9d11dcce33","mac_address":"5409d920e86b1169992326ea688e0715","os":"mac","os_version":"15.3.2"},"materials":{"ai_translates":[],"audio_balances":[],"audio_effects":[],"audio_fades":[],"audio_track_indexes":[],"audios":[],"beats":[],"canvases":[],"chromas":[],"color_curves":[],"digital_humans":[],"drafts":[],"effects":[],"flowers":[],"green_screens":[],"handwrites":[],"hsl":[],"images":[],"log_color_wheels":[],"loudnesses":[],"manual_deformations":[],"masks":[],"material_animations":[],"material_colors":[],"multi_language_refs":[],"placeholders":[],"plugin_effects":[],"primary_color_wheels":[],"realtime_denoises":[],"shapes":[],"smart_crops":[],"smart_relights":[],"sound_channel_mappings":[],"speeds":[],"stickers":[],"tail_leaders":[],"text_templates":[],"texts":[],"time_marks":[],"transitions":[],"video_effects":[],"video_trackings":[],"videos":[],"vocal_beautifys":[],"vocal_separations":[]},"mutable_config":null,"name":"","new_version":"110.0.0","platform":{"app_id":3704,"app_source":"lv","app_version":"5.9.0","device_id":"9c1d156ba20d5d2ef89a71f1a92596fd","hard_disk_id":"81f3587033961b2ad1d1ef9d11dcce33","mac_address":"5409d920e86b1169992326ea688e0715","os":"mac","os_version":"15.3.2"},"relationships":[],"render_index_track_mode_on":true,"retouch_cover":null,"source":"default","static_cover_image_path":"","time_marks":null,"tracks":[],"update_time":0,"version":360000} -------------------------------------------------------------------------------- /template_jianying/template-2.tmp: -------------------------------------------------------------------------------- 1 | {"canvas_config":{"height":1080,"ratio":"original","width":1920},"color_space":-1,"config":{"adjust_max_index":1,"attachment_info":[],"combination_max_index":1,"export_range":null,"extract_audio_last_index":1,"lyrics_recognition_id":"","lyrics_sync":true,"lyrics_taskinfo":[],"maintrack_adsorb":true,"material_save_mode":0,"multi_language_current":"none","multi_language_list":[],"multi_language_main":"none","multi_language_mode":"none","original_sound_last_index":1,"record_audio_last_index":1,"sticker_max_index":1,"subtitle_keywords_config":null,"subtitle_recognition_id":"","subtitle_sync":true,"subtitle_taskinfo":[],"system_font_list":[],"video_mute":false,"zoom_info_params":null},"cover":null,"create_time":0,"duration":0,"extra_info":null,"fps":30.0,"free_render_index_mode_on":false,"group_container":null,"id":"DF724EE8-CA13-430C-B9E1-643276C263DA","keyframe_graph_list":[],"keyframes":{"adjusts":[],"audios":[],"effects":[],"filters":[],"handwrites":[],"stickers":[],"texts":[],"videos":[]},"last_modified_platform":{"app_id":3704,"app_source":"lv","app_version":"5.9.0","device_id":"9c1d156ba20d5d2ef89a71f1a92596fd","hard_disk_id":"81f3587033961b2ad1d1ef9d11dcce33","mac_address":"5409d920e86b1169992326ea688e0715","os":"mac","os_version":"15.3.2"},"materials":{"ai_translates":[],"audio_balances":[],"audio_effects":[],"audio_fades":[],"audio_track_indexes":[],"audios":[],"beats":[],"canvases":[],"chromas":[],"color_curves":[],"digital_humans":[],"drafts":[],"effects":[],"flowers":[],"green_screens":[],"handwrites":[],"hsl":[],"images":[],"log_color_wheels":[],"loudnesses":[],"manual_deformations":[],"masks":[],"material_animations":[],"material_colors":[],"multi_language_refs":[],"placeholders":[],"plugin_effects":[],"primary_color_wheels":[],"realtime_denoises":[],"shapes":[],"smart_crops":[],"smart_relights":[],"sound_channel_mappings":[],"speeds":[],"stickers":[],"tail_leaders":[],"text_templates":[],"texts":[],"time_marks":[],"transitions":[],"video_effects":[],"video_trackings":[],"videos":[],"vocal_beautifys":[],"vocal_separations":[]},"mutable_config":null,"name":"","new_version":"110.0.0","platform":{"app_id":3704,"app_source":"lv","app_version":"5.9.0","device_id":"9c1d156ba20d5d2ef89a71f1a92596fd","hard_disk_id":"81f3587033961b2ad1d1ef9d11dcce33","mac_address":"5409d920e86b1169992326ea688e0715","os":"mac","os_version":"15.3.2"},"relationships":[],"render_index_track_mode_on":true,"retouch_cover":null,"source":"default","static_cover_image_path":"","time_marks":null,"tracks":[],"update_time":0,"version":360000} -------------------------------------------------------------------------------- /template_jianying/draft_info.json.bak: -------------------------------------------------------------------------------- 1 | {"canvas_config":{"height":1080,"ratio":"original","width":1920},"color_space":-1,"config":{"adjust_max_index":1,"attachment_info":[],"combination_max_index":1,"export_range":null,"extract_audio_last_index":1,"lyrics_recognition_id":"","lyrics_sync":true,"lyrics_taskinfo":[],"maintrack_adsorb":true,"material_save_mode":0,"multi_language_current":"none","multi_language_list":[],"multi_language_main":"none","multi_language_mode":"none","original_sound_last_index":1,"record_audio_last_index":1,"sticker_max_index":1,"subtitle_keywords_config":null,"subtitle_recognition_id":"","subtitle_sync":true,"subtitle_taskinfo":[],"system_font_list":[],"video_mute":false,"zoom_info_params":null},"cover":null,"create_time":0,"duration":0,"extra_info":null,"fps":30.0,"free_render_index_mode_on":false,"group_container":null,"id":"DF724EE8-CA13-430C-B9E1-643276C263DA","keyframe_graph_list":[],"keyframes":{"adjusts":[],"audios":[],"effects":[],"filters":[],"handwrites":[],"stickers":[],"texts":[],"videos":[]},"last_modified_platform":{"app_id":3704,"app_source":"lv","app_version":"5.9.0","device_id":"9c1d156ba20d5d2ef89a71f1a92596fd","hard_disk_id":"81f3587033961b2ad1d1ef9d11dcce33","mac_address":"5409d920e86b1169992326ea688e0715","os":"mac","os_version":"15.3.2"},"materials":{"ai_translates":[],"audio_balances":[],"audio_effects":[],"audio_fades":[],"audio_track_indexes":[],"audios":[],"beats":[],"canvases":[],"chromas":[],"color_curves":[],"digital_humans":[],"drafts":[],"effects":[],"flowers":[],"green_screens":[],"handwrites":[],"hsl":[],"images":[],"log_color_wheels":[],"loudnesses":[],"manual_deformations":[],"masks":[],"material_animations":[],"material_colors":[],"multi_language_refs":[],"placeholders":[],"plugin_effects":[],"primary_color_wheels":[],"realtime_denoises":[],"shapes":[],"smart_crops":[],"smart_relights":[],"sound_channel_mappings":[],"speeds":[],"stickers":[],"tail_leaders":[],"text_templates":[],"texts":[],"time_marks":[],"transitions":[],"video_effects":[],"video_trackings":[],"videos":[],"vocal_beautifys":[],"vocal_separations":[]},"mutable_config":null,"name":"","new_version":"110.0.0","platform":{"app_id":3704,"app_source":"lv","app_version":"5.9.0","device_id":"9c1d156ba20d5d2ef89a71f1a92596fd","hard_disk_id":"81f3587033961b2ad1d1ef9d11dcce33","mac_address":"5409d920e86b1169992326ea688e0715","os":"mac","os_version":"15.3.2"},"relationships":[],"render_index_track_mode_on":true,"retouch_cover":null,"source":"default","static_cover_image_path":"","time_marks":null,"tracks":[],"update_time":0,"version":360000} -------------------------------------------------------------------------------- /template/template.tmp: -------------------------------------------------------------------------------- 1 | {"canvas_config":{"background":null,"height":0,"ratio":"original","width":0},"color_space":-1,"config":{"adjust_max_index":1,"attachment_info":[],"combination_max_index":1,"export_range":null,"extract_audio_last_index":1,"lyrics_recognition_id":"","lyrics_sync":true,"lyrics_taskinfo":[],"maintrack_adsorb":true,"material_save_mode":0,"multi_language_current":"none","multi_language_list":[],"multi_language_main":"none","multi_language_mode":"none","original_sound_last_index":1,"record_audio_last_index":1,"sticker_max_index":1,"subtitle_keywords_config":null,"subtitle_recognition_id":"","subtitle_sync":true,"subtitle_taskinfo":[],"system_font_list":[],"use_float_render":false,"video_mute":false,"zoom_info_params":null},"cover":null,"create_time":0,"duration":0,"extra_info":null,"fps":30.0,"free_render_index_mode_on":false,"group_container":null,"id":"470E26D0-9077-4231-A7A7-619F75FF92B3","is_drop_frame_timecode":false,"keyframe_graph_list":[],"keyframes":{"adjusts":[],"audios":[],"effects":[],"filters":[],"handwrites":[],"stickers":[],"texts":[],"videos":[]},"last_modified_platform":{"app_id":0,"app_source":"","app_version":"","device_id":"","hard_disk_id":"","mac_address":"","os":"","os_version":""},"lyrics_effects":[],"materials":{"ai_translates":[],"audio_balances":[],"audio_effects":[],"audio_fades":[],"audio_track_indexes":[],"audios":[],"beats":[],"canvases":[],"chromas":[],"color_curves":[],"common_mask":[],"digital_human_model_dressing":[],"digital_humans":[],"drafts":[],"effects":[],"flowers":[],"green_screens":[],"handwrites":[],"hsl":[],"images":[],"log_color_wheels":[],"loudnesses":[],"manual_beautys":[],"manual_deformations":[],"material_animations":[],"material_colors":[],"multi_language_refs":[],"placeholder_infos":[],"placeholders":[],"plugin_effects":[],"primary_color_wheels":[],"realtime_denoises":[],"shapes":[],"smart_crops":[],"smart_relights":[],"sound_channel_mappings":[],"speeds":[],"stickers":[],"tail_leaders":[],"text_templates":[],"texts":[],"time_marks":[],"transitions":[],"video_effects":[],"video_trackings":[],"videos":[],"vocal_beautifys":[],"vocal_separations":[]},"mutable_config":null,"name":"","new_version":"75.0.0","path":"","platform":{"app_id":0,"app_source":"","app_version":"","device_id":"","hard_disk_id":"","mac_address":"","os":"","os_version":""},"relationships":[],"render_index_track_mode_on":false,"retouch_cover":null,"source":"default","static_cover_image_path":"","time_marks":null,"tracks":[],"uneven_animation_template_info":{"composition":"","content":"","order":"","sub_template_info_list":[]},"update_time":0,"version":360000} -------------------------------------------------------------------------------- /template_jianying/.backup/20250516114846_07a08f0b2c07e8e8f544c49e495344ea.save.bak: -------------------------------------------------------------------------------- 1 | {"canvas_config":{"height":1080,"ratio":"original","width":1920},"color_space":-1,"config":{"adjust_max_index":1,"attachment_info":[],"combination_max_index":1,"export_range":null,"extract_audio_last_index":1,"lyrics_recognition_id":"","lyrics_sync":true,"lyrics_taskinfo":[],"maintrack_adsorb":true,"material_save_mode":0,"multi_language_current":"none","multi_language_list":[],"multi_language_main":"none","multi_language_mode":"none","original_sound_last_index":1,"record_audio_last_index":1,"sticker_max_index":1,"subtitle_keywords_config":null,"subtitle_recognition_id":"","subtitle_sync":true,"subtitle_taskinfo":[],"system_font_list":[],"video_mute":false,"zoom_info_params":null},"cover":null,"create_time":0,"duration":0,"extra_info":null,"fps":30.0,"free_render_index_mode_on":false,"group_container":null,"id":"DF724EE8-CA13-430C-B9E1-643276C263DA","keyframe_graph_list":[],"keyframes":{"adjusts":[],"audios":[],"effects":[],"filters":[],"handwrites":[],"stickers":[],"texts":[],"videos":[]},"last_modified_platform":{"app_id":3704,"app_source":"lv","app_version":"5.9.0","device_id":"9c1d156ba20d5d2ef89a71f1a92596fd","hard_disk_id":"81f3587033961b2ad1d1ef9d11dcce33","mac_address":"5409d920e86b1169992326ea688e0715","os":"mac","os_version":"15.3.2"},"materials":{"ai_translates":[],"audio_balances":[],"audio_effects":[],"audio_fades":[],"audio_track_indexes":[],"audios":[],"beats":[],"canvases":[],"chromas":[],"color_curves":[],"digital_humans":[],"drafts":[],"effects":[],"flowers":[],"green_screens":[],"handwrites":[],"hsl":[],"images":[],"log_color_wheels":[],"loudnesses":[],"manual_deformations":[],"masks":[],"material_animations":[],"material_colors":[],"multi_language_refs":[],"placeholders":[],"plugin_effects":[],"primary_color_wheels":[],"realtime_denoises":[],"shapes":[],"smart_crops":[],"smart_relights":[],"sound_channel_mappings":[],"speeds":[],"stickers":[],"tail_leaders":[],"text_templates":[],"texts":[],"time_marks":[],"transitions":[],"video_effects":[],"video_trackings":[],"videos":[],"vocal_beautifys":[],"vocal_separations":[]},"mutable_config":null,"name":"","new_version":"110.0.0","platform":{"app_id":3704,"app_source":"lv","app_version":"5.9.0","device_id":"9c1d156ba20d5d2ef89a71f1a92596fd","hard_disk_id":"81f3587033961b2ad1d1ef9d11dcce33","mac_address":"5409d920e86b1169992326ea688e0715","os":"mac","os_version":"15.3.2"},"relationships":[],"render_index_track_mode_on":true,"retouch_cover":null,"source":"default","static_cover_image_path":"","time_marks":null,"tracks":[],"update_time":0,"version":360000} -------------------------------------------------------------------------------- /template_jianying/.backup/20250516114846_541eea452c3d8402ca82d1bb791a9eb6.save.bak: -------------------------------------------------------------------------------- 1 | {"canvas_config":{"height":1080,"ratio":"original","width":1920},"color_space":-1,"config":{"adjust_max_index":1,"attachment_info":[],"combination_max_index":1,"export_range":null,"extract_audio_last_index":1,"lyrics_recognition_id":"","lyrics_sync":true,"lyrics_taskinfo":[],"maintrack_adsorb":true,"material_save_mode":0,"multi_language_current":"none","multi_language_list":[],"multi_language_main":"none","multi_language_mode":"none","original_sound_last_index":1,"record_audio_last_index":1,"sticker_max_index":1,"subtitle_keywords_config":null,"subtitle_recognition_id":"","subtitle_sync":true,"subtitle_taskinfo":[],"system_font_list":[],"video_mute":false,"zoom_info_params":null},"cover":null,"create_time":0,"duration":0,"extra_info":null,"fps":30.0,"free_render_index_mode_on":false,"group_container":null,"id":"DF724EE8-CA13-430C-B9E1-643276C263DA","keyframe_graph_list":[],"keyframes":{"adjusts":[],"audios":[],"effects":[],"filters":[],"handwrites":[],"stickers":[],"texts":[],"videos":[]},"last_modified_platform":{"app_id":3704,"app_source":"lv","app_version":"5.9.0","device_id":"9c1d156ba20d5d2ef89a71f1a92596fd","hard_disk_id":"81f3587033961b2ad1d1ef9d11dcce33","mac_address":"5409d920e86b1169992326ea688e0715","os":"mac","os_version":"15.3.2"},"materials":{"ai_translates":[],"audio_balances":[],"audio_effects":[],"audio_fades":[],"audio_track_indexes":[],"audios":[],"beats":[],"canvases":[],"chromas":[],"color_curves":[],"digital_humans":[],"drafts":[],"effects":[],"flowers":[],"green_screens":[],"handwrites":[],"hsl":[],"images":[],"log_color_wheels":[],"loudnesses":[],"manual_deformations":[],"masks":[],"material_animations":[],"material_colors":[],"multi_language_refs":[],"placeholders":[],"plugin_effects":[],"primary_color_wheels":[],"realtime_denoises":[],"shapes":[],"smart_crops":[],"smart_relights":[],"sound_channel_mappings":[],"speeds":[],"stickers":[],"tail_leaders":[],"text_templates":[],"texts":[],"time_marks":[],"transitions":[],"video_effects":[],"video_trackings":[],"videos":[],"vocal_beautifys":[],"vocal_separations":[]},"mutable_config":null,"name":"","new_version":"110.0.0","platform":{"app_id":3704,"app_source":"lv","app_version":"5.9.0","device_id":"9c1d156ba20d5d2ef89a71f1a92596fd","hard_disk_id":"81f3587033961b2ad1d1ef9d11dcce33","mac_address":"5409d920e86b1169992326ea688e0715","os":"mac","os_version":"15.3.2"},"relationships":[],"render_index_track_mode_on":false,"retouch_cover":null,"source":"default","static_cover_image_path":"","time_marks":null,"tracks":[],"update_time":0,"version":360000} -------------------------------------------------------------------------------- /examples/example_capcut_effect.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | 5 | # 添加父目录到系统路径 6 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 7 | 8 | # 从example.py中导入必要的函数 9 | from example import add_video_impl, add_effect, save_draft_impl 10 | 11 | def example_capcut_effect(): 12 | """Test service for adding effects""" 13 | # draft_folder = "/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft" 14 | draft_folder = "/Users/sunguannan/Movies/CapCut/User Data/Projects/com.lveditor.draft" 15 | 16 | print("\nTest: Adding effects") 17 | # First add video track 18 | image_result = add_video_impl( 19 | video_url="https://pan.superbed.cn/share/1nbrg1fl/jimeng_daweidai.mp4", 20 | start=0, 21 | end=3.0, 22 | target_start=0, 23 | width=1080, 24 | height=1920 25 | ) 26 | print(f"Video added successfully! {image_result['output']['draft_id']}") 27 | image_result = add_video_impl( 28 | video_url="https://pan.superbed.cn/share/1nbrg1fl/jimeng_daweidai.mp4", 29 | draft_id=image_result['output']['draft_id'], 30 | start=0, 31 | end=3.0, 32 | target_start=3, 33 | ) 34 | print(f"Video added successfully! {image_result['output']['draft_id']}") 35 | 36 | # Then add effect 37 | effect_result = add_effect( 38 | effect_type="Like", 39 | effect_category="character", # Explicitly specify as character effect 40 | start=3, 41 | end=6, 42 | draft_id=image_result['output']['draft_id'], 43 | track_name="effect_01" 44 | ) 45 | print(f"Effect adding result: {effect_result}") 46 | print(save_draft_impl(effect_result['output']['draft_id'], draft_folder)) 47 | 48 | source_folder = os.path.join(os.getcwd(), effect_result['output']['draft_id']) 49 | destination_folder = os.path.join(draft_folder, effect_result['output']['draft_id']) 50 | 51 | if os.path.exists(source_folder): 52 | print(f"Moving {effect_result['output']['draft_id']} to {draft_folder}") 53 | shutil.move(source_folder, destination_folder) 54 | print("Folder moved successfully!") 55 | else: 56 | print(f"Source folder {source_folder} does not exist") 57 | 58 | # Add log to prompt user to find the draft in CapCut 59 | print(f"\n===== IMPORTANT =====\nPlease open CapCut and find the draft named '{effect_result['output']['draft_id']}'\n=======================") 60 | 61 | # Return the first test result for subsequent operations (if any) 62 | return effect_result 63 | 64 | 65 | if __name__ == "__main__": 66 | example_capcut_effect() -------------------------------------------------------------------------------- /template/template-2.tmp: -------------------------------------------------------------------------------- 1 | {"canvas_config":{"background":null,"height":1080,"ratio":"original","width":1920},"color_space":-1,"config":{"adjust_max_index":1,"attachment_info":[],"combination_max_index":1,"export_range":null,"extract_audio_last_index":1,"lyrics_recognition_id":"","lyrics_sync":true,"lyrics_taskinfo":[],"maintrack_adsorb":true,"material_save_mode":0,"multi_language_current":"none","multi_language_list":[],"multi_language_main":"none","multi_language_mode":"none","original_sound_last_index":1,"record_audio_last_index":1,"sticker_max_index":1,"subtitle_keywords_config":null,"subtitle_recognition_id":"","subtitle_sync":true,"subtitle_taskinfo":[],"system_font_list":[],"use_float_render":false,"video_mute":false,"zoom_info_params":null},"cover":null,"create_time":0,"duration":0,"extra_info":null,"fps":30.0,"free_render_index_mode_on":false,"group_container":null,"id":"26FDE5CD-364C-45A3-8930-A40DF2F7EB2B","is_drop_frame_timecode":false,"keyframe_graph_list":[],"keyframes":{"adjusts":[],"audios":[],"effects":[],"filters":[],"handwrites":[],"stickers":[],"texts":[],"videos":[]},"last_modified_platform":{"app_id":359289,"app_source":"cc","app_version":"6.5.0","device_id":"c4ca4238a0b923820dcc509a6f75849b","hard_disk_id":"307563e0192a94465c0e927fbc482942","mac_address":"c3371f2d4fb02791c067ce44d8fb4ed5","os":"mac","os_version":"15.5"},"lyrics_effects":[],"materials":{"ai_translates":[],"audio_balances":[],"audio_effects":[],"audio_fades":[],"audio_track_indexes":[],"audios":[],"beats":[],"canvases":[],"chromas":[],"color_curves":[],"common_mask":[],"digital_human_model_dressing":[],"digital_humans":[],"drafts":[],"effects":[],"flowers":[],"green_screens":[],"handwrites":[],"hsl":[],"images":[],"log_color_wheels":[],"loudnesses":[],"manual_beautys":[],"manual_deformations":[],"material_animations":[],"material_colors":[],"multi_language_refs":[],"placeholder_infos":[],"placeholders":[],"plugin_effects":[],"primary_color_wheels":[],"realtime_denoises":[],"shapes":[],"smart_crops":[],"smart_relights":[],"sound_channel_mappings":[],"speeds":[],"stickers":[],"tail_leaders":[],"text_templates":[],"texts":[],"time_marks":[],"transitions":[],"video_effects":[],"video_trackings":[],"videos":[],"vocal_beautifys":[],"vocal_separations":[]},"mutable_config":null,"name":"","new_version":"138.0.0","path":"","platform":{"app_id":359289,"app_source":"cc","app_version":"6.5.0","device_id":"c4ca4238a0b923820dcc509a6f75849b","hard_disk_id":"307563e0192a94465c0e927fbc482942","mac_address":"c3371f2d4fb02791c067ce44d8fb4ed5","os":"mac","os_version":"15.5"},"relationships":[],"render_index_track_mode_on":true,"retouch_cover":null,"source":"default","static_cover_image_path":"","time_marks":null,"tracks":[],"uneven_animation_template_info":{"composition":"","content":"","order":"","sub_template_info_list":[]},"update_time":0,"version":360000} -------------------------------------------------------------------------------- /pyJianYingDraft/metadata/capcut_mask_meta.py: -------------------------------------------------------------------------------- 1 | from .effect_meta import Effect_enum 2 | from .mask_meta import Mask_meta 3 | 4 | class CapCut_Mask_type(Effect_enum): 5 | """CapCut自带的蒙版类型""" 6 | 7 | Split = Mask_meta("Split", "line", "7374020197990011409", "B52CD1BC-63CE-4B74-B180-0B61E4AC928A", "4c6a0ef5de6a844342d40330e00c59eb", 1.0) 8 | """配置: centerX: 0.0, centerY: 0.0, expansion: 0.0, feather: 0.0, height: 0.0, invert: False, rotation: 0.0, roundCorner: 0.0, width: 0.16""" 9 | Filmstrip = Mask_meta("Filmstrip", "mirror", "7374021024985125377", "C4A9A4BD-280D-4625-AA7D-F5F70E97B438", "95ac211c99063c41b86b9b63742f4a6d", 1.0) 10 | """配置: centerX: 0.0, centerY: 0.0, expansion: 0.0, feather: 0.0, height: 0.5, invert: False, rotation: 0.0, roundCorner: 0.0, width: 0.16""" 11 | Circle = Mask_meta("Circle", "circle", "7374021188315517456", "E827751C-8DA7-412C-800D-DF2FE8712F77", "3ab1c47350d987c8ad415497e020a38b", 1.0) 12 | """配置: centerX: 0.0, centerY: 0.0, expansion: 0.0, feather: 0.0, height: 0.5, invert: False, rotation: 0.0, roundCorner: 0.0, width: 0.16""" 13 | Rectangle = Mask_meta("Rectangle", "rectangle", "7374021450748924432", "E55A3414-9C81-4664-BB2B-42528D098F2F", "02b8999168d121538a98ea59127483ef", 1.0) 14 | """配置: centerX: 0.0, centerY: 0.0, expansion: 0.0, feather: 0.0, height: 0.5, invert: False, rotation: 0.0, roundCorner: 0.0, width: 0.16""" 15 | Stars = Mask_meta("Stars", "pentagram", "7374021798087627265", "D824ED5D-D0C1-4EE2-B098-65F99CB38B95", "d3eb0298b2b1c345c123470c9194c8ad", 1.0471014493) 16 | """配置: centerX: 0.0, centerY: 0.0, expansion: 0.0, feather: 0.0, height: 0.5, invert: False, rotation: 0.0, roundCorner: 0.0, width: 0.16""" 17 | Heart = Mask_meta("Heart", "heart", "7350964630979613186", "2CA4F90A-87A6-483E-B71B-FDA65EE46860", "cfa1154e4873fda2c09716c8aa546236", 1.1148148148) 18 | """配置: centerX: 0.0, centerY: 0.0, expansion: 0.0, feather: 0.0, height: 0.5, invert: False, rotation: 0.0, roundCorner: 0.0, width: 0.16""" 19 | Text = Mask_meta("Text", "text", "7439320146876830225", "61E0D039-1A51-4570-B3F1-6AAC82AC1520", "ada210b1e21e860006c8324db359d8a3", 1.0) 20 | """配置: centerX: 0.0, centerY: 0.0, expansion: 0.0, feather: 0.0, height: 0.5, invert: False, rotation: 0.0, roundCorner: 0.0, width: 0.16""" 21 | Brush = Mask_meta("Brush", "custom", "7374021798087627265", "31AC68F1-9EC3-4A4E-8D53-D556B7CDAC9E", "7e3c26bd14a0b68a84fee058db7f1ade", 1.0) 22 | """配置: centerX: 0.0, centerY: 0.0, expansion: 0.0, feather: 0.0, height: 0.5, invert: False, rotation: 0.0, roundCorner: 0.0, width: 0.16""" 23 | Pen = Mask_meta("Pen", "contour", "7414333113955783185", "E3F45CAF-D445-4975-BE57-67AA716425D3", "9ee4e93af66335a75843446555efd8a6", 1.0) 24 | """配置: centerX: 0.0, centerY: 0.0, expansion: 0.0, feather: 0.0, height: 1.0, invert: False, rotation: 0.0, roundCorner: 0.0, width: 1.0""" 25 | -------------------------------------------------------------------------------- /pyJianYingDraft/__init__.py: -------------------------------------------------------------------------------- 1 | from .local_materials import Crop_settings, Video_material, Audio_material 2 | from .keyframe import Keyframe_property 3 | 4 | from .time_util import Timerange 5 | from .audio_segment import Audio_segment 6 | from .video_segment import Video_segment, Sticker_segment, Clip_settings 7 | from .effect_segment import Effect_segment, Filter_segment 8 | from .text_segment import Text_segment, Text_style, Text_border, Text_background, Text_shadow 9 | 10 | from .metadata import Font_type 11 | from .metadata import Mask_type 12 | from .metadata import CapCut_Mask_type 13 | from .metadata import Transition_type, Filter_type 14 | from .metadata import CapCut_Transition_type 15 | from .metadata import Intro_type, Outro_type, Group_animation_type 16 | from .metadata import CapCut_Intro_type, CapCut_Outro_type, CapCut_Group_animation_type 17 | from .metadata import Text_intro, Text_outro, Text_loop_anim 18 | from .metadata import CapCut_Text_intro, CapCut_Text_outro, CapCut_Text_loop_anim 19 | from .metadata import Audio_scene_effect_type, Tone_effect_type, Speech_to_song_type 20 | from .metadata import CapCut_Voice_filters_effect_type, CapCut_Voice_characters_effect_type, CapCut_Speech_to_song_effect_type 21 | from .metadata import Video_scene_effect_type, Video_character_effect_type 22 | from .metadata import CapCut_Video_scene_effect_type, CapCut_Video_character_effect_type 23 | 24 | from .track import Track_type 25 | from .template_mode import Shrink_mode, Extend_mode 26 | from .script_file import Script_file 27 | from .draft_folder import Draft_folder 28 | 29 | from .time_util import SEC, tim, trange 30 | 31 | __all__ = [ 32 | "Font_type", 33 | "Mask_type", 34 | "CapCut_Mask_type", 35 | "Filter_type", 36 | "Transition_type", 37 | "CapCut_Transition_type", 38 | "Intro_type", 39 | "Outro_type", 40 | "Group_animation_type", 41 | "CapCut_Intro_type", 42 | "CapCut_Outro_type", 43 | "CapCut_Group_animation_type", 44 | "Text_intro", 45 | "Text_outro", 46 | "Text_loop_anim", 47 | "CapCut_Text_intro", 48 | "CapCut_Text_outro", 49 | "CapCut_Text_loop_anim", 50 | "Audio_scene_effect_type", 51 | "Tone_effect_type", 52 | "Speech_to_song_type", 53 | "Video_scene_effect_type", 54 | "Video_character_effect_type", 55 | "CapCut_Voice_filters_effect_type", 56 | "CapCut_Voice_characters_effect_type", 57 | "CapCut_Speech_to_song_effect_type", 58 | "CapCut_Video_scene_effect_type", 59 | "CapCut_Video_character_effect_type", 60 | "Crop_settings", 61 | "Video_material", 62 | "Audio_material", 63 | "Keyframe_property", 64 | "Timerange", 65 | "Audio_segment", 66 | "Video_segment", 67 | "Sticker_segment", 68 | "Clip_settings", 69 | "Effect_segment", 70 | "Filter_segment", 71 | "Text_segment", 72 | "Text_style", 73 | "Text_border", 74 | "Text_background", 75 | "Text_shadow", 76 | "Track_type", 77 | "Shrink_mode", 78 | "Extend_mode", 79 | "Script_file", 80 | "Draft_folder", 81 | "SEC", 82 | "tim", 83 | "trange" 84 | ] 85 | -------------------------------------------------------------------------------- /pyJianYingDraft/keyframe.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from enum import Enum 4 | from typing import Dict, List, Any 5 | 6 | class Keyframe: 7 | """一个关键帧(关键点), 目前只支持线性插值""" 8 | 9 | kf_id: str 10 | """关键帧全局id, 自动生成""" 11 | time_offset: int 12 | """相对于素材起始点的时间偏移量""" 13 | values: List[float] 14 | """关键帧的值, 似乎一般只有一个元素""" 15 | 16 | def __init__(self, time_offset: int, value: float): 17 | """给定时间偏移量及关键值, 初始化关键帧""" 18 | self.kf_id = uuid.uuid4().hex 19 | 20 | self.time_offset = time_offset 21 | self.values = [value] 22 | 23 | def export_json(self) -> Dict[str, Any]: 24 | return { 25 | # 默认值 26 | "curveType": "Line", 27 | "graphID": "", 28 | "left_control": {"x": 0.0, "y": 0.0}, 29 | "right_control": {"x": 0.0, "y": 0.0}, 30 | # 自定义属性 31 | "id": self.kf_id, 32 | "time_offset": self.time_offset, 33 | "values": self.values 34 | } 35 | 36 | class Keyframe_property(Enum): 37 | """关键帧所控制的属性类型""" 38 | 39 | position_x = "KFTypePositionX" 40 | """右移为正, 此处的数值应该为`剪映中显示的值` / `草稿宽度`, 也即单位是半个画布宽""" 41 | position_y = "KFTypePositionY" 42 | """上移为正, 此处的数值应该为`剪映中显示的值` / `草稿高度`, 也即单位是半个画布高""" 43 | rotation = "KFTypeRotation" 44 | """顺时针旋转的**角度**""" 45 | 46 | scale_x = "KFTypeScaleX" 47 | """单独控制X轴缩放比例(1.0为不缩放), 与`uniform_scale`互斥""" 48 | scale_y = "KFTypeScaleY" 49 | """单独控制Y轴缩放比例(1.0为不缩放), 与`uniform_scale`互斥""" 50 | uniform_scale = "UNIFORM_SCALE" 51 | """同时控制X轴及Y轴缩放比例(1.0为不缩放), 与`scale_x`和`scale_y`互斥""" 52 | 53 | alpha = "KFTypeAlpha" 54 | """不透明度, 1.0为完全不透明, 仅对`Video_segment`有效""" 55 | saturation = "KFTypeSaturation" 56 | """饱和度, 0.0为原始饱和度, 范围为-1.0到1.0, 仅对`Video_segment`有效""" 57 | contrast = "KFTypeContrast" 58 | """对比度, 0.0为原始对比度, 范围为-1.0到1.0, 仅对`Video_segment`有效""" 59 | brightness = "KFTypeBrightness" 60 | """亮度, 0.0为原始亮度, 范围为-1.0到1.0, 仅对`Video_segment`有效""" 61 | 62 | volume = "KFTypeVolume" 63 | """音量, 1.0为原始音量, 仅对`Audio_segment`和`Video_segment`有效""" 64 | 65 | class Keyframe_list: 66 | """关键帧列表, 记录与某个特定属性相关的一系列关键帧""" 67 | 68 | list_id: str 69 | """关键帧列表全局id, 自动生成""" 70 | keyframe_property: Keyframe_property 71 | """关键帧对应的属性""" 72 | keyframes: List[Keyframe] 73 | """关键帧列表""" 74 | 75 | def __init__(self, keyframe_property: Keyframe_property): 76 | """为给定的关键帧属性初始化关键帧列表""" 77 | self.list_id = uuid.uuid4().hex 78 | 79 | self.keyframe_property = keyframe_property 80 | self.keyframes = [] 81 | 82 | def add_keyframe(self, time_offset: int, value: float): 83 | """给定时间偏移量及关键值, 向此关键帧列表中添加一个关键帧""" 84 | keyframe = Keyframe(time_offset, value) 85 | self.keyframes.append(keyframe) 86 | self.keyframes.sort(key=lambda x: x.time_offset) 87 | 88 | def export_json(self) -> Dict[str, Any]: 89 | return { 90 | "id": self.list_id, 91 | "keyframe_list": [kf.export_json() for kf in self.keyframes], 92 | "material_id": "", 93 | "property_type": self.keyframe_property.value 94 | } 95 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | import json 4 | import re 5 | import os 6 | import hashlib 7 | import functools 8 | import time 9 | from settings.local import DRAFT_DOMAIN, PREVIEW_ROUTER, IS_CAPCUT_ENV 10 | 11 | def hex_to_rgb(hex_color: str) -> tuple: 12 | """Convert hexadecimal color code to RGB tuple (range 0.0-1.0)""" 13 | hex_color = hex_color.lstrip('#') 14 | if len(hex_color) == 3: 15 | hex_color = ''.join([c*2 for c in hex_color]) # Handle shorthand form (e.g. #fff) 16 | try: 17 | r = int(hex_color[0:2], 16) / 255.0 18 | g = int(hex_color[2:4], 16) / 255.0 19 | b = int(hex_color[4:6], 16) / 255.0 20 | return (r, g, b) 21 | except ValueError: 22 | raise ValueError(f"Invalid hexadecimal color code: {hex_color}") 23 | 24 | 25 | def is_windows_path(path): 26 | """Detect if the path is Windows style""" 27 | # Check if it starts with a drive letter (e.g. C:\) or contains Windows style separators 28 | return re.match(r'^[a-zA-Z]:\\|\\\\', path) is not None 29 | 30 | 31 | def zip_draft(draft_id): 32 | current_dir = os.path.dirname(os.path.abspath(__file__)) 33 | # Compress folder 34 | zip_dir = os.path.join(current_dir, "tmp/zip") 35 | os.makedirs(zip_dir, exist_ok=True) 36 | zip_path = os.path.join(zip_dir, f"{draft_id}.zip") 37 | shutil.make_archive(os.path.join(zip_dir, draft_id), 'zip', os.path.join(current_dir, draft_id)) 38 | return zip_path 39 | 40 | def url_to_hash(url, length=16): 41 | """ 42 | Convert URL to a fixed-length hash string (without extension) 43 | 44 | Parameters: 45 | - url: Original URL string 46 | - length: Length of the hash string (maximum 64, default 16) 47 | 48 | Returns: 49 | - Hash string (e.g.: 3a7f9e7d9a1b4e2d) 50 | """ 51 | # Ensure URL is bytes type 52 | url_bytes = url.encode('utf-8') 53 | 54 | # Use SHA-256 to generate hash (secure and highly unique) 55 | hash_object = hashlib.sha256(url_bytes) 56 | 57 | # Truncate to specified length of hexadecimal string 58 | return hash_object.hexdigest()[:length] 59 | 60 | 61 | def timing_decorator(func_name): 62 | """Decorator: Used to monitor function execution time""" 63 | def decorator(func): 64 | @functools.wraps(func) 65 | def wrapper(*args, **kwargs): 66 | start_time = time.time() 67 | print(f"[{func_name}] Starting execution...") 68 | try: 69 | result = func(*args, **kwargs) 70 | end_time = time.time() 71 | duration = end_time - start_time 72 | print(f"[{func_name}] Execution completed, time taken: {duration:.3f} seconds") 73 | return result 74 | except Exception as e: 75 | end_time = time.time() 76 | duration = end_time - start_time 77 | print(f"[{func_name}] Execution failed, time taken: {duration:.3f} seconds, error: {e}") 78 | raise 79 | return wrapper 80 | return decorator 81 | 82 | def generate_draft_url(draft_id): 83 | return f"{DRAFT_DOMAIN}{PREVIEW_ROUTER}?draft_id={draft_id}&is_capcut={1 if IS_CAPCUT_ENV else 0}" -------------------------------------------------------------------------------- /pyJianYingDraft/time_util.py: -------------------------------------------------------------------------------- 1 | """定义时间范围类以及与时间相关的辅助函数""" 2 | 3 | from typing import Union 4 | from typing import Dict 5 | 6 | SEC = 1000000 7 | """一秒=1e6微秒""" 8 | 9 | def tim(inp: Union[str, float]) -> int: 10 | """将输入的字符串转换为微秒, 也可直接输入微秒数 11 | 12 | 支持类似 "1h52m3s" 或 "0.15s" 这样的格式, 可包含负号以表示负偏移 13 | """ 14 | if isinstance(inp, (int, float)): 15 | return int(round(inp)) 16 | 17 | sign: int = 1 18 | inp = inp.strip().lower() 19 | if inp.startswith("-"): 20 | sign = -1 21 | inp = inp[1:] 22 | 23 | last_index: int = 0 24 | total_time: float = 0 25 | for unit, factor in zip(["h", "m", "s"], [3600*SEC, 60*SEC, SEC]): 26 | unit_index = inp.find(unit) 27 | if unit_index == -1: continue 28 | 29 | total_time += float(inp[last_index:unit_index]) * factor 30 | last_index = unit_index + 1 31 | 32 | return int(round(total_time) * sign) 33 | 34 | class Timerange: 35 | """记录了起始时间及持续长度的时间范围""" 36 | start: int 37 | """起始时间, 单位为微秒""" 38 | duration: int 39 | """持续长度, 单位为微秒""" 40 | 41 | def __init__(self, start: int, duration: int): 42 | """构造一个时间范围 43 | 44 | Args: 45 | start (int): 起始时间, 单位为微秒 46 | duration (int): 持续长度, 单位为微秒 47 | """ 48 | 49 | self.start = start 50 | self.duration = duration 51 | 52 | @classmethod 53 | def import_json(cls, json_obj: Dict[str, str]) -> "Timerange": 54 | """从json对象中恢复Timerange""" 55 | return cls(int(json_obj["start"]), int(json_obj["duration"])) 56 | 57 | @property 58 | def end(self) -> int: 59 | """结束时间, 单位为微秒""" 60 | return self.start + self.duration 61 | 62 | def __eq__(self, other: object) -> bool: 63 | if not isinstance(other, Timerange): 64 | return False 65 | return self.start == other.start and self.duration == other.duration 66 | 67 | def overlaps(self, other: "Timerange") -> bool: 68 | """判断两个时间范围是否有重叠""" 69 | return not (self.end <= other.start or other.end <= self.start) 70 | 71 | def __repr__(self) -> str: 72 | return f"Timerange(start={self.start}, duration={self.duration})" 73 | 74 | def __str__(self) -> str: 75 | return f"[start={self.start}, end={self.end}]" 76 | 77 | def export_json(self) -> Dict[str, int]: 78 | return {"start": self.start, "duration": self.duration} 79 | 80 | def trange(start: Union[str, float], duration: Union[str, float]) -> Timerange: 81 | """Timerange的简便构造函数, 接受字符串或微秒数作为参数 82 | 83 | 支持类似 "1h52m3s" 或 "0.15s" 这样的格式 84 | 85 | Args: 86 | start (Union[str, float]): 起始时间 87 | duration (Union[str, float]): 持续长度, 注意**不是结束时间** 88 | """ 89 | return Timerange(tim(start), tim(duration)) 90 | 91 | def srt_tstamp(srt_tstamp: str) -> int: 92 | """解析srt中的时间戳字符串, 返回微秒数""" 93 | sec_str, ms_str = srt_tstamp.split(",") 94 | parts = sec_str.split(":") + [ms_str] 95 | 96 | total_time = 0 97 | for value, factor in zip(parts, [3600*SEC, 60*SEC, SEC, 1000]): 98 | total_time += int(value) * factor 99 | return total_time 100 | -------------------------------------------------------------------------------- /add_sticker_impl.py: -------------------------------------------------------------------------------- 1 | import pyJianYingDraft as draft 2 | from pyJianYingDraft import trange 3 | from typing import Optional, Dict 4 | from pyJianYingDraft import exceptions 5 | from create_draft import get_or_create_draft 6 | from util import generate_draft_url 7 | 8 | def add_sticker_impl( 9 | resource_id: str, 10 | start: float, 11 | end: float, 12 | draft_id: str = None, 13 | transform_y: float = 0, 14 | transform_x: float = 0, 15 | alpha: float = 1.0, 16 | flip_horizontal: bool = False, 17 | flip_vertical: bool = False, 18 | rotation: float = 0.0, 19 | scale_x: float = 1.0, 20 | scale_y: float = 1.0, 21 | track_name: str = "sticker_main", 22 | relative_index: int = 0, 23 | width: int = 1080, 24 | height: int = 1920 25 | ) -> Dict[str, str]: 26 | """ 27 | Add sticker to specified draft 28 | :param resource_id: Sticker resource ID 29 | :param start: Start time (seconds) 30 | :param end: End time (seconds) 31 | :param draft_id: Draft ID (optional, default None creates a new draft) 32 | :param transform_y: Y-axis position (default 0, screen center) 33 | :param transform_x: X-axis position (default 0, screen center) 34 | :param alpha: Image opacity, range 0-1 (default 1.0, completely opaque) 35 | :param flip_horizontal: Whether to flip horizontally (default False) 36 | :param flip_vertical: Whether to flip vertically (default False) 37 | :param rotation: Clockwise rotation angle, can be positive or negative (default 0.0) 38 | :param scale_x: Horizontal scale ratio (default 1.0) 39 | :param scale_y: Vertical scale ratio (default 1.0) 40 | :param track_name: Track name 41 | :param relative_index: Relative layer position (of the same track type), higher is closer to foreground (default 0) 42 | :param width: Video width, default 1080 43 | :param height: Video height, default 1920 44 | :return: Updated draft information 45 | """ 46 | # Get or create draft 47 | draft_id, script = get_or_create_draft( 48 | draft_id=draft_id, 49 | width=width, 50 | height=height 51 | ) 52 | 53 | # Add sticker track 54 | if track_name is not None: 55 | try: 56 | imported_track = script.get_imported_track(draft.Track_type.sticker, name=track_name) 57 | # If no exception is thrown, the track already exists 58 | except exceptions.TrackNotFound: 59 | # Track doesn't exist, create a new track 60 | script.add_track(draft.Track_type.sticker, track_name=track_name, relative_index=relative_index) 61 | else: 62 | script.add_track(draft.Track_type.sticker, relative_index=relative_index) 63 | 64 | # Create sticker segment 65 | sticker_segment = draft.Sticker_segment( 66 | resource_id, 67 | trange(f"{start}s", f"{end-start}s"), 68 | clip_settings=draft.Clip_settings( 69 | transform_y=transform_y, 70 | transform_x=transform_x, 71 | alpha=alpha, 72 | flip_horizontal=flip_horizontal, 73 | flip_vertical=flip_vertical, 74 | rotation=rotation, 75 | scale_x=scale_x, 76 | scale_y=scale_y 77 | ) 78 | ) 79 | 80 | # Add sticker segment to track 81 | script.add_segment(sticker_segment, track_name=track_name) 82 | 83 | return { 84 | "draft_id": draft_id, 85 | "draft_url": generate_draft_url(draft_id) 86 | } 87 | -------------------------------------------------------------------------------- /pyJianYingDraft/metadata/effect_meta.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from typing import List, Dict, Any 4 | from typing import TypeVar, Optional 5 | 6 | class Effect_param: 7 | """特效参数信息""" 8 | 9 | name: str 10 | """参数名称""" 11 | default_value: float 12 | """默认值""" 13 | min_value: float 14 | """最小值""" 15 | max_value: float 16 | """最大值""" 17 | 18 | def __init__(self, name: str, default_value: float, min_value: float, max_value: float): 19 | self.name = name 20 | self.default_value = default_value 21 | self.min_value = min_value 22 | self.max_value = max_value 23 | 24 | class Effect_param_instance(Effect_param): 25 | """特效参数实例""" 26 | 27 | index: int 28 | """参数索引""" 29 | value: float 30 | """当前值""" 31 | 32 | def __init__(self, meta: Effect_param, index: int, value: float): 33 | super().__init__(meta.name, meta.default_value, meta.min_value, meta.max_value) 34 | self.index = index 35 | self.value = value 36 | 37 | def export_json(self) -> Dict[str, Any]: 38 | return { 39 | "default_value": self.default_value, 40 | "max_value": self.max_value, 41 | "min_value": self.min_value, 42 | "name": self.name, 43 | "parameterIndex": self.index, 44 | "portIndex": 0, 45 | "value": self.value 46 | } 47 | 48 | class Effect_meta: 49 | """特效元数据""" 50 | 51 | name: str 52 | """效果名称""" 53 | is_vip: bool 54 | """是否为VIP特权""" 55 | 56 | resource_id: str 57 | """资源ID""" 58 | effect_id: str 59 | """效果ID""" 60 | md5: str 61 | 62 | params: List[Effect_param] 63 | """效果的参数信息""" 64 | 65 | def __init__(self, name: str, is_vip: bool, resource_id: str, effect_id: str, md5: str, params: List[Effect_param] = []): 66 | self.name = name 67 | self.is_vip = is_vip 68 | self.resource_id = resource_id 69 | self.effect_id = effect_id 70 | self.md5 = md5 71 | self.params = params 72 | 73 | def parse_params(self, params: Optional[List[Optional[float]]]) -> List[Effect_param_instance]: 74 | """解析参数列表(范围0~100), 返回参数实例列表""" 75 | ret: List[Effect_param_instance] = [] 76 | 77 | if params is None: params = [] 78 | for i, param in enumerate(self.params): 79 | val = param.default_value 80 | if i < len(params): 81 | input_v = params[i] 82 | if input_v is not None: 83 | if input_v < 0 or input_v > 100: 84 | raise ValueError("Invalid parameter value %f within %s" % (input_v, str(param))) 85 | val = param.min_value + (param.max_value - param.min_value) * input_v / 100.0 # 从0~100映射到实际值 86 | ret.append(Effect_param_instance(param, i, val)) 87 | return ret 88 | 89 | 90 | Effect_enum_subclass = TypeVar("Effect_enum_subclass", bound="Effect_enum") 91 | 92 | class Effect_enum(Enum): 93 | """特效枚举基类, 提供一个`from_name`方法用于根据名称获取特效元数据""" 94 | 95 | @classmethod 96 | def from_name(cls: "type[Effect_enum_subclass]", name: str) -> Effect_enum_subclass: 97 | """根据名称获取特效元数据, 忽略大小写、空格和下划线 98 | 99 | Args: 100 | name (str): 特效名称 101 | 102 | Raises: 103 | `ValueError`: 特效名称不存在 104 | """ 105 | name = name.lower().replace(" ", "").replace("_", "") 106 | for effect in cls: 107 | if effect.name.lower().replace(" ", "").replace("_", "") == name: 108 | return effect 109 | raise ValueError(f"Effect named '{name}' not found") 110 | -------------------------------------------------------------------------------- /pyJianYingDraft/draft_content_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "canvas_config": { 3 | "height": 1080, 4 | "ratio": "original", 5 | "width": 1920 6 | }, 7 | "color_space": 0, 8 | "config": { 9 | "adjust_max_index": 1, 10 | "attachment_info": [], 11 | "combination_max_index": 1, 12 | "export_range": null, 13 | "extract_audio_last_index": 1, 14 | "lyrics_recognition_id": "", 15 | "lyrics_sync": true, 16 | "lyrics_taskinfo": [], 17 | "maintrack_adsorb": true, 18 | "material_save_mode": 0, 19 | "multi_language_current": "none", 20 | "multi_language_list": [], 21 | "multi_language_main": "none", 22 | "multi_language_mode": "none", 23 | "original_sound_last_index": 1, 24 | "record_audio_last_index": 1, 25 | "sticker_max_index": 1, 26 | "subtitle_keywords_config": null, 27 | "subtitle_recognition_id": "", 28 | "subtitle_sync": true, 29 | "subtitle_taskinfo": [], 30 | "system_font_list": [], 31 | "video_mute": false, 32 | "zoom_info_params": null 33 | }, 34 | "cover": null, 35 | "create_time": 0, 36 | "duration": 0, 37 | "extra_info": null, 38 | "fps": 30.0, 39 | "free_render_index_mode_on": false, 40 | "group_container": null, 41 | "id": "91E08AC5-22FB-47e2-9AA0-7DC300FAEA2B", 42 | "keyframe_graph_list": [], 43 | "keyframes": { 44 | "adjusts": [], 45 | "audios": [], 46 | "effects": [], 47 | "filters": [], 48 | "handwrites": [], 49 | "stickers": [], 50 | "texts": [], 51 | "videos": [] 52 | }, 53 | "last_modified_platform": { 54 | "app_id": 3704, 55 | "app_source": "lv", 56 | "app_version": "5.9.0", 57 | "os": "windows" 58 | }, 59 | "materials": { 60 | "ai_translates": [], 61 | "audio_balances": [], 62 | "audio_effects": [], 63 | "audio_fades": [], 64 | "audio_track_indexes": [], 65 | "audios": [], 66 | "beats": [], 67 | "canvases": [], 68 | "chromas": [], 69 | "color_curves": [], 70 | "digital_humans": [], 71 | "drafts": [], 72 | "effects": [], 73 | "flowers": [], 74 | "green_screens": [], 75 | "handwrites": [], 76 | "hsl": [], 77 | "images": [], 78 | "log_color_wheels": [], 79 | "loudnesses": [], 80 | "manual_deformations": [], 81 | "masks": [], 82 | "material_animations": [], 83 | "material_colors": [], 84 | "multi_language_refs": [], 85 | "placeholders": [], 86 | "plugin_effects": [], 87 | "primary_color_wheels": [], 88 | "realtime_denoises": [], 89 | "shapes": [], 90 | "smart_crops": [], 91 | "smart_relights": [], 92 | "sound_channel_mappings": [], 93 | "speeds": [], 94 | "stickers": [], 95 | "tail_leaders": [], 96 | "text_templates": [], 97 | "texts": [], 98 | "time_marks": [], 99 | "transitions": [], 100 | "video_effects": [], 101 | "video_trackings": [], 102 | "videos": [], 103 | "vocal_beautifys": [], 104 | "vocal_separations": [] 105 | }, 106 | "mutable_config": null, 107 | "name": "", 108 | "new_version": "110.0.0", 109 | "relationships": [], 110 | "render_index_track_mode_on": true, 111 | "retouch_cover": null, 112 | "source": "default", 113 | "static_cover_image_path": "", 114 | "time_marks": null, 115 | "tracks": [ 116 | ], 117 | "update_time": 0, 118 | "version": 360000 119 | } -------------------------------------------------------------------------------- /pyJianYingDraft/draft_folder.py: -------------------------------------------------------------------------------- 1 | """草稿文件夹管理器""" 2 | 3 | import os 4 | import shutil 5 | 6 | from typing import List 7 | 8 | from .script_file import Script_file 9 | 10 | class Draft_folder: 11 | """管理一个文件夹及其内的一系列草稿""" 12 | 13 | folder_path: str 14 | """根路径""" 15 | 16 | def __init__(self, folder_path: str): 17 | """初始化草稿文件夹管理器 18 | 19 | Args: 20 | folder_path (`str`): 包含若干草稿的文件夹, 一般取剪映保存草稿的位置即可 21 | 22 | Raises: 23 | `FileNotFoundError`: 路径不存在 24 | """ 25 | self.folder_path = folder_path 26 | 27 | if not os.path.exists(self.folder_path): 28 | raise FileNotFoundError(f"根文件夹 {self.folder_path} 不存在") 29 | 30 | def list_drafts(self) -> List[str]: 31 | """列出文件夹中所有草稿的名称 32 | 33 | 注意: 本函数只是如实地列出子文件夹的名称, 并不检查它们是否符合草稿的格式 34 | """ 35 | return [f for f in os.listdir(self.folder_path) if os.path.isdir(os.path.join(self.folder_path, f))] 36 | 37 | def remove(self, draft_name: str) -> None: 38 | """删除指定名称的草稿 39 | 40 | Args: 41 | draft_name (`str`): 草稿名称, 即相应文件夹名称 42 | 43 | Raises: 44 | `FileNotFoundError`: 对应的草稿不存在 45 | """ 46 | draft_path = os.path.join(self.folder_path, draft_name) 47 | if not os.path.exists(draft_path): 48 | raise FileNotFoundError(f"草稿文件夹 {draft_name} 不存在") 49 | 50 | shutil.rmtree(draft_path) 51 | 52 | def inspect_material(self, draft_name: str) -> None: 53 | """输出指定名称草稿中的贴纸素材元数据 54 | 55 | Args: 56 | draft_name (`str`): 草稿名称, 即相应文件夹名称 57 | 58 | Raises: 59 | `FileNotFoundError`: 对应的草稿不存在 60 | """ 61 | draft_path = os.path.join(self.folder_path, draft_name) 62 | if not os.path.exists(draft_path): 63 | raise FileNotFoundError(f"草稿文件夹 {draft_name} 不存在") 64 | 65 | script_file = self.load_template(draft_name) 66 | script_file.inspect_material() 67 | 68 | def load_template(self, draft_name: str) -> Script_file: 69 | """在文件夹中打开一个草稿作为模板, 并在其上进行编辑 70 | 71 | Args: 72 | draft_name (`str`): 草稿名称, 即相应文件夹名称 73 | 74 | Returns: 75 | `Script_file`: 以模板模式打开的草稿对象 76 | 77 | Raises: 78 | `FileNotFoundError`: 对应的草稿不存在 79 | """ 80 | draft_path = os.path.join(self.folder_path, draft_name) 81 | if not os.path.exists(draft_path): 82 | raise FileNotFoundError(f"草稿文件夹 {draft_name} 不存在") 83 | 84 | return Script_file.load_template(os.path.join(draft_path, "draft_info.json")) 85 | 86 | def duplicate_as_template(self, template_name: str, new_draft_name: str, allow_replace: bool = False) -> Script_file: 87 | """复制一份给定的草稿, 并在复制出的新草稿上进行编辑 88 | 89 | Args: 90 | template_name (`str`): 原草稿名称 91 | new_draft_name (`str`): 新草稿名称 92 | allow_replace (`bool`, optional): 是否允许覆盖与`new_draft_name`重名的草稿. 默认为否. 93 | 94 | Returns: 95 | `Script_file`: 以模板模式打开的**复制后的**草稿对象 96 | 97 | Raises: 98 | `FileNotFoundError`: 原始草稿不存在 99 | `FileExistsError`: 已存在与`new_draft_name`重名的草稿, 但不允许覆盖. 100 | """ 101 | template_path = os.path.join(self.folder_path, template_name) 102 | new_draft_path = os.path.join(self.folder_path, new_draft_name) 103 | if not os.path.exists(template_path): 104 | raise FileNotFoundError(f"模板草稿 {template_name} 不存在") 105 | if os.path.exists(new_draft_path) and not allow_replace: 106 | raise FileExistsError(f"新草稿 {new_draft_name} 已存在且不允许覆盖") 107 | 108 | # 复制草稿文件夹 109 | shutil.copytree(template_path, new_draft_path, dirs_exist_ok=allow_replace) 110 | 111 | # 打开草稿 112 | return self.load_template(new_draft_name) 113 | -------------------------------------------------------------------------------- /add_effect_impl.py: -------------------------------------------------------------------------------- 1 | from pyJianYingDraft import trange, Video_scene_effect_type, Video_character_effect_type, CapCut_Video_scene_effect_type, CapCut_Video_character_effect_type, exceptions 2 | import pyJianYingDraft as draft 3 | from typing import Optional, Dict, List, Union, Literal 4 | from create_draft import get_or_create_draft 5 | from util import generate_draft_url 6 | from settings import IS_CAPCUT_ENV 7 | 8 | def add_effect_impl( 9 | effect_type: str, # Changed to string type 10 | effect_category: Literal["scene", "character"], 11 | start: float = 0, 12 | end: float = 3.0, 13 | draft_id: Optional[str] = None, 14 | track_name: Optional[str] = "effect_01", 15 | params: Optional[List[Optional[float]]] = None, 16 | width: int = 1080, 17 | height: int = 1920 18 | ) -> Dict[str, str]: 19 | """ 20 | Add an effect to the specified draft 21 | :param effect_type: Effect type name, will be matched from Video_scene_effect_type or Video_character_effect_type 22 | :param effect_category: Effect category, "scene" or "character", default "scene" 23 | :param start: Start time (seconds), default 0 24 | :param end: End time (seconds), default 3 seconds 25 | :param draft_id: Draft ID, if None or corresponding zip file not found, a new draft will be created 26 | :param track_name: Track name, can be omitted when there is only one effect track 27 | :param params: Effect parameter list, items not provided or None in the parameter list will use default values 28 | :param width: Video width, default 1080 29 | :param height: Video height, default 1920 30 | :return: Updated draft information 31 | """ 32 | # Get or create draft 33 | draft_id, script = get_or_create_draft( 34 | draft_id=draft_id, 35 | width=width, 36 | height=height 37 | ) 38 | 39 | # Calculate time range 40 | duration = end - start 41 | t_range = trange(f"{start}s", f"{duration}s") 42 | 43 | # Select the corresponding effect type based on effect category and environment 44 | effect_enum = None 45 | if IS_CAPCUT_ENV: 46 | # If in CapCut environment, use CapCut effects 47 | if effect_category == "scene": 48 | try: 49 | effect_enum = CapCut_Video_scene_effect_type[effect_type] 50 | except: 51 | effect_enum = None 52 | elif effect_category == "character": 53 | try: 54 | effect_enum = CapCut_Video_character_effect_type[effect_type] 55 | except: 56 | effect_enum = None 57 | else: 58 | # Default to using JianYing effects 59 | if effect_category == "scene": 60 | try: 61 | effect_enum = Video_scene_effect_type[effect_type] 62 | except: 63 | effect_enum = None 64 | elif effect_category == "character": 65 | try: 66 | effect_enum = Video_character_effect_type[effect_type] 67 | except: 68 | effect_enum = None 69 | 70 | if effect_enum is None: 71 | raise ValueError(f"Unknown {effect_category} effect type: {effect_type}") 72 | 73 | # Add effect track (only when track doesn't exist) 74 | if track_name is not None: 75 | try: 76 | imported_track=script.get_imported_track(draft.Track_type.effect, name=track_name) 77 | # If no exception is thrown, the track already exists 78 | except exceptions.TrackNotFound: 79 | # Track doesn't exist, create a new track 80 | script.add_track(draft.Track_type.effect, track_name=track_name) 81 | else: 82 | script.add_track(draft.Track_type.effect) 83 | 84 | # Add effect 85 | script.add_effect(effect_enum, t_range, params=params[::-1], track_name=track_name) 86 | 87 | return { 88 | "draft_id": draft_id, 89 | "draft_url": generate_draft_url(draft_id) 90 | } 91 | -------------------------------------------------------------------------------- /get_duration_impl.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import json 3 | import time 4 | 5 | def get_video_duration(video_url): 6 | """ 7 | Get video duration with timeout retry support. 8 | :param video_url: Video URL 9 | :return: Video duration (seconds) 10 | """ 11 | 12 | # Define retry count and wait time for each retry 13 | max_retries = 3 14 | retry_delay_seconds = 1 # 1 second interval between retries 15 | timeout_seconds = 10 # Set timeout for each attempt 16 | 17 | for attempt in range(max_retries): 18 | print(f"Attempting to get video duration (Attempt {attempt + 1}/{max_retries}) ...") 19 | result = {"success": False, "output": 0, "error": None} # Reset result before each retry 20 | 21 | try: 22 | command = [ 23 | 'ffprobe', 24 | '-v', 'error', 25 | '-show_entries', 'stream=duration', 26 | '-show_entries', 'format=duration', 27 | '-print_format', 'json', 28 | video_url 29 | ] 30 | 31 | # Use subprocess.run for more flexible handling of timeout and output 32 | process = subprocess.run(command, 33 | capture_output=True, 34 | text=True, # Auto decode to text 35 | timeout=timeout_seconds, # Use variable to set timeout 36 | check=True) # Raise CalledProcessError if non-zero exit code 37 | 38 | info = json.loads(process.stdout) 39 | 40 | # Prioritize getting duration from streams because it's more accurate 41 | media_streams = [s for s in info.get('streams', []) if 'duration' in s] 42 | 43 | if media_streams: 44 | duration = float(media_streams[0]['duration']) 45 | result["output"] = duration 46 | result["success"] = True 47 | # Otherwise get duration from format information 48 | elif 'format' in info and 'duration' in info['format']: 49 | duration = float(info['format']['duration']) 50 | result["output"] = duration 51 | result["success"] = True 52 | else: 53 | result["error"] = "Audio/video duration information not found." 54 | 55 | # If duration is successfully obtained, return result directly without retrying 56 | if result["success"]: 57 | print(f"Successfully obtained duration: {result['output']:.2f} seconds") 58 | return result 59 | 60 | except subprocess.TimeoutExpired: 61 | result["error"] = f"Getting video duration timed out (exceeded {timeout_seconds} seconds)." 62 | print(f"Attempt {attempt + 1} timed out.") 63 | except subprocess.CalledProcessError as e: 64 | result["error"] = f"Error executing ffprobe command (exit code {e.returncode}): {e.stderr.strip()}" 65 | print(f"Attempt {attempt + 1} failed. Error: {e.stderr.strip()}") 66 | except json.JSONDecodeError as e: 67 | result["error"] = f"Error parsing JSON data: {e}" 68 | print(f"Attempt {attempt + 1} failed. JSON parsing error: {e}") 69 | except FileNotFoundError: 70 | result["error"] = "ffprobe command not found. Please ensure FFmpeg is installed and in system PATH." 71 | print("Error: ffprobe command not found, please check installation.") 72 | return result # No need to retry if ffprobe itself is not found 73 | except Exception as e: 74 | result["error"] = f"Unknown error occurred: {e}" 75 | print(f"Attempt {attempt + 1} failed. Unknown error: {e}") 76 | 77 | # Try using remote service to get duration after each local failure 78 | if not result["success"]: 79 | print(f"Local retrieval failed") 80 | # try: 81 | # remote_duration = get_duration(video_url) 82 | # if remote_duration is not None: 83 | # result["success"] = True 84 | # result["output"] = remote_duration 85 | # result["error"] = None 86 | # print(f"Remote service successfully obtained duration: {remote_duration:.2f} seconds") 87 | # return result # Remote service succeeded, return directly 88 | # else: 89 | # print(f"Remote service also unable to get duration (Attempt {attempt + 1})") 90 | # except Exception as e: 91 | # print(f"Remote service failed to get duration (Attempt {attempt + 1}): {e}") 92 | 93 | # If current attempt failed and max retries not reached, wait and prepare for next retry 94 | if not result["success"] and attempt < max_retries - 1: 95 | print(f"Waiting {retry_delay_seconds} seconds before retrying...") 96 | time.sleep(retry_delay_seconds) 97 | elif not result["success"] and attempt == max_retries - 1: 98 | print(f"Maximum retry count {max_retries} reached, both local and remote services unable to get duration.") 99 | 100 | return result # Return the last failure result after all retries fail -------------------------------------------------------------------------------- /MCP_文档_中文.md: -------------------------------------------------------------------------------- 1 | # CapCut API MCP 服务器使用文档 2 | 3 | ## 概述 4 | 5 | CapCut API MCP 服务器是一个基于 Model Context Protocol (MCP) 的视频编辑服务,提供了完整的 CapCut 视频编辑功能接口。通过 MCP 协议,您可以轻松地在各种应用中集成专业级的视频编辑能力。 6 | 7 | ## 功能特性 8 | 9 | ### 🎬 核心功能 10 | - **草稿管理**: 创建、保存和管理视频项目 11 | - **多媒体支持**: 视频、音频、图片、文本处理 12 | - **高级效果**: 特效、动画、转场、滤镜 13 | - **精确控制**: 时间轴、关键帧、图层管理 14 | 15 | ### 🛠️ 可用工具 (11个) 16 | 17 | | 工具名称 | 功能描述 | 主要参数 | 18 | |---------|----------|----------| 19 | | `create_draft` | 创建新的视频草稿项目 | width, height | 20 | | `add_text` | 添加文字元素 | text, font_size, color, shadow, background | 21 | | `add_video` | 添加视频轨道 | video_url, start, end, transform, volume | 22 | | `add_audio` | 添加音频轨道 | audio_url, volume, speed, effects | 23 | | `add_image` | 添加图片素材 | image_url, transform, animation, transition | 24 | | `add_subtitle` | 添加字幕文件 | srt_path, font_style, position | 25 | | `add_effect` | 添加视觉特效 | effect_type, parameters, duration | 26 | | `add_sticker` | 添加贴纸元素 | resource_id, position, scale, rotation | 27 | | `add_video_keyframe` | 添加关键帧动画 | property_types, times, values | 28 | | `get_video_duration` | 获取视频时长 | video_url | 29 | | `save_draft` | 保存草稿项目 | draft_id | 30 | 31 | ## 安装配置 32 | 33 | ### 环境要求 34 | - Python 3.10+ 35 | - CapCut 应用 (macOS/Windows) 36 | - MCP 客户端支持 37 | 38 | ### 依赖安装 39 | ```bash 40 | # 创建虚拟环境 41 | python3.10 -m venv venv-mcp 42 | source venv-mcp/bin/activate # macOS/Linux 43 | # 或 venv-mcp\Scripts\activate # Windows 44 | 45 | # 安装依赖 46 | pip install -r requirements-mcp.txt 47 | ``` 48 | 49 | ### MCP 配置 50 | 创建或更新 `mcp_config.json` 文件: 51 | 52 | ```json 53 | { 54 | "mcpServers": { 55 | "capcut-api": { 56 | "command": "python3.10", 57 | "args": ["mcp_server.py"], 58 | "cwd": "/path/to/CapCutAPI-dev", 59 | "env": { 60 | "PYTHONPATH": "/path/to/CapCutAPI-dev" 61 | } 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ## 使用指南 68 | 69 | ### 基础工作流程 70 | 71 | #### 1. 创建草稿 72 | ```python 73 | # 创建 1080x1920 竖屏项目 74 | result = mcp_client.call_tool("create_draft", { 75 | "width": 1080, 76 | "height": 1920 77 | }) 78 | draft_id = result["draft_id"] 79 | ``` 80 | 81 | #### 2. 添加内容 82 | ```python 83 | # 添加标题文字 84 | mcp_client.call_tool("add_text", { 85 | "text": "我的视频标题", 86 | "start": 0, 87 | "end": 5, 88 | "draft_id": draft_id, 89 | "font_size": 48, 90 | "font_color": "#FFFFFF" 91 | }) 92 | 93 | # 添加背景视频 94 | mcp_client.call_tool("add_video", { 95 | "video_url": "https://example.com/video.mp4", 96 | "draft_id": draft_id, 97 | "start": 0, 98 | "end": 10, 99 | "volume": 0.8 100 | }) 101 | ``` 102 | 103 | #### 3. 保存项目 104 | ```python 105 | # 保存草稿 106 | result = mcp_client.call_tool("save_draft", { 107 | "draft_id": draft_id 108 | }) 109 | ``` 110 | 111 | ### 高级功能示例 112 | 113 | #### 文字样式设置 114 | ```python 115 | # 带阴影和背景的文字 116 | mcp_client.call_tool("add_text", { 117 | "text": "高级文字效果", 118 | "draft_id": draft_id, 119 | "font_size": 56, 120 | "font_color": "#FFD700", 121 | "shadow_enabled": True, 122 | "shadow_color": "#000000", 123 | "shadow_alpha": 0.8, 124 | "background_color": "#1E1E1E", 125 | "background_alpha": 0.7, 126 | "background_round_radius": 15 127 | }) 128 | ``` 129 | 130 | #### 关键帧动画 131 | ```python 132 | # 缩放和透明度动画 133 | mcp_client.call_tool("add_video_keyframe", { 134 | "draft_id": draft_id, 135 | "track_name": "video_main", 136 | "property_types": ["scale_x", "scale_y", "alpha"], 137 | "times": [0, 2, 4], 138 | "values": ["1.0", "1.5", "0.5"] 139 | }) 140 | ``` 141 | 142 | #### 多样式文本 143 | ```python 144 | # 不同颜色的文字段落 145 | mcp_client.call_tool("add_text", { 146 | "text": "彩色文字效果", 147 | "draft_id": draft_id, 148 | "text_styles": [ 149 | {"start": 0, "end": 2, "font_color": "#FF0000"}, 150 | {"start": 2, "end": 4, "font_color": "#00FF00"} 151 | ] 152 | }) 153 | ``` 154 | 155 | ## 测试验证 156 | 157 | ### 使用测试客户端 158 | ```bash 159 | # 运行测试客户端 160 | python test_mcp_client.py 161 | ``` 162 | 163 | ### 功能验证清单 164 | - [ ] 服务器启动成功 165 | - [ ] 工具列表获取正常 166 | - [ ] 草稿创建功能 167 | - [ ] 文本添加功能 168 | - [ ] 视频/音频/图片添加 169 | - [ ] 特效和动画功能 170 | - [ ] 草稿保存功能 171 | 172 | ## 故障排除 173 | 174 | ### 常见问题 175 | 176 | #### 1. "CapCut modules not available" 177 | **解决方案**: 178 | - 确认 CapCut 应用已安装 179 | - 检查 Python 路径配置 180 | - 验证依赖包安装 181 | 182 | #### 2. 服务器启动失败 183 | **解决方案**: 184 | - 检查虚拟环境激活 185 | - 验证配置文件路径 186 | - 查看错误日志 187 | 188 | #### 3. 工具调用错误 189 | **解决方案**: 190 | - 检查参数格式 191 | - 验证媒体文件URL 192 | - 确认时间范围设置 193 | 194 | ### 调试模式 195 | ```bash 196 | # 启用详细日志 197 | export DEBUG=1 198 | python mcp_server.py 199 | ``` 200 | 201 | ## 最佳实践 202 | 203 | ### 性能优化 204 | 1. **媒体文件**: 使用压缩格式,避免过大文件 205 | 2. **时间管理**: 合理规划元素时间轴,避免重叠 206 | 3. **内存使用**: 及时保存草稿,清理临时文件 207 | 208 | ### 错误处理 209 | 1. **参数验证**: 调用前检查必需参数 210 | 2. **异常捕获**: 处理网络和文件错误 211 | 3. **重试机制**: 对临时失败进行重试 212 | 213 | ## API 参考 214 | 215 | ### 通用参数 216 | - `draft_id`: 草稿唯一标识符 217 | - `start/end`: 时间范围(秒) 218 | - `width/height`: 项目尺寸 219 | - `transform_x/y`: 位置坐标 220 | - `scale_x/y`: 缩放比例 221 | 222 | ### 返回格式 223 | ```json 224 | { 225 | "success": true, 226 | "result": { 227 | "draft_id": "dfd_cat_xxx", 228 | "draft_url": "https://..." 229 | }, 230 | "features_used": { 231 | "shadow": false, 232 | "background": false, 233 | "multi_style": false 234 | } 235 | } 236 | ``` 237 | 238 | ## 更新日志 239 | 240 | ### v1.0.0 241 | - 初始版本发布 242 | - 支持 11 个核心工具 243 | - 完整的 MCP 协议实现 244 | 245 | ## 技术支持 246 | 247 | 如有问题或建议,请通过以下方式联系: 248 | - GitHub Issues 249 | - 技术文档 250 | - 社区论坛 251 | 252 | --- 253 | 254 | *本文档持续更新,请关注最新版本。* -------------------------------------------------------------------------------- /save_task_cache.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | import threading 3 | from typing import Dict, Any 4 | 5 | # Using OrderedDict to implement LRU cache, limiting the maximum number to 1000 6 | DRAFT_TASKS: Dict[str, dict] = OrderedDict() # Using Dict for type hinting 7 | MAX_TASKS_CACHE_SIZE = 1000 8 | 9 | 10 | def update_tasks_cache(task_id: str, task_status: dict) -> None: 11 | """Update task status LRU cache 12 | 13 | :param task_id: Task ID 14 | :param task_status: Task status information dictionary 15 | """ 16 | 17 | if task_id in DRAFT_TASKS: 18 | # If the key exists, delete the old item 19 | DRAFT_TASKS.pop(task_id) 20 | elif len(DRAFT_TASKS) >= MAX_TASKS_CACHE_SIZE: 21 | # If the cache is full, delete the least recently used item (the first item) 22 | DRAFT_TASKS.popitem(last=False) 23 | # Add new item to the end (most recently used) 24 | DRAFT_TASKS[task_id] = task_status 25 | 26 | def update_task_field(task_id: str, field: str, value: Any) -> None: 27 | """Update a single field in the task status 28 | 29 | :param task_id: Task ID 30 | :param field: Field name to update 31 | :param value: New value for the field 32 | """ 33 | if task_id in DRAFT_TASKS: 34 | # Copy the current status, modify the specified field, then update the cache 35 | task_status = DRAFT_TASKS[task_id].copy() 36 | task_status[field] = value 37 | # Delete the old item and add the updated item 38 | DRAFT_TASKS.pop(task_id) 39 | DRAFT_TASKS[task_id] = task_status 40 | else: 41 | # If the task doesn't exist, create a default status and set the specified field 42 | task_status = { 43 | "status": "initialized", 44 | "message": "Task initialized", 45 | "progress": 0, 46 | "completed_files": 0, 47 | "total_files": 0, 48 | "draft_url": "" 49 | } 50 | task_status[field] = value 51 | # If the cache is full, delete the least recently used item 52 | if len(DRAFT_TASKS) >= MAX_TASKS_CACHE_SIZE: 53 | DRAFT_TASKS.popitem(last=False) 54 | # Add new item 55 | DRAFT_TASKS[task_id] = task_status 56 | 57 | def update_task_fields(task_id: str, **fields) -> None: 58 | """Update multiple fields in the task status 59 | 60 | :param task_id: Task ID 61 | :param fields: Fields to update and their values, provided as keyword arguments 62 | """ 63 | if task_id in DRAFT_TASKS: 64 | # Copy the current status, modify the specified fields, then update the cache 65 | task_status = DRAFT_TASKS[task_id].copy() 66 | for field, value in fields.items(): 67 | task_status[field] = value 68 | # Delete the old item and add the updated item 69 | DRAFT_TASKS.pop(task_id) 70 | DRAFT_TASKS[task_id] = task_status 71 | else: 72 | # If the task doesn't exist, create a default status and set the specified fields 73 | task_status = { 74 | "status": "initialized", 75 | "message": "Task initialized", 76 | "progress": 0, 77 | "completed_files": 0, 78 | "total_files": 0, 79 | "draft_url": "" 80 | } 81 | for field, value in fields.items(): 82 | task_status[field] = value 83 | # If the cache is full, delete the least recently used item 84 | if len(DRAFT_TASKS) >= MAX_TASKS_CACHE_SIZE: 85 | DRAFT_TASKS.popitem(last=False) 86 | # Add new item 87 | DRAFT_TASKS[task_id] = task_status 88 | 89 | def increment_task_field(task_id: str, field: str, increment: int = 1) -> None: 90 | """Increment a numeric field in the task status 91 | 92 | :param task_id: Task ID 93 | :param field: Field name to increment 94 | :param increment: Value to increment by, default is 1 95 | """ 96 | if task_id in DRAFT_TASKS: 97 | # Copy the current status, increment the specified field, then update the cache 98 | task_status = DRAFT_TASKS[task_id].copy() 99 | if field in task_status and isinstance(task_status[field], (int, float)): 100 | task_status[field] += increment 101 | else: 102 | task_status[field] = increment 103 | # Delete the old item and add the updated item 104 | DRAFT_TASKS.pop(task_id) 105 | DRAFT_TASKS[task_id] = task_status 106 | 107 | def get_task_status(task_id: str) -> dict: 108 | """Get task status 109 | 110 | :param task_id: Task ID 111 | :return: Task status information dictionary 112 | """ 113 | task_status = DRAFT_TASKS.get(task_id, { 114 | "status": "not_found", 115 | "message": "Task does not exist", 116 | "progress": 0, 117 | "completed_files": 0, 118 | "total_files": 0, 119 | "draft_url": "" 120 | }) 121 | 122 | # If the task is found, update its position in the LRU cache 123 | if task_id in DRAFT_TASKS: 124 | # First delete, then add to the end, implementing LRU update 125 | update_tasks_cache(task_id, task_status) 126 | 127 | return task_status 128 | 129 | def create_task(task_id: str) -> None: 130 | """Create a new task and initialize its status 131 | 132 | :param task_id: Task ID 133 | """ 134 | task_status = { 135 | "status": "initialized", 136 | "message": "Task initialized", 137 | "progress": 0, 138 | "completed_files": 0, 139 | "total_files": 0, 140 | "draft_url": "" 141 | } 142 | update_tasks_cache(task_id, task_status) -------------------------------------------------------------------------------- /pyJianYingDraft/animation.py: -------------------------------------------------------------------------------- 1 | """定义视频/文本动画相关类""" 2 | 3 | import uuid 4 | 5 | from typing import Union, Optional 6 | from typing import Literal, Dict, List, Any 7 | 8 | from .time_util import Timerange 9 | 10 | from .metadata.animation_meta import Animation_meta 11 | from .metadata import Intro_type, Outro_type, Group_animation_type 12 | from .metadata import CapCut_Intro_type, CapCut_Outro_type, CapCut_Group_animation_type 13 | from .metadata import Text_intro, Text_outro, Text_loop_anim 14 | from .metadata import CapCut_Text_intro, CapCut_Text_loop_anim, CapCut_Text_outro 15 | 16 | class Animation: 17 | """一个视频/文本动画效果""" 18 | 19 | name: str 20 | """动画名称, 默认取为动画效果的名称""" 21 | effect_id: str 22 | """另一种动画id, 由剪映本身提供""" 23 | animation_type: str 24 | """动画类型, 在子类中定义""" 25 | resource_id: str 26 | """资源id, 由剪映本身提供""" 27 | 28 | start: int 29 | """动画相对此片段开头的偏移, 单位为微秒""" 30 | duration: int 31 | """动画持续时间, 单位为微秒""" 32 | 33 | is_video_animation: bool 34 | """是否为视频动画, 在子类中定义""" 35 | 36 | def __init__(self, animation_meta: Animation_meta, start: int, duration: int): 37 | self.name = animation_meta.title 38 | self.effect_id = animation_meta.effect_id 39 | self.resource_id = animation_meta.resource_id 40 | 41 | self.start = start 42 | self.duration = duration 43 | 44 | def export_json(self) -> Dict[str, Any]: 45 | return { 46 | "anim_adjust_params": None, 47 | "platform": "all", 48 | "panel": "video" if self.is_video_animation else "", 49 | "material_type": "video" if self.is_video_animation else "sticker", 50 | 51 | "name": self.name, 52 | "id": self.effect_id, 53 | "type": self.animation_type, 54 | "resource_id": self.resource_id, 55 | 56 | "start": self.start, 57 | "duration": self.duration, 58 | # 不导出path和request_id 59 | } 60 | 61 | class Video_animation(Animation): 62 | """一个视频动画效果""" 63 | 64 | animation_type: Literal["in", "out", "group"] 65 | 66 | def __init__(self, animation_type: Union[Intro_type, Outro_type, Group_animation_type, CapCut_Intro_type, CapCut_Outro_type, CapCut_Group_animation_type], 67 | start: int, duration: int): 68 | super().__init__(animation_type.value, start, duration) 69 | 70 | if ((isinstance(animation_type, Intro_type) or isinstance(animation_type, CapCut_Intro_type))): 71 | self.animation_type = "in" 72 | elif isinstance(animation_type, Outro_type) or isinstance(animation_type, CapCut_Outro_type): 73 | self.animation_type = "out" 74 | elif isinstance(animation_type, Group_animation_type) or isinstance(animation_type, CapCut_Group_animation_type): 75 | self.animation_type = "group" 76 | 77 | self.is_video_animation = True 78 | 79 | class Text_animation(Animation): 80 | """一个文本动画效果""" 81 | 82 | animation_type: Literal["in", "out", "loop"] 83 | 84 | def __init__(self, animation_type: Union[Text_intro, Text_outro, Text_loop_anim, CapCut_Text_intro, CapCut_Text_outro, CapCut_Text_loop_anim], 85 | start: int, duration: int): 86 | super().__init__(animation_type.value, start, duration) 87 | 88 | if (isinstance(animation_type, Text_intro) or isinstance(animation_type, CapCut_Text_intro)): 89 | self.animation_type = "in" 90 | elif (isinstance(animation_type, Text_outro) or isinstance(animation_type, CapCut_Text_outro)): 91 | self.animation_type = "out" 92 | elif (isinstance(animation_type, Text_loop_anim) or isinstance(animation_type, CapCut_Text_loop_anim)): 93 | self.animation_type = "loop" 94 | 95 | self.is_video_animation = False 96 | 97 | class Segment_animations: 98 | """附加于某素材上的一系列动画 99 | 100 | 对视频片段:入场、出场或组合动画;对文本片段:入场、出场或循环动画""" 101 | 102 | animation_id: str 103 | """系列动画的全局id, 自动生成""" 104 | 105 | animations: List[Animation] 106 | """动画列表""" 107 | 108 | def __init__(self): 109 | self.animation_id = uuid.uuid4().hex 110 | self.animations = [] 111 | 112 | def get_animation_trange(self, animation_type: Literal["in", "out", "group", "loop"]) -> Optional[Timerange]: 113 | """获取指定类型的动画的时间范围""" 114 | for animation in self.animations: 115 | if animation.animation_type == animation_type: 116 | return Timerange(animation.start, animation.duration) 117 | return None 118 | 119 | def add_animation(self, animation: Union[Video_animation, Text_animation]) -> None: 120 | # 不允许添加超过一个同类型的动画(如两个入场动画) 121 | if animation.animation_type in [ani.animation_type for ani in self.animations]: 122 | raise ValueError(f"当前片段已存在类型为 '{animation.animation_type}' 的动画") 123 | 124 | if isinstance(animation, Video_animation): 125 | # 不允许组合动画与出入场动画同时出现 126 | if any(ani.animation_type == "group" for ani in self.animations): 127 | raise ValueError("当前片段已存在组合动画, 此时不能添加其它动画") 128 | if animation.animation_type == "group" and len(self.animations) > 0: 129 | raise ValueError("当前片段已存在动画时, 不能添加组合动画") 130 | elif isinstance(animation, Text_animation): 131 | if any(ani.animation_type == "loop" for ani in self.animations): 132 | raise ValueError("当前片段已存在循环动画, 若希望同时使用循环动画和入出场动画, 请先添加出入场动画再添加循环动画") 133 | 134 | self.animations.append(animation) 135 | 136 | def export_json(self) -> Dict[str, Any]: 137 | return { 138 | "id": self.animation_id, 139 | "type": "sticker_animation", 140 | "multi_language_current": "none", 141 | "animations": [animation.export_json() for animation in self.animations] 142 | } 143 | -------------------------------------------------------------------------------- /add_subtitle_impl.py: -------------------------------------------------------------------------------- 1 | import pyJianYingDraft as draft 2 | from util import generate_draft_url, hex_to_rgb 3 | from create_draft import get_or_create_draft 4 | from pyJianYingDraft.text_segment import TextBubble, TextEffect 5 | from typing import Optional 6 | import requests 7 | import os 8 | 9 | def add_subtitle_impl( 10 | srt_path: str, 11 | draft_id: str = None, 12 | track_name: str = "subtitle", 13 | time_offset: float = 0, 14 | # Font style parameters 15 | font: str = None, 16 | font_size: float = 8.0, 17 | bold: bool = False, 18 | italic: bool = False, 19 | underline: bool = False, 20 | font_color: str = "#FFFFFF", 21 | 22 | # Border parameters 23 | border_alpha: float = 1.0, 24 | border_color: str = "#000000", 25 | border_width: float = 0.0, # Default no border display 26 | 27 | # Background parameters 28 | background_color: str = "#000000", 29 | background_style: int = 1, 30 | background_alpha: float = 0.0, # Default no background display 31 | 32 | # Bubble effect 33 | bubble_effect_id: Optional[str] = None, 34 | bubble_resource_id: Optional[str] = None, 35 | 36 | # Text effect 37 | effect_effect_id: Optional[str] = None, 38 | # Image adjustment parameters 39 | transform_x: float = 0.0, 40 | transform_y: float = -0.8, # Default subtitle position at bottom 41 | scale_x: float = 1.0, 42 | scale_y: float = 1.0, 43 | rotation: float = 0.0, 44 | style_reference: draft.Text_segment = None, 45 | vertical: bool = True, # New parameter: whether to display vertically 46 | alpha: float = 0.4, 47 | width: int = 1080, # New parameter 48 | height: int = 1920 # New parameter 49 | ): 50 | """ 51 | Add subtitles to draft 52 | :param srt_path: Subtitle file path or URL or SRT text content 53 | :param draft_id: Draft ID, if None, create a new draft 54 | :param track_name: Track name, default is "subtitle" 55 | :param time_offset: Time offset, default is "0"s 56 | :param text_style: Text style, default is None 57 | :param clip_settings: Clip settings, default is None 58 | :param style_reference: Style reference, default is None 59 | :return: Draft information 60 | """ 61 | # Get or create draft 62 | draft_id, script = get_or_create_draft( 63 | draft_id=draft_id, 64 | width=width, 65 | height=height 66 | ) 67 | 68 | # Process subtitle content 69 | srt_content = None 70 | 71 | # Check if it's a URL 72 | if srt_path.startswith(('http://', 'https://')): 73 | try: 74 | response = requests.get(srt_path) 75 | response.raise_for_status() 76 | 77 | response.encoding = 'utf-8' 78 | srt_content = response.text 79 | except Exception as e: 80 | raise Exception(f"Failed to download subtitle file: {str(e)}") 81 | elif os.path.isfile(srt_path): # Check if it's a file 82 | try: 83 | with open(srt_path, 'r', encoding='utf-8-sig') as f: 84 | srt_content = f.read() 85 | except Exception as e: 86 | raise Exception(f"Failed to read local subtitle file: {str(e)}") 87 | else: 88 | # If not a URL or local file, use content directly 89 | srt_content = srt_path 90 | # Handle possible escape characters 91 | srt_content = srt_content.replace('\\n', '\n').replace('/n', '\n') 92 | 93 | # Import subtitles 94 | # Convert hexadecimal color to RGB 95 | rgb_color = hex_to_rgb(font_color) 96 | 97 | # Create text_border 98 | text_border = None 99 | if border_width > 0: 100 | text_border = draft.Text_border( 101 | alpha=border_alpha, 102 | color=hex_to_rgb(border_color), 103 | width=border_width 104 | ) 105 | 106 | # Create text_background 107 | text_background = None 108 | if background_alpha > 0: 109 | text_background = draft.Text_background( 110 | color=background_color, 111 | style=background_style, 112 | alpha=background_alpha 113 | ) 114 | 115 | # Create text_style 116 | text_style = draft.Text_style( 117 | size=font_size, 118 | bold=bold, 119 | italic=italic, 120 | underline=underline, 121 | color=rgb_color, 122 | align=1, # Keep center alignment 123 | vertical=vertical, # Use the passed vertical parameter 124 | alpha=alpha # Use the passed alpha parameter 125 | ) 126 | 127 | # Create bubble effect 128 | text_bubble = None 129 | if bubble_effect_id and bubble_resource_id: 130 | text_bubble = TextBubble( 131 | effect_id=bubble_effect_id, 132 | resource_id=bubble_resource_id 133 | ) 134 | 135 | # Create text effect 136 | text_effect = None 137 | if effect_effect_id: 138 | text_effect = TextEffect( 139 | effect_id=effect_effect_id, 140 | resource_id=effect_effect_id 141 | ) 142 | 143 | # Create clip_settings 144 | clip_settings = draft.Clip_settings( 145 | transform_x=transform_x, 146 | transform_y=transform_y, 147 | scale_x=scale_x, 148 | scale_y=scale_y, 149 | rotation=rotation 150 | ) 151 | 152 | script.import_srt( 153 | srt_content, 154 | track_name=track_name, 155 | time_offset=int(time_offset * 1000000), # Convert seconds to microseconds 156 | text_style=text_style, 157 | font=font, 158 | clip_settings=clip_settings, 159 | style_reference=style_reference, 160 | border=text_border, 161 | background=text_background, 162 | bubble=text_bubble, 163 | effect=text_effect 164 | ) 165 | 166 | return { 167 | "draft_id": draft_id, 168 | "draft_url": generate_draft_url(draft_id) 169 | } 170 | -------------------------------------------------------------------------------- /template/draft_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "canvas_config": { 3 | "background": null, 4 | "height": 1080, 5 | "ratio": "original", 6 | "width": 1920 7 | }, 8 | "color_space": -1, 9 | "config": { 10 | "adjust_max_index": 1, 11 | "attachment_info": [ 12 | 13 | ], 14 | "combination_max_index": 1, 15 | "export_range": null, 16 | "extract_audio_last_index": 1, 17 | "lyrics_recognition_id": "", 18 | "lyrics_sync": true, 19 | "lyrics_taskinfo": [ 20 | 21 | ], 22 | "maintrack_adsorb": true, 23 | "material_save_mode": 0, 24 | "multi_language_current": "none", 25 | "multi_language_list": [ 26 | 27 | ], 28 | "multi_language_main": "none", 29 | "multi_language_mode": "none", 30 | "original_sound_last_index": 1, 31 | "record_audio_last_index": 1, 32 | "sticker_max_index": 1, 33 | "subtitle_keywords_config": null, 34 | "subtitle_recognition_id": "", 35 | "subtitle_sync": true, 36 | "subtitle_taskinfo": [ 37 | 38 | ], 39 | "system_font_list": [ 40 | 41 | ], 42 | "use_float_render": false, 43 | "video_mute": false, 44 | "zoom_info_params": null 45 | }, 46 | "cover": null, 47 | "create_time": 0, 48 | "duration": 0, 49 | "extra_info": null, 50 | "fps": 30, 51 | "free_render_index_mode_on": false, 52 | "group_container": null, 53 | "id": "26FDE5CD-364C-45A3-8930-A40DF2F7EB2B", 54 | "is_drop_frame_timecode": false, 55 | "keyframe_graph_list": [ 56 | 57 | ], 58 | "keyframes": { 59 | "adjusts": [ 60 | 61 | ], 62 | "audios": [ 63 | 64 | ], 65 | "effects": [ 66 | 67 | ], 68 | "filters": [ 69 | 70 | ], 71 | "handwrites": [ 72 | 73 | ], 74 | "stickers": [ 75 | 76 | ], 77 | "texts": [ 78 | 79 | ], 80 | "videos": [ 81 | 82 | ] 83 | }, 84 | "last_modified_platform": { 85 | "app_id": 359289, 86 | "app_source": "cc", 87 | "app_version": "6.5.0", 88 | "device_id": "c4ca4238a0b923820dcc509a6f75849b", 89 | "hard_disk_id": "307563e0192a94465c0e927fbc482942", 90 | "mac_address": "c3371f2d4fb02791c067ce44d8fb4ed5", 91 | "os": "mac", 92 | "os_version": "15.5" 93 | }, 94 | "lyrics_effects": [ 95 | 96 | ], 97 | "materials": { 98 | "ai_translates": [ 99 | 100 | ], 101 | "audio_balances": [ 102 | 103 | ], 104 | "audio_effects": [ 105 | 106 | ], 107 | "audio_fades": [ 108 | 109 | ], 110 | "audio_track_indexes": [ 111 | 112 | ], 113 | "audios": [ 114 | 115 | ], 116 | "beats": [ 117 | 118 | ], 119 | "canvases": [ 120 | 121 | ], 122 | "chromas": [ 123 | 124 | ], 125 | "color_curves": [ 126 | 127 | ], 128 | "common_mask": [ 129 | 130 | ], 131 | "digital_human_model_dressing": [ 132 | 133 | ], 134 | "digital_humans": [ 135 | 136 | ], 137 | "drafts": [ 138 | 139 | ], 140 | "effects": [ 141 | 142 | ], 143 | "flowers": [ 144 | 145 | ], 146 | "green_screens": [ 147 | 148 | ], 149 | "handwrites": [ 150 | 151 | ], 152 | "hsl": [ 153 | 154 | ], 155 | "images": [ 156 | 157 | ], 158 | "log_color_wheels": [ 159 | 160 | ], 161 | "loudnesses": [ 162 | 163 | ], 164 | "manual_beautys": [ 165 | 166 | ], 167 | "manual_deformations": [ 168 | 169 | ], 170 | "material_animations": [ 171 | 172 | ], 173 | "material_colors": [ 174 | 175 | ], 176 | "multi_language_refs": [ 177 | 178 | ], 179 | "placeholder_infos": [ 180 | 181 | ], 182 | "placeholders": [ 183 | 184 | ], 185 | "plugin_effects": [ 186 | 187 | ], 188 | "primary_color_wheels": [ 189 | 190 | ], 191 | "realtime_denoises": [ 192 | 193 | ], 194 | "shapes": [ 195 | 196 | ], 197 | "smart_crops": [ 198 | 199 | ], 200 | "smart_relights": [ 201 | 202 | ], 203 | "sound_channel_mappings": [ 204 | 205 | ], 206 | "speeds": [ 207 | 208 | ], 209 | "stickers": [ 210 | 211 | ], 212 | "tail_leaders": [ 213 | 214 | ], 215 | "text_templates": [ 216 | 217 | ], 218 | "texts": [ 219 | 220 | ], 221 | "time_marks": [ 222 | 223 | ], 224 | "transitions": [ 225 | 226 | ], 227 | "video_effects": [ 228 | 229 | ], 230 | "video_trackings": [ 231 | 232 | ], 233 | "videos": [ 234 | 235 | ], 236 | "vocal_beautifys": [ 237 | 238 | ], 239 | "vocal_separations": [ 240 | 241 | ] 242 | }, 243 | "mutable_config": null, 244 | "name": "", 245 | "new_version": "138.0.0", 246 | "path": "", 247 | "platform": { 248 | "app_id": 359289, 249 | "app_source": "cc", 250 | "app_version": "6.5.0", 251 | "device_id": "c4ca4238a0b923820dcc509a6f75849b", 252 | "hard_disk_id": "307563e0192a94465c0e927fbc482942", 253 | "mac_address": "c3371f2d4fb02791c067ce44d8fb4ed5", 254 | "os": "mac", 255 | "os_version": "15.5" 256 | }, 257 | "relationships": [ 258 | 259 | ], 260 | "render_index_track_mode_on": true, 261 | "retouch_cover": null, 262 | "source": "default", 263 | "static_cover_image_path": "", 264 | "time_marks": null, 265 | "tracks": [ 266 | 267 | ], 268 | "uneven_animation_template_info": { 269 | "composition": "", 270 | "content": "", 271 | "order": "", 272 | "sub_template_info_list": [ 273 | 274 | ] 275 | }, 276 | "update_time": 0, 277 | "version": 360000 278 | } -------------------------------------------------------------------------------- /template/draft_info.json.bak: -------------------------------------------------------------------------------- 1 | { 2 | "canvas_config": { 3 | "background": null, 4 | "height": 1080, 5 | "ratio": "original", 6 | "width": 1920 7 | }, 8 | "color_space": -1, 9 | "config": { 10 | "adjust_max_index": 1, 11 | "attachment_info": [ 12 | 13 | ], 14 | "combination_max_index": 1, 15 | "export_range": null, 16 | "extract_audio_last_index": 1, 17 | "lyrics_recognition_id": "", 18 | "lyrics_sync": true, 19 | "lyrics_taskinfo": [ 20 | 21 | ], 22 | "maintrack_adsorb": true, 23 | "material_save_mode": 0, 24 | "multi_language_current": "none", 25 | "multi_language_list": [ 26 | 27 | ], 28 | "multi_language_main": "none", 29 | "multi_language_mode": "none", 30 | "original_sound_last_index": 1, 31 | "record_audio_last_index": 1, 32 | "sticker_max_index": 1, 33 | "subtitle_keywords_config": null, 34 | "subtitle_recognition_id": "", 35 | "subtitle_sync": true, 36 | "subtitle_taskinfo": [ 37 | 38 | ], 39 | "system_font_list": [ 40 | 41 | ], 42 | "use_float_render": false, 43 | "video_mute": false, 44 | "zoom_info_params": null 45 | }, 46 | "cover": null, 47 | "create_time": 0, 48 | "duration": 0, 49 | "extra_info": null, 50 | "fps": 30, 51 | "free_render_index_mode_on": false, 52 | "group_container": null, 53 | "id": "26FDE5CD-364C-45A3-8930-A40DF2F7EB2B", 54 | "is_drop_frame_timecode": false, 55 | "keyframe_graph_list": [ 56 | 57 | ], 58 | "keyframes": { 59 | "adjusts": [ 60 | 61 | ], 62 | "audios": [ 63 | 64 | ], 65 | "effects": [ 66 | 67 | ], 68 | "filters": [ 69 | 70 | ], 71 | "handwrites": [ 72 | 73 | ], 74 | "stickers": [ 75 | 76 | ], 77 | "texts": [ 78 | 79 | ], 80 | "videos": [ 81 | 82 | ] 83 | }, 84 | "last_modified_platform": { 85 | "app_id": 359289, 86 | "app_source": "cc", 87 | "app_version": "6.5.0", 88 | "device_id": "c4ca4238a0b923820dcc509a6f75849b", 89 | "hard_disk_id": "307563e0192a94465c0e927fbc482942", 90 | "mac_address": "c3371f2d4fb02791c067ce44d8fb4ed5", 91 | "os": "mac", 92 | "os_version": "15.5" 93 | }, 94 | "lyrics_effects": [ 95 | 96 | ], 97 | "materials": { 98 | "ai_translates": [ 99 | 100 | ], 101 | "audio_balances": [ 102 | 103 | ], 104 | "audio_effects": [ 105 | 106 | ], 107 | "audio_fades": [ 108 | 109 | ], 110 | "audio_track_indexes": [ 111 | 112 | ], 113 | "audios": [ 114 | 115 | ], 116 | "beats": [ 117 | 118 | ], 119 | "canvases": [ 120 | 121 | ], 122 | "chromas": [ 123 | 124 | ], 125 | "color_curves": [ 126 | 127 | ], 128 | "common_mask": [ 129 | 130 | ], 131 | "digital_human_model_dressing": [ 132 | 133 | ], 134 | "digital_humans": [ 135 | 136 | ], 137 | "drafts": [ 138 | 139 | ], 140 | "effects": [ 141 | 142 | ], 143 | "flowers": [ 144 | 145 | ], 146 | "green_screens": [ 147 | 148 | ], 149 | "handwrites": [ 150 | 151 | ], 152 | "hsl": [ 153 | 154 | ], 155 | "images": [ 156 | 157 | ], 158 | "log_color_wheels": [ 159 | 160 | ], 161 | "loudnesses": [ 162 | 163 | ], 164 | "manual_beautys": [ 165 | 166 | ], 167 | "manual_deformations": [ 168 | 169 | ], 170 | "material_animations": [ 171 | 172 | ], 173 | "material_colors": [ 174 | 175 | ], 176 | "multi_language_refs": [ 177 | 178 | ], 179 | "placeholder_infos": [ 180 | 181 | ], 182 | "placeholders": [ 183 | 184 | ], 185 | "plugin_effects": [ 186 | 187 | ], 188 | "primary_color_wheels": [ 189 | 190 | ], 191 | "realtime_denoises": [ 192 | 193 | ], 194 | "shapes": [ 195 | 196 | ], 197 | "smart_crops": [ 198 | 199 | ], 200 | "smart_relights": [ 201 | 202 | ], 203 | "sound_channel_mappings": [ 204 | 205 | ], 206 | "speeds": [ 207 | 208 | ], 209 | "stickers": [ 210 | 211 | ], 212 | "tail_leaders": [ 213 | 214 | ], 215 | "text_templates": [ 216 | 217 | ], 218 | "texts": [ 219 | 220 | ], 221 | "time_marks": [ 222 | 223 | ], 224 | "transitions": [ 225 | 226 | ], 227 | "video_effects": [ 228 | 229 | ], 230 | "video_trackings": [ 231 | 232 | ], 233 | "videos": [ 234 | 235 | ], 236 | "vocal_beautifys": [ 237 | 238 | ], 239 | "vocal_separations": [ 240 | 241 | ] 242 | }, 243 | "mutable_config": null, 244 | "name": "", 245 | "new_version": "138.0.0", 246 | "path": "", 247 | "platform": { 248 | "app_id": 359289, 249 | "app_source": "cc", 250 | "app_version": "6.5.0", 251 | "device_id": "c4ca4238a0b923820dcc509a6f75849b", 252 | "hard_disk_id": "307563e0192a94465c0e927fbc482942", 253 | "mac_address": "c3371f2d4fb02791c067ce44d8fb4ed5", 254 | "os": "mac", 255 | "os_version": "15.5" 256 | }, 257 | "relationships": [ 258 | 259 | ], 260 | "render_index_track_mode_on": true, 261 | "retouch_cover": null, 262 | "source": "default", 263 | "static_cover_image_path": "", 264 | "time_marks": null, 265 | "tracks": [ 266 | 267 | ], 268 | "uneven_animation_template_info": { 269 | "composition": "", 270 | "content": "", 271 | "order": "", 272 | "sub_template_info_list": [ 273 | 274 | ] 275 | }, 276 | "update_time": 0, 277 | "version": 360000 278 | } -------------------------------------------------------------------------------- /MCP_Documentation_English.md: -------------------------------------------------------------------------------- 1 | # CapCut API MCP Server Documentation 2 | 3 | ## Overview 4 | 5 | The CapCut API MCP Server is a video editing service based on the Model Context Protocol (MCP), providing complete CapCut video editing functionality interfaces. Through the MCP protocol, you can easily integrate professional-grade video editing capabilities into various applications. 6 | 7 | ## Features 8 | 9 | ### 🎬 Core Capabilities 10 | - **Draft Management**: Create, save, and manage video projects 11 | - **Multimedia Support**: Video, audio, image, and text processing 12 | - **Advanced Effects**: Effects, animations, transitions, and filters 13 | - **Precise Control**: Timeline, keyframes, and layer management 14 | 15 | ### 🛠️ Available Tools (11 Tools) 16 | 17 | | Tool Name | Description | Key Parameters | 18 | |-----------|-------------|----------------| 19 | | `create_draft` | Create new video draft project | width, height | 20 | | `add_text` | Add text elements | text, font_size, color, shadow, background | 21 | | `add_video` | Add video track | video_url, start, end, transform, volume | 22 | | `add_audio` | Add audio track | audio_url, volume, speed, effects | 23 | | `add_image` | Add image assets | image_url, transform, animation, transition | 24 | | `add_subtitle` | Add subtitle files | srt_path, font_style, position | 25 | | `add_effect` | Add visual effects | effect_type, parameters, duration | 26 | | `add_sticker` | Add sticker elements | resource_id, position, scale, rotation | 27 | | `add_video_keyframe` | Add keyframe animations | property_types, times, values | 28 | | `get_video_duration` | Get video duration | video_url | 29 | | `save_draft` | Save draft project | draft_id | 30 | 31 | ## Installation & Setup 32 | 33 | ### Requirements 34 | - Python 3.10+ 35 | - CapCut Application (macOS/Windows) 36 | - MCP Client Support 37 | 38 | ### Dependencies Installation 39 | ```bash 40 | # Create virtual environment 41 | python3.10 -m venv venv-mcp 42 | source venv-mcp/bin/activate # macOS/Linux 43 | # or venv-mcp\Scripts\activate # Windows 44 | 45 | # Install dependencies 46 | pip install -r requirements-mcp.txt 47 | ``` 48 | 49 | ### MCP Configuration 50 | Create or update `mcp_config.json` file: 51 | 52 | ```json 53 | { 54 | "mcpServers": { 55 | "capcut-api": { 56 | "command": "python3.10", 57 | "args": ["mcp_server.py"], 58 | "cwd": "/path/to/CapCutAPI-dev", 59 | "env": { 60 | "PYTHONPATH": "/path/to/CapCutAPI-dev" 61 | } 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | ## Usage Guide 68 | 69 | ### Basic Workflow 70 | 71 | #### 1. Create Draft 72 | ```python 73 | # Create 1080x1920 portrait project 74 | result = mcp_client.call_tool("create_draft", { 75 | "width": 1080, 76 | "height": 1920 77 | }) 78 | draft_id = result["draft_id"] 79 | ``` 80 | 81 | #### 2. Add Content 82 | ```python 83 | # Add title text 84 | mcp_client.call_tool("add_text", { 85 | "text": "My Video Title", 86 | "start": 0, 87 | "end": 5, 88 | "draft_id": draft_id, 89 | "font_size": 48, 90 | "font_color": "#FFFFFF" 91 | }) 92 | 93 | # Add background video 94 | mcp_client.call_tool("add_video", { 95 | "video_url": "https://example.com/video.mp4", 96 | "draft_id": draft_id, 97 | "start": 0, 98 | "end": 10, 99 | "volume": 0.8 100 | }) 101 | ``` 102 | 103 | #### 3. Save Project 104 | ```python 105 | # Save draft 106 | result = mcp_client.call_tool("save_draft", { 107 | "draft_id": draft_id 108 | }) 109 | ``` 110 | 111 | ### Advanced Features 112 | 113 | #### Text Styling 114 | ```python 115 | # Text with shadow and background 116 | mcp_client.call_tool("add_text", { 117 | "text": "Advanced Text Effects", 118 | "draft_id": draft_id, 119 | "font_size": 56, 120 | "font_color": "#FFD700", 121 | "shadow_enabled": True, 122 | "shadow_color": "#000000", 123 | "shadow_alpha": 0.8, 124 | "background_color": "#1E1E1E", 125 | "background_alpha": 0.7, 126 | "background_round_radius": 15 127 | }) 128 | ``` 129 | 130 | #### Keyframe Animation 131 | ```python 132 | # Scale and opacity animation 133 | mcp_client.call_tool("add_video_keyframe", { 134 | "draft_id": draft_id, 135 | "track_name": "video_main", 136 | "property_types": ["scale_x", "scale_y", "alpha"], 137 | "times": [0, 2, 4], 138 | "values": ["1.0", "1.5", "0.5"] 139 | }) 140 | ``` 141 | 142 | #### Multi-Style Text 143 | ```python 144 | # Different colored text segments 145 | mcp_client.call_tool("add_text", { 146 | "text": "Colorful Text Effect", 147 | "draft_id": draft_id, 148 | "text_styles": [ 149 | {"start": 0, "end": 2, "font_color": "#FF0000"}, 150 | {"start": 2, "end": 4, "font_color": "#00FF00"} 151 | ] 152 | }) 153 | ``` 154 | 155 | ## Testing & Validation 156 | 157 | ### Using Test Client 158 | ```bash 159 | # Run test client 160 | python test_mcp_client.py 161 | ``` 162 | 163 | ### Functionality Checklist 164 | - [ ] Server starts successfully 165 | - [ ] Tool list retrieval works 166 | - [ ] Draft creation functionality 167 | - [ ] Text addition functionality 168 | - [ ] Video/audio/image addition 169 | - [ ] Effects and animation functionality 170 | - [ ] Draft saving functionality 171 | 172 | ## Troubleshooting 173 | 174 | ### Common Issues 175 | 176 | #### 1. "CapCut modules not available" 177 | **Solution**: 178 | - Confirm CapCut application is installed 179 | - Check Python path configuration 180 | - Verify dependency package installation 181 | 182 | #### 2. Server startup failure 183 | **Solution**: 184 | - Check virtual environment activation 185 | - Verify configuration file paths 186 | - Review error logs 187 | 188 | #### 3. Tool call errors 189 | **Solution**: 190 | - Check parameter format 191 | - Verify media file URLs 192 | - Confirm time range settings 193 | 194 | ### Debug Mode 195 | ```bash 196 | # Enable verbose logging 197 | export DEBUG=1 198 | python mcp_server.py 199 | ``` 200 | 201 | ## Best Practices 202 | 203 | ### Performance Optimization 204 | 1. **Media Files**: Use compressed formats, avoid oversized files 205 | 2. **Time Management**: Plan element timelines reasonably, avoid overlaps 206 | 3. **Memory Usage**: Save drafts promptly, clean temporary files 207 | 208 | ### Error Handling 209 | 1. **Parameter Validation**: Check required parameters before calling 210 | 2. **Exception Catching**: Handle network and file errors 211 | 3. **Retry Mechanism**: Retry on temporary failures 212 | 213 | ## API Reference 214 | 215 | ### Common Parameters 216 | - `draft_id`: Unique draft identifier 217 | - `start/end`: Time range (seconds) 218 | - `width/height`: Project dimensions 219 | - `transform_x/y`: Position coordinates 220 | - `scale_x/y`: Scale ratios 221 | 222 | ### Response Format 223 | ```json 224 | { 225 | "success": true, 226 | "result": { 227 | "draft_id": "dfd_cat_xxx", 228 | "draft_url": "https://..." 229 | }, 230 | "features_used": { 231 | "shadow": false, 232 | "background": false, 233 | "multi_style": false 234 | } 235 | } 236 | ``` 237 | 238 | ## Changelog 239 | 240 | ### v1.0.0 241 | - Initial release 242 | - Support for 11 core tools 243 | - Complete MCP protocol implementation 244 | 245 | ## Technical Support 246 | 247 | For questions or suggestions, please contact us through: 248 | - GitHub Issues 249 | - Technical Documentation 250 | - Community Forums 251 | 252 | --- 253 | 254 | *This documentation is continuously updated. Please follow the latest version.* -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # 通过VectCutAPI连接AI生成的一切 [在线体验](https://www.vectcut.com) 2 | 3 |