From 4dde4c0f51a171b95cedab5398e98e53cd933111 Mon Sep 17 00:00:00 2001 From: pygent-ai <260234539+pygent-ai@users.noreply.github.com> Date: Sat, 30 May 2026 15:17:06 +0800 Subject: [PATCH] Add Jianying Pro 10.x draft profile Introduce draft profiles so CapCut, legacy Jianying, and Jianying Pro 10.x can select separate template directories and output filenames. Add a Jianying Pro 10.x Windows template layout that writes draft_content.json, template mirrors, and timeline template content instead of the legacy draft_info.json file. Centralize draft asset path generation to preserve Windows drive roots and add tests for profile selection, mirrored content writes, and Windows paths. --- README.md | 6 + add_audio_track.py | 15 +- add_image_impl.py | 17 +- add_video_track.py | 17 +- config.json.example | 5 +- draft_profiles.py | 174 ++++++++++++++++++ pyJianYingDraft/draft_folder.py | 3 +- pyJianYingDraft/script_file.py | 40 ++-- save_draft_impl.py | 58 +++--- settings/__init__.py | 3 +- settings/local.py | 23 ++- template_jianying_10_2/Resources/.gitkeep | 1 + .../attachment_editing.json | 1 + .../attachment_pc_common.json | 1 + .../attachment_pc_timeline.json | 1 + .../attachment_script_video.json | 1 + .../draft_content.json | 5 + .../draft_content.json.bak | 5 + .../template-2.tmp | 5 + .../template.tmp | 5 + template_jianying_10_2/Timelines/project.json | 1 + .../Timelines/project.json.bak | 1 + template_jianying_10_2/adjust_mask/.gitkeep | 1 + .../attachment_pc_common.json | 1 + .../attachment_pc_timeline.json | 1 + .../draft_agency_config.json | 1 + template_jianying_10_2/draft_biz_config.json | 1 + template_jianying_10_2/draft_content.json | 124 +++++++++++++ template_jianying_10_2/draft_content.json.bak | 5 + template_jianying_10_2/draft_meta_info.json | 1 + template_jianying_10_2/draft_settings | 6 + .../draft_virtual_store.json | 1 + template_jianying_10_2/key_value.json | 1 + template_jianying_10_2/matting/.gitkeep | 1 + .../performance_opt_info.json | 1 + template_jianying_10_2/qr_upload/.gitkeep | 1 + template_jianying_10_2/smart_crop/.gitkeep | 1 + template_jianying_10_2/subdraft/.gitkeep | 1 + template_jianying_10_2/template-2.tmp | 5 + template_jianying_10_2/template.tmp | 5 + template_jianying_10_2/timeline_layout.json | 1 + tests/conftest.py | 7 + tests/test_draft_profiles.py | 135 ++++++++++++++ util.py | 14 +- 44 files changed, 596 insertions(+), 107 deletions(-) create mode 100644 draft_profiles.py create mode 100644 template_jianying_10_2/Resources/.gitkeep create mode 100644 template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/attachment_editing.json create mode 100644 template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/attachment_pc_common.json create mode 100644 template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/common_attachment/attachment_pc_timeline.json create mode 100644 template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/common_attachment/attachment_script_video.json create mode 100644 template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/draft_content.json create mode 100644 template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/draft_content.json.bak create mode 100644 template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/template-2.tmp create mode 100644 template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/template.tmp create mode 100644 template_jianying_10_2/Timelines/project.json create mode 100644 template_jianying_10_2/Timelines/project.json.bak create mode 100644 template_jianying_10_2/adjust_mask/.gitkeep create mode 100644 template_jianying_10_2/attachment_pc_common.json create mode 100644 template_jianying_10_2/common_attachment/attachment_pc_timeline.json create mode 100644 template_jianying_10_2/draft_agency_config.json create mode 100644 template_jianying_10_2/draft_biz_config.json create mode 100644 template_jianying_10_2/draft_content.json create mode 100644 template_jianying_10_2/draft_content.json.bak create mode 100644 template_jianying_10_2/draft_meta_info.json create mode 100644 template_jianying_10_2/draft_settings create mode 100644 template_jianying_10_2/draft_virtual_store.json create mode 100644 template_jianying_10_2/key_value.json create mode 100644 template_jianying_10_2/matting/.gitkeep create mode 100644 template_jianying_10_2/performance_opt_info.json create mode 100644 template_jianying_10_2/qr_upload/.gitkeep create mode 100644 template_jianying_10_2/smart_crop/.gitkeep create mode 100644 template_jianying_10_2/subdraft/.gitkeep create mode 100644 template_jianying_10_2/template-2.tmp create mode 100644 template_jianying_10_2/template.tmp create mode 100644 template_jianying_10_2/timeline_layout.json create mode 100644 tests/conftest.py create mode 100644 tests/test_draft_profiles.py diff --git a/README.md b/README.md index 1902078..e6a0dda 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,12 @@ mcp_client.call_tool("add_text", { Calling `save_draft` will generate a folder starting with `dfd_` in the current directory of `capcut_server.py`. Copy this to the CapCut/Jianying drafts directory to see the generated draft in the application. +Draft output is selected by `draft_profile` in `config.json`: + +- `capcut_legacy`: existing CapCut template. +- `jianying_legacy`: existing Jianying template. +- `jianying_pro_10`: Jianying Pro 10.x Windows-style folder layout using `draft_content.json`. + ## Pattern You can find a lot of pattern in the `pattern` directory. diff --git a/add_audio_track.py b/add_audio_track.py index b79a57a..92dd41a 100644 --- a/add_audio_track.py +++ b/add_audio_track.py @@ -1,9 +1,7 @@ # 导入必要的模块 -import os import pyJianYingDraft as draft import time -from util import generate_draft_url, is_windows_path, url_to_hash -import re +from util import generate_draft_url, url_to_hash, build_draft_asset_path from typing import Optional, Dict, Tuple, List from pyJianYingDraft import exceptions, Audio_scene_effect_type, Tone_effect_type, Speech_to_song_type, CapCut_Voice_filters_effect_type,CapCut_Voice_characters_effect_type,CapCut_Speech_to_song_effect_type, trange from create_draft import get_or_create_draft @@ -81,16 +79,7 @@ def add_audio_track( # Build draft_audio_path draft_audio_path = None if draft_folder: - if is_windows_path(draft_folder): - # Windows path processing - windows_drive, windows_path = re.match(r'([a-zA-Z]:)(.*)', draft_folder).groups() - parts = [p for p in windows_path.split('\\') if p] - draft_audio_path = os.path.join(windows_drive, *parts, draft_id, "assets", "audio", material_name) - # Normalize path (ensure consistent separators) - draft_audio_path = draft_audio_path.replace('/', '\\') - else: - # macOS/Linux path processing - draft_audio_path = os.path.join(draft_folder, draft_id, "assets", "audio", material_name) + draft_audio_path = build_draft_asset_path(draft_folder, draft_id, "audio", material_name) # Set default value for audio end time audio_end = end if end is not None else audio_duration diff --git a/add_image_impl.py b/add_image_impl.py index 342ee68..299eb60 100644 --- a/add_image_impl.py +++ b/add_image_impl.py @@ -1,11 +1,9 @@ -import os import uuid import pyJianYingDraft as draft import time from settings.local import IS_CAPCUT_ENV -from util import generate_draft_url, is_windows_path, url_to_hash +from util import generate_draft_url, url_to_hash, build_draft_asset_path from pyJianYingDraft import trange, Clip_settings -import re from typing import Optional, Dict from pyJianYingDraft import exceptions from create_draft import get_or_create_draft @@ -116,18 +114,7 @@ def add_image_impl( # Build draft_image_path draft_image_path = None if draft_folder: - # Detect input path type and process - if is_windows_path(draft_folder): - # Windows path processing - windows_drive, windows_path = re.match(r'([a-zA-Z]:)(.*)', draft_folder).groups() - parts = [p for p in windows_path.split('\\') if p] # Split path and filter empty parts - draft_image_path = os.path.join(windows_drive, *parts, draft_id, "assets", "image", material_name) - # Normalize path (ensure consistent separators) - draft_image_path = draft_image_path.replace('/', '\\') - else: - # macOS/Linux path processing - draft_image_path = os.path.join(draft_folder, draft_id, "assets", "image", material_name) - + draft_image_path = build_draft_asset_path(draft_folder, draft_id, "image", material_name) # Print path information print('replace_path:', draft_image_path) diff --git a/add_video_track.py b/add_video_track.py index 46ebc62..274f617 100644 --- a/add_video_track.py +++ b/add_video_track.py @@ -1,10 +1,8 @@ -import os import pyJianYingDraft as draft import time from settings.local import IS_CAPCUT_ENV -from util import generate_draft_url, is_windows_path, url_to_hash +from util import generate_draft_url, url_to_hash, build_draft_asset_path from pyJianYingDraft import trange, Clip_settings -import re from typing import Optional, Dict from pyJianYingDraft import exceptions from create_draft import get_or_create_draft @@ -125,18 +123,7 @@ def add_video_track( # Build draft_video_path draft_video_path = None if draft_folder: - # Detect input path type and process - if is_windows_path(draft_folder): - # Windows path processing - windows_drive, windows_path = re.match(r'([a-zA-Z]:)(.*)', draft_folder).groups() - parts = [p for p in windows_path.split('\\') if p] # Split path and filter empty parts - draft_video_path = os.path.join(windows_drive, *parts, draft_id, "assets", "video", material_name) - # Normalize path (ensure consistent separators) - draft_video_path = draft_video_path.replace('/', '\\') - else: - # macOS/Linux path processing - draft_video_path = os.path.join(draft_folder, draft_id, "assets", "video", material_name) - + draft_video_path = build_draft_asset_path(draft_folder, draft_id, "video", material_name) # Print path information print('replace_path:', draft_video_path) diff --git a/config.json.example b/config.json.example index 2c156ee..0db2d45 100644 --- a/config.json.example +++ b/config.json.example @@ -1,5 +1,6 @@ { - "is_capcut_env": true, // Whether to use CapCut environment (true) or JianYing environment (false) + "draft_profile": "capcut_legacy", // capcut_legacy, jianying_legacy, or jianying_pro_10 for Jianying Pro 10.x + "is_capcut_env": true, // Backward-compatible fallback. Ignored when draft_profile is set. "draft_domain": "https://www.capcutapi.top", // Base domain for draft operations "port": 9001, // Port number for the local server "preview_router": "/draft/downloader", // Router path for preview functionality @@ -17,4 +18,4 @@ "region": "your-region", // Region for MP4 storage "endpoint": "http://your-custom-domain" // Custom domain endpoint for MP4 access } -} \ No newline at end of file +} diff --git a/draft_profiles.py b/draft_profiles.py new file mode 100644 index 0000000..acda744 --- /dev/null +++ b/draft_profiles.py @@ -0,0 +1,174 @@ +import json +import os +import shutil +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, Iterable, List, Optional + + +@dataclass(frozen=True) +class DraftProfile: + name: str + template_dir: str + content_file: str + content_mirrors: tuple[str, ...] = () + timeline_content_file: Optional[str] = None + is_capcut_env: bool = True + platform: Optional[Dict[str, object]] = None + + +CAPCUT_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", +} + +JIANYING_10_PLATFORM = { + "app_id": 3704, + "app_source": "lv", + "app_version": "10.2.0", + "os": "windows", +} + +PROFILES: Dict[str, DraftProfile] = { + "capcut_legacy": DraftProfile( + name="capcut_legacy", + template_dir="template", + content_file="draft_info.json", + is_capcut_env=True, + platform=CAPCUT_PLATFORM, + ), + "jianying_legacy": DraftProfile( + name="jianying_legacy", + template_dir="template_jianying", + content_file="draft_info.json", + is_capcut_env=False, + platform=JIANYING_10_PLATFORM, + ), + "jianying_pro_10": DraftProfile( + name="jianying_pro_10", + template_dir="template_jianying_10_2", + content_file="draft_content.json", + content_mirrors=("draft_content.json.bak", "template-2.tmp"), + timeline_content_file="template.tmp", + is_capcut_env=False, + platform=JIANYING_10_PLATFORM, + ), +} + +PROFILE_ALIASES = { + "capcut": "capcut_legacy", + "capcut_legacy": "capcut_legacy", + "jianying": "jianying_legacy", + "jianying_legacy": "jianying_legacy", + "jianying_10": "jianying_pro_10", + "jianying_10_x": "jianying_pro_10", + "jianying_pro_10": "jianying_pro_10", + "jianying_pro_10_2": "jianying_pro_10", + "jianying_pro_10_2_0": "jianying_pro_10", +} + + +def normalize_profile_name(name: Optional[str]) -> str: + if not name: + return "capcut_legacy" + key = name.strip().lower().replace(".", "_").replace("-", "_") + if key not in PROFILE_ALIASES: + raise ValueError( + f"Unknown draft profile '{name}'. Supported profiles: {', '.join(sorted(PROFILE_ALIASES))}" + ) + return PROFILE_ALIASES[key] + + +def get_draft_profile(name: Optional[str] = None) -> DraftProfile: + if name is None: + try: + from settings.local import DRAFT_PROFILE + except Exception: + DRAFT_PROFILE = "capcut_legacy" + name = DRAFT_PROFILE + return PROFILES[normalize_profile_name(name)] + + +def get_template_dir(name: Optional[str] = None) -> str: + return get_draft_profile(name).template_dir + + +def write_profile_content(profile: DraftProfile, draft_dir: os.PathLike, content: str) -> List[Path]: + draft_path = Path(draft_dir) + written: List[Path] = [] + content_data = json.loads(content) + + targets = [profile.content_file, *profile.content_mirrors] + for relative_path in targets: + path = draft_path / relative_path + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + written.append(path) + + if profile.timeline_content_file: + timelines_dir = draft_path / "Timelines" + if timelines_dir.exists(): + timeline_dirs = [path for path in timelines_dir.iterdir() if path.is_dir()] + timeline_id = content_data.get("id") + if timeline_id and timeline_dirs: + timeline_dir = timeline_dirs[0] + desired_timeline_dir = timelines_dir / timeline_id + if timeline_dir != desired_timeline_dir: + if desired_timeline_dir.exists(): + shutil.rmtree(desired_timeline_dir) + timeline_dir.rename(desired_timeline_dir) + timeline_dirs = [desired_timeline_dir] + + for timeline_dir in timeline_dirs: + timeline_targets = set(targets) + timeline_targets.add(profile.timeline_content_file) + for relative_path in timeline_targets: + path = timeline_dir / relative_path + path.write_text(content, encoding="utf-8") + written.append(path) + + if timeline_id: + now_us = int(time.time() * 1_000_000) + project = { + "config": { + "color_space": -1, + "render_index_track_mode_on": False, + "use_float_render": False, + }, + "create_time": now_us, + "id": timeline_id, + "main_timeline_id": timeline_id, + "timelines": [ + { + "create_time": now_us, + "id": timeline_id, + "is_marked_delete": False, + "name": "时间线01", + "update_time": now_us, + } + ], + "update_time": now_us, + "version": 0, + } + for relative_path in ("project.json", "project.json.bak"): + path = timelines_dir / relative_path + path.write_text(json.dumps(project, ensure_ascii=False, separators=(",", ":")), encoding="utf-8") + written.append(path) + + layout_path = draft_path / "timeline_layout.json" + if layout_path.exists(): + layout = json.loads(layout_path.read_text(encoding="utf-8")) + for item in layout.get("dockItems", []): + item["timelineIds"] = [timeline_id] + item["timelineNames"] = ["时间线01"] + layout_path.write_text(json.dumps(layout, ensure_ascii=False, separators=(",", ":")), encoding="utf-8") + written.append(layout_path) + + return written diff --git a/pyJianYingDraft/draft_folder.py b/pyJianYingDraft/draft_folder.py index a77aa86..97fac35 100644 --- a/pyJianYingDraft/draft_folder.py +++ b/pyJianYingDraft/draft_folder.py @@ -6,6 +6,7 @@ from typing import List from .script_file import Script_file +from draft_profiles import get_draft_profile class Draft_folder: """管理一个文件夹及其内的一系列草稿""" @@ -81,7 +82,7 @@ def load_template(self, draft_name: str) -> Script_file: if not os.path.exists(draft_path): raise FileNotFoundError(f"草稿文件夹 {draft_name} 不存在") - return Script_file.load_template(os.path.join(draft_path, "draft_info.json")) + return Script_file.load_template(os.path.join(draft_path, get_draft_profile().content_file)) def duplicate_as_template(self, template_name: str, new_draft_name: str, allow_replace: bool = False) -> Script_file: """复制一份给定的草稿, 并在复制出的新草稿上进行编辑 diff --git a/pyJianYingDraft/script_file.py b/pyJianYingDraft/script_file.py index 5d082b6..c6d590d 100644 --- a/pyJianYingDraft/script_file.py +++ b/pyJianYingDraft/script_file.py @@ -20,6 +20,7 @@ from .track import Track_type, Base_track, Track from settings.local import IS_CAPCUT_ENV +from draft_profiles import get_draft_profile from .metadata import Video_scene_effect_type, Video_character_effect_type, Filter_type, Font_type class Script_material: @@ -98,7 +99,7 @@ def __contains__(self, item) -> bool: else: raise TypeError("Invalid argument type '%s'" % type(item)) - def export_json(self) -> Dict[str, List[Any]]: + def export_json(self, is_capcut_env: Optional[bool] = None) -> Dict[str, List[Any]]: result = { "ai_translates": [], "audio_balances": [], @@ -146,8 +147,10 @@ def export_json(self) -> Dict[str, List[Any]]: "vocal_separations": [] } - # 根据IS_CAPCUT_ENV决定使用common_mask还是masks - if IS_CAPCUT_ENV: + # 根据当前草稿 profile 决定使用 common_mask 还是 masks + if is_capcut_env is None: + is_capcut_env = get_draft_profile().is_capcut_env + if is_capcut_env: result["common_mask"] = self.masks else: result["masks"] = self.masks @@ -876,34 +879,19 @@ def inspect_material(self) -> None: if effect["type"] == "text_effect": print("\tResource id: %s '%s'" % (effect["resource_id"], effect.get("name", ""))) - def dumps(self) -> str: + def dumps(self, profile=None) -> str: """将草稿文件内容导出为JSON字符串""" + if profile is None: + profile = get_draft_profile() self.content["fps"] = self.fps self.content["duration"] = self.duration self.content["canvas_config"] = {"width": self.width, "height": self.height, "ratio": "original"} - self.content["materials"] = self.materials.export_json() - - self.content["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" - } + self.content["materials"] = self.materials.export_json(profile.is_capcut_env) - self.content["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" - } + platform = profile.platform + if platform: + self.content["last_modified_platform"] = dict(platform) + self.content["platform"] = dict(platform) # 合并导入的素材 for material_type, material_list in self.imported_materials.items(): diff --git a/save_draft_impl.py b/save_draft_impl.py index 9454a85..bc8f332 100644 --- a/save_draft_impl.py +++ b/save_draft_impl.py @@ -1,8 +1,7 @@ import os -import re import pyJianYingDraft as draft import shutil -from util import zip_draft, is_windows_path +from util import zip_draft, build_draft_asset_path from oss import upload_to_oss from typing import Dict, Literal from draft_cache import DRAFT_CACHE @@ -20,7 +19,8 @@ import requests # Import requests for making HTTP calls import logging # Import configuration -from settings import IS_CAPCUT_ENV, IS_UPLOAD_DRAFT +from settings import IS_UPLOAD_DRAFT +from draft_profiles import get_draft_profile, write_profile_content # --- Get your Logger instance --- # The name here must match the logger name you configured in app.py @@ -38,17 +38,7 @@ def build_asset_path(draft_folder: str, draft_id: str, asset_type: str, material :param material_name: Material name :return: Built path """ - if is_windows_path(draft_folder): - if os.name == 'nt': # 'nt' for Windows - draft_real_path = os.path.join(draft_folder, draft_id, "assets", asset_type, material_name) - else: - windows_drive, windows_path = re.match(r'([a-zA-Z]:)(.*)', draft_folder).groups() - parts = [p for p in windows_path.split('\\') if p] - draft_real_path = os.path.join(windows_drive, *parts, draft_id, "assets", asset_type, material_name) - draft_real_path = draft_real_path.replace('/', '\\') - else: - draft_real_path = os.path.join(draft_folder, draft_id, "assets", asset_type, material_name) - return draft_real_path + return build_draft_asset_path(draft_folder, draft_id, asset_type, material_name) def save_draft_background(draft_id, draft_folder, task_id): """Background save draft to OSS""" @@ -82,18 +72,24 @@ def save_draft_background(draft_id, draft_folder, task_id): update_tasks_cache(task_id, task_status) # Use new cache management function logger.info(f"Task {task_id} status updated to 'processing': Preparing draft files.") - # Delete possibly existing draft_id folder - if os.path.exists(draft_id): - logger.warning(f"Deleting existing draft folder (current working directory): {draft_id}") - shutil.rmtree(draft_id) - logger.info(f"Starting to save draft: {draft_id}") # Save draft current_dir = os.path.dirname(os.path.abspath(__file__)) - draft_folder_for_duplicate = draft.Draft_folder(current_dir) + output_base_dir = draft_folder or current_dir + draft_dir = os.path.join(output_base_dir, draft_id) + + # Delete possibly existing draft_id folder in the target output location. + if os.path.exists(draft_dir): + logger.warning(f"Deleting existing draft folder: {draft_dir}") + shutil.rmtree(draft_dir) + # Choose different template directory based on configuration - template_dir = "template" if IS_CAPCUT_ENV else "template_jianying" - draft_folder_for_duplicate.duplicate_as_template(template_dir, draft_id) + draft_profile = get_draft_profile() + template_dir = draft_profile.template_dir + template_source_dir = os.path.join(current_dir, template_dir) + if not os.path.exists(template_source_dir): + raise FileNotFoundError(f"Template draft {template_dir} does not exist") + shutil.copytree(template_source_dir, draft_dir) # Update task status update_task_field(task_id, "message", "Updating media file metadata") @@ -120,7 +116,7 @@ def save_draft_background(draft_id, draft_folder, task_id): download_tasks.append({ 'type': 'audio', 'func': download_file, - 'args': (remote_url, os.path.join(current_dir, f"{draft_id}/assets/audio/{material_name}")), + 'args': (remote_url, os.path.join(output_base_dir, f"{draft_id}/assets/audio/{material_name}")), 'material': audio }) @@ -143,7 +139,7 @@ def save_draft_background(draft_id, draft_folder, task_id): download_tasks.append({ 'type': 'image', 'func': download_file, - 'args': (remote_url, os.path.join(current_dir, f"{draft_id}/assets/image/{material_name}")), + 'args': (remote_url, os.path.join(output_base_dir, f"{draft_id}/assets/image/{material_name}")), 'material': video }) @@ -159,7 +155,7 @@ def save_draft_background(draft_id, draft_folder, task_id): download_tasks.append({ 'type': 'video', 'func': download_file, - 'args': (remote_url, os.path.join(current_dir, f"{draft_id}/assets/video/{material_name}")), + 'args': (remote_url, os.path.join(output_base_dir, f"{draft_id}/assets/video/{material_name}")), 'material': video }) @@ -212,8 +208,8 @@ def save_draft_background(draft_id, draft_folder, task_id): update_task_field(task_id, "message", "Saving draft information") logger.info(f"Task {task_id} progress 70%: Saving draft information.") - script.dump(os.path.join(current_dir, f"{draft_id}/draft_info.json")) - logger.info(f"Draft information has been saved to {os.path.join(current_dir, draft_id)}/draft_info.json.") + written_files = write_profile_content(draft_profile, draft_dir, script.dumps(draft_profile)) + logger.info(f"Draft information has been saved to {[str(path) for path in written_files]}.") draft_url = "" # Only upload draft information when IS_UPLOAD_DRAFT is True @@ -575,7 +571,8 @@ def download_script(draft_id: str, draft_folder: str = None, script_data: Dict = logger.info(f"Starting to download draft: {draft_id} to folder: {draft_folder}") # Copy template to target directory - template_path = os.path.join("./", 'template') if IS_CAPCUT_ENV else os.path.join("./", 'template_jianying') + draft_profile = get_draft_profile() + template_path = os.path.join("./", draft_profile.template_dir) new_draft_path = os.path.join(draft_folder, draft_id) if os.path.exists(new_draft_path): logger.warning(f"Deleting existing draft target folder: {new_draft_path}") @@ -696,8 +693,7 @@ def download_script(draft_id: str, draft_folder: str = None, script_data: Dict = logger.info(f"Concurrent download completed, downloaded {len(downloaded_paths)} files in total.") """Write draft file content to file""" - with open(f"{draft_folder}/{draft_id}/draft_info.json", "w", encoding="utf-8") as f: - f.write(json.dumps(script_data)) + write_profile_content(draft_profile, os.path.join(draft_folder, draft_id), json.dumps(script_data, ensure_ascii=False)) logger.info(f"Draft has been saved.") # No draft_url for download, but return success @@ -712,4 +708,4 @@ def download_script(draft_id: str, draft_folder: str = None, script_data: Dict = if __name__ == "__main__": print('hello') - download_script("dfd_cat_1751012163_a7e8c315",'/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft') \ No newline at end of file + download_script("dfd_cat_1751012163_a7e8c315",'/Users/sunguannan/Movies/JianyingPro/User Data/Projects/com.lveditor.draft') diff --git a/settings/__init__.py b/settings/__init__.py index b2517bd..8f0c5da 100644 --- a/settings/__init__.py +++ b/settings/__init__.py @@ -7,6 +7,7 @@ __all__ = [ "IS_CAPCUT_ENV", + "DRAFT_PROFILE", "API_KEYS", "MODEL_CONFIG", "PURCHASE_LINKS", @@ -33,4 +34,4 @@ def get_platform_info(): "mac_address": "c3371f2d4fb02791c067ce44d8fb4ed5", "os": "mac", "os_version": "15.5" - } \ No newline at end of file + } diff --git a/settings/local.py b/settings/local.py index cae0d94..b231282 100644 --- a/settings/local.py +++ b/settings/local.py @@ -3,12 +3,16 @@ """ import os -import json5 # 替换原来的json模块 +try: + import json5 # 支持带注释的配置文件 +except ModuleNotFoundError: + import json as json5 # 配置文件路径 CONFIG_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.json") # 默认配置 +DRAFT_PROFILE = "capcut_legacy" IS_CAPCUT_ENV = True # 默认域名配置 @@ -33,9 +37,24 @@ # 使用json5.load替代json.load local_config = json5.load(f) + # 更新草稿模板配置。draft_profile 优先,is_capcut_env 保持向后兼容。 + if "draft_profile" in local_config: + profile_name = str(local_config["draft_profile"]).lower().replace(".", "_").replace("-", "_") + if profile_name in ("capcut", "capcut_legacy"): + DRAFT_PROFILE = "capcut_legacy" + IS_CAPCUT_ENV = True + elif profile_name in ("jianying", "jianying_legacy"): + DRAFT_PROFILE = "jianying_legacy" + IS_CAPCUT_ENV = False + elif profile_name in ("jianying_10", "jianying_10_x", "jianying_pro_10", "jianying_pro_10_2", "jianying_pro_10_2_0"): + DRAFT_PROFILE = "jianying_pro_10" + IS_CAPCUT_ENV = False + # 更新是否是国际版 if "is_capcut_env" in local_config: IS_CAPCUT_ENV = local_config["is_capcut_env"] + if "draft_profile" not in local_config: + DRAFT_PROFILE = "capcut_legacy" if IS_CAPCUT_ENV else "jianying_legacy" # 更新域名配置 if "draft_domain" in local_config: @@ -63,4 +82,4 @@ except Exception as e: # 配置文件加载失败,使用默认配置 - pass \ No newline at end of file + pass diff --git a/template_jianying_10_2/Resources/.gitkeep b/template_jianying_10_2/Resources/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/template_jianying_10_2/Resources/.gitkeep @@ -0,0 +1 @@ + diff --git a/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/attachment_editing.json b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/attachment_editing.json new file mode 100644 index 0000000..8337f51 --- /dev/null +++ b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/attachment_editing.json @@ -0,0 +1 @@ +{"draft_operation_log":[]} diff --git a/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/attachment_pc_common.json b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/attachment_pc_common.json new file mode 100644 index 0000000..10a8e94 --- /dev/null +++ b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/attachment_pc_common.json @@ -0,0 +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":[],"template_item_infos":[],"unlock_template_ids":[]} diff --git a/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/common_attachment/attachment_pc_timeline.json b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/common_attachment/attachment_pc_timeline.json new file mode 100644 index 0000000..d3a7849 --- /dev/null +++ b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/common_attachment/attachment_pc_timeline.json @@ -0,0 +1 @@ +{"reference_lines_config":{"horizontal_lines":[],"is_lock":false,"is_visible":false,"vertical_lines":[]},"safe_area_type":0} diff --git a/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/common_attachment/attachment_script_video.json b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/common_attachment/attachment_script_video.json new file mode 100644 index 0000000..5180382 --- /dev/null +++ b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/common_attachment/attachment_script_video.json @@ -0,0 +1 @@ +{"video_segments":[]} diff --git a/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/draft_content.json b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/draft_content.json new file mode 100644 index 0000000..ee9d016 --- /dev/null +++ b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/draft_content.json @@ -0,0 +1,5 @@ +{ + "tracks": [], + "materials": {}, + "duration": 0 +} diff --git a/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/draft_content.json.bak b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/draft_content.json.bak new file mode 100644 index 0000000..ee9d016 --- /dev/null +++ b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/draft_content.json.bak @@ -0,0 +1,5 @@ +{ + "tracks": [], + "materials": {}, + "duration": 0 +} diff --git a/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/template-2.tmp b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/template-2.tmp new file mode 100644 index 0000000..ee9d016 --- /dev/null +++ b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/template-2.tmp @@ -0,0 +1,5 @@ +{ + "tracks": [], + "materials": {}, + "duration": 0 +} diff --git a/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/template.tmp b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/template.tmp new file mode 100644 index 0000000..ee9d016 --- /dev/null +++ b/template_jianying_10_2/Timelines/00000000-0000-4000-8000-000000000001/template.tmp @@ -0,0 +1,5 @@ +{ + "tracks": [], + "materials": {}, + "duration": 0 +} diff --git a/template_jianying_10_2/Timelines/project.json b/template_jianying_10_2/Timelines/project.json new file mode 100644 index 0000000..94a7713 --- /dev/null +++ b/template_jianying_10_2/Timelines/project.json @@ -0,0 +1 @@ +{"config":{"color_space":-1,"render_index_track_mode_on":false,"use_float_render":false},"create_time":0,"id":"00000000-0000-4000-8000-000000000002","main_timeline_id":"00000000-0000-4000-8000-000000000001","timelines":[{"create_time":0,"id":"00000000-0000-4000-8000-000000000001","is_marked_delete":false,"name":"Timeline 1","update_time":0}],"update_time":0,"version":0} diff --git a/template_jianying_10_2/Timelines/project.json.bak b/template_jianying_10_2/Timelines/project.json.bak new file mode 100644 index 0000000..94a7713 --- /dev/null +++ b/template_jianying_10_2/Timelines/project.json.bak @@ -0,0 +1 @@ +{"config":{"color_space":-1,"render_index_track_mode_on":false,"use_float_render":false},"create_time":0,"id":"00000000-0000-4000-8000-000000000002","main_timeline_id":"00000000-0000-4000-8000-000000000001","timelines":[{"create_time":0,"id":"00000000-0000-4000-8000-000000000001","is_marked_delete":false,"name":"Timeline 1","update_time":0}],"update_time":0,"version":0} diff --git a/template_jianying_10_2/adjust_mask/.gitkeep b/template_jianying_10_2/adjust_mask/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/template_jianying_10_2/adjust_mask/.gitkeep @@ -0,0 +1 @@ + diff --git a/template_jianying_10_2/attachment_pc_common.json b/template_jianying_10_2/attachment_pc_common.json new file mode 100644 index 0000000..10a8e94 --- /dev/null +++ b/template_jianying_10_2/attachment_pc_common.json @@ -0,0 +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":[],"template_item_infos":[],"unlock_template_ids":[]} diff --git a/template_jianying_10_2/common_attachment/attachment_pc_timeline.json b/template_jianying_10_2/common_attachment/attachment_pc_timeline.json new file mode 100644 index 0000000..d3a7849 --- /dev/null +++ b/template_jianying_10_2/common_attachment/attachment_pc_timeline.json @@ -0,0 +1 @@ +{"reference_lines_config":{"horizontal_lines":[],"is_lock":false,"is_visible":false,"vertical_lines":[]},"safe_area_type":0} diff --git a/template_jianying_10_2/draft_agency_config.json b/template_jianying_10_2/draft_agency_config.json new file mode 100644 index 0000000..0cae1a7 --- /dev/null +++ b/template_jianying_10_2/draft_agency_config.json @@ -0,0 +1 @@ +{"is_auto_agency_enabled":false,"is_auto_agency_popup":false,"is_single_agency_mode":false,"marterials":null,"use_converter":false,"video_resolution":720} diff --git a/template_jianying_10_2/draft_biz_config.json b/template_jianying_10_2/draft_biz_config.json new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/template_jianying_10_2/draft_biz_config.json @@ -0,0 +1 @@ + diff --git a/template_jianying_10_2/draft_content.json b/template_jianying_10_2/draft_content.json new file mode 100644 index 0000000..05006e7 --- /dev/null +++ b/template_jianying_10_2/draft_content.json @@ -0,0 +1,124 @@ +{ + "canvas_config": { + "height": 1080, + "ratio": "original", + "width": 1920 + }, + "color_space": 0, + "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": "00000000-0000-4000-8000-000000000000", + "keyframe_graph_list": [], + "keyframes": { + "adjusts": [], + "audios": [], + "effects": [], + "filters": [], + "handwrites": [], + "stickers": [], + "texts": [], + "videos": [] + }, + "last_modified_platform": { + "app_id": 3704, + "app_source": "lv", + "app_version": "10.2.0", + "os": "windows" + }, + "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": "103000", + "platform": { + "app_id": 3704, + "app_source": "lv", + "app_version": "10.2.0", + "os": "windows" + }, + "relationships": [], + "render_index_track_mode_on": false, + "retouch_cover": null, + "source": "default", + "static_cover_image_path": "", + "time_marks": [], + "tracks": [], + "update_time": 0, + "version": 360000 +} diff --git a/template_jianying_10_2/draft_content.json.bak b/template_jianying_10_2/draft_content.json.bak new file mode 100644 index 0000000..ee9d016 --- /dev/null +++ b/template_jianying_10_2/draft_content.json.bak @@ -0,0 +1,5 @@ +{ + "tracks": [], + "materials": {}, + "duration": 0 +} diff --git a/template_jianying_10_2/draft_meta_info.json b/template_jianying_10_2/draft_meta_info.json new file mode 100644 index 0000000..68fb00e --- /dev/null +++ b/template_jianying_10_2/draft_meta_info.json @@ -0,0 +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_fold_path":"","draft_id":"","draft_name":"","draft_root_path":"","draft_timeline_materials_size":0,"tm_draft_cloud_completed":"","tm_draft_cloud_modified":""} diff --git a/template_jianying_10_2/draft_settings b/template_jianying_10_2/draft_settings new file mode 100644 index 0000000..eb93116 --- /dev/null +++ b/template_jianying_10_2/draft_settings @@ -0,0 +1,6 @@ +[General] +draft_create_time=0 +draft_last_edit_time=0 +real_edit_seconds=0 +real_edit_keys=0 +cloud_last_modify_platform=windows diff --git a/template_jianying_10_2/draft_virtual_store.json b/template_jianying_10_2/draft_virtual_store.json new file mode 100644 index 0000000..82e264c --- /dev/null +++ b/template_jianying_10_2/draft_virtual_store.json @@ -0,0 +1 @@ +{"key_values":[],"material_infos":[]} diff --git a/template_jianying_10_2/key_value.json b/template_jianying_10_2/key_value.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/template_jianying_10_2/key_value.json @@ -0,0 +1 @@ +{} diff --git a/template_jianying_10_2/matting/.gitkeep b/template_jianying_10_2/matting/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/template_jianying_10_2/matting/.gitkeep @@ -0,0 +1 @@ + diff --git a/template_jianying_10_2/performance_opt_info.json b/template_jianying_10_2/performance_opt_info.json new file mode 100644 index 0000000..0491301 --- /dev/null +++ b/template_jianying_10_2/performance_opt_info.json @@ -0,0 +1 @@ +{"manual_cancle_precombine_segs":null,"need_auto_precombine_segs":null} diff --git a/template_jianying_10_2/qr_upload/.gitkeep b/template_jianying_10_2/qr_upload/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/template_jianying_10_2/qr_upload/.gitkeep @@ -0,0 +1 @@ + diff --git a/template_jianying_10_2/smart_crop/.gitkeep b/template_jianying_10_2/smart_crop/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/template_jianying_10_2/smart_crop/.gitkeep @@ -0,0 +1 @@ + diff --git a/template_jianying_10_2/subdraft/.gitkeep b/template_jianying_10_2/subdraft/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/template_jianying_10_2/subdraft/.gitkeep @@ -0,0 +1 @@ + diff --git a/template_jianying_10_2/template-2.tmp b/template_jianying_10_2/template-2.tmp new file mode 100644 index 0000000..ee9d016 --- /dev/null +++ b/template_jianying_10_2/template-2.tmp @@ -0,0 +1,5 @@ +{ + "tracks": [], + "materials": {}, + "duration": 0 +} diff --git a/template_jianying_10_2/template.tmp b/template_jianying_10_2/template.tmp new file mode 100644 index 0000000..ee9d016 --- /dev/null +++ b/template_jianying_10_2/template.tmp @@ -0,0 +1,5 @@ +{ + "tracks": [], + "materials": {}, + "duration": 0 +} diff --git a/template_jianying_10_2/timeline_layout.json b/template_jianying_10_2/timeline_layout.json new file mode 100644 index 0000000..cdac3a8 --- /dev/null +++ b/template_jianying_10_2/timeline_layout.json @@ -0,0 +1 @@ +{"dockItems":[{"dockIndex":0,"ratio":1,"timelineIds":["00000000-0000-4000-8000-000000000001"],"timelineNames":["Timeline 1"]}],"layoutOrientation":1} diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b9e1808 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +import sys +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) diff --git a/tests/test_draft_profiles.py b/tests/test_draft_profiles.py new file mode 100644 index 0000000..6911a6b --- /dev/null +++ b/tests/test_draft_profiles.py @@ -0,0 +1,135 @@ +import json +import shutil +from types import SimpleNamespace +from pathlib import Path + +import pytest + + +def test_jianying_10_profile_uses_versioned_template_and_content_names(): + from draft_profiles import get_draft_profile + + profile = get_draft_profile("jianying_pro_10") + + assert profile.name == "jianying_pro_10" + assert profile.template_dir == "template_jianying_10_2" + assert profile.content_file == "draft_content.json" + assert "template-2.tmp" in profile.content_mirrors + assert profile.is_capcut_env is False + + +def test_legacy_profiles_keep_existing_template_names(): + from draft_profiles import get_draft_profile + + capcut = get_draft_profile("capcut_legacy") + jianying = get_draft_profile("jianying_legacy") + + assert capcut.template_dir == "template" + assert capcut.content_file == "draft_info.json" + assert capcut.is_capcut_env is True + assert jianying.template_dir == "template_jianying" + assert jianying.content_file == "draft_info.json" + assert jianying.is_capcut_env is False + + +def test_write_profile_content_updates_main_mirrors_and_timeline(tmp_path): + from draft_profiles import get_draft_profile, write_profile_content + + draft_dir = tmp_path / "draft" + timeline_dir = draft_dir / "Timelines" / "timeline-1" + timeline_dir.mkdir(parents=True) + (draft_dir / "timeline_layout.json").write_text( + json.dumps({"dockItems": [{"timelineIds": ["timeline-1"], "timelineNames": ["Timeline 1"]}]}), + encoding="utf-8", + ) + + payload = {"id": "timeline-fixed", "tracks": [], "materials": {}, "duration": 0} + profile = get_draft_profile("jianying_pro_10") + + written = write_profile_content(profile, draft_dir, json.dumps(payload, ensure_ascii=False)) + + renamed_timeline_dir = draft_dir / "Timelines" / "timeline-fixed" + expected = { + draft_dir / "draft_content.json", + draft_dir / "template-2.tmp", + renamed_timeline_dir / "draft_content.json", + renamed_timeline_dir / "template-2.tmp", + renamed_timeline_dir / "template.tmp", + } + assert expected.issubset(set(written)) + for path in expected: + assert json.loads(path.read_text(encoding="utf-8")) == payload + project = json.loads((draft_dir / "Timelines" / "project.json").read_text(encoding="utf-8")) + layout = json.loads((draft_dir / "timeline_layout.json").read_text(encoding="utf-8")) + assert project["main_timeline_id"] == "timeline-fixed" + assert layout["dockItems"][0]["timelineIds"] == ["timeline-fixed"] + + +def test_script_dumps_uses_requested_profile_platform_and_mask_key(): + import pyJianYingDraft as draft + from draft_profiles import get_draft_profile + + profile = get_draft_profile("jianying_pro_10") + script = draft.Script_file(1080, 1920) + + payload = json.loads(script.dumps(profile)) + + assert payload["platform"] == profile.platform + assert payload["last_modified_platform"] == profile.platform + assert "masks" in payload["materials"] + assert "common_mask" not in payload["materials"] + + +def test_windows_draft_asset_path_keeps_drive_root(): + from save_draft_impl import build_asset_path + + path = build_asset_path( + r"D:\JianyingPro Drafts", + "draft-1", + "video", + "clip.mp4", + ) + + assert path == r"D:\JianyingPro Drafts\draft-1\assets\video\clip.mp4" + + +def test_shared_draft_asset_path_keeps_drive_root(): + from util import build_draft_asset_path + + assert build_draft_asset_path( + r"D:\JianyingPro Drafts", + "draft-1", + "image", + "cover.png", + ) == r"D:\JianyingPro Drafts\draft-1\assets\image\cover.png" + + +def test_save_draft_writes_to_requested_draft_folder(tmp_path, monkeypatch): + import save_draft_impl + from draft_cache import DRAFT_CACHE + from draft_profiles import get_draft_profile + from save_task_cache import create_task + + draft_id = "draft-target-folder" + payload = {"tracks": [], "materials": {}, "duration": 0} + project_draft_dir = Path(save_draft_impl.__file__).resolve().parent / draft_id + if project_draft_dir.exists(): + shutil.rmtree(project_draft_dir) + + script = SimpleNamespace( + materials=SimpleNamespace(audios=[], videos=[]), + tracks={}, + dumps=lambda profile=None: json.dumps(payload), + ) + DRAFT_CACHE[draft_id] = script + create_task(draft_id) + + monkeypatch.setattr(save_draft_impl, "get_draft_profile", lambda: get_draft_profile("jianying_pro_10")) + monkeypatch.setattr(save_draft_impl, "update_media_metadata", lambda script, task_id=None: None) + monkeypatch.setattr(save_draft_impl, "IS_UPLOAD_DRAFT", False) + + save_draft_impl.save_draft_background(draft_id, str(tmp_path), draft_id) + + assert (tmp_path / draft_id / "draft_content.json").exists() + assert (tmp_path / draft_id / "Timelines").exists() + assert not project_draft_dir.exists() diff --git a/util.py b/util.py index 225f68f..99aeb98 100644 --- a/util.py +++ b/util.py @@ -27,6 +27,18 @@ def is_windows_path(path): # Check if it starts with a drive letter (e.g. C:\) or contains Windows style separators return re.match(r'^[a-zA-Z]:\\|\\\\', path) is not None +def build_draft_asset_path(draft_folder: str, draft_id: str, asset_type: str, material_name: str) -> str: + """Build the path Jianying/CapCut should use for a material inside a draft.""" + if is_windows_path(draft_folder): + if os.name == 'nt': + return os.path.join(draft_folder, draft_id, "assets", asset_type, material_name) + + windows_drive, windows_path = re.match(r'([a-zA-Z]:)(.*)', draft_folder).groups() + parts = [p for p in windows_path.split('\\') if p] + return os.path.join(f"{windows_drive}\\", *parts, draft_id, "assets", asset_type, material_name).replace('/', '\\') + + return os.path.join(draft_folder, draft_id, "assets", asset_type, material_name) + def zip_draft(draft_id): current_dir = os.path.dirname(os.path.abspath(__file__)) @@ -80,4 +92,4 @@ def wrapper(*args, **kwargs): return decorator def generate_draft_url(draft_id): - return f"{DRAFT_DOMAIN}{PREVIEW_ROUTER}?draft_id={draft_id}&is_capcut={1 if IS_CAPCUT_ENV else 0}" \ No newline at end of file + return f"{DRAFT_DOMAIN}{PREVIEW_ROUTER}?draft_id={draft_id}&is_capcut={1 if IS_CAPCUT_ENV else 0}"