220 lines
8.3 KiB
Python
220 lines
8.3 KiB
Python
|
|
import json
|
|||
|
|
import zipfile
|
|||
|
|
import os
|
|||
|
|
import shutil
|
|||
|
|
from typing import Optional, Dict, Any
|
|||
|
|
|
|||
|
|
|
|||
|
|
class JianYingConverter:
|
|||
|
|
def __init__(self, input_cmp: str, output_cmp: str, target_version: str = "4.0"):
|
|||
|
|
"""
|
|||
|
|
初始化剪映草稿转换器
|
|||
|
|
:param input_cmp: 输入高版本.cmp草稿路径
|
|||
|
|
:param output_cmp: 输出低版本.cmp草稿路径
|
|||
|
|
:param target_version: 目标低版本(支持 "3.0" / "4.0")
|
|||
|
|
"""
|
|||
|
|
self.input_cmp = input_cmp
|
|||
|
|
self.output_cmp = output_cmp
|
|||
|
|
self.target_version = target_version
|
|||
|
|
self.temp_dir = "temp_jianying" # 临时解压目录
|
|||
|
|
self.core_json = "project.json" # 核心配置文件
|
|||
|
|
|
|||
|
|
def _unzip_cmp(self) -> bool:
|
|||
|
|
"""解压.cmp文件到临时目录"""
|
|||
|
|
if not os.path.exists(self.input_cmp):
|
|||
|
|
print(f"错误:输入文件不存在 → {self.input_cmp}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 清空并创建临时目录
|
|||
|
|
if os.path.exists(self.temp_dir):
|
|||
|
|
shutil.rmtree(self.temp_dir)
|
|||
|
|
os.makedirs(self.temp_dir)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
with zipfile.ZipFile(self.input_cmp, 'r') as zip_ref:
|
|||
|
|
zip_ref.extractall(self.temp_dir)
|
|||
|
|
print(f"成功解压到临时目录 → {self.temp_dir}")
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"解压失败:{str(e)}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def _load_project_json(self) -> Optional[Dict[str, Any]]:
|
|||
|
|
"""读取project.json"""
|
|||
|
|
json_path = os.path.join(self.temp_dir, self.core_json)
|
|||
|
|
if not os.path.exists(json_path):
|
|||
|
|
print(f"错误:未找到核心配置文件 → {json_path}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
with open(json_path, 'r', encoding='utf-8') as f:
|
|||
|
|
data = json.load(f)
|
|||
|
|
print("成功读取project.json")
|
|||
|
|
return data
|
|||
|
|
except json.JSONDecodeError:
|
|||
|
|
# 部分高版本草稿可能有简单加密(如Base64),尝试解密
|
|||
|
|
try:
|
|||
|
|
import base64
|
|||
|
|
with open(json_path, 'r', encoding='utf-8') as f:
|
|||
|
|
encrypted = f.read()
|
|||
|
|
decrypted = base64.b64decode(encrypted).decode('utf-8')
|
|||
|
|
data = json.loads(decrypted)
|
|||
|
|
print("成功解密并读取project.json")
|
|||
|
|
return data
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"读取/解密JSON失败:{str(e)}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def _downgrade_json(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|||
|
|
"""根据目标版本降级JSON结构"""
|
|||
|
|
print(f"正在降级到剪映 v{self.target_version} 格式...")
|
|||
|
|
|
|||
|
|
# 1. 移除高版本新增的顶层字段(基于逆向分析)
|
|||
|
|
high_version_fields = [
|
|||
|
|
"meta_info", "plugin_version", "advanced_settings",
|
|||
|
|
"ai_editor_info", "cloud_project_info", "multi_track_info"
|
|||
|
|
]
|
|||
|
|
for field in high_version_fields:
|
|||
|
|
data.pop(field, None)
|
|||
|
|
|
|||
|
|
# 2. 降级项目配置(强制设置兼容版本号)
|
|||
|
|
if "project_config" in data:
|
|||
|
|
if self.target_version == "3.0":
|
|||
|
|
data["project_config"]["version"] = "3.9.0"
|
|||
|
|
data["project_config"]["compatible_version"] = "3.0.0"
|
|||
|
|
else: # 4.0
|
|||
|
|
data["project_config"]["version"] = "4.9.0"
|
|||
|
|
data["project_config"]["compatible_version"] = "4.0.0"
|
|||
|
|
|
|||
|
|
# 3. 处理轨道数据(移除高版本特效/转场)
|
|||
|
|
if "tracks" in data:
|
|||
|
|
self._downgrade_tracks(data["tracks"])
|
|||
|
|
|
|||
|
|
# 4. 处理资源列表(移除云资源引用)
|
|||
|
|
if "resources" in data:
|
|||
|
|
data["resources"] = [res for res in data["resources"] if not res.get("is_cloud", False)]
|
|||
|
|
|
|||
|
|
print("JSON降级完成")
|
|||
|
|
return data
|
|||
|
|
|
|||
|
|
def _downgrade_tracks(self, tracks: list):
|
|||
|
|
"""降级轨道数据(移除低版本不支持的效果)"""
|
|||
|
|
for track in tracks:
|
|||
|
|
if "clips" not in track:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
for clip in track["clips"]:
|
|||
|
|
# 移除高版本转场(保留基础转场)
|
|||
|
|
if "transition" in clip:
|
|||
|
|
transition_type = clip["transition"].get("type", "")
|
|||
|
|
# 低版本支持的基础转场列表(可根据需求扩展)
|
|||
|
|
supported_transitions = ["none", "fade", "slide", "push", "zoom"]
|
|||
|
|
if transition_type not in supported_transitions:
|
|||
|
|
clip["transition"] = {"type": "none", "duration": 0.3}
|
|||
|
|
|
|||
|
|
# 移除高版本特效(如AI特效、高级滤镜)
|
|||
|
|
if "effects" in clip:
|
|||
|
|
clip["effects"] = [
|
|||
|
|
eff for eff in clip["effects"]
|
|||
|
|
if eff.get("type") in ["filter", "adjust", "text", "sticker"] # 保留基础效果
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
# 降级音频效果(移除3D音效等高级功能)
|
|||
|
|
if "audio_effects" in clip:
|
|||
|
|
clip["audio_effects"] = [eff for eff in clip["audio_effects"] if eff.get("type") == "volume"]
|
|||
|
|
|
|||
|
|
def _save_project_json(self, data: Dict[str, Any]) -> bool:
|
|||
|
|
"""保存降级后的JSON到临时目录"""
|
|||
|
|
json_path = os.path.join(self.temp_dir, self.core_json)
|
|||
|
|
try:
|
|||
|
|
with open(json_path, 'w', encoding='utf-8') as f:
|
|||
|
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
|||
|
|
print("成功保存降级后的project.json")
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"保存JSON失败:{str(e)}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def _zip_to_cmp(self) -> bool:
|
|||
|
|
"""将临时目录重新压缩为.cmp文件"""
|
|||
|
|
try:
|
|||
|
|
with zipfile.ZipFile(self.output_cmp, 'w', zipfile.ZIP_DEFLATED) as zip_ref:
|
|||
|
|
# 遍历临时目录所有文件,添加到压缩包
|
|||
|
|
for root, dirs, files in os.walk(self.temp_dir):
|
|||
|
|
for file in files:
|
|||
|
|
file_path = os.path.join(root, file)
|
|||
|
|
# 保持压缩包内的相对路径
|
|||
|
|
arcname = os.path.relpath(file_path, self.temp_dir)
|
|||
|
|
zip_ref.write(file_path, arcname)
|
|||
|
|
print(f"成功生成低版本草稿 → {self.output_cmp}")
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"压缩失败:{str(e)}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def clean_temp(self):
|
|||
|
|
"""清理临时目录"""
|
|||
|
|
if os.path.exists(self.temp_dir):
|
|||
|
|
shutil.rmtree(self.temp_dir)
|
|||
|
|
print("临时目录已清理")
|
|||
|
|
|
|||
|
|
def convert(self) -> bool:
|
|||
|
|
"""执行完整转换流程"""
|
|||
|
|
print("=" * 50)
|
|||
|
|
print("剪映草稿高版本转低版本工具")
|
|||
|
|
print(f"输入:{self.input_cmp}")
|
|||
|
|
print(f"输出:{self.output_cmp}")
|
|||
|
|
print(f"目标版本:v{self.target_version}")
|
|||
|
|
print("=" * 50)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 1. 解压
|
|||
|
|
if not self._unzip_cmp():
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 2. 读取JSON
|
|||
|
|
data = self._load_project_json()
|
|||
|
|
if not data:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 3. 降级JSON
|
|||
|
|
downgraded_data = self._downgrade_json(data)
|
|||
|
|
|
|||
|
|
# 4. 保存JSON
|
|||
|
|
if not self._save_project_json(downgraded_data):
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 5. 重新压缩
|
|||
|
|
if not self._zip_to_cmp():
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 6. 清理临时文件
|
|||
|
|
self.clean_temp()
|
|||
|
|
|
|||
|
|
print("=" * 50)
|
|||
|
|
print("转换成功!请用目标版本剪映打开输出文件")
|
|||
|
|
print("注意:复杂特效/AI功能可能已降级为基础效果")
|
|||
|
|
print("=" * 50)
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"转换异常:{str(e)}")
|
|||
|
|
self.clean_temp()
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ------------------------------
|
|||
|
|
# 用法示例
|
|||
|
|
# ------------------------------
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
# 请修改以下参数
|
|||
|
|
INPUT_CMP = "high_version_project.cmp" # 高版本草稿路径
|
|||
|
|
OUTPUT_CMP = "low_version_project.cmp" # 输出低版本草稿路径
|
|||
|
|
TARGET_VERSION = "4.0" # 目标低版本(3.0 或 4.0)
|
|||
|
|
|
|||
|
|
# 创建转换器并执行
|
|||
|
|
converter = JianYingConverter(
|
|||
|
|
input_cmp=INPUT_CMP,
|
|||
|
|
output_cmp=OUTPUT_CMP,
|
|||
|
|
target_version=TARGET_VERSION
|
|||
|
|
)
|
|||
|
|
converter.convert()
|