SolarumAsteridion commited on
Commit
ad048b8
·
verified ·
1 Parent(s): 9517d02

Update clipchamp_export.py

Browse files
Files changed (1) hide show
  1. clipchamp_export.py +138 -25
clipchamp_export.py CHANGED
@@ -1,61 +1,172 @@
1
- # clipchamp_export.py
2
- import os, json, zlib
3
- from typing import Dict
 
 
 
 
 
4
 
5
  CLIPCHAMP_OUTPUT_FOLDER = os.environ.get("CLIPCHAMP_OUTPUT_FOLDER", "clipchamp_projects")
6
 
7
 
8
- def build_clipchamp_bytes(project_name: str, ai_json: Dict, source_to_filename: Dict, source_to_duration: Dict = None) -> bytes:
9
- """Serialize AI editing plan into a zlib-compressed .clipchamp project file."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  if source_to_duration is None:
11
  source_to_duration = {}
12
 
 
 
 
 
13
  project = {
14
  "version": "1.0",
15
- "assets": {"byId": {}},
16
- "items": {"byId": {}},
17
- "tracks": {"byId": {"track_1": {"id": "track_1", "type": "video", "index": 0}}},
18
- "metadata": {"description": ai_json.get("description"), "projectName": project_name}
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  # Build assets
22
  for key, fname in source_to_filename.items():
23
  aid = f"asset_{key}"
24
- project["assets"]["byId"][aid] = {
25
- "id": aid, "fileName": fname, "originalFileName": fname,
26
- "assetKey": key, "metadata": {"duration": source_to_duration.get(key)},
27
- "importedSize": None, "uri": fname
 
 
 
 
 
 
 
 
 
 
 
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  # Build timeline items in order
31
  sorted_clips = sorted(ai_json.get("clips", []), key=lambda c: (c.get("order") or 0))
32
  cursor = 0.0
33
  for n, c in enumerate(sorted_clips, start=1):
 
34
  start = float(c["start_time"])
35
  end = float(c["end_time"])
36
  duration = max(0.0, end - start)
 
37
  item_id = f"item_{n}"
38
- project["items"]["byId"][item_id] = {
 
 
39
  "id": item_id,
40
- "assetId": f"asset_{c['source']}",
41
- "trackId": "track_1",
 
42
  "startTime": cursor,
43
  "duration": duration,
 
44
  "trim": {"from": start, "to": end},
 
 
 
 
45
  "speed": 1.0,
46
  "volume": 0.0 if c.get("mute") else 1.0,
 
 
 
 
 
47
  "order": c.get("order"),
48
- "source_start_ms": int(c.get("start_time_ms", round(start * 1000))),
49
- "source_end_ms": int(c.get("end_time_ms", round(end * 1000)))
50
  }
 
 
 
 
 
 
 
51
  cursor += duration
52
 
53
- project["timeline"] = {"duration": cursor}
54
- return zlib.compress(json.dumps(project, indent=2).encode("utf-8"))
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
 
57
- def save_clipchamp_project(project_name: str, ai_json: Dict, source_to_filename: Dict,
58
- source_to_duration: Dict = None, output_folder: str = None) -> str:
 
 
 
 
 
 
59
  """
60
  Build a .clipchamp project from the AI JSON and save it to a local folder.
61
  Returns the absolute path of the saved file.
@@ -63,10 +174,12 @@ def save_clipchamp_project(project_name: str, ai_json: Dict, source_to_filename:
63
  folder = output_folder or CLIPCHAMP_OUTPUT_FOLDER
64
  os.makedirs(folder, exist_ok=True)
65
 
66
- clip_bytes = build_clipchamp_bytes(project_name, ai_json, source_to_filename, source_to_duration)
67
 
68
- out_path = os.path.join(folder, f"{project_name}.clipchamp")
 
 
69
  with open(out_path, "wb") as f:
70
  f.write(clip_bytes)
71
 
72
- return os.path.abspath(out_path)
 
1
+ # clipchamp_export.py (improved)
2
+ import os
3
+ import json
4
+ import zlib
5
+ import time
6
+ import datetime
7
+ import mimetypes
8
+ from typing import Dict, Optional
9
 
10
  CLIPCHAMP_OUTPUT_FOLDER = os.environ.get("CLIPCHAMP_OUTPUT_FOLDER", "clipchamp_projects")
11
 
12
 
13
+ def _now_iso():
14
+ return datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
15
+
16
+
17
+ def build_clipchamp_bytes(
18
+ project_name: str,
19
+ ai_json: Dict,
20
+ source_to_filename: Dict,
21
+ source_to_duration: Dict = None,
22
+ aspect_ratio: Optional[str] = "16:9",
23
+ render_settings: Optional[Dict] = None,
24
+ schema_version: str = "1.0.0",
25
+ ) -> bytes:
26
+ """
27
+ Serialize AI editing plan into a zlib-compressed .clipchamp project file.
28
+ Produces a richer structure (assets.allIds, items.allIds, tracks, createdAt/updatedAt, renderSettings).
29
+ """
30
  if source_to_duration is None:
31
  source_to_duration = {}
32
 
33
+ created_at = _now_iso()
34
+ updated_at = created_at
35
+
36
+ # Base project skeleton (richer metadata than the minimal version)
37
  project = {
38
  "version": "1.0",
39
+ "schemaVersion": schema_version,
40
+ "name": project_name,
41
+ "createdAt": created_at,
42
+ "updatedAt": updated_at,
43
+ "aspectRatio": aspect_ratio,
44
+ "renderSettings": render_settings or {
45
+ "format": "mp4",
46
+ "resolution": {"width": 1920, "height": 1080},
47
+ "frameRate": 30
48
+ },
49
+ "metadata": {"description": ai_json.get("description"), "projectName": project_name},
50
+ # assets/items/tracks use both byId and allIds for predictable ordering
51
+ "assets": {"byId": {}, "allIds": []},
52
+ "items": {"byId": {}, "allIds": []},
53
+ "tracks": {"byId": {}, "allIds": []},
54
+ # timeline will include tracks and duration
55
+ "timeline": {"duration": 0, "tracks": {}}
56
  }
57
 
58
+ # Helper to resolve file metadata when available
59
+ def _file_meta(path):
60
+ try:
61
+ if os.path.exists(path):
62
+ size = os.path.getsize(path)
63
+ else:
64
+ size = None
65
+ except Exception:
66
+ size = None
67
+ mime = mimetypes.guess_type(path)[0] or None
68
+ return {"size": size, "mimeType": mime}
69
+
70
  # Build assets
71
  for key, fname in source_to_filename.items():
72
  aid = f"asset_{key}"
73
+ meta = _file_meta(fname)
74
+ asset_obj = {
75
+ "id": aid,
76
+ "fileName": os.path.basename(fname),
77
+ "originalFileName": os.path.basename(fname),
78
+ "assetKey": key,
79
+ "metadata": {
80
+ "duration": source_to_duration.get(key),
81
+ "size": meta["size"],
82
+ "mimeType": meta["mimeType"],
83
+ },
84
+ # uri kept as given so Clipchamp can find it; you can change to absolute path if you prefer
85
+ "uri": fname,
86
+ # importedSize kept for compatibility with some exports
87
+ "importedSize": meta["size"],
88
  }
89
+ project["assets"]["byId"][aid] = asset_obj
90
+ project["assets"]["allIds"].append(aid)
91
+
92
+ # single video track for now (expandable)
93
+ track_id = "track_1"
94
+ project["tracks"]["byId"][track_id] = {
95
+ "id": track_id,
96
+ "type": "video",
97
+ "index": 0,
98
+ "items": [], # ordered list of item IDs on this track
99
+ "muted": False
100
+ }
101
+ project["tracks"]["allIds"].append(track_id)
102
 
103
  # Build timeline items in order
104
  sorted_clips = sorted(ai_json.get("clips", []), key=lambda c: (c.get("order") or 0))
105
  cursor = 0.0
106
  for n, c in enumerate(sorted_clips, start=1):
107
+ # ensure types
108
  start = float(c["start_time"])
109
  end = float(c["end_time"])
110
  duration = max(0.0, end - start)
111
+
112
  item_id = f"item_{n}"
113
+ asset_id = f"asset_{c['source']}"
114
+
115
+ item_obj = {
116
  "id": item_id,
117
+ "assetId": asset_id,
118
+ "trackId": track_id,
119
+ # position on timeline (seconds)
120
  "startTime": cursor,
121
  "duration": duration,
122
+ # trim ranges on the source asset (seconds)
123
  "trim": {"from": start, "to": end},
124
+ # source ms fields for accuracy
125
+ "source_start_ms": int(c.get("start_time_ms", round(start * 1000))),
126
+ "source_end_ms": int(c.get("end_time_ms", round(end * 1000))),
127
+ # playback settings
128
  "speed": 1.0,
129
  "volume": 0.0 if c.get("mute") else 1.0,
130
+ # boolean flags commonly present in project files
131
+ "enabled": True,
132
+ "locked": False,
133
+ # decorative/position data (Clipchamp sometimes stores this)
134
+ "position": {"x": 0, "y": 0, "scale": 1.0, "rotation": 0},
135
  "order": c.get("order"),
 
 
136
  }
137
+
138
+ project["items"]["byId"][item_id] = item_obj
139
+ project["items"]["allIds"].append(item_id)
140
+
141
+ # append item id to track ordering
142
+ project["tracks"]["byId"][track_id]["items"].append(item_id)
143
+
144
  cursor += duration
145
 
146
+ # timeline info
147
+ project["timeline"]["duration"] = cursor
148
+ project["timeline"]["tracks"][track_id] = {
149
+ "id": track_id,
150
+ "items": project["tracks"]["byId"][track_id]["items"],
151
+ "duration": cursor,
152
+ }
153
+
154
+ # update updatedAt timestamp
155
+ project["updatedAt"] = _now_iso()
156
+
157
+ # ensure stable pretty JSON to ease differences/debugging
158
+ json_bytes = json.dumps(project, indent=2, ensure_ascii=False).encode("utf-8")
159
+ return zlib.compress(json_bytes)
160
 
161
 
162
+ def save_clipchamp_project(
163
+ project_name: str,
164
+ ai_json: Dict,
165
+ source_to_filename: Dict,
166
+ source_to_duration: Dict = None,
167
+ output_folder: str = None,
168
+ **build_kwargs
169
+ ) -> str:
170
  """
171
  Build a .clipchamp project from the AI JSON and save it to a local folder.
172
  Returns the absolute path of the saved file.
 
174
  folder = output_folder or CLIPCHAMP_OUTPUT_FOLDER
175
  os.makedirs(folder, exist_ok=True)
176
 
177
+ clip_bytes = build_clipchamp_bytes(project_name, ai_json, source_to_filename, source_to_duration, **build_kwargs)
178
 
179
+ # sanitize project_name for filename
180
+ safe_name = "".join(c if c.isalnum() or c in (" ", "_", "-") else "_" for c in project_name).strip()
181
+ out_path = os.path.join(folder, f"{safe_name}.clipchamp")
182
  with open(out_path, "wb") as f:
183
  f.write(clip_bytes)
184
 
185
+ return os.path.abspath(out_path)