videobook-engine 2.0.1 → 4.2.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/README.md +87 -41
- package/dist/artifact-names.d.ts +6 -0
- package/dist/artifact-names.d.ts.map +1 -0
- package/dist/artifact-names.js +56 -0
- package/dist/artifact-names.js.map +1 -0
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +57 -23
- package/dist/artifacts.js.map +1 -1
- package/dist/books.js +2 -1
- package/dist/books.js.map +1 -1
- package/dist/cas.d.ts +27 -1
- package/dist/cas.d.ts.map +1 -1
- package/dist/cas.js +78 -6
- package/dist/cas.js.map +1 -1
- package/dist/catalog-metadata.d.ts +56 -0
- package/dist/catalog-metadata.d.ts.map +1 -0
- package/dist/catalog-metadata.js +42 -0
- package/dist/catalog-metadata.js.map +1 -0
- package/dist/communications.js +4 -2
- package/dist/communications.js.map +1 -1
- package/dist/context.d.ts +6 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +35 -6
- package/dist/context.js.map +1 -1
- package/dist/domain.d.ts +4 -0
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +870 -58
- package/dist/domain.js.map +1 -1
- package/dist/edit-policy.d.ts +3 -0
- package/dist/edit-policy.d.ts.map +1 -0
- package/dist/edit-policy.js +6 -0
- package/dist/edit-policy.js.map +1 -0
- package/dist/edits.d.ts +10 -0
- package/dist/edits.d.ts.map +1 -0
- package/dist/edits.js +1392 -0
- package/dist/edits.js.map +1 -0
- package/dist/engine-types.d.ts +110 -60
- package/dist/engine-types.d.ts.map +1 -1
- package/dist/engine-types.js +33 -0
- package/dist/engine-types.js.map +1 -1
- package/dist/engine.d.ts +69 -8
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +24 -18
- package/dist/engine.js.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +31 -27
- package/dist/files.js.map +1 -1
- package/dist/fork.d.ts +147 -0
- package/dist/fork.d.ts.map +1 -0
- package/dist/fork.js +609 -0
- package/dist/fork.js.map +1 -0
- package/dist/history.d.ts +0 -4
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +166 -595
- package/dist/history.js.map +1 -1
- package/dist/index.d.ts +16 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/job-queue.d.ts +2 -0
- package/dist/job-queue.d.ts.map +1 -1
- package/dist/job-queue.js +29 -2
- package/dist/job-queue.js.map +1 -1
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +1 -0
- package/dist/media.js.map +1 -1
- package/dist/merge-policy.d.ts +99 -0
- package/dist/merge-policy.d.ts.map +1 -0
- package/dist/merge-policy.js +310 -0
- package/dist/merge-policy.js.map +1 -0
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +15 -20
- package/dist/metadata.js.map +1 -1
- package/dist/migration.d.ts +9 -0
- package/dist/migration.d.ts.map +1 -0
- package/dist/migration.js +534 -0
- package/dist/migration.js.map +1 -0
- package/dist/mvp-contract-fixtures.d.ts +244 -0
- package/dist/mvp-contract-fixtures.d.ts.map +1 -0
- package/dist/mvp-contract-fixtures.js +556 -0
- package/dist/mvp-contract-fixtures.js.map +1 -0
- package/dist/mvp-contracts.d.ts +761 -0
- package/dist/mvp-contracts.d.ts.map +1 -0
- package/dist/mvp-contracts.js +59 -0
- package/dist/mvp-contracts.js.map +1 -0
- package/dist/mvp-time.d.ts +54 -0
- package/dist/mvp-time.d.ts.map +1 -0
- package/dist/mvp-time.js +148 -0
- package/dist/mvp-time.js.map +1 -0
- package/dist/notebook/types.d.ts +113 -8
- package/dist/notebook/types.d.ts.map +1 -1
- package/dist/order-keys.d.ts +22 -0
- package/dist/order-keys.d.ts.map +1 -0
- package/dist/order-keys.js +262 -0
- package/dist/order-keys.js.map +1 -0
- package/dist/runtime-services.js +4 -2
- package/dist/runtime-services.js.map +1 -1
- package/dist/schema.d.ts +24 -5
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +644 -132
- package/dist/schema.js.map +1 -1
- package/dist/search-benchmark.d.ts +4 -0
- package/dist/search-benchmark.d.ts.map +1 -0
- package/dist/search-benchmark.js +144 -0
- package/dist/search-benchmark.js.map +1 -0
- package/dist/sequences.d.ts +20 -0
- package/dist/sequences.d.ts.map +1 -0
- package/dist/sequences.js +825 -0
- package/dist/sequences.js.map +1 -0
- package/dist/similarity.js +1 -1
- package/dist/storage.d.ts +3 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +388 -7
- package/dist/storage.js.map +1 -1
- package/dist/store.d.ts +63 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +351 -65
- package/dist/store.js.map +1 -1
- package/dist/streams.d.ts +10 -0
- package/dist/streams.d.ts.map +1 -0
- package/dist/streams.js +183 -0
- package/dist/streams.js.map +1 -0
- package/dist/temporal-models.d.ts +48 -0
- package/dist/temporal-models.d.ts.map +1 -0
- package/dist/temporal-models.js +278 -0
- package/dist/temporal-models.js.map +1 -0
- package/dist/temporal-search.d.ts +29 -0
- package/dist/temporal-search.d.ts.map +1 -0
- package/dist/temporal-search.js +1507 -0
- package/dist/temporal-search.js.map +1 -0
- package/dist/transcripts.d.ts +16 -0
- package/dist/transcripts.d.ts.map +1 -0
- package/dist/transcripts.js +519 -0
- package/dist/transcripts.js.map +1 -0
- package/docs/engine-layout.md +346 -106
- package/docs/mvp-prd.md +918 -0
- package/fixtures/media/vancat.mp4 +0 -0
- package/fixtures/media/vancat_profile.jpg +0 -0
- package/fixtures/v5/contract-fixtures.json +1532 -0
- package/package.json +30 -5
- package/dist/history-types.d.ts +0 -74
- package/dist/history-types.d.ts.map +0 -1
- package/dist/history-types.js +0 -2
- package/dist/history-types.js.map +0 -1
- package/dist/timeline.d.ts +0 -9
- package/dist/timeline.d.ts.map +0 -1
- package/dist/timeline.js +0 -236
- package/dist/timeline.js.map +0 -1
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
import { ok } from "./engine-types.js";
|
|
2
|
+
import { resultOf, syncResultOf } from "./context.js";
|
|
3
|
+
import { assertUuidV7, newUuidV7 } from "./ids.js";
|
|
4
|
+
import { initialOrderKeys, orderKeyAfter, orderKeyBetween, reconcileOrderKeys, } from "./order-keys.js";
|
|
5
|
+
import { normalizeRational, normalizeSourceRange } from "./mvp-time.js";
|
|
6
|
+
import { canonicalJson, parseJson } from "./store.js";
|
|
7
|
+
import { EngineFault } from "./store.js";
|
|
8
|
+
export function createSequencesApi(context) {
|
|
9
|
+
return {
|
|
10
|
+
create: (input) => createSequence(context, input),
|
|
11
|
+
list: () => listSequences(context),
|
|
12
|
+
get: (sequenceId) => syncResultOf(() => requiredSequence(context, sequenceId)),
|
|
13
|
+
getPrimary: () => primarySequence(context),
|
|
14
|
+
getAtRevision: (sequenceId, revision) => syncResultOf(() => requiredSequence(context, sequenceId, revision)),
|
|
15
|
+
rename: (sequenceId, name) => renameSequence(context, sequenceId, name),
|
|
16
|
+
updateCanvas: (sequenceId, input) => updateSequenceCanvas(context, sequenceId, input),
|
|
17
|
+
updateTrack: (trackId, input) => updateTrack(context, trackId, input),
|
|
18
|
+
addTrack: (sequenceId, input) => addTrack(context, sequenceId, input),
|
|
19
|
+
moveTrack: (trackId, toOrdinal) => moveTrack(context, trackId, toOrdinal),
|
|
20
|
+
removeTrack: (trackId) => removeTrack(context, trackId),
|
|
21
|
+
delete: (sequenceId) => deleteSequence(context, sequenceId),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function addTrack(context, sequenceId, input) {
|
|
25
|
+
return resultOf(async () => {
|
|
26
|
+
assertUuidV7(sequenceId, "Sequence ID");
|
|
27
|
+
requiredSequenceRow(context, sequenceId);
|
|
28
|
+
const siblings = context.store.db
|
|
29
|
+
.prepare(`SELECT order_key FROM sequence_tracks
|
|
30
|
+
WHERE sequence_id=? AND kind=?
|
|
31
|
+
ORDER BY order_key, track_id`)
|
|
32
|
+
.all(sequenceId, input.kind);
|
|
33
|
+
const trackId = newUuidV7();
|
|
34
|
+
const ordinal = siblings.length;
|
|
35
|
+
const orderKey = orderKeyAfter(siblings.length > 0 ? siblings[siblings.length - 1].order_key : null);
|
|
36
|
+
const name = input.name === undefined
|
|
37
|
+
? input.kind === "caption"
|
|
38
|
+
? `Captions ${ordinal + 1}`
|
|
39
|
+
: `${input.kind === "video" ? "Video" : "Audio"} ${ordinal + 1}`
|
|
40
|
+
: requiredText(input.name, "Track name");
|
|
41
|
+
const common = {
|
|
42
|
+
trackId,
|
|
43
|
+
sequenceId,
|
|
44
|
+
ordinal,
|
|
45
|
+
name,
|
|
46
|
+
enabled: true,
|
|
47
|
+
locked: false,
|
|
48
|
+
};
|
|
49
|
+
const track = input.kind === "video"
|
|
50
|
+
? { ...common, kind: "video", blendMode: "normal" }
|
|
51
|
+
: input.kind === "audio"
|
|
52
|
+
? { ...common, kind: "audio", muted: false, solo: false }
|
|
53
|
+
: { ...common, kind: "caption" };
|
|
54
|
+
const mutation = await context.store.semantic({
|
|
55
|
+
operation: "add_sequence_track",
|
|
56
|
+
tables: ["sequence_tracks"],
|
|
57
|
+
details: { sequenceId, trackId, kind: input.kind, ordinal },
|
|
58
|
+
writeSet: [`sequence:${sequenceId}`, `track:${trackId}`],
|
|
59
|
+
}, () => insertTracks(context, [{ track, orderKey }]));
|
|
60
|
+
return ok(requiredSequence(context, sequenceId, mutation.revision), mutation.revision);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function moveTrack(context, trackId, toOrdinal) {
|
|
64
|
+
return resultOf(async () => {
|
|
65
|
+
assertUuidV7(trackId, "Track ID");
|
|
66
|
+
if (!Number.isInteger(toOrdinal)) {
|
|
67
|
+
throw new Error("Track ordinal must be an integer");
|
|
68
|
+
}
|
|
69
|
+
const row = requiredTrackRow(context, trackId);
|
|
70
|
+
const siblings = context.store.db
|
|
71
|
+
.prepare(`SELECT track_id, order_key FROM sequence_tracks
|
|
72
|
+
WHERE sequence_id=? AND kind=?
|
|
73
|
+
ORDER BY order_key, track_id`)
|
|
74
|
+
.all(row.sequence_id, row.kind);
|
|
75
|
+
const currentOrdinal = siblings.findIndex((candidate) => candidate.track_id === trackId);
|
|
76
|
+
// A merge can leave siblings with identical order keys (forks mint keys
|
|
77
|
+
// independently; readers tie-break by track_id). The writer path
|
|
78
|
+
// self-repairs via reconcileOrderKeys before computing a between-key,
|
|
79
|
+
// otherwise the position between two duplicates would be unreachable.
|
|
80
|
+
const keyByTrack = new Map(siblings.map((sibling) => [sibling.track_id, sibling.order_key]));
|
|
81
|
+
const repairs = new Map();
|
|
82
|
+
if (new Set(keyByTrack.values()).size !== siblings.length) {
|
|
83
|
+
const reconciled = reconcileOrderKeys(siblings.map((sibling) => sibling.track_id), keyByTrack);
|
|
84
|
+
for (const [id, key] of reconciled) {
|
|
85
|
+
if (keyByTrack.get(id) !== key) {
|
|
86
|
+
repairs.set(id, key);
|
|
87
|
+
keyByTrack.set(id, key);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const others = siblings.filter((candidate) => candidate.track_id !== trackId);
|
|
92
|
+
const target = Math.max(0, Math.min(toOrdinal, others.length));
|
|
93
|
+
if (target === currentOrdinal && repairs.size === 0) {
|
|
94
|
+
return requiredSequence(context, row.sequence_id);
|
|
95
|
+
}
|
|
96
|
+
const keyOf = (index) => keyByTrack.get(others[index].track_id);
|
|
97
|
+
const orderKey = target === others.length
|
|
98
|
+
? orderKeyAfter(others.length > 0 ? keyOf(others.length - 1) : null)
|
|
99
|
+
: orderKeyBetween(target > 0 ? keyOf(target - 1) : null, keyOf(target));
|
|
100
|
+
const mutation = await context.store.semantic({
|
|
101
|
+
operation: "move_sequence_track",
|
|
102
|
+
tables: ["sequence_tracks"],
|
|
103
|
+
details: {
|
|
104
|
+
sequenceId: row.sequence_id,
|
|
105
|
+
trackId,
|
|
106
|
+
kind: row.kind,
|
|
107
|
+
fromOrdinal: currentOrdinal,
|
|
108
|
+
toOrdinal: target,
|
|
109
|
+
...(repairs.size > 0 ? { repairedOrderKeys: repairs.size } : {}),
|
|
110
|
+
},
|
|
111
|
+
writeSet: [`sequence:${row.sequence_id}`, `track:${trackId}`],
|
|
112
|
+
}, () => {
|
|
113
|
+
const update = context.store.db.prepare("UPDATE sequence_tracks SET order_key=? WHERE track_id=?");
|
|
114
|
+
for (const [id, key] of repairs) {
|
|
115
|
+
if (id !== trackId)
|
|
116
|
+
update.run(key, id);
|
|
117
|
+
}
|
|
118
|
+
update.run(orderKey, trackId);
|
|
119
|
+
});
|
|
120
|
+
return ok(requiredSequence(context, row.sequence_id, mutation.revision), mutation.revision);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async function removeTrack(context, trackId) {
|
|
124
|
+
return resultOf(async () => {
|
|
125
|
+
const row = requiredTrackRow(context, trackId);
|
|
126
|
+
const clipCount = context.store.db
|
|
127
|
+
.prepare("SELECT COUNT(*) AS count FROM sequence_clips WHERE track_id=?")
|
|
128
|
+
.get(trackId);
|
|
129
|
+
const captionCount = context.store.db
|
|
130
|
+
.prepare("SELECT COUNT(*) AS count FROM caption_cues WHERE track_id=?")
|
|
131
|
+
.get(trackId);
|
|
132
|
+
if (clipCount.count > 0 || captionCount.count > 0) {
|
|
133
|
+
throw new EngineFault({
|
|
134
|
+
code: "IN_USE",
|
|
135
|
+
message: "A non-empty sequence track cannot be removed",
|
|
136
|
+
ownerId: trackId,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const kindCount = context.store.db
|
|
140
|
+
.prepare("SELECT COUNT(*) AS count FROM sequence_tracks WHERE sequence_id=? AND kind=?")
|
|
141
|
+
.get(row.sequence_id, row.kind);
|
|
142
|
+
if (kindCount.count <= 1) {
|
|
143
|
+
throw new EngineFault({
|
|
144
|
+
code: "IN_USE",
|
|
145
|
+
message: `The final ${row.kind} track cannot be removed`,
|
|
146
|
+
ownerId: trackId,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const ordinal = trackOrdinal(context, row);
|
|
150
|
+
const mutation = await context.store.semantic({
|
|
151
|
+
operation: "remove_sequence_track",
|
|
152
|
+
tables: [
|
|
153
|
+
"sequence_tracks",
|
|
154
|
+
"sequence_clips",
|
|
155
|
+
"clip_links",
|
|
156
|
+
"clip_transforms",
|
|
157
|
+
"transitions",
|
|
158
|
+
"caption_cues",
|
|
159
|
+
],
|
|
160
|
+
details: {
|
|
161
|
+
sequenceId: row.sequence_id,
|
|
162
|
+
trackId,
|
|
163
|
+
kind: row.kind,
|
|
164
|
+
ordinal,
|
|
165
|
+
},
|
|
166
|
+
writeSet: [`sequence:${row.sequence_id}`, `track:${trackId}`],
|
|
167
|
+
}, () => {
|
|
168
|
+
context.store.db
|
|
169
|
+
.prepare("DELETE FROM sequence_tracks WHERE track_id=?")
|
|
170
|
+
.run(trackId);
|
|
171
|
+
});
|
|
172
|
+
return ok(requiredSequence(context, row.sequence_id, mutation.revision), mutation.revision);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async function updateTrack(context, trackId, input) {
|
|
176
|
+
return resultOf(async () => {
|
|
177
|
+
const row = requiredTrackRow(context, trackId);
|
|
178
|
+
if (row.kind !== "audio" &&
|
|
179
|
+
(input.muted !== undefined || input.solo !== undefined)) {
|
|
180
|
+
throw new EngineFault({
|
|
181
|
+
code: "INVALID_INPUT",
|
|
182
|
+
message: "Muted and solo are only valid for audio tracks",
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const name = input.name === undefined
|
|
186
|
+
? row.name
|
|
187
|
+
: requiredText(input.name, "Track name");
|
|
188
|
+
const mutation = await context.store.semantic({
|
|
189
|
+
operation: "update_sequence_track",
|
|
190
|
+
tables: ["sequence_tracks"],
|
|
191
|
+
details: { trackId, sequenceId: row.sequence_id },
|
|
192
|
+
writeSet: [`sequence:${row.sequence_id}`, `track:${trackId}`],
|
|
193
|
+
}, () => {
|
|
194
|
+
context.store.db
|
|
195
|
+
.prepare(`UPDATE sequence_tracks
|
|
196
|
+
SET name=?, enabled=?, locked=?, muted=?, solo=?
|
|
197
|
+
WHERE track_id=?`)
|
|
198
|
+
.run(name, (input.enabled ?? row.enabled === 1) ? 1 : 0, (input.locked ?? row.locked === 1) ? 1 : 0, row.kind === "audio"
|
|
199
|
+
? (input.muted ?? row.muted === 1)
|
|
200
|
+
? 1
|
|
201
|
+
: 0
|
|
202
|
+
: null, row.kind === "audio"
|
|
203
|
+
? (input.solo ?? row.solo === 1)
|
|
204
|
+
? 1
|
|
205
|
+
: 0
|
|
206
|
+
: null, trackId);
|
|
207
|
+
});
|
|
208
|
+
return ok(requiredSequence(context, row.sequence_id, mutation.revision), mutation.revision);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
async function createSequence(context, input) {
|
|
212
|
+
return resultOf(async () => {
|
|
213
|
+
const sequenceId = input.sequenceId ?? newUuidV7();
|
|
214
|
+
assertUuidV7(sequenceId, "Sequence ID");
|
|
215
|
+
const normalized = normalizeCreateInput(input);
|
|
216
|
+
const bookId = context.bookRow().book_id;
|
|
217
|
+
const tracks = defaultTracks(sequenceId, normalized.videoTrackCount, normalized.audioTrackCount, normalized.captionTrackCount);
|
|
218
|
+
const mutation = await context.store.semantic({
|
|
219
|
+
operation: "create_sequence",
|
|
220
|
+
tables: ["sequences", "sequence_tracks"],
|
|
221
|
+
details: {
|
|
222
|
+
sequenceId,
|
|
223
|
+
name: normalized.name,
|
|
224
|
+
width: normalized.width,
|
|
225
|
+
height: normalized.height,
|
|
226
|
+
frameRate: normalized.frameRate,
|
|
227
|
+
trackCount: tracks.length,
|
|
228
|
+
},
|
|
229
|
+
writeSet: [
|
|
230
|
+
`sequence:${sequenceId}`,
|
|
231
|
+
...tracks.map(({ track }) => `track:${track.trackId}`),
|
|
232
|
+
],
|
|
233
|
+
}, (_operationId, now) => {
|
|
234
|
+
insertSequence(context, sequenceId, bookId, normalized, now);
|
|
235
|
+
insertTracks(context, tracks);
|
|
236
|
+
});
|
|
237
|
+
return ok(requiredSequence(context, sequenceId, mutation.revision), mutation.revision);
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async function renameSequence(context, sequenceId, requestedName) {
|
|
241
|
+
return resultOf(async () => {
|
|
242
|
+
const current = requiredSequence(context, sequenceId);
|
|
243
|
+
const name = requiredText(requestedName, "Sequence name");
|
|
244
|
+
if (name === current.name)
|
|
245
|
+
return current;
|
|
246
|
+
const mutation = await context.store.semantic({
|
|
247
|
+
operation: "rename_sequence",
|
|
248
|
+
tables: ["sequences"],
|
|
249
|
+
details: { sequenceId, oldName: current.name, newName: name },
|
|
250
|
+
writeSet: [`sequence:${sequenceId}`],
|
|
251
|
+
}, () => {
|
|
252
|
+
context.store.db
|
|
253
|
+
.prepare("UPDATE sequences SET name=? WHERE sequence_id=?")
|
|
254
|
+
.run(name, sequenceId);
|
|
255
|
+
});
|
|
256
|
+
return ok(requiredSequence(context, sequenceId, mutation.revision), mutation.revision);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async function updateSequenceCanvas(context, sequenceId, input) {
|
|
260
|
+
return resultOf(async () => {
|
|
261
|
+
const current = requiredSequence(context, sequenceId);
|
|
262
|
+
safeIntegerAtLeast(input.width, 1, "Sequence width");
|
|
263
|
+
safeIntegerAtLeast(input.height, 1, "Sequence height");
|
|
264
|
+
if (input.width === current.width && input.height === current.height) {
|
|
265
|
+
return current;
|
|
266
|
+
}
|
|
267
|
+
const mutation = await context.store.semantic({
|
|
268
|
+
operation: "update_sequence_canvas",
|
|
269
|
+
tables: ["sequences"],
|
|
270
|
+
details: {
|
|
271
|
+
sequenceId,
|
|
272
|
+
oldWidth: current.width,
|
|
273
|
+
oldHeight: current.height,
|
|
274
|
+
width: input.width,
|
|
275
|
+
height: input.height,
|
|
276
|
+
},
|
|
277
|
+
writeSet: [`sequence:${sequenceId}`],
|
|
278
|
+
}, () => {
|
|
279
|
+
context.store.db
|
|
280
|
+
.prepare("UPDATE sequences SET width=?, height=? WHERE sequence_id=?")
|
|
281
|
+
.run(input.width, input.height, sequenceId);
|
|
282
|
+
});
|
|
283
|
+
return ok(requiredSequence(context, sequenceId, mutation.revision), mutation.revision);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async function deleteSequence(context, sequenceId) {
|
|
287
|
+
return resultOf(async () => {
|
|
288
|
+
assertUuidV7(sequenceId, "Sequence ID");
|
|
289
|
+
const row = requiredSequenceRow(context, sequenceId);
|
|
290
|
+
if (row.is_primary === 1) {
|
|
291
|
+
throw new EngineFault({
|
|
292
|
+
code: "IN_USE",
|
|
293
|
+
message: "The primary sequence cannot be deleted",
|
|
294
|
+
ownerId: sequenceId,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const references = context.store.db
|
|
298
|
+
.prepare(`SELECT notebook_id, cell_id, reference_id
|
|
299
|
+
FROM cell_references
|
|
300
|
+
WHERE kind='sequence' AND target_id=?
|
|
301
|
+
ORDER BY notebook_id, cell_id, reference_id`)
|
|
302
|
+
.all(sequenceId);
|
|
303
|
+
if (references.length > 0) {
|
|
304
|
+
throw new EngineFault({
|
|
305
|
+
code: "IN_USE",
|
|
306
|
+
message: `Sequence is still referenced: ${sequenceId}`,
|
|
307
|
+
ownerId: sequenceId,
|
|
308
|
+
details: { references },
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const mutation = await context.store.semantic({
|
|
312
|
+
operation: "delete_sequence",
|
|
313
|
+
tables: [
|
|
314
|
+
"sequences",
|
|
315
|
+
"sequence_tracks",
|
|
316
|
+
"sequence_clips",
|
|
317
|
+
"clip_links",
|
|
318
|
+
"clip_transforms",
|
|
319
|
+
"transitions",
|
|
320
|
+
"caption_cues",
|
|
321
|
+
],
|
|
322
|
+
details: { sequenceId },
|
|
323
|
+
writeSet: [`sequence:${sequenceId}`],
|
|
324
|
+
}, () => {
|
|
325
|
+
context.store.db
|
|
326
|
+
.prepare("DELETE FROM sequences WHERE sequence_id=?")
|
|
327
|
+
.run(sequenceId);
|
|
328
|
+
});
|
|
329
|
+
return ok({ sequenceId }, mutation.revision);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function listSequences(context) {
|
|
333
|
+
const rows = context.store.db
|
|
334
|
+
.prepare(`${SEQUENCE_SELECT} FROM sequences ORDER BY is_primary DESC, created_at, sequence_id`)
|
|
335
|
+
.all();
|
|
336
|
+
return rows.map((row) => sequenceFromRow(context, row));
|
|
337
|
+
}
|
|
338
|
+
function primarySequence(context) {
|
|
339
|
+
// is_primary is a derived singleton: a merge can transiently yield more
|
|
340
|
+
// than one primary row per book (see docs/engine-layout.md "Merge policy
|
|
341
|
+
// per constraint class"), so the read resolves a deterministic winner —
|
|
342
|
+
// earliest created_at, then lowest sequence_id — matching the post-merge
|
|
343
|
+
// reconcile in src/merge-policy.ts instead of returning an arbitrary row.
|
|
344
|
+
const row = context.store.db
|
|
345
|
+
.prepare(`${SEQUENCE_SELECT} FROM sequences
|
|
346
|
+
WHERE is_primary=1
|
|
347
|
+
ORDER BY created_at, sequence_id
|
|
348
|
+
LIMIT 1`)
|
|
349
|
+
.get();
|
|
350
|
+
if (!row) {
|
|
351
|
+
throw new EngineFault({
|
|
352
|
+
code: "SCHEMA_INCOMPATIBLE",
|
|
353
|
+
message: "Book is missing its primary sequence",
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return sequenceFromRow(context, row);
|
|
357
|
+
}
|
|
358
|
+
function requiredSequence(context, sequenceId, revision) {
|
|
359
|
+
const row = requiredSequenceRow(context, sequenceId, revision);
|
|
360
|
+
return sequenceFromRow(context, row, revision);
|
|
361
|
+
}
|
|
362
|
+
function requiredSequenceRow(context, sequenceId, revision) {
|
|
363
|
+
assertUuidV7(sequenceId, "Sequence ID");
|
|
364
|
+
const source = semanticSource("sequences", revision);
|
|
365
|
+
const row = context.store.db
|
|
366
|
+
.prepare(`${SEQUENCE_SELECT} FROM ${source} WHERE sequence_id=?`)
|
|
367
|
+
.get(...revisionParams(revision, sequenceId));
|
|
368
|
+
if (!row) {
|
|
369
|
+
throw new EngineFault({
|
|
370
|
+
code: "NOT_FOUND",
|
|
371
|
+
message: revision
|
|
372
|
+
? `Sequence not found at revision: ${sequenceId}`
|
|
373
|
+
: `Sequence not found: ${sequenceId}`,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return row;
|
|
377
|
+
}
|
|
378
|
+
function sequenceFromRow(context, row, revision) {
|
|
379
|
+
const trackRows = queryRows(context, `${TRACK_SELECT} FROM ${semanticSource("sequence_tracks", revision)}
|
|
380
|
+
WHERE sequence_id=? ORDER BY kind, order_key, track_id`, revision, row.sequence_id);
|
|
381
|
+
const ordinalsByKind = new Map();
|
|
382
|
+
const tracks = trackRows.map((trackRow) => {
|
|
383
|
+
const ordinal = ordinalsByKind.get(trackRow.kind) ?? 0;
|
|
384
|
+
ordinalsByKind.set(trackRow.kind, ordinal + 1);
|
|
385
|
+
return trackFromRow(trackRow, ordinal);
|
|
386
|
+
});
|
|
387
|
+
const trackIds = tracks.map((track) => track.trackId);
|
|
388
|
+
const clips = trackIds.length === 0 ? [] : readClips(context, trackIds, revision);
|
|
389
|
+
const transitions = trackIds.length === 0 ? [] : readTransitions(context, trackIds, revision);
|
|
390
|
+
const captions = trackIds.length === 0 ? [] : readCaptions(context, trackIds, revision);
|
|
391
|
+
const background = parseJson(row.background_rgba_json, [0, 0, 0, 1]);
|
|
392
|
+
if (!isRgba(background)) {
|
|
393
|
+
throw new EngineFault({
|
|
394
|
+
code: "SCHEMA_INCOMPATIBLE",
|
|
395
|
+
message: `Sequence has invalid background RGBA: ${row.sequence_id}`,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
sequenceId: row.sequence_id,
|
|
400
|
+
name: row.name,
|
|
401
|
+
width: row.width,
|
|
402
|
+
height: row.height,
|
|
403
|
+
pixelAspect: {
|
|
404
|
+
numerator: row.pixel_aspect_numerator,
|
|
405
|
+
denominator: row.pixel_aspect_denominator,
|
|
406
|
+
},
|
|
407
|
+
frameRate: {
|
|
408
|
+
numerator: row.frame_rate_numerator,
|
|
409
|
+
denominator: row.frame_rate_denominator,
|
|
410
|
+
},
|
|
411
|
+
audioSampleRateHz: row.audio_sample_rate_hz,
|
|
412
|
+
audioChannelLayout: row.audio_channel_layout,
|
|
413
|
+
backgroundRgba: background,
|
|
414
|
+
revision: revision ?? context.store.head,
|
|
415
|
+
tracks,
|
|
416
|
+
clips,
|
|
417
|
+
transitions,
|
|
418
|
+
captions,
|
|
419
|
+
createdAt: row.created_at,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function readClips(context, trackIds, revision) {
|
|
423
|
+
const placeholders = trackIds.map(() => "?").join(",");
|
|
424
|
+
const clips = queryRows(context, `${CLIP_SELECT} FROM ${semanticSource("sequence_clips", revision)}
|
|
425
|
+
WHERE track_id IN (${placeholders})
|
|
426
|
+
ORDER BY track_id, timeline_start_frame, clip_id`, revision, ...trackIds);
|
|
427
|
+
const clipIds = clips.map((clip) => clip.clip_id);
|
|
428
|
+
if (clipIds.length === 0)
|
|
429
|
+
return [];
|
|
430
|
+
const clipPlaceholders = clipIds.map(() => "?").join(",");
|
|
431
|
+
const links = queryRows(context, `SELECT link_group_id, clip_id
|
|
432
|
+
FROM ${semanticSource("clip_links", revision)}
|
|
433
|
+
WHERE clip_id IN (${clipPlaceholders})`, revision, ...clipIds);
|
|
434
|
+
const transforms = queryRows(context, `${TRANSFORM_SELECT}
|
|
435
|
+
FROM ${semanticSource("clip_transforms", revision)}
|
|
436
|
+
WHERE clip_id IN (${clipPlaceholders})`, revision, ...clipIds);
|
|
437
|
+
const linkByClip = new Map(links.map((link) => [link.clip_id, link.link_group_id]));
|
|
438
|
+
const transformByClip = new Map(transforms.map((transformRow) => [
|
|
439
|
+
transformRow.clip_id,
|
|
440
|
+
transformFromRow(transformRow),
|
|
441
|
+
]));
|
|
442
|
+
return clips.map((clip) => clipFromRow(clip, linkByClip.get(clip.clip_id), transformByClip.get(clip.clip_id)));
|
|
443
|
+
}
|
|
444
|
+
function readTransitions(context, trackIds, revision) {
|
|
445
|
+
const placeholders = trackIds.map(() => "?").join(",");
|
|
446
|
+
return queryRows(context, `${TRANSITION_SELECT}
|
|
447
|
+
FROM ${semanticSource("transitions", revision)}
|
|
448
|
+
WHERE track_id IN (${placeholders})
|
|
449
|
+
ORDER BY track_id, outgoing_clip_id, incoming_clip_id`, revision, ...trackIds).map((row) => ({
|
|
450
|
+
transitionId: row.transition_id,
|
|
451
|
+
trackId: row.track_id,
|
|
452
|
+
outgoingClipId: row.outgoing_clip_id,
|
|
453
|
+
incomingClipId: row.incoming_clip_id,
|
|
454
|
+
kind: row.kind,
|
|
455
|
+
durationFrames: row.duration_frames,
|
|
456
|
+
alignment: row.alignment,
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
function readCaptions(context, trackIds, revision) {
|
|
460
|
+
const placeholders = trackIds.map(() => "?").join(",");
|
|
461
|
+
return queryRows(context, `${CAPTION_SELECT}
|
|
462
|
+
FROM ${semanticSource("caption_cues", revision)}
|
|
463
|
+
WHERE track_id IN (${placeholders})
|
|
464
|
+
ORDER BY track_id, timeline_start_frame, cue_id`, revision, ...trackIds).map(captionFromRow);
|
|
465
|
+
}
|
|
466
|
+
function clipFromRow(row, linkGroupId, transform) {
|
|
467
|
+
const common = {
|
|
468
|
+
clipId: row.clip_id,
|
|
469
|
+
trackId: row.track_id,
|
|
470
|
+
timelineStartFrame: row.timeline_start_frame,
|
|
471
|
+
durationFrames: row.duration_frames,
|
|
472
|
+
enabled: row.enabled === 1,
|
|
473
|
+
...(linkGroupId ? { linkGroupId } : {}),
|
|
474
|
+
...(transform ? { transform } : {}),
|
|
475
|
+
...clipAudio(row),
|
|
476
|
+
};
|
|
477
|
+
if (row.source_kind === "still") {
|
|
478
|
+
if (!row.source_path) {
|
|
479
|
+
throw invalidClip(row.clip_id, "still source path");
|
|
480
|
+
}
|
|
481
|
+
return {
|
|
482
|
+
...common,
|
|
483
|
+
source: {
|
|
484
|
+
kind: "still",
|
|
485
|
+
artifactId: row.artifact_id,
|
|
486
|
+
sourcePath: row.source_path,
|
|
487
|
+
objectHash: row.object_hash,
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
if (!row.stream_id ||
|
|
492
|
+
row.source_start_tick === null ||
|
|
493
|
+
row.source_duration_ticks === null ||
|
|
494
|
+
row.time_base_numerator === null ||
|
|
495
|
+
row.time_base_denominator === null ||
|
|
496
|
+
row.speed_numerator === null ||
|
|
497
|
+
row.speed_denominator === null ||
|
|
498
|
+
row.reverse === null ||
|
|
499
|
+
row.audio_policy === null) {
|
|
500
|
+
throw invalidClip(row.clip_id, "timed source mapping");
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
...common,
|
|
504
|
+
source: {
|
|
505
|
+
kind: "timed",
|
|
506
|
+
artifactId: row.artifact_id,
|
|
507
|
+
range: {
|
|
508
|
+
streamId: row.stream_id,
|
|
509
|
+
objectHash: row.object_hash,
|
|
510
|
+
startTick: row.source_start_tick,
|
|
511
|
+
durationTicks: row.source_duration_ticks,
|
|
512
|
+
timeBase: {
|
|
513
|
+
numerator: row.time_base_numerator,
|
|
514
|
+
denominator: row.time_base_denominator,
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
speed: {
|
|
519
|
+
numerator: row.speed_numerator,
|
|
520
|
+
denominator: row.speed_denominator,
|
|
521
|
+
},
|
|
522
|
+
reverse: row.reverse === 1,
|
|
523
|
+
audioPolicy: row.audio_policy,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function clipAudio(row) {
|
|
527
|
+
const values = [
|
|
528
|
+
row.gain_db,
|
|
529
|
+
row.audio_muted,
|
|
530
|
+
row.fade_in_frames,
|
|
531
|
+
row.fade_out_frames,
|
|
532
|
+
];
|
|
533
|
+
if (values.every((value) => value === null))
|
|
534
|
+
return {};
|
|
535
|
+
if (values.some((value) => value === null)) {
|
|
536
|
+
throw invalidClip(row.clip_id, "audio projection");
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
audio: {
|
|
540
|
+
gainDb: row.gain_db,
|
|
541
|
+
muted: row.audio_muted === 1,
|
|
542
|
+
fadeInFrames: row.fade_in_frames,
|
|
543
|
+
fadeOutFrames: row.fade_out_frames,
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function transformFromRow(row) {
|
|
548
|
+
return {
|
|
549
|
+
fit: row.fit,
|
|
550
|
+
positionX: row.position_x,
|
|
551
|
+
positionY: row.position_y,
|
|
552
|
+
scaleX: row.scale_x,
|
|
553
|
+
scaleY: row.scale_y,
|
|
554
|
+
anchorX: row.anchor_x,
|
|
555
|
+
anchorY: row.anchor_y,
|
|
556
|
+
rotationDegrees: row.rotation_degrees,
|
|
557
|
+
cropTop: row.crop_top,
|
|
558
|
+
cropRight: row.crop_right,
|
|
559
|
+
cropBottom: row.crop_bottom,
|
|
560
|
+
cropLeft: row.crop_left,
|
|
561
|
+
opacity: row.opacity,
|
|
562
|
+
blendMode: row.blend_mode,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
function captionFromRow(row) {
|
|
566
|
+
const transcriptSelection = captionTranscriptSelection(row);
|
|
567
|
+
return {
|
|
568
|
+
cueId: row.cue_id,
|
|
569
|
+
trackId: row.track_id,
|
|
570
|
+
timelineStartFrame: row.timeline_start_frame,
|
|
571
|
+
durationFrames: row.duration_frames,
|
|
572
|
+
text: row.text,
|
|
573
|
+
...(row.speaker ? { speaker: row.speaker } : {}),
|
|
574
|
+
styleId: row.style_id,
|
|
575
|
+
...(transcriptSelection ? { transcriptSelection } : {}),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function captionTranscriptSelection(row) {
|
|
579
|
+
const values = [
|
|
580
|
+
row.transcript_id,
|
|
581
|
+
row.transcript_revision,
|
|
582
|
+
row.start_word_id,
|
|
583
|
+
row.end_word_id,
|
|
584
|
+
row.source_range_json,
|
|
585
|
+
];
|
|
586
|
+
if (values.every((value) => value === null))
|
|
587
|
+
return null;
|
|
588
|
+
if (values.some((value) => value === null)) {
|
|
589
|
+
throw new EngineFault({
|
|
590
|
+
code: "SCHEMA_INCOMPATIBLE",
|
|
591
|
+
message: `Caption has an incomplete transcript selection: ${row.cue_id}`,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
transcriptId: row.transcript_id,
|
|
596
|
+
transcriptRevision: row.transcript_revision,
|
|
597
|
+
startWordId: row.start_word_id,
|
|
598
|
+
endWordId: row.end_word_id,
|
|
599
|
+
range: normalizeSourceRange(parseJson(row.source_range_json, {
|
|
600
|
+
streamId: "",
|
|
601
|
+
objectHash: "",
|
|
602
|
+
startTick: 0,
|
|
603
|
+
durationTicks: 0,
|
|
604
|
+
timeBase: { numerator: 1, denominator: 1 },
|
|
605
|
+
})),
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function trackFromRow(row, ordinal) {
|
|
609
|
+
const common = {
|
|
610
|
+
trackId: row.track_id,
|
|
611
|
+
sequenceId: row.sequence_id,
|
|
612
|
+
ordinal,
|
|
613
|
+
name: row.name,
|
|
614
|
+
enabled: row.enabled === 1,
|
|
615
|
+
locked: row.locked === 1,
|
|
616
|
+
};
|
|
617
|
+
if (row.kind === "video") {
|
|
618
|
+
return {
|
|
619
|
+
...common,
|
|
620
|
+
kind: "video",
|
|
621
|
+
blendMode: "normal",
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
if (row.kind === "audio") {
|
|
625
|
+
return {
|
|
626
|
+
...common,
|
|
627
|
+
kind: "audio",
|
|
628
|
+
muted: row.muted === 1,
|
|
629
|
+
solo: row.solo === 1,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
...common,
|
|
634
|
+
kind: "caption",
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function normalizeCreateInput(input) {
|
|
638
|
+
const pixelAspect = normalizeRational(input.pixelAspect ?? { numerator: 1, denominator: 1 });
|
|
639
|
+
const frameRate = normalizeRational(input.frameRate);
|
|
640
|
+
const backgroundRgba = input.backgroundRgba ?? [0, 0, 0, 1];
|
|
641
|
+
if (!isRgba(backgroundRgba)) {
|
|
642
|
+
throw new Error("Sequence background RGBA must contain four finite values");
|
|
643
|
+
}
|
|
644
|
+
const videoTrackCount = trackCount(input.videoTrackCount ?? 2, "Video track count");
|
|
645
|
+
const audioTrackCount = trackCount(input.audioTrackCount ?? 4, "Audio track count");
|
|
646
|
+
const captionTrackCount = trackCount(input.captionTrackCount ?? 1, "Caption track count");
|
|
647
|
+
safeIntegerAtLeast(input.width, 1, "Sequence width");
|
|
648
|
+
safeIntegerAtLeast(input.height, 1, "Sequence height");
|
|
649
|
+
const audioSampleRateHz = input.audioSampleRateHz ?? 48_000;
|
|
650
|
+
safeIntegerAtLeast(audioSampleRateHz, 1, "Sequence audio sample rate");
|
|
651
|
+
return {
|
|
652
|
+
name: requiredText(input.name, "Sequence name"),
|
|
653
|
+
width: input.width,
|
|
654
|
+
height: input.height,
|
|
655
|
+
pixelAspect,
|
|
656
|
+
frameRate,
|
|
657
|
+
audioSampleRateHz,
|
|
658
|
+
audioChannelLayout: requiredText(input.audioChannelLayout ?? "stereo", "Sequence audio channel layout"),
|
|
659
|
+
backgroundRgba,
|
|
660
|
+
videoTrackCount,
|
|
661
|
+
audioTrackCount,
|
|
662
|
+
captionTrackCount,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
function insertSequence(context, sequenceId, bookId, input, now) {
|
|
666
|
+
context.store.db
|
|
667
|
+
.prepare(`INSERT INTO sequences(
|
|
668
|
+
sequence_id, book_id, name, is_primary, width, height,
|
|
669
|
+
pixel_aspect_numerator, pixel_aspect_denominator,
|
|
670
|
+
frame_rate_numerator, frame_rate_denominator,
|
|
671
|
+
audio_sample_rate_hz, audio_channel_layout,
|
|
672
|
+
background_rgba_json, created_at
|
|
673
|
+
) VALUES (?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
674
|
+
.run(sequenceId, bookId, input.name, input.width, input.height, input.pixelAspect.numerator, input.pixelAspect.denominator, input.frameRate.numerator, input.frameRate.denominator, input.audioSampleRateHz, input.audioChannelLayout, canonicalJson(input.backgroundRgba), now);
|
|
675
|
+
}
|
|
676
|
+
function defaultTracks(sequenceId, videoCount, audioCount, captionCount) {
|
|
677
|
+
const tracks = [];
|
|
678
|
+
const videoKeys = initialOrderKeys(videoCount);
|
|
679
|
+
for (let ordinal = 0; ordinal < videoCount; ordinal += 1) {
|
|
680
|
+
tracks.push({
|
|
681
|
+
orderKey: videoKeys[ordinal],
|
|
682
|
+
track: {
|
|
683
|
+
trackId: newUuidV7(),
|
|
684
|
+
sequenceId,
|
|
685
|
+
kind: "video",
|
|
686
|
+
ordinal,
|
|
687
|
+
name: `Video ${ordinal + 1}`,
|
|
688
|
+
enabled: true,
|
|
689
|
+
locked: false,
|
|
690
|
+
blendMode: "normal",
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
const audioKeys = initialOrderKeys(audioCount);
|
|
695
|
+
for (let ordinal = 0; ordinal < audioCount; ordinal += 1) {
|
|
696
|
+
tracks.push({
|
|
697
|
+
orderKey: audioKeys[ordinal],
|
|
698
|
+
track: {
|
|
699
|
+
trackId: newUuidV7(),
|
|
700
|
+
sequenceId,
|
|
701
|
+
kind: "audio",
|
|
702
|
+
ordinal,
|
|
703
|
+
name: `Audio ${ordinal + 1}`,
|
|
704
|
+
enabled: true,
|
|
705
|
+
locked: false,
|
|
706
|
+
muted: false,
|
|
707
|
+
solo: false,
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
const captionKeys = initialOrderKeys(captionCount);
|
|
712
|
+
for (let ordinal = 0; ordinal < captionCount; ordinal += 1) {
|
|
713
|
+
tracks.push({
|
|
714
|
+
orderKey: captionKeys[ordinal],
|
|
715
|
+
track: {
|
|
716
|
+
trackId: newUuidV7(),
|
|
717
|
+
sequenceId,
|
|
718
|
+
kind: "caption",
|
|
719
|
+
ordinal,
|
|
720
|
+
name: captionCount === 1 ? "Captions" : `Captions ${ordinal + 1}`,
|
|
721
|
+
enabled: true,
|
|
722
|
+
locked: false,
|
|
723
|
+
},
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return tracks;
|
|
727
|
+
}
|
|
728
|
+
function insertTracks(context, tracks) {
|
|
729
|
+
const insert = context.store.db.prepare(`INSERT INTO sequence_tracks(
|
|
730
|
+
track_id, sequence_id, kind, order_key, name,
|
|
731
|
+
enabled, locked, muted, solo, blend_mode
|
|
732
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
733
|
+
for (const { track, orderKey } of tracks) {
|
|
734
|
+
insert.run(track.trackId, track.sequenceId, track.kind, orderKey, track.name, track.enabled ? 1 : 0, track.locked ? 1 : 0, track.kind === "audio" ? (track.muted ? 1 : 0) : null, track.kind === "audio" ? (track.solo ? 1 : 0) : null, track.kind === "video" ? track.blendMode : null);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function queryRows(context, sql, revision, ...params) {
|
|
738
|
+
return context.store.db
|
|
739
|
+
.prepare(sql)
|
|
740
|
+
.all(...(revision ? [revision, ...params] : params));
|
|
741
|
+
}
|
|
742
|
+
function requiredTrackRow(context, trackId) {
|
|
743
|
+
assertUuidV7(trackId, "Track ID");
|
|
744
|
+
const row = context.store.db
|
|
745
|
+
.prepare(`${TRACK_SELECT} FROM sequence_tracks WHERE track_id=?`)
|
|
746
|
+
.get(trackId);
|
|
747
|
+
if (!row) {
|
|
748
|
+
throw new EngineFault({
|
|
749
|
+
code: "NOT_FOUND",
|
|
750
|
+
message: `Sequence track not found: ${trackId}`,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
return row;
|
|
754
|
+
}
|
|
755
|
+
/** Current rank of a track within its sequence and kind. */
|
|
756
|
+
function trackOrdinal(context, row) {
|
|
757
|
+
const siblings = context.store.db
|
|
758
|
+
.prepare(`SELECT track_id FROM sequence_tracks
|
|
759
|
+
WHERE sequence_id=? AND kind=?
|
|
760
|
+
ORDER BY order_key, track_id`)
|
|
761
|
+
.all(row.sequence_id, row.kind);
|
|
762
|
+
return siblings.findIndex((candidate) => candidate.track_id === row.track_id);
|
|
763
|
+
}
|
|
764
|
+
function semanticSource(table, revision) {
|
|
765
|
+
return revision ? `dolt_at_${table}(?)` : table;
|
|
766
|
+
}
|
|
767
|
+
function revisionParams(revision, ...params) {
|
|
768
|
+
return revision ? [revision, ...params] : params;
|
|
769
|
+
}
|
|
770
|
+
function isRgba(value) {
|
|
771
|
+
return (Array.isArray(value) &&
|
|
772
|
+
value.length === 4 &&
|
|
773
|
+
value.every((channel) => Number.isFinite(channel)));
|
|
774
|
+
}
|
|
775
|
+
function invalidClip(clipId, field) {
|
|
776
|
+
return new EngineFault({
|
|
777
|
+
code: "SCHEMA_INCOMPATIBLE",
|
|
778
|
+
message: `Clip has an invalid ${field}: ${clipId}`,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
function requiredText(value, label) {
|
|
782
|
+
const normalized = value.trim();
|
|
783
|
+
if (!normalized)
|
|
784
|
+
throw new Error(`${label} is required`);
|
|
785
|
+
return normalized;
|
|
786
|
+
}
|
|
787
|
+
function safeIntegerAtLeast(value, minimum, label) {
|
|
788
|
+
if (!Number.isSafeInteger(value) || value < minimum) {
|
|
789
|
+
throw new Error(`${label} must be a safe integer of at least ${minimum}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function trackCount(value, label) {
|
|
793
|
+
safeIntegerAtLeast(value, 0, label);
|
|
794
|
+
if (value > 32)
|
|
795
|
+
throw new Error(`${label} must not exceed 32`);
|
|
796
|
+
return value;
|
|
797
|
+
}
|
|
798
|
+
const SEQUENCE_SELECT = `
|
|
799
|
+
SELECT sequence_id, name, is_primary, width, height,
|
|
800
|
+
pixel_aspect_numerator, pixel_aspect_denominator,
|
|
801
|
+
frame_rate_numerator, frame_rate_denominator,
|
|
802
|
+
audio_sample_rate_hz, audio_channel_layout,
|
|
803
|
+
background_rgba_json, created_at`;
|
|
804
|
+
const TRACK_SELECT = `
|
|
805
|
+
SELECT track_id, sequence_id, kind, order_key, name,
|
|
806
|
+
enabled, locked, muted, solo, blend_mode`;
|
|
807
|
+
const CLIP_SELECT = `
|
|
808
|
+
SELECT clip_id, track_id, source_kind, artifact_id, source_path,
|
|
809
|
+
stream_id, object_hash, source_start_tick, source_duration_ticks,
|
|
810
|
+
time_base_numerator, time_base_denominator,
|
|
811
|
+
timeline_start_frame, duration_frames, speed_numerator,
|
|
812
|
+
speed_denominator, reverse, audio_policy, gain_db, audio_muted,
|
|
813
|
+
fade_in_frames, fade_out_frames, enabled`;
|
|
814
|
+
const TRANSFORM_SELECT = `
|
|
815
|
+
SELECT clip_id, fit, position_x, position_y, scale_x, scale_y,
|
|
816
|
+
anchor_x, anchor_y, rotation_degrees, crop_top, crop_right,
|
|
817
|
+
crop_bottom, crop_left, opacity, blend_mode`;
|
|
818
|
+
const TRANSITION_SELECT = `
|
|
819
|
+
SELECT transition_id, track_id, outgoing_clip_id, incoming_clip_id,
|
|
820
|
+
kind, duration_frames, alignment`;
|
|
821
|
+
const CAPTION_SELECT = `
|
|
822
|
+
SELECT cue_id, track_id, timeline_start_frame, duration_frames,
|
|
823
|
+
text, speaker, style_id, transcript_id, transcript_revision,
|
|
824
|
+
start_word_id, end_word_id, source_range_json`;
|
|
825
|
+
//# sourceMappingURL=sequences.js.map
|