pycapcut 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. pyJianYingDraft/__init__.py +276 -0
  2. pyJianYingDraft/animation.py +140 -0
  3. pyJianYingDraft/assets/__init__.py +45 -0
  4. pyJianYingDraft/assets/draft_content_template.json +125 -0
  5. pyJianYingDraft/assets/draft_meta_info.json +67 -0
  6. pyJianYingDraft/audio_segment.py +225 -0
  7. pyJianYingDraft/draft_content_template.json +119 -0
  8. pyJianYingDraft/draft_folder.py +153 -0
  9. pyJianYingDraft/effect_segment.py +36 -0
  10. pyJianYingDraft/exceptions.py +23 -0
  11. pyJianYingDraft/jianying_controller.py +230 -0
  12. pyJianYingDraft/keyframe.py +94 -0
  13. pyJianYingDraft/local_materials.py +198 -0
  14. pyJianYingDraft/metadata/__init__.py +51 -0
  15. pyJianYingDraft/metadata/animation_meta.py +689 -0
  16. pyJianYingDraft/metadata/audio_effect_meta.py +367 -0
  17. pyJianYingDraft/metadata/audio_scene_effect.py +252 -0
  18. pyJianYingDraft/metadata/effect_meta.py +191 -0
  19. pyJianYingDraft/metadata/filter_meta.py +603 -0
  20. pyJianYingDraft/metadata/font_meta.py +341 -0
  21. pyJianYingDraft/metadata/mask_meta.py +15 -0
  22. pyJianYingDraft/metadata/speech_to_song.py +12 -0
  23. pyJianYingDraft/metadata/text_intro.py +152 -0
  24. pyJianYingDraft/metadata/text_loop.py +105 -0
  25. pyJianYingDraft/metadata/text_outro.py +105 -0
  26. pyJianYingDraft/metadata/tone_effect.py +107 -0
  27. pyJianYingDraft/metadata/transition_meta.py +733 -0
  28. pyJianYingDraft/metadata/video_character_effect.py +1913 -0
  29. pyJianYingDraft/metadata/video_effect_meta.py +9619 -0
  30. pyJianYingDraft/metadata/video_group_animation.py +131 -0
  31. pyJianYingDraft/metadata/video_intro.py +103 -0
  32. pyJianYingDraft/metadata/video_outro.py +80 -0
  33. pyJianYingDraft/metadata/video_scene_effect.py +7706 -0
  34. pyJianYingDraft/script_file.py +808 -0
  35. pyJianYingDraft/segment.py +256 -0
  36. pyJianYingDraft/template_mode.py +221 -0
  37. pyJianYingDraft/text_segment.py +436 -0
  38. pyJianYingDraft/time_util.py +99 -0
  39. pyJianYingDraft/track.py +138 -0
  40. pyJianYingDraft/util.py +58 -0
  41. pyJianYingDraft/video_segment.py +526 -0
  42. pycapcut/__init__.py +276 -0
  43. pycapcut/animation.py +140 -0
  44. pycapcut/assets/__init__.py +45 -0
  45. pycapcut/assets/draft_content_template.json +132 -0
  46. pycapcut/assets/draft_meta_info.json +73 -0
  47. pycapcut/audio_segment.py +225 -0
  48. pycapcut/draft_folder.py +153 -0
  49. pycapcut/effect_segment.py +36 -0
  50. pycapcut/exceptions.py +23 -0
  51. pycapcut/jianying_controller.py +230 -0
  52. pycapcut/keyframe.py +94 -0
  53. pycapcut/local_materials.py +198 -0
  54. pycapcut/metadata/__init__.py +51 -0
  55. pycapcut/metadata/audio_scene_effect.py +252 -0
  56. pycapcut/metadata/effect_meta.py +191 -0
  57. pycapcut/metadata/filter_meta.py +603 -0
  58. pycapcut/metadata/font_meta.py +341 -0
  59. pycapcut/metadata/mask_meta.py +15 -0
  60. pycapcut/metadata/speech_to_song.py +12 -0
  61. pycapcut/metadata/text_intro.py +152 -0
  62. pycapcut/metadata/text_loop.py +105 -0
  63. pycapcut/metadata/text_outro.py +105 -0
  64. pycapcut/metadata/tone_effect.py +107 -0
  65. pycapcut/metadata/transition_meta.py +733 -0
  66. pycapcut/metadata/video_character_effect.py +1913 -0
  67. pycapcut/metadata/video_group_animation.py +131 -0
  68. pycapcut/metadata/video_intro.py +103 -0
  69. pycapcut/metadata/video_outro.py +80 -0
  70. pycapcut/metadata/video_scene_effect.py +7706 -0
  71. pycapcut/script_file.py +808 -0
  72. pycapcut/segment.py +256 -0
  73. pycapcut/template_mode.py +221 -0
  74. pycapcut/text_segment.py +436 -0
  75. pycapcut/time_util.py +99 -0
  76. pycapcut/track.py +138 -0
  77. pycapcut/util.py +58 -0
  78. pycapcut/video_segment.py +526 -0
  79. pycapcut-0.0.1.dist-info/METADATA +77 -0
  80. pycapcut-0.0.1.dist-info/RECORD +88 -0
  81. pycapcut-0.0.1.dist-info/WHEEL +5 -0
  82. pycapcut-0.0.1.dist-info/top_level.txt +1 -0
  83. tools/__init__.py +17 -0
  84. tools/build_release.py +270 -0
  85. tools/code_generator.py +247 -0
  86. tools/extract_all.py +256 -0
  87. tools/extractor_base.py +132 -0
  88. tools/jianying_extractor.py +262 -0
tools/extract_all.py ADDED
@@ -0,0 +1,256 @@
1
+
2
+ """
3
+ 统一元数据提取脚本
4
+
5
+ 用于从剪映原始数据中提取各种类型的元数据
6
+ 替换原来分散在 ignored/meta 中的各个提取脚本
7
+
8
+ 使用方法:
9
+ python extract_all.py --type fonts --source ignored/meta/fonts.json
10
+ python extract_all.py --type all # 提取所有类型
11
+ python extract_all.py --config config.yaml # 使用配置文件
12
+ """
13
+
14
+ import argparse
15
+ import json
16
+ import yaml
17
+ from pathlib import Path
18
+ from typing import Dict, Any, Optional
19
+
20
+ from .jianying_extractor import (
21
+ FontExtractor, TextAnimationExtractor, MaskExtractor,
22
+ FilterExtractor, TransitionExtractor, VideoEffectExtractor, AudioEffectExtractor
23
+ )
24
+ from .code_generator import CodeGenerator, LegacyCodeGenerator
25
+
26
+
27
+ class MetadataExtractorManager:
28
+ """元数据提取管理器"""
29
+
30
+ def __init__(self, config_file: Optional[str] = None):
31
+ self.config = self._load_config(config_file)
32
+ self.extractors = self._init_extractors()
33
+
34
+ def _load_config(self, config_file: Optional[str]) -> Dict[str, Any]:
35
+ """加载配置文件"""
36
+ default_config = {
37
+ "data_sources": {
38
+ "fonts": "ignored/meta/fonts.json",
39
+ "text_intro": "ignored/meta/text_intro.json",
40
+ "text_outro": "ignored/meta/text_outro.json",
41
+ "text_repeat": "ignored/meta/text_repeat.json",
42
+ "masks": "ignored/meta/masks.json",
43
+ "filters": "ignored/meta/视频特效/filters.json",
44
+ "transitions": "ignored/meta/视频特效/transitions.json",
45
+ "video_character": "ignored/meta/视频特效/video_character_effect.json",
46
+ "video_scene": "ignored/meta/视频特效/video_scene_effect.json",
47
+ "audio_effects": "ignored/meta/audio_effect.json"
48
+ },
49
+ "output_dir": "pyJianYingDraft/metadata",
50
+ "data_dir": "pyJianYingDraft/data",
51
+ "legacy_output": "tools/legacy_output"
52
+ }
53
+
54
+ if config_file and Path(config_file).exists():
55
+ with open(config_file, 'r', encoding='utf-8') as f:
56
+ if config_file.endswith('.yaml') or config_file.endswith('.yml'):
57
+ user_config = yaml.safe_load(f)
58
+ else:
59
+ user_config = json.load(f)
60
+
61
+ # 合并配置
62
+ default_config.update(user_config)
63
+
64
+ return default_config
65
+
66
+ def _init_extractors(self) -> Dict[str, Any]:
67
+ """初始化提取器"""
68
+ sources = self.config["data_sources"]
69
+ return {
70
+ "fonts": FontExtractor,
71
+ "text_intro": TextAnimationExtractor,
72
+ "text_outro": TextAnimationExtractor,
73
+ "text_repeat": TextAnimationExtractor,
74
+ "masks": MaskExtractor,
75
+ "filters": FilterExtractor,
76
+ "transitions": TransitionExtractor,
77
+ "video_character": VideoEffectExtractor,
78
+ "video_scene": VideoEffectExtractor,
79
+ "audio_effects": AudioEffectExtractor
80
+ }
81
+
82
+ def extract_single_type(self, data_type: str, source_file: Optional[str] = None,
83
+ output_file: Optional[str] = None, legacy_format: bool = False) -> None:
84
+ """提取单一类型的元数据"""
85
+ if data_type not in self.extractors:
86
+ print(f"Error: Unknown data type '{data_type}'")
87
+ print(f"Available types: {list(self.extractors.keys())}")
88
+ return
89
+
90
+ # 确定源文件
91
+ if source_file is None:
92
+ source_file = self.config["data_sources"].get(data_type)
93
+ if not source_file:
94
+ print(f"Error: No source file configured for type '{data_type}'")
95
+ return
96
+
97
+ source_path = Path(source_file)
98
+ if not source_path.exists():
99
+ print(f"Error: Source file not found: {source_path}")
100
+ return
101
+
102
+ # 确定输出文件
103
+ if output_file is None:
104
+ if legacy_format:
105
+ output_file = f"{self.config['legacy_output']}/{data_type}_meta.py"
106
+ else:
107
+ output_file = f"{self.config['data_dir']}/{data_type}.json"
108
+
109
+ # 创建提取器
110
+ extractor_class = self.extractors[data_type]
111
+ extractor = extractor_class(source_file, output_file)
112
+
113
+ try:
114
+ print(f"Extracting {data_type} from {source_file}...")
115
+
116
+ if legacy_format:
117
+ # 使用旧版格式生成器
118
+ template_type = self._get_template_type(data_type)
119
+ LegacyCodeGenerator.generate_from_extractor(extractor, output_file, template_type)
120
+ else:
121
+ # 使用新版JSON格式
122
+ data = extractor.extract()
123
+ print(f"Successfully extracted {len(data)} items")
124
+
125
+ except Exception as e:
126
+ print(f"Error extracting {data_type}: {e}")
127
+
128
+ def extract_all(self, legacy_format: bool = False) -> None:
129
+ """提取所有类型的元数据"""
130
+ print("Starting extraction of all metadata types...")
131
+
132
+ for data_type in self.extractors.keys():
133
+ print(f"\n--- Extracting {data_type} ---")
134
+ try:
135
+ self.extract_single_type(data_type, legacy_format=legacy_format)
136
+ except Exception as e:
137
+ print(f"Failed to extract {data_type}: {e}")
138
+ continue
139
+
140
+ print("\nExtraction completed!")
141
+
142
+ def generate_code(self, data_type: str, input_file: Optional[str] = None) -> None:
143
+ """生成Python代码文件"""
144
+ if input_file is None:
145
+ input_file = f"{self.config['data_dir']}/{data_type}.json"
146
+
147
+ input_path = Path(input_file)
148
+ if not input_path.exists():
149
+ print(f"Error: Input file not found: {input_path}")
150
+ return
151
+
152
+ # 加载数据
153
+ with open(input_path, 'r', encoding='utf-8') as f:
154
+ data_list = json.load(f)
155
+
156
+ import pandas as pd
157
+ data = pd.DataFrame(data_list)
158
+
159
+ # 生成代码
160
+ generator = CodeGenerator(data, data_type)
161
+
162
+ # 确定类名和输出文件
163
+ class_mapping = {
164
+ "fonts": ("FontType", "font_meta.py", "EffectEnum", "EffectMeta"),
165
+ "text_intro": ("IntroType", "animation_meta.py", "EffectEnum", "Animation_meta"),
166
+ "text_outro": ("OutroType", "animation_meta.py", "EffectEnum", "Animation_meta"),
167
+ "text_repeat": ("GroupAnimationType", "animation_meta.py", "EffectEnum", "Animation_meta"),
168
+ "masks": ("MaskType", "mask_meta.py", "EffectEnum", "EffectMeta"),
169
+ "filters": ("FilterType", "filter_meta.py", "EffectEnum", "EffectMeta"),
170
+ "transitions": ("TransitionType", "transition_meta.py", "EffectEnum", "Transition_meta"),
171
+ "video_character": ("VideoCharacterEffectType", "video_effect_meta.py", "EffectEnum", "EffectMeta"),
172
+ "video_scene": ("VideoSceneEffectType", "video_effect_meta.py", "EffectEnum", "EffectMeta"),
173
+ "audio_effects": ("ToneEffectType", "audio_effect_meta.py", "EffectEnum", "Audio_effect_meta")
174
+ }
175
+
176
+ if data_type in class_mapping:
177
+ class_name, filename, base_class, meta_class = class_mapping[data_type]
178
+ output_file = f"{self.config['output_dir']}/{filename}"
179
+ generator.save_to_file(output_file, class_name, base_class, meta_class)
180
+ else:
181
+ print(f"Error: No code generation mapping for type '{data_type}'")
182
+
183
+ def _get_template_type(self, data_type: str) -> str:
184
+ """获取模板类型"""
185
+ mapping = {
186
+ "fonts": "font",
187
+ "text_intro": "animation",
188
+ "text_outro": "animation",
189
+ "text_repeat": "animation",
190
+ "masks": "effect",
191
+ "filters": "effect",
192
+ "transitions": "transition",
193
+ "video_character": "effect",
194
+ "video_scene": "effect",
195
+ "audio_effects": "audio_effect"
196
+ }
197
+ return mapping.get(data_type, "effect")
198
+
199
+ def cleanup_legacy_files(self) -> None:
200
+ """清理旧版提取脚本生成的文件"""
201
+ legacy_patterns = [
202
+ "ignored/meta/extract_*.py",
203
+ "ignored/meta/video_effect_meta.py",
204
+ "ignored/meta/metadata.py"
205
+ ]
206
+
207
+ print("Cleaning up legacy files...")
208
+ cleaned_count = 0
209
+
210
+ for pattern in legacy_patterns:
211
+ for file_path in Path(".").glob(pattern):
212
+ if file_path.is_file():
213
+ file_path.unlink()
214
+ print(f"Removed: {file_path}")
215
+ cleaned_count += 1
216
+
217
+ print(f"Cleaned up {cleaned_count} legacy files")
218
+
219
+
220
+ def main():
221
+ parser = argparse.ArgumentParser(description="剪映元数据提取工具")
222
+ parser.add_argument("--type", choices=["fonts", "text_intro", "text_outro", "text_repeat",
223
+ "masks", "filters", "transitions", "video_character",
224
+ "video_scene", "audio_effects", "all"],
225
+ help="要提取的数据类型")
226
+ parser.add_argument("--source", help="源数据文件路径")
227
+ parser.add_argument("--output", help="输出文件路径")
228
+ parser.add_argument("--config", help="配置文件路径")
229
+ parser.add_argument("--legacy", action="store_true", help="使用旧版格式输出")
230
+ parser.add_argument("--generate-code", help="从JSON数据生成Python代码")
231
+ parser.add_argument("--cleanup", action="store_true", help="清理旧版文件")
232
+
233
+ args = parser.parse_args()
234
+
235
+ if not any([args.type, args.generate_code, args.cleanup]):
236
+ parser.print_help()
237
+ return
238
+
239
+ manager = MetadataExtractorManager(args.config)
240
+
241
+ if args.cleanup:
242
+ manager.cleanup_legacy_files()
243
+ return
244
+
245
+ if args.generate_code:
246
+ manager.generate_code(args.generate_code)
247
+ return
248
+
249
+ if args.type == "all":
250
+ manager.extract_all(legacy_format=args.legacy)
251
+ else:
252
+ manager.extract_single_type(args.type, args.source, args.output, legacy_format=args.legacy)
253
+
254
+
255
+ if __name__ == "__main__":
256
+ main()
@@ -0,0 +1,132 @@
1
+ """
2
+ 元数据提取器基类
3
+ 定义统一的提取接口和通用功能
4
+ """
5
+
6
+ import json
7
+ import pandas as pd
8
+ from abc import ABC, abstractmethod
9
+ from dataclasses import dataclass, field
10
+ from pathlib import Path
11
+ from typing import Dict, List, Any, Optional, Union
12
+
13
+
14
+ @dataclass
15
+ class ExtractorConfig:
16
+ """提取器配置"""
17
+ source_file: Union[str, Path]
18
+ """源数据文件路径"""
19
+
20
+ output_file: Optional[Union[str, Path]] = None
21
+ """输出文件路径,如果为None则不自动保存"""
22
+
23
+ sort_by: List[str] = field(default_factory=lambda: ["is_vip", "name"])
24
+ """排序字段"""
25
+
26
+ dedup_by: str = "resource_id"
27
+ """去重字段"""
28
+
29
+ name_field: str = "title"
30
+ """名称字段"""
31
+
32
+
33
+ class MetadataExtractor(ABC):
34
+ """元数据提取器基类"""
35
+
36
+ def __init__(self, config: ExtractorConfig):
37
+ self.config = config
38
+ self.raw_data: List[Dict[str, Any]] = []
39
+ self.processed_data: pd.DataFrame = pd.DataFrame()
40
+
41
+ def extract(self) -> pd.DataFrame:
42
+ """执行完整的提取流程"""
43
+ self.load_source_data()
44
+ self.raw_data = self.parse_json_data()
45
+ self.processed_data = self.process_data()
46
+
47
+ if self.config.output_file:
48
+ self.save_output()
49
+
50
+ return self.processed_data
51
+
52
+ def load_source_data(self) -> None:
53
+ """加载源数据文件"""
54
+ with open(self.config.source_file, 'r', encoding='utf-8') as f:
55
+ self.source_content = f.readlines()
56
+
57
+ @abstractmethod
58
+ def parse_json_data(self) -> List[Dict[str, Any]]:
59
+ """解析JSON数据,提取所需字段"""
60
+ pass
61
+
62
+ @abstractmethod
63
+ def extract_item_data(self, item: Dict[str, Any]) -> Dict[str, Any]:
64
+ """从单个item中提取数据"""
65
+ pass
66
+
67
+ def process_data(self) -> pd.DataFrame:
68
+ """处理数据:排序、去重等"""
69
+ df = pd.DataFrame(self.raw_data)
70
+
71
+ if len(df) == 0:
72
+ return df
73
+
74
+ # 排序
75
+ if self.config.sort_by:
76
+ available_cols = [col for col in self.config.sort_by if col in df.columns]
77
+ if available_cols:
78
+ df = df.sort_values(by=available_cols, ascending=True)
79
+
80
+ # 去重
81
+ if self.config.dedup_by and self.config.dedup_by in df.columns:
82
+ df = df.drop_duplicates(subset=[self.config.dedup_by])
83
+
84
+ return df.reset_index(drop=True)
85
+
86
+ @abstractmethod
87
+ def save_output(self) -> None:
88
+ """保存输出文件"""
89
+ pass
90
+
91
+ @staticmethod
92
+ def pad_to_length(s: str, length: int, pad_char: str = " ") -> str:
93
+ """字符串填充,考虑中文字符宽度"""
94
+ non_ascii_count = sum(1 for c in s if ord(c) > 127)
95
+ return s.ljust(length - non_ascii_count, pad_char)
96
+
97
+ @staticmethod
98
+ def safe_json_loads(json_str: str, default: Any = None) -> Any:
99
+ """安全解析JSON字符串"""
100
+ try:
101
+ if not json_str:
102
+ return default or {}
103
+ return json.loads(json_str)
104
+ except (json.JSONDecodeError, TypeError):
105
+ return default or {}
106
+
107
+ @staticmethod
108
+ def clean_identifier(name: str) -> str:
109
+ """清理标识符名称,用于Python变量名"""
110
+ cleaned = name.replace(' ', '_').replace('-', '_').replace('.', '_')
111
+ # 如果以数字开头,添加下划线前缀
112
+ if cleaned and cleaned[0].isdigit():
113
+ cleaned = "_" + cleaned
114
+ return cleaned
115
+
116
+ def extract_common_fields(self, item: Dict[str, Any]) -> Dict[str, Any]:
117
+ """提取通用字段"""
118
+ common_attr = item.get("common_attr", {})
119
+ business_info = self.safe_json_loads(
120
+ common_attr.get("business_info", {}).get("json_str", "{}"), {}
121
+ )
122
+
123
+ return {
124
+ "name": common_attr.get(self.config.name_field, ""),
125
+ "title": common_attr.get("title", ""),
126
+ "is_vip": business_info.get("is_vip", False),
127
+ "resource_id": common_attr.get("id", ""),
128
+ "effect_id": common_attr.get("effect_id", ""),
129
+ "md5": common_attr.get("md5", ""),
130
+ "common_attr": common_attr,
131
+ "business_info": business_info
132
+ }
@@ -0,0 +1,262 @@
1
+ """
2
+ 剪映元数据提取器
3
+ 实现剪映特定的数据提取逻辑
4
+ """
5
+
6
+ import json
7
+ from typing import Dict, List, Any
8
+ from .extractor_base import MetadataExtractor, ExtractorConfig
9
+
10
+
11
+ class JianYingExtractor(MetadataExtractor):
12
+ """剪映通用提取器"""
13
+
14
+ def __init__(self, config: ExtractorConfig, data_type: str = "general"):
15
+ super().__init__(config)
16
+ self.data_type = data_type
17
+
18
+ def parse_json_data(self) -> List[Dict[str, Any]]:
19
+ """解析剪映JSON数据格式"""
20
+ rows = []
21
+
22
+ for line in self.source_content:
23
+ line = line.strip()
24
+ if not line:
25
+ continue
26
+
27
+ try:
28
+ # 解析JSON数据
29
+ data = json.loads(line)
30
+
31
+ # 提取effect_item_list
32
+ effect_items = data.get("data", {}).get("effect_item_list", [])
33
+
34
+ # 遍历effect_items并提取所需属性
35
+ for item in effect_items:
36
+ item_data = self.extract_item_data(item)
37
+ if item_data:
38
+ rows.append(item_data)
39
+
40
+ except json.JSONDecodeError:
41
+ print(f"Warning: Failed to parse line: {line[:100]}...")
42
+ continue
43
+
44
+ return rows
45
+
46
+ def extract_item_data(self, item: Dict[str, Any]) -> Dict[str, Any]:
47
+ """提取item数据,子类可重写以处理特定格式"""
48
+ return self.extract_common_fields(item)
49
+
50
+ def save_output(self) -> None:
51
+ """保存为JSON格式"""
52
+ output_data = self.processed_data.to_dict('records')
53
+
54
+ with open(self.config.output_file, 'w', encoding='utf-8') as f:
55
+ json.dump(output_data, f, ensure_ascii=False, indent=2)
56
+
57
+ print(f"Saved {len(output_data)} items to {self.config.output_file}")
58
+
59
+
60
+ class FontExtractor(JianYingExtractor):
61
+ """字体提取器"""
62
+
63
+ def __init__(self, source_file: str, output_file: str = None):
64
+ config = ExtractorConfig(
65
+ source_file=source_file,
66
+ output_file=output_file,
67
+ sort_by=["is_vip", "title"],
68
+ name_field="title"
69
+ )
70
+ super().__init__(config, "fonts")
71
+
72
+
73
+ class TextAnimationExtractor(JianYingExtractor):
74
+ """文本动画提取器"""
75
+
76
+ def __init__(self, source_file: str, output_file: str = None):
77
+ config = ExtractorConfig(
78
+ source_file=source_file,
79
+ output_file=output_file,
80
+ sort_by=["is_vip", "name"],
81
+ name_field="title"
82
+ )
83
+ super().__init__(config, "text_animations")
84
+
85
+ def extract_item_data(self, item: Dict[str, Any]) -> Dict[str, Any]:
86
+ """提取文本动画特定数据"""
87
+ base_data = super().extract_item_data(item)
88
+
89
+ # 文本动画使用name字段而不是title
90
+ base_data["name"] = base_data["title"]
91
+
92
+ return base_data
93
+
94
+
95
+ class MaskExtractor(JianYingExtractor):
96
+ """蒙版提取器"""
97
+
98
+ def __init__(self, source_file: str, output_file: str = None):
99
+ config = ExtractorConfig(
100
+ source_file=source_file,
101
+ output_file=output_file,
102
+ sort_by=["is_vip", "name"],
103
+ name_field="title"
104
+ )
105
+ super().__init__(config, "masks")
106
+
107
+ def extract_item_data(self, item: Dict[str, Any]) -> Dict[str, Any]:
108
+ """提取蒙版特定数据"""
109
+ base_data = super().extract_item_data(item)
110
+ base_data["name"] = base_data["title"]
111
+ return base_data
112
+
113
+
114
+ class FilterExtractor(JianYingExtractor):
115
+ """滤镜提取器"""
116
+
117
+ def __init__(self, source_file: str, output_file: str = None):
118
+ config = ExtractorConfig(
119
+ source_file=source_file,
120
+ output_file=output_file,
121
+ sort_by=["is_vip", "title"],
122
+ name_field="title"
123
+ )
124
+ super().__init__(config, "filters")
125
+
126
+ def extract_item_data(self, item: Dict[str, Any]) -> Dict[str, Any]:
127
+ """提取滤镜特定数据"""
128
+ base_data = super().extract_item_data(item)
129
+
130
+ # 提取参数信息
131
+ sdk_extra = self.safe_json_loads(
132
+ base_data["common_attr"].get("sdk_extra", "{}"), {}
133
+ )
134
+ param_list = sdk_extra.get("setting", {}).get("effect_adjust_params", [])
135
+
136
+ base_data["params"] = [
137
+ [param["effect_key"], param["default"], param["min"], param["max"]]
138
+ for param in param_list
139
+ ]
140
+
141
+ return base_data
142
+
143
+
144
+ class TransitionExtractor(JianYingExtractor):
145
+ """转场提取器"""
146
+
147
+ def __init__(self, source_file: str, output_file: str = None):
148
+ config = ExtractorConfig(
149
+ source_file=source_file,
150
+ output_file=output_file,
151
+ sort_by=["is_vip", "name"],
152
+ name_field="title"
153
+ )
154
+ super().__init__(config, "transitions")
155
+
156
+ def extract_item_data(self, item: Dict[str, Any]) -> Dict[str, Any]:
157
+ """提取转场特定数据"""
158
+ base_data = super().extract_item_data(item)
159
+
160
+ # 提取转场特定信息
161
+ sdk_extra = self.safe_json_loads(
162
+ base_data["common_attr"].get("sdk_extra", "{}"), {}
163
+ )
164
+ transition_info = sdk_extra.get("transition", {})
165
+
166
+ base_data.update({
167
+ "name": base_data["title"],
168
+ "default_duration": transition_info.get("defaultDura", 1.0),
169
+ "is_overlap": transition_info.get("isOverlap", False)
170
+ })
171
+
172
+ return base_data
173
+
174
+
175
+ class VideoEffectExtractor(JianYingExtractor):
176
+ """视频特效提取器"""
177
+
178
+ def __init__(self, source_file: str, output_file: str = None):
179
+ config = ExtractorConfig(
180
+ source_file=source_file,
181
+ output_file=output_file,
182
+ sort_by=["is_vip", "title"],
183
+ name_field="title"
184
+ )
185
+ super().__init__(config, "video_effects")
186
+
187
+ def extract_item_data(self, item: Dict[str, Any]) -> Dict[str, Any]:
188
+ """提取视频特效特定数据"""
189
+ base_data = super().extract_item_data(item)
190
+
191
+ # 提取参数信息
192
+ sdk_extra = self.safe_json_loads(
193
+ base_data["common_attr"].get("sdk_extra", "{}"), {}
194
+ )
195
+ param_list = sdk_extra.get("setting", {}).get("effect_adjust_params", [])
196
+
197
+ base_data["params"] = [
198
+ [param["effect_key"], param["default"], param["min"], param["max"]]
199
+ for param in param_list
200
+ ]
201
+
202
+ return base_data
203
+
204
+
205
+ class AudioEffectExtractor(JianYingExtractor):
206
+ """音频特效提取器"""
207
+
208
+ def __init__(self, source_file: str, output_file: str = None):
209
+ config = ExtractorConfig(
210
+ source_file=source_file,
211
+ output_file=output_file,
212
+ sort_by=["is_vip", "effect_name"],
213
+ name_field="name"
214
+ )
215
+ super().__init__(config, "audio_effects")
216
+
217
+ def parse_json_data(self) -> List[Dict[str, Any]]:
218
+ """解析音频特效的特殊JSON格式"""
219
+ rows = []
220
+
221
+ try:
222
+ # 音频特效是单个JSON文件,不是按行的JSON
223
+ content = "".join(self.source_content)
224
+ data = json.loads(content)
225
+
226
+ # 提取effects列表
227
+ effect_items = data.get("data", {}).get("effects", [])
228
+
229
+ for item in effect_items:
230
+ item_data = self.extract_item_data(item)
231
+ if item_data:
232
+ rows.append(item_data)
233
+
234
+ except json.JSONDecodeError as e:
235
+ print(f"Error parsing audio effects JSON: {e}")
236
+
237
+ return rows
238
+
239
+ def extract_item_data(self, item: Dict[str, Any]) -> Dict[str, Any]:
240
+ """提取音频特效特定数据"""
241
+ extra = self.safe_json_loads(item.get("extra", "{}"), {})
242
+
243
+ # 只提取"音色"类型的特效
244
+ if "is_voice_conversion" not in extra:
245
+ return None
246
+
247
+ # 提取参数信息
248
+ sdk_extra = self.safe_json_loads(item.get("sdk_extra", "{}"), {})
249
+ param_list = sdk_extra.get("setting", {}).get("audio_effect_parameters_change", [])
250
+
251
+ return {
252
+ "effect_name": item.get("name", ""),
253
+ "name": item.get("name", ""),
254
+ "is_vip": extra.get("is_vip", False),
255
+ "resource_id": item.get("resource_id", ""),
256
+ "effect_id": item.get("effect_id", ""),
257
+ "md5": item.get("id", ""),
258
+ "params": [
259
+ [param["sliderName"], param["defaultValue"], param["minValue"], param["maxValue"]]
260
+ for param in param_list
261
+ ]
262
+ }