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() |