playrig 0.0.0-stage → 0.1.0
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.
- package/AGENTS.md +106 -0
- package/LICENSE +37 -0
- package/README.md +77 -2
- package/bin/playrig.js +4 -0
- package/editions/ae/README.md +144 -0
- package/editions/ae/client/aeb.js +9 -0
- package/editions/ae/examples/hello.jsx +3 -0
- package/editions/ae/examples/inspect.jsx +7 -0
- package/editions/ae/examples/snap.jsx +15 -0
- package/editions/ae/lib/actions.json +159 -0
- package/editions/ae/lib/bridge-api.jsxinc +474 -0
- package/editions/ae/lib/dev.jsxinc +21 -0
- package/editions/ae/lib/icons/README.md +4 -0
- package/editions/ae/lib/icons/dot-error.svg +3 -0
- package/editions/ae/lib/icons/dot-idle.svg +3 -0
- package/editions/ae/lib/icons/dot-running.svg +3 -0
- package/editions/ae/lib/icons/make-png.js +25 -0
- package/editions/ae/lib/icons/png/dot-error.png +0 -0
- package/editions/ae/lib/icons/png/dot-error@2x.png +0 -0
- package/editions/ae/lib/icons/png/dot-idle.png +0 -0
- package/editions/ae/lib/icons/png/dot-idle@2x.png +0 -0
- package/editions/ae/lib/icons/png/dot-running.png +0 -0
- package/editions/ae/lib/icons/png/dot-running@2x.png +0 -0
- package/editions/ae/lib/icons/png/power-off.png +0 -0
- package/editions/ae/lib/icons/png/power-off@2x.png +0 -0
- package/editions/ae/lib/icons/png/power-on.png +0 -0
- package/editions/ae/lib/icons/png/power-on@2x.png +0 -0
- package/editions/ae/lib/icons/png/settings.png +0 -0
- package/editions/ae/lib/icons/png/settings@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-error.png +0 -0
- package/editions/ae/lib/icons/png/status-error@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-ok.png +0 -0
- package/editions/ae/lib/icons/png/status-ok@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-rejected.png +0 -0
- package/editions/ae/lib/icons/png/status-rejected@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-timeout.png +0 -0
- package/editions/ae/lib/icons/png/status-timeout@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-unknown.png +0 -0
- package/editions/ae/lib/icons/png/status-unknown@2x.png +0 -0
- package/editions/ae/lib/icons/power-off.svg +4 -0
- package/editions/ae/lib/icons/power-on.svg +4 -0
- package/editions/ae/lib/icons/settings.svg +4 -0
- package/editions/ae/lib/icons/status-error.svg +4 -0
- package/editions/ae/lib/icons/status-ok.svg +4 -0
- package/editions/ae/lib/icons/status-rejected.svg +4 -0
- package/editions/ae/lib/icons/status-timeout.svg +4 -0
- package/editions/ae/lib/icons/status-unknown.svg +5 -0
- package/editions/ae/lib/panel-core.jsxinc +249 -0
- package/editions/ae/lib/update-key.pem +11 -0
- package/editions/ae/lib/updater.jsxinc +148 -0
- package/editions/ae/library-starter/INDEX.md +5 -0
- package/editions/ae/library-starter/README.md +161 -0
- package/editions/ae/library-starter/_common.jsx +187 -0
- package/editions/ae/library-starter/categories.json +9 -0
- package/editions/ae/library-starter/recipes/.gitkeep +0 -0
- package/editions/ae/package.json +7 -0
- package/editions/ae/panel/Playrig.jsx +39 -0
- package/editions/ae/panel/core.jsx +1026 -0
- package/editions/ae/skills/create-ae-recipe/SKILL.md +85 -0
- package/editions/ae/skills/create-ae-recipe/references/ae-patterns.md +88 -0
- package/editions/ae/skills/create-ae-recipe/references/video-analysis.md +72 -0
- package/editions/ae/skills/create-ae-recipe/scripts/media +5 -0
- package/editions/ae/skills/create-ae-recipe/scripts/media.py +253 -0
- package/editions/ae/skills/create-ae-recipe/scripts/setup.sh +17 -0
- package/editions/ae/skills/create-ae-video/SKILL.md +88 -0
- package/editions/ae/skills/create-ae-video/references/templates.md +90 -0
- package/editions/ae/skills/create-ae-video/scripts/preflight.js +99 -0
- package/editions/ae/skills/use-ae-recipes/SKILL.md +99 -0
- package/editions/ae/skills/use-ae-recipes/scripts/contact-sheet.jsx +10 -0
- package/editions/ae/skills/use-ae-recipes/scripts/edit-text.jsx +12 -0
- package/editions/ae/skills/use-ae-recipes/scripts/remove-comp.jsx +18 -0
- package/editions/pr/README.md +60 -0
- package/editions/pr/SCORE_PROCESS.md +41 -0
- package/editions/pr/client/aeb.js +9 -0
- package/editions/pr/examples/frame.js +6 -0
- package/editions/pr/examples/hello.js +4 -0
- package/editions/pr/organize.default.json +245 -0
- package/editions/pr/package.json +7 -0
- package/editions/pr/plugin/actions.js +79 -0
- package/editions/pr/plugin/dev.js +19 -0
- package/editions/pr/plugin/icons/README.md +4 -0
- package/editions/pr/plugin/icons/file-code.svg +6 -0
- package/editions/pr/plugin/icons/folder.svg +3 -0
- package/editions/pr/plugin/icons/power-off.svg +4 -0
- package/editions/pr/plugin/icons/power-on.svg +4 -0
- package/editions/pr/plugin/icons/settings.svg +4 -0
- package/editions/pr/plugin/icons/status-error.svg +4 -0
- package/editions/pr/plugin/icons/status-ok.svg +4 -0
- package/editions/pr/plugin/icons/status-rejected.svg +4 -0
- package/editions/pr/plugin/icons/status-timeout.svg +4 -0
- package/editions/pr/plugin/icons/status-unknown.svg +5 -0
- package/editions/pr/plugin/incremental.js +212 -0
- package/editions/pr/plugin/index.html +195 -0
- package/editions/pr/plugin/index.js +710 -0
- package/editions/pr/plugin/manifest.json +45 -0
- package/editions/pr/plugin/organize.js +223 -0
- package/editions/pr/skills/score-video/SKILL.md +69 -0
- package/editions/pr/skills/score-video/scripts/mix.py +112 -0
- package/editions/pr/skills/score-video/scripts/place.js +63 -0
- package/lib/ae.js +754 -0
- package/lib/common.js +132 -0
- package/lib/install.js +148 -0
- package/lib/main.js +107 -0
- package/lib/payload.js +60 -0
- package/lib/pr.js +239 -0
- package/lib/update.js +118 -0
- package/package.json +35 -4
- package/skills/create-video/SKILL.md +72 -0
- package/skills/create-video/references/lessons.md +31 -0
- package/skills/create-video/references/project-conventions.md +46 -0
- package/skills/create-video/scripts/ae/list-project.jsx +8 -0
- package/skills/create-video/scripts/ae/move-chips.jsx +14 -0
- package/skills/create-video/scripts/ae/organize-project.jsx +25 -0
- package/skills/create-video/scripts/ae/quadrant-chips.jsx +143 -0
- package/skills/create-video/scripts/ae/render-comps.jsx +16 -0
- package/skills/create-video/scripts/premiere/export-sequences.js +22 -0
- package/skills/create-video/scripts/premiere/make-sequences.js +24 -0
- package/skills/create-video/scripts/premiere/swap-media.js +43 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"name": "Dan Mace scheme",
|
|
4
|
+
"help": [
|
|
5
|
+
"bins: the structure. 'path' uses / for sub-bins. 'meaning' is documentation for people. 'create: true' = the Organize button creates it when missing.",
|
|
6
|
+
"archive: bin for things added but not used. Items are only moved there on request, never deleted.",
|
|
7
|
+
"protect: bins whose contents are never moved by organizing (the archive is a deliberate decision).",
|
|
8
|
+
"rules: checked in order, the first match wins. 'when' can have: kind (sequence | media | generated), name (regex on the item name),",
|
|
9
|
+
" path (regex on the media file path, case-insensitive), ext (list of file extensions without the dot). 'bin' = destination, 'skip: true' = leave alone.",
|
|
10
|
+
" Items no rule matches are reported as 'needs a decision' and not moved.",
|
|
11
|
+
"auto-filing (after every job) only files NEW loose items at the project root; the Organize button also corrects items in the wrong scheme bin."
|
|
12
|
+
],
|
|
13
|
+
"archive": "99_ARCHIVE",
|
|
14
|
+
"protect": [
|
|
15
|
+
"99_ARCHIVE"
|
|
16
|
+
],
|
|
17
|
+
"bins": [
|
|
18
|
+
{
|
|
19
|
+
"path": "1_MASTER",
|
|
20
|
+
"create": true,
|
|
21
|
+
"meaning": "The deliverables (what gets exported). Several allowed, one per format. Name them 'MASTER - Title (Format)'."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"path": "2_FOOTAGE/IMAGES",
|
|
25
|
+
"create": true,
|
|
26
|
+
"meaning": "Stills to insert somewhere."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"path": "2_FOOTAGE/RAW",
|
|
30
|
+
"create": true,
|
|
31
|
+
"meaning": "Camera recordings and other raw source video."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"path": "2_FOOTAGE/RENDERS",
|
|
35
|
+
"create": true,
|
|
36
|
+
"meaning": "Video rendered elsewhere (e.g. After Effects renders) and used as source."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"path": "2_FOOTAGE/GRADED",
|
|
40
|
+
"create": true,
|
|
41
|
+
"meaning": "Graded versions of the raw footage."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"path": "3_SELECTS",
|
|
45
|
+
"create": true,
|
|
46
|
+
"meaning": "Sequences that are scenes / parts of the video, later merged into the master. Name them 'SEL nn - <scene>'."
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"path": "4_INTERVIEW",
|
|
50
|
+
"create": false,
|
|
51
|
+
"meaning": "Only for podcast / interview projects. Set create to true to use it."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"path": "5_SOUND",
|
|
55
|
+
"create": true,
|
|
56
|
+
"meaning": "Voiceovers, recorded audio, sound effects."
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"path": "6_MUSIC",
|
|
60
|
+
"create": true,
|
|
61
|
+
"meaning": "Music (non-diegetic)."
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"path": "7_LAYERS",
|
|
65
|
+
"create": true,
|
|
66
|
+
"meaning": "Adjustment layers, mattes and other generated layers."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"path": "99_ARCHIVE",
|
|
70
|
+
"create": true,
|
|
71
|
+
"meaning": "Things added but not used in the project."
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"rules": [
|
|
75
|
+
{
|
|
76
|
+
"when": {
|
|
77
|
+
"kind": "sequence",
|
|
78
|
+
"name": " Copy( \\d+)?$"
|
|
79
|
+
},
|
|
80
|
+
"skip": true,
|
|
81
|
+
"note": "scratch clones"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"when": {
|
|
85
|
+
"kind": "sequence",
|
|
86
|
+
"name": "^Sequence Template$"
|
|
87
|
+
},
|
|
88
|
+
"skip": true,
|
|
89
|
+
"note": "the template for new sequences"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"when": {
|
|
93
|
+
"kind": "sequence",
|
|
94
|
+
"name": "^MASTER\\b"
|
|
95
|
+
},
|
|
96
|
+
"bin": "1_MASTER"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"when": {
|
|
100
|
+
"kind": "sequence",
|
|
101
|
+
"name": "^SEL\\b"
|
|
102
|
+
},
|
|
103
|
+
"bin": "3_SELECTS"
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"when": {
|
|
107
|
+
"kind": "generated"
|
|
108
|
+
},
|
|
109
|
+
"bin": "7_LAYERS",
|
|
110
|
+
"note": "items with no media file: adjustment layers, mattes, bars"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"when": {
|
|
114
|
+
"kind": "media",
|
|
115
|
+
"ext": [
|
|
116
|
+
"mp4",
|
|
117
|
+
"mov",
|
|
118
|
+
"mxf",
|
|
119
|
+
"m4v",
|
|
120
|
+
"avi",
|
|
121
|
+
"mkv",
|
|
122
|
+
"r3d",
|
|
123
|
+
"braw",
|
|
124
|
+
"webm",
|
|
125
|
+
"mts"
|
|
126
|
+
],
|
|
127
|
+
"path": "/graded/"
|
|
128
|
+
},
|
|
129
|
+
"bin": "2_FOOTAGE/GRADED"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"when": {
|
|
133
|
+
"kind": "media",
|
|
134
|
+
"ext": [
|
|
135
|
+
"mp4",
|
|
136
|
+
"mov",
|
|
137
|
+
"mxf",
|
|
138
|
+
"m4v",
|
|
139
|
+
"avi",
|
|
140
|
+
"mkv",
|
|
141
|
+
"r3d",
|
|
142
|
+
"braw",
|
|
143
|
+
"webm",
|
|
144
|
+
"mts"
|
|
145
|
+
],
|
|
146
|
+
"name": "^V\\d+-\\d+_"
|
|
147
|
+
},
|
|
148
|
+
"bin": "2_FOOTAGE/GRADED",
|
|
149
|
+
"note": "DaVinci Resolve render names"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
"when": {
|
|
153
|
+
"kind": "media",
|
|
154
|
+
"ext": [
|
|
155
|
+
"mp4",
|
|
156
|
+
"mov",
|
|
157
|
+
"mxf",
|
|
158
|
+
"m4v",
|
|
159
|
+
"avi",
|
|
160
|
+
"mkv",
|
|
161
|
+
"r3d",
|
|
162
|
+
"braw",
|
|
163
|
+
"webm",
|
|
164
|
+
"mts"
|
|
165
|
+
],
|
|
166
|
+
"path": "/(ae_motion|renders?|ae_renders?)/"
|
|
167
|
+
},
|
|
168
|
+
"bin": "2_FOOTAGE/RENDERS"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"when": {
|
|
172
|
+
"kind": "media",
|
|
173
|
+
"ext": [
|
|
174
|
+
"mp4",
|
|
175
|
+
"mov",
|
|
176
|
+
"mxf",
|
|
177
|
+
"m4v",
|
|
178
|
+
"avi",
|
|
179
|
+
"mkv",
|
|
180
|
+
"r3d",
|
|
181
|
+
"braw",
|
|
182
|
+
"webm",
|
|
183
|
+
"mts"
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
"bin": "2_FOOTAGE/RAW"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"when": {
|
|
190
|
+
"kind": "media",
|
|
191
|
+
"ext": [
|
|
192
|
+
"png",
|
|
193
|
+
"jpg",
|
|
194
|
+
"jpeg",
|
|
195
|
+
"tif",
|
|
196
|
+
"tiff",
|
|
197
|
+
"psd",
|
|
198
|
+
"ai",
|
|
199
|
+
"gif",
|
|
200
|
+
"bmp",
|
|
201
|
+
"exr",
|
|
202
|
+
"dpx",
|
|
203
|
+
"heic",
|
|
204
|
+
"webp",
|
|
205
|
+
"svg"
|
|
206
|
+
]
|
|
207
|
+
},
|
|
208
|
+
"bin": "2_FOOTAGE/IMAGES"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"when": {
|
|
212
|
+
"kind": "media",
|
|
213
|
+
"ext": [
|
|
214
|
+
"wav",
|
|
215
|
+
"mp3",
|
|
216
|
+
"aif",
|
|
217
|
+
"aiff",
|
|
218
|
+
"m4a",
|
|
219
|
+
"flac",
|
|
220
|
+
"aac",
|
|
221
|
+
"ogg"
|
|
222
|
+
],
|
|
223
|
+
"path": "/(\\d+_)?music/"
|
|
224
|
+
},
|
|
225
|
+
"bin": "6_MUSIC",
|
|
226
|
+
"note": "audio from a music folder"
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"when": {
|
|
230
|
+
"kind": "media",
|
|
231
|
+
"ext": [
|
|
232
|
+
"wav",
|
|
233
|
+
"mp3",
|
|
234
|
+
"aif",
|
|
235
|
+
"aiff",
|
|
236
|
+
"m4a",
|
|
237
|
+
"flac",
|
|
238
|
+
"aac",
|
|
239
|
+
"ogg"
|
|
240
|
+
]
|
|
241
|
+
},
|
|
242
|
+
"bin": "5_SOUND"
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// The defined set of ACTIONS a bridge job can be. This is what the "Action" column of the jobs table shows, so a person can tell at a glance
|
|
2
|
+
// what happened to the project. Each job has exactly one action id.
|
|
3
|
+
//
|
|
4
|
+
// How a job gets its action:
|
|
5
|
+
// 1. It declares one: a header line in the job script `// @action score.place`, or `aeb run ... --action score.place`.
|
|
6
|
+
// 2. Otherwise the panel infers it from what the script does (see inferAction); old jobs are classified the same way when listed.
|
|
7
|
+
// 3. If nothing fits it is "custom" (Custom action). Prefer declaring an action; add a new entry here when a new kind of job appears.
|
|
8
|
+
// Labels and summaries are display text only: renaming one here renames it for every past and future job.
|
|
9
|
+
|
|
10
|
+
const ACTIONS = {
|
|
11
|
+
"bridge.test": { label: "Playrig test", summary: "Checked that Playrig can run a job. No project changes." },
|
|
12
|
+
"project.inspect": { label: "Inspect project", summary: "Read sequences, tracks, clips or bins. No project changes." },
|
|
13
|
+
"frame.export": { label: "Export frames", summary: "Exported still frames from a sequence." },
|
|
14
|
+
"media.import": { label: "Import media", summary: "Imported files into the project." },
|
|
15
|
+
"sequence.clone": { label: "Duplicate sequence", summary: "Made a copy of a sequence." },
|
|
16
|
+
"sequence.rename": { label: "Rename sequence", summary: "Renamed a sequence or bin." },
|
|
17
|
+
"sequence.delete": { label: "Delete sequence", summary: "Removed a sequence from the project." },
|
|
18
|
+
"clip.place": { label: "Place clips", summary: "Added clips to a sequence." },
|
|
19
|
+
"clip.edit": { label: "Edit clips", summary: "Moved, trimmed or removed clips on the timeline." },
|
|
20
|
+
"score.place": { label: "Add music and sound effects", summary: "Imported and placed music and sound effects on a sequence." },
|
|
21
|
+
"score.sync": { label: "Re-time sound effects", summary: "Moved, swapped or removed bridge-owned sound clips to match a new timing." },
|
|
22
|
+
"audio.level": { label: "Adjust audio levels", summary: "Changed clip volume or its automation." },
|
|
23
|
+
"audio.effect": { label: "Add audio effect", summary: "Added an audio effect (such as reverb) to a clip." },
|
|
24
|
+
"organize.project": { label: "Organize project", summary: "Created bins and filed items into the project's bin structure." },
|
|
25
|
+
"organize.archive": { label: "Archive unused items", summary: "Moved unused items into the archive bin." },
|
|
26
|
+
"organize.structure":{ label: "Update organization structure", summary: "Wrote or changed the project's bin structure file." },
|
|
27
|
+
"scratch.baseline": { label: "Check test items", summary: "Recorded or listed the temporary items created while experimenting. No project changes." },
|
|
28
|
+
"scratch.cleanup": { label: "Clean up test items", summary: "Removed temporary sequences and items created while experimenting." },
|
|
29
|
+
"job.rejected": { label: "Rejected job", summary: "The job was refused before it ran." },
|
|
30
|
+
"custom": { label: "Custom action", summary: "A job with no declared action." }
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const actionInfo = (id) => ACTIONS[id] || ACTIONS.custom;
|
|
34
|
+
|
|
35
|
+
// First `// ...` lines of a job script: `// @action <id>` declares the action; the first other comment line is the description.
|
|
36
|
+
function parseJobHeader(src) {
|
|
37
|
+
const out = { action: null, description: null };
|
|
38
|
+
for (const line of String(src || "").split(/\r\n|\r|\n/).slice(0, 10)) {
|
|
39
|
+
const m = /^\s*\/\/\s?(.*)$/.exec(line);
|
|
40
|
+
if (!m) { if (line.trim()) break; else continue; }
|
|
41
|
+
const t = m[1].trim(), a = /^@action\s+([A-Za-z0-9_.-]+)/.exec(t);
|
|
42
|
+
if (a) out.action = a[1]; else if (t && !out.description) out.description = t.slice(0, 240);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Best guess from what the script calls. Order matters: the first rule that matches wins. Unknown declared ids are treated as "custom".
|
|
48
|
+
function inferAction(src, id) {
|
|
49
|
+
let s = String(src || "");
|
|
50
|
+
if (/^scratch-clean/i.test(String(id || ""))) return "scratch.cleanup";
|
|
51
|
+
if (/^scratch-(start|status)/i.test(String(id || ""))) return "scratch.baseline";
|
|
52
|
+
// jobs that inline the organizer library mention every API name in it: judge only the part after the library
|
|
53
|
+
const lib = s.lastIndexOf("return { organizeScan");
|
|
54
|
+
if (lib >= 0) { const end = s.indexOf("\n}", lib); if (end >= 0) s = s.slice(end + 2); }
|
|
55
|
+
const rules = [
|
|
56
|
+
[/deleteSequence/, "sequence.delete"],
|
|
57
|
+
[/createCloneAction/, "sequence.clone"],
|
|
58
|
+
[/writeStructure/, "organize.structure"],
|
|
59
|
+
[/moveItem\([^)]*99_ARCHIVE|99_ARCHIVE[^\n]*moveItem\(/, "organize.archive"],
|
|
60
|
+
[/organizeProject|fileStrays|createBinAction|createMoveItemAction|ensureBin|moveItem\(/, "organize.project"],
|
|
61
|
+
[/importFiles|importInto/, null],
|
|
62
|
+
[/(?:PLAYRIG|PLAYPILOT|BRIDGE)\.sync\(|(?:PLAYRIG|PLAYPILOT|BRIDGE)\.adopt\(/, "score.sync"],
|
|
63
|
+
[/AudioFilterFactory|createAppendComponentAction/, "audio.effect"],
|
|
64
|
+
[/levelKeys|setLevelDb|createAddKeyframeAction|createSetTimeVaryingAction/, "audio.level"],
|
|
65
|
+
[/createOverwriteItemAction|(?:PLAYRIG|PLAYPILOT|BRIDGE)\.place\(/, "clip.place"],
|
|
66
|
+
[/createSetNameAction/, "sequence.rename"],
|
|
67
|
+
[/createMoveAction|createSetInPointAction|createSetOutPointAction|createSetEndAction|createRemoveItemsAction|(?:PLAYRIG|PLAYPILOT|BRIDGE)\.remove\(/, "clip.edit"],
|
|
68
|
+
[/(?:PLAYRIG|PLAYPILOT|BRIDGE)\.inspect|getSequences\(|getTrackItems|getVideoTrack|getAudioTrack|getRootItem/, "project.inspect"],
|
|
69
|
+
[/hi from Premiere|(?:PLAYRIG|PLAYPILOT|BRIDGE)\.log\(["']hi/, "bridge.test"],
|
|
70
|
+
[/lstat|readdir|openPath|getMedia\(\)/, "project.inspect"]
|
|
71
|
+
];
|
|
72
|
+
for (const [re, act, also] of rules) {
|
|
73
|
+
if (!re.test(s)) continue;
|
|
74
|
+
if (also && !also.test(s)) continue;
|
|
75
|
+
if (act === null) return /createOverwriteItemAction|(?:PLAYRIG|PLAYPILOT|BRIDGE)\.place\(/.test(s) ? "score.place" : "media.import";
|
|
76
|
+
return act;
|
|
77
|
+
}
|
|
78
|
+
return "custom";
|
|
79
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// DEV ONLY. Developer-facing extras for the panel. This whole file (and its <script> line in index.html) is left out of packaged builds:
|
|
2
|
+
// index.js checks `typeof DEV !== "undefined"` before using anything here, so removing the file removes these features without other changes.
|
|
3
|
+
// Currently: the "Job script" section in the job detail window (the source of the job as it was run, for debugging jobs).
|
|
4
|
+
|
|
5
|
+
const DEV = {
|
|
6
|
+
// Called by openJobDetail() with { body, el, job, ws, readText }; appends dev-only sections to the detail window.
|
|
7
|
+
async jobDetail({ body, el, job, ws, readText }) {
|
|
8
|
+
if (!ws) return;
|
|
9
|
+
let script = null;
|
|
10
|
+
try { script = await readText(ws + "/jobs/" + job.id + ".js"); } catch (e) { }
|
|
11
|
+
if (script) {
|
|
12
|
+
const lines = script.split(/\r\n|\r|\n/);
|
|
13
|
+
body.appendChild(el("div", "jd-h", "Job script (dev only, " + lines.length + " lines)"));
|
|
14
|
+
body.appendChild(el("div", "jd-pre", lines.slice(0, 60).join("\n") + (lines.length > 60 ? "\n... " + (lines.length - 60) + " more lines in jobs/" + job.id + ".js" : "")));
|
|
15
|
+
}
|
|
16
|
+
body.appendChild(el("div", "jd-h", "Script file (dev only)"));
|
|
17
|
+
body.appendChild(el("div", "jd-pre", ws + "/jobs/" + job.id + ".js"));
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
Icons are from Tabler Icons (https://tabler.io/icons), MIT License, (c) 2020-2024 Pawel Kuna. Outline set v3.34.1.
|
|
2
|
+
Every path carries its own fill="none" and stroke attributes: the UXP SVG renderer ignores the ones Tabler puts on the root <svg> and fills shapes black. The stroke colour is baked in too (currentColor does not resolve in an image).
|
|
3
|
+
Settings uses stroke-width 1; the power icon is power-off.svg (gray) and power-on.svg (green, stroke #3fb950), swapped by the panel.
|
|
4
|
+
To add an icon: curl https://cdn.jsdelivr.net/npm/@tabler/icons@3.34.1/icons/outline/<name>.svg, then move fill="none", stroke, stroke-width, stroke-linecap and stroke-linejoin onto every <path> and drop the 24x24 bounding-box path.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
+
<path d="M14 3v4a1 1 0 0 0 1 1h4" fill="none" stroke="#c8c8c8" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" fill="none" stroke="#c8c8c8" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M10 13l-1 2l1 2" fill="none" stroke="#c8c8c8" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
<path d="M14 13l1 2l-1 2" fill="none" stroke="#c8c8c8" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
+
<path d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2" fill="none" stroke="#c8c8c8" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
+
<path d="M7 6a7.75 7.75 0 1 0 10 0" fill="none" stroke="#8c8c8c" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M12 4l0 8" fill="none" stroke="#8c8c8c" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
+
<path d="M7 6a7.75 7.75 0 1 0 10 0" fill="none" stroke="#3fb950" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M12 4l0 8" fill="none" stroke="#3fb950" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
+
<path d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z" fill="none" stroke="#c8c8c8" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" fill="none" stroke="#c8c8c8" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
+
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" fill="none" stroke="#f85149" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M10 10l4 4m0 -4l-4 4" fill="none" stroke="#f85149" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
+
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" fill="none" stroke="#3fb950" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M9 12l2 2l4 -4" fill="none" stroke="#3fb950" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
+
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" fill="none" stroke="#f0883e" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M5.7 5.7l12.6 12.6" fill="none" stroke="#f0883e" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
+
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" fill="none" stroke="#e3b341" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M12 7v5l3 3" fill="none" stroke="#e3b341" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
|
2
|
+
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" fill="none" stroke="#8c8c8c" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
|
+
<path d="M12 16v.01" fill="none" stroke="#8c8c8c" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M12 13a2 2 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483" fill="none" stroke="#8c8c8c" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Incremental editing helpers for jobs (PLAYRIG.owned / sync / adopt / remove / place / levelKeys ...).
|
|
2
|
+
// Premiere counterpart of the the AE edition's "--replace": the bridge tags the clips it creates, and a re-run changes only
|
|
3
|
+
// those clips, in place (move / swap / add / remove), instead of deleting a track or sequence and rebuilding it.
|
|
4
|
+
//
|
|
5
|
+
// Ownership tag = a suffix on the clip NAME: "pop_glass_ui_lv.wav [pb:sfx:chip3]" -> family "sfx", key "chip3".
|
|
6
|
+
// (Premiere track items have no comment field; the name follows the clip when it is moved or trimmed.)
|
|
7
|
+
// Everything here needs the premierepro module and the open project; it never touches clips without a tag.
|
|
8
|
+
|
|
9
|
+
function makeIncremental(ppro, project, B) {
|
|
10
|
+
const TAG_RE = /\s*\[pb:([A-Za-z0-9_-]+):([A-Za-z0-9_-]+)\]\s*$/;
|
|
11
|
+
const SNAP = 0.05; // seconds; Premiere snaps placements to frames (1 frame at 24 fps = 0.042 s)
|
|
12
|
+
const UNITY = 0.17782799899578094; // Volume > Level value for 0 dB on Premiere's scale
|
|
13
|
+
const tk = (s) => ppro.TickTime.createWithSeconds(s);
|
|
14
|
+
const sec = (t) => Math.round(t.seconds * 1000) / 1000;
|
|
15
|
+
const ID_RE = /^[A-Za-z0-9_-]{1,32}$/;
|
|
16
|
+
|
|
17
|
+
const tagOf = (family, key) => {
|
|
18
|
+
if (!ID_RE.test(family) || !ID_RE.test(key)) throw new Error("BRIDGE tag: family and key must match ^[A-Za-z0-9_-]{1,32}$ (got '" + family + "', '" + key + "')");
|
|
19
|
+
return "[pb:" + family + ":" + key + "]";
|
|
20
|
+
};
|
|
21
|
+
const parse = (name) => { const m = TAG_RE.exec(name || ""); return m ? { family: m[1], key: m[2], base: String(name).replace(TAG_RE, "") } : null; };
|
|
22
|
+
const trackCount = async (seq, kind) => (kind === "video" ? seq.getVideoTrackCount() : seq.getAudioTrackCount());
|
|
23
|
+
const getTrack = async (seq, kind, i) => (kind === "video" ? seq.getVideoTrack(i) : seq.getAudioTrack(i));
|
|
24
|
+
|
|
25
|
+
// Every clip on every track as { kind, track, clip, name, start, end, tag } (tag = parsed ownership or null).
|
|
26
|
+
async function clips(seq) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const kind of ["video", "audio"]) {
|
|
29
|
+
const n = await trackCount(seq, kind);
|
|
30
|
+
for (let i = 0; i < n; i++) {
|
|
31
|
+
for (const clip of await (await getTrack(seq, kind, i)).getTrackItems(1, false)) {
|
|
32
|
+
const name = await clip.getName();
|
|
33
|
+
out.push({ kind, track: i, clip, name, start: sec(await clip.getStartTime()), end: sec(await clip.getEndTime()), tag: parse(name) });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Clips this bridge owns in `seq`, optionally only one family.
|
|
41
|
+
async function owned(seq, family) {
|
|
42
|
+
return (await clips(seq)).filter((c) => c.tag && (!family || c.tag.family === family))
|
|
43
|
+
.map((c) => ({ kind: c.kind, track: c.track, clip: c.clip, family: c.tag.family, key: c.tag.key, base: c.tag.base, start: c.start, end: c.end }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function rename(clip, name) {
|
|
47
|
+
await B.transact("Playrig: tag " + name, (ca) => { ca.addAction(clip.createSetNameAction(name)); });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Tag an existing clip (adopt it) or re-tag an owned one.
|
|
51
|
+
async function tag(clip, family, key) {
|
|
52
|
+
const p = parse(await clip.getName());
|
|
53
|
+
const base = p ? p.base : await clip.getName();
|
|
54
|
+
await rename(clip, base + " " + tagOf(family, key));
|
|
55
|
+
}
|
|
56
|
+
async function untag(clip) {
|
|
57
|
+
const p = parse(await clip.getName());
|
|
58
|
+
if (p) await rename(clip, p.base);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Tag untagged clips that match `select(info)` as family/key; key = keyOf(info, indexInMatch) (default family + order).
|
|
62
|
+
// Use once to bring clips placed by hand or by an earlier job under management.
|
|
63
|
+
async function adopt(seq, family, select, keyOf) {
|
|
64
|
+
const hits = (await clips(seq)).filter((c) => !c.tag && select(c)).sort((a, b) => a.start - b.start);
|
|
65
|
+
const keys = [];
|
|
66
|
+
for (let i = 0; i < hits.length; i++) {
|
|
67
|
+
const key = keyOf ? keyOf(hits[i], i) : family + (i + 1);
|
|
68
|
+
await tag(hits[i].clip, family, key);
|
|
69
|
+
keys.push(key + "@" + hits[i].start);
|
|
70
|
+
}
|
|
71
|
+
return keys;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Remove clips (items from owned()/clips()) without rippling; gaps stay where they were.
|
|
75
|
+
async function remove(seq, items) {
|
|
76
|
+
if (!items.length) return 0;
|
|
77
|
+
const ed = ppro.SequenceEditor.getEditor(seq);
|
|
78
|
+
await B.transact("Playrig: remove " + items.length + " owned clip(s)", (ca) => {
|
|
79
|
+
ppro.TrackItemSelection.createEmptySelection((sel) => {
|
|
80
|
+
for (const it of items) sel.addItem(it.clip, true);
|
|
81
|
+
ca.addAction(ed.createRemoveItemsAction(sel, false, ppro.Constants.MediaType.ANY, false));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
return items.length;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Source length of a project item in seconds, or null when Premiere will not say (still images, sequences).
|
|
88
|
+
async function lenOf(item) {
|
|
89
|
+
try { const m = await ppro.ClipProjectItem.cast(item).getMedia(); const d = await m.getDuration(); return d && d.seconds !== undefined ? d.seconds : null; }
|
|
90
|
+
catch (e) { return null; }
|
|
91
|
+
}
|
|
92
|
+
// Overlaps among ranges [{label, kind, track, start, end}] on the same track (touching ends are fine; frame snapping gets 20 ms slack).
|
|
93
|
+
function overlaps(ranges) {
|
|
94
|
+
const out = [], by = {};
|
|
95
|
+
for (const r of ranges) (by[r.kind + r.track] = by[r.kind + r.track] || []).push(r);
|
|
96
|
+
for (const k in by) {
|
|
97
|
+
const rs = by[k].sort((a, b) => a.start - b.start);
|
|
98
|
+
for (let i = 1; i < rs.length; i++) if (rs[i].start < rs[i - 1].end - 0.02) out.push(rs[i - 1].label + " (" + rs[i - 1].start + "-" + rs[i - 1].end + ") overlaps " + rs[i].label + " (" + rs[i].start + "-" + rs[i].end + ") on " + rs[i].kind + " " + (rs[i].track + 1));
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Place project item at `start` s and tag it. opts: { kind:'audio'|'video', track:0, audioTrack:0 (for video items with audio), family, key }
|
|
104
|
+
// Premiere overwrites whatever is there; clips this would overwrite that are not ours are listed in the result.
|
|
105
|
+
async function place(seq, item, start, opts) {
|
|
106
|
+
const kind = opts.kind || "audio", track = opts.track || 0, tag_ = tagOf(opts.family, opts.key);
|
|
107
|
+
const ed = ppro.SequenceEditor.getEditor(seq);
|
|
108
|
+
const vIdx = kind === "video" ? track : 0, aIdx = kind === "audio" ? track : (opts.audioTrack || 0);
|
|
109
|
+
const before = (await clips(seq)).filter((c) => c.kind === kind && c.track === track);
|
|
110
|
+
if (!opts.allowOverwrite) {
|
|
111
|
+
const len = await lenOf(item);
|
|
112
|
+
const hit = len === null ? [] : before.filter((c) => c.start < start + len - 0.02 && c.end > start + 0.02);
|
|
113
|
+
if (hit.length) throw new Error("PLAYRIG.place: would overwrite " + hit.map((c) => c.name + " (" + c.start + "-" + c.end + ")").join(", ") + " on " + kind + " " + (track + 1) + "; free the space, use another track, or pass allowOverwrite:true");
|
|
114
|
+
}
|
|
115
|
+
await B.transact("Playrig: place " + tag_, (ca) => { ca.addAction(ed.createOverwriteItemAction(item, tk(start), vIdx, aIdx)); });
|
|
116
|
+
const itemId = String(await item.getId());
|
|
117
|
+
const after = (await clips(seq)).filter((c) => c.kind === kind && c.track === track);
|
|
118
|
+
const known = new Set(before.map((c) => c.start + "|" + c.end + "|" + c.name));
|
|
119
|
+
let fresh = null, bestD = 1e9;
|
|
120
|
+
for (const c of after) {
|
|
121
|
+
if (c.tag || known.has(c.start + "|" + c.end + "|" + c.name)) continue;
|
|
122
|
+
const d = Math.abs(c.start - start);
|
|
123
|
+
if (d <= SNAP && d < bestD && String(await (await c.clip.getProjectItem()).getId()) === itemId) { fresh = c; bestD = d; }
|
|
124
|
+
}
|
|
125
|
+
if (!fresh) throw new Error("PLAYRIG.place: could not find the clip just placed at " + start + " s on " + kind + " " + (track + 1));
|
|
126
|
+
await rename(fresh.clip, fresh.name + " " + tag_);
|
|
127
|
+
return { start: fresh.start, end: fresh.end, kind, track };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Desired-state sync for one family of clips. desired = [{ key, item, start, kind?, track?, audioTrack? }].
|
|
131
|
+
// - same key, same source, same track, start within a frame -> unchanged
|
|
132
|
+
// - same key, start differs -> moved in place (no delete)
|
|
133
|
+
// - same key, different source or track -> replaced (old removed, new placed at the new spot)
|
|
134
|
+
// - key not owned yet -> placed + tagged
|
|
135
|
+
// - owned key not in desired -> removed
|
|
136
|
+
// opts.dryRun returns the plan without changing anything. Running the same call twice is a no-op.
|
|
137
|
+
async function sync(seq, family, desired, opts) {
|
|
138
|
+
opts = opts || {};
|
|
139
|
+
const seen = {};
|
|
140
|
+
for (const d of desired) { tagOf(family, d.key); if (seen[d.key]) throw new Error("PLAYRIG.sync: duplicate key '" + d.key + "'"); seen[d.key] = 1; }
|
|
141
|
+
const have = {};
|
|
142
|
+
for (const o of await owned(seq, family)) have[o.key] = o;
|
|
143
|
+
const plan = { unchanged: [], moved: [], replaced: [], placed: [], removed: [] };
|
|
144
|
+
const moves = [], swaps = [], adds = [];
|
|
145
|
+
for (const d of desired) {
|
|
146
|
+
const kind = d.kind || "audio", track = d.track || 0, o = have[d.key];
|
|
147
|
+
delete have[d.key];
|
|
148
|
+
if (!o) { adds.push(d); plan.placed.push(d.key + " @ " + d.start); continue; }
|
|
149
|
+
const sameItem = String(await (await o.clip.getProjectItem()).getId()) === String(await d.item.getId());
|
|
150
|
+
if (!sameItem || o.kind !== kind || o.track !== track) { swaps.push({ o, d }); plan.replaced.push(d.key + " -> " + d.start); }
|
|
151
|
+
else if (Math.abs(o.start - d.start) > SNAP) { moves.push({ o, d, delta: d.start - o.start }); plan.moved.push(d.key + ": " + o.start + " -> " + d.start); }
|
|
152
|
+
else plan.unchanged.push(d.key);
|
|
153
|
+
}
|
|
154
|
+
const extras = Object.keys(have).map((k) => have[k]);
|
|
155
|
+
for (const e of extras) plan.removed.push(e.key + " @ " + e.start);
|
|
156
|
+
// Final layout check, before anything changes: nothing may end up overwriting another clip.
|
|
157
|
+
{
|
|
158
|
+
const gone = new Set(extras.concat(swaps.map((s) => s.o)).map((o) => o.clip));
|
|
159
|
+
const moved = new Map(moves.map((m) => [m.o.clip, m.d.start]));
|
|
160
|
+
const final = [];
|
|
161
|
+
for (const c of await clips(seq)) {
|
|
162
|
+
if (gone.has(c.clip)) continue;
|
|
163
|
+
const st = moved.has(c.clip) ? moved.get(c.clip) : c.start;
|
|
164
|
+
final.push({ label: c.name, kind: c.kind, track: c.track, start: st, end: st + (c.end - c.start) });
|
|
165
|
+
}
|
|
166
|
+
for (const x of swaps.map((s) => s.d).concat(adds)) {
|
|
167
|
+
const len = await lenOf(x.item);
|
|
168
|
+
if (len === null) { plan.unchecked = (plan.unchecked || []).concat(x.key + " (length unknown)"); continue; }
|
|
169
|
+
final.push({ label: "new " + x.key, kind: x.kind || "audio", track: x.track || 0, start: x.start, end: x.start + len });
|
|
170
|
+
}
|
|
171
|
+
plan.collisions = overlaps(final);
|
|
172
|
+
}
|
|
173
|
+
if (opts.dryRun) return plan;
|
|
174
|
+
if (plan.collisions.length) throw new Error("PLAYRIG.sync refused, nothing was changed:\n " + plan.collisions.join("\n "));
|
|
175
|
+
// 1. remove what is gone or about to be swapped, 2. move (away from the direction of travel first so clips never land on each other), 3. place.
|
|
176
|
+
await remove(seq, extras.concat(swaps.map((s) => s.o)));
|
|
177
|
+
const pos = moves.filter((m) => m.delta > 0).sort((a, b) => b.o.start - a.o.start), neg = moves.filter((m) => m.delta < 0).sort((a, b) => a.o.start - b.o.start);
|
|
178
|
+
for (const m of pos.concat(neg)) await B.transact("Playrig: move " + m.d.key, (ca) => { ca.addAction(m.o.clip.createMoveAction(tk(m.delta))); });
|
|
179
|
+
for (const x of swaps.map((s) => s.d).concat(adds)) await place(seq, x.item, x.start, { kind: x.kind || "audio", track: x.track || 0, audioTrack: x.audioTrack, family, key: x.key, allowOverwrite: true });
|
|
180
|
+
return plan;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---- audio level helpers. Premiere's scale: 0 dB = 0.1778. Keyframe times are SOURCE time (clip in-point based), not timeline time.
|
|
184
|
+
const dbToLevel = (db) => UNITY * Math.pow(10, db / 20);
|
|
185
|
+
const levelToDb = (v) => 20 * Math.log10(v / UNITY);
|
|
186
|
+
async function levelParam(clip) { return (await clip.getComponentChain()).getComponentAtIndex(0).getParam(1); }
|
|
187
|
+
|
|
188
|
+
// Static level, replacing any keyframes.
|
|
189
|
+
async function setLevelDb(clip, db) {
|
|
190
|
+
const lv = await levelParam(clip);
|
|
191
|
+
await clearKeys(lv);
|
|
192
|
+
await B.transact("Playrig: level " + db + " dB", (ca) => { ca.addAction(lv.createSetValueAction(lv.createKeyframe(dbToLevel(db)))); });
|
|
193
|
+
}
|
|
194
|
+
async function clearKeys(lv) {
|
|
195
|
+
const keys = await lv.getKeyframeListAsTickTimes();
|
|
196
|
+
if (!keys.length && !(await lv.isTimeVarying())) return;
|
|
197
|
+
await B.transact("Playrig: clear level keys", (ca) => { for (const k of keys) ca.addAction(lv.createRemoveKeyframeAction(k)); ca.addAction(lv.createSetTimeVaryingAction(false)); });
|
|
198
|
+
}
|
|
199
|
+
// Level automation as absolute state: points = [[timelineSeconds, dB], ...]; existing keys are removed first, so re-running is safe.
|
|
200
|
+
async function levelKeys(clip, points) {
|
|
201
|
+
const lv = await levelParam(clip);
|
|
202
|
+
const shift = (await clip.getInPoint()).seconds - (await clip.getStartTime()).seconds; // source time = timeline time + shift
|
|
203
|
+
await clearKeys(lv);
|
|
204
|
+
await B.transact("Playrig: level keys", (ca) => {
|
|
205
|
+
ca.addAction(lv.createSetTimeVaryingAction(true));
|
|
206
|
+
for (const p of points) { const k = lv.createKeyframe(dbToLevel(p[1])); k.position = tk(p[0] + shift); ca.addAction(lv.createAddKeyframeAction(k)); }
|
|
207
|
+
});
|
|
208
|
+
return points.length;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { tag, untag, tagOf, clips, owned, adopt, remove, place, sync, lenOf, dbToLevel, levelToDb, setLevelDb, levelKeys };
|
|
212
|
+
}
|