@mevdragon/vidfarm-devcli 0.20.5 → 0.20.8
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/skills/editor-capabilities/SKILL.md +15 -1
- package/.agents/skills/vidfarm-director/SKILL.md +106 -0
- package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
- package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
- package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
- package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
- package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
- package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
- package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
- package/SKILL.director.md +521 -261
- package/dist/src/cli.js +404 -96
- package/dist/src/devcli/clips.js +15 -10
- package/dist/src/devcli/doctor.js +2 -2
- package/dist/src/devcli/local-backend.js +46 -2
- package/dist/src/services/clip-curation/cost.js +5 -1
- package/dist/src/services/clip-curation/gemini.js +10 -0
- package/package.json +30 -10
- package/SKILL.platform.md +0 -432
- package/demo/README.md +0 -28
- package/demo/dist/app.css +0 -1
- package/demo/dist/app.js +0 -1850
- package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
- package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
- package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
- package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
- package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
- package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
- package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
- package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
- package/demo/dist/favicon.ico +0 -0
- package/demo/dist/icons/timeline/audio.svg +0 -7
- package/demo/dist/icons/timeline/captions.svg +0 -5
- package/demo/dist/icons/timeline/composition.svg +0 -12
- package/demo/dist/icons/timeline/image.svg +0 -18
- package/demo/dist/icons/timeline/music.svg +0 -10
- package/demo/dist/icons/timeline/text.svg +0 -3
- package/demo/dist/index.html +0 -15
- package/dist/src/account-pages-legacy.js +0 -9396
- package/dist/src/account-pages.js +0 -61
- package/dist/src/app.js +0 -21602
- package/dist/src/composition-runtime.js +0 -1053
- package/dist/src/config.js +0 -217
- package/dist/src/context.js +0 -447
- package/dist/src/dev-app-legacy.js +0 -739
- package/dist/src/dev-app.js +0 -6
- package/dist/src/devcli/migrate-local.js +0 -140
- package/dist/src/devcli/sync.js +0 -368
- package/dist/src/domain.js +0 -5
- package/dist/src/editor-chat-history.js +0 -82
- package/dist/src/editor-chat.js +0 -756
- package/dist/src/editor-dark-theme.js +0 -1128
- package/dist/src/frontend/debug.js +0 -71
- package/dist/src/frontend/discover-client.js +0 -130
- package/dist/src/frontend/discover-store.js +0 -23
- package/dist/src/frontend/file-directory.js +0 -1018
- package/dist/src/frontend/flockposter-cache-store.js +0 -124
- package/dist/src/frontend/homepage-client.js +0 -446
- package/dist/src/frontend/homepage-shared.js +0 -201
- package/dist/src/frontend/homepage-store.js +0 -66
- package/dist/src/frontend/homepage-view.js +0 -705
- package/dist/src/frontend/page-runtime-client.js +0 -132
- package/dist/src/frontend/page-runtime-store.js +0 -9
- package/dist/src/frontend/sentry.js +0 -42
- package/dist/src/frontend/template-editor-chat.js +0 -4181
- package/dist/src/help-page.js +0 -346
- package/dist/src/homepage.js +0 -1458
- package/dist/src/index.js +0 -16
- package/dist/src/instrument.js +0 -30
- package/dist/src/landing-page.js +0 -384
- package/dist/src/page-runtime.js +0 -2
- package/dist/src/page-shell.js +0 -1452
- package/dist/src/primitive-context.js +0 -416
- package/dist/src/primitive-registry.js +0 -3940
- package/dist/src/primitive-sdk.js +0 -4
- package/dist/src/primitives/hyperframes-media.js +0 -108
- package/dist/src/react-page-shell.js +0 -35
- package/dist/src/ready-post-schedule-component.js +0 -1540
- package/dist/src/registry.js +0 -296
- package/dist/src/reskin/agency-page.js +0 -299
- package/dist/src/reskin/calendar-page.js +0 -568
- package/dist/src/reskin/chat-page.js +0 -942
- package/dist/src/reskin/discover-page.js +0 -1788
- package/dist/src/reskin/document.js +0 -1587
- package/dist/src/reskin/help-page.js +0 -357
- package/dist/src/reskin/index-page.js +0 -62
- package/dist/src/reskin/inpaint-clipper-page.js +0 -890
- package/dist/src/reskin/inpaint-page.js +0 -2554
- package/dist/src/reskin/inpaint-video-page.js +0 -1339
- package/dist/src/reskin/job-runs-page.js +0 -477
- package/dist/src/reskin/library-page.js +0 -1634
- package/dist/src/reskin/login-page.js +0 -262
- package/dist/src/reskin/portfolio-page.js +0 -687
- package/dist/src/reskin/pricing-page.js +0 -390
- package/dist/src/reskin/settings-page.js +0 -732
- package/dist/src/reskin/theme.js +0 -711
- package/dist/src/runtime.js +0 -35
- package/dist/src/services/api-call-history.js +0 -249
- package/dist/src/services/auth.js +0 -152
- package/dist/src/services/billing-pricing.js +0 -39
- package/dist/src/services/billing.js +0 -241
- package/dist/src/services/cast.js +0 -127
- package/dist/src/services/chat-threads.js +0 -92
- package/dist/src/services/clip-records.js +0 -250
- package/dist/src/services/clip-search.js +0 -77
- package/dist/src/services/clip-vectors.js +0 -125
- package/dist/src/services/composition-sanitize.js +0 -124
- package/dist/src/services/composition-watch.js +0 -79
- package/dist/src/services/elevenlabs.js +0 -222
- package/dist/src/services/file-directory.js +0 -117
- package/dist/src/services/fork-access.js +0 -93
- package/dist/src/services/fork-manifest.js +0 -42
- package/dist/src/services/ghostcut.js +0 -179
- package/dist/src/services/hyperframes.js +0 -3517
- package/dist/src/services/job-capacity.js +0 -14
- package/dist/src/services/job-logs.js +0 -197
- package/dist/src/services/jobs.js +0 -136
- package/dist/src/services/local-dynamo.js +0 -0
- package/dist/src/services/media-processing.js +0 -766
- package/dist/src/services/primitive-media-lambda.js +0 -280
- package/dist/src/services/providers.js +0 -2748
- package/dist/src/services/rate-limits.js +0 -262
- package/dist/src/services/scene-annotations.js +0 -32
- package/dist/src/services/serverless-auth.js +0 -382
- package/dist/src/services/serverless-jobs.js +0 -1084
- package/dist/src/services/serverless-provider-keys.js +0 -409
- package/dist/src/services/serverless-records.js +0 -1515
- package/dist/src/services/serverless-template-configs.js +0 -75
- package/dist/src/services/storage.js +0 -461
- package/dist/src/services/swipe-customize.js +0 -437
- package/dist/src/services/template-certification.js +0 -413
- package/dist/src/services/template-loader.js +0 -99
- package/dist/src/services/template-runtime-bundles.js +0 -217
- package/dist/src/services/template-sources.js +0 -1017
- package/dist/src/services/upstream.js +0 -248
- package/dist/src/services/video-normalization.js +0 -2
- package/dist/src/services/webhooks.js +0 -62
- package/dist/src/template-editor-pages.js +0 -2576
- package/dist/src/template-editor-shell.js +0 -2893
- package/dist/src/template-sdk.js +0 -4
- package/dist/src/worker.js +0 -17
- package/public/assets/discover-client-app.js +0 -1
- package/public/assets/file-directory-app.js +0 -3
- package/public/assets/homepage-app.js +0 -54
- package/public/assets/homepage-client-app.js +0 -80
- package/public/assets/page-runtime-client-app.js +0 -94
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
- package/src/assets/SELLING_WITH_HOOKS.md +0 -377
- package/src/assets/SELLING_WITH_VSLS.md +0 -606
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
// Cloud clip records — DynamoDB single-table store for clip curation, mirroring
|
|
2
|
-
// the serverless-records / serverless-jobs conventions (pk/sk + gsi1 overload,
|
|
3
|
-
// entity_type tag, local-dynamo shim in `serve` mode). Same Clip shape as the
|
|
4
|
-
// devcli SQLite store so users move between local and cloud without lock-in.
|
|
5
|
-
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
6
|
-
import { DeleteCommand, DynamoDBDocumentClient, GetCommand, PutCommand, QueryCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
|
|
7
|
-
import { config } from "../config.js";
|
|
8
|
-
import { createLocalDynamoClient } from "./local-dynamo.js";
|
|
9
|
-
import { BUILTIN_PRESETS } from "./clip-curation/index.js";
|
|
10
|
-
// Same client construction as serverless-records.ts: disk shim locally, real
|
|
11
|
-
// client when deployed. Call sites are identical either way.
|
|
12
|
-
const dynamodb = config.RECORDS_DRIVER === "local"
|
|
13
|
-
? createLocalDynamoClient()
|
|
14
|
-
: DynamoDBDocumentClient.from(new DynamoDBClient({}), {
|
|
15
|
-
marshallOptions: { removeUndefinedValues: true }
|
|
16
|
-
});
|
|
17
|
-
function requireMainTableName() {
|
|
18
|
-
if (config.RECORDS_DRIVER === "local") {
|
|
19
|
-
return config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME || "vidfarm-local-main";
|
|
20
|
-
}
|
|
21
|
-
if (!config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME) {
|
|
22
|
-
throw new Error("Clip record store requires VIDFARM_SERVERLESS_MAIN_TABLE_NAME.");
|
|
23
|
-
}
|
|
24
|
-
return config.VIDFARM_SERVERLESS_MAIN_TABLE_NAME;
|
|
25
|
-
}
|
|
26
|
-
const ENTITY_CLIP = "clip_curation_clip";
|
|
27
|
-
const ENTITY_PRESET = "clip_curation_preset";
|
|
28
|
-
const ENTITY_SOURCE = "clip_curation_source";
|
|
29
|
-
const ENTITY_SCAN = "clip_curation_scan";
|
|
30
|
-
// Key scheme (one owner partition, sk-prefixed by entity; gsi1 lists by recency).
|
|
31
|
-
const clipPk = (owner) => `USER#${owner}`;
|
|
32
|
-
const clipSk = (clipId) => `CLIP#${clipId}`;
|
|
33
|
-
const clipListPk = (owner) => `USER#${owner}#CLIPS`;
|
|
34
|
-
const clipSourceListPk = (owner, sourceId) => `USER#${owner}#CLIPS#SRC#${sourceId}`;
|
|
35
|
-
const presetSk = (presetId) => `CLIPPRESET#${presetId}`;
|
|
36
|
-
const presetListPk = (owner) => `USER#${owner}#CLIPPRESETS`;
|
|
37
|
-
const sourceSk = (sourceId) => `CLIPSRC#${sourceId}`;
|
|
38
|
-
const sourceListPk = (owner) => `USER#${owner}#CLIPSRCS`;
|
|
39
|
-
const scanSk = (scanId) => `CLIPSCAN#${scanId}`;
|
|
40
|
-
export class ClipRecordsService {
|
|
41
|
-
// ── Clips ────────────────────────────────────────────────────────────────
|
|
42
|
-
async putClip(clip) {
|
|
43
|
-
const owner = requireOwner(clip.owner_id);
|
|
44
|
-
await dynamodb.send(new PutCommand({
|
|
45
|
-
TableName: requireMainTableName(),
|
|
46
|
-
Item: {
|
|
47
|
-
pk: clipPk(owner),
|
|
48
|
-
sk: clipSk(clip.clip_id),
|
|
49
|
-
entity_type: ENTITY_CLIP,
|
|
50
|
-
gsi1pk: clipListPk(owner),
|
|
51
|
-
gsi1sk: `${clip.created_at}#${clip.clip_id}`,
|
|
52
|
-
gsi2pk: clipSourceListPk(owner, clip.source_video_id),
|
|
53
|
-
gsi2sk: `${clip.start_time_sec.toFixed(3)}#${clip.clip_id}`,
|
|
54
|
-
...clip
|
|
55
|
-
}
|
|
56
|
-
}));
|
|
57
|
-
}
|
|
58
|
-
async getClip(owner, clipId) {
|
|
59
|
-
const res = await dynamodb.send(new GetCommand({ TableName: requireMainTableName(), Key: { pk: clipPk(owner), sk: clipSk(clipId) } }));
|
|
60
|
-
return res.Item ? stripKeys(res.Item) : null;
|
|
61
|
-
}
|
|
62
|
-
/** List a user's clips (newest first), optionally scoped to a source video. */
|
|
63
|
-
async listClips(owner, opts = {}) {
|
|
64
|
-
const limit = opts.limit ?? 200;
|
|
65
|
-
if (opts.sourceVideoId) {
|
|
66
|
-
const res = await dynamodb.send(new QueryCommand({
|
|
67
|
-
TableName: requireMainTableName(),
|
|
68
|
-
IndexName: "gsi2",
|
|
69
|
-
KeyConditionExpression: "gsi2pk = :pk",
|
|
70
|
-
ExpressionAttributeValues: { ":pk": clipSourceListPk(owner, opts.sourceVideoId) },
|
|
71
|
-
Limit: limit
|
|
72
|
-
}));
|
|
73
|
-
return (res.Items ?? []).map(stripKeys);
|
|
74
|
-
}
|
|
75
|
-
const res = await dynamodb.send(new QueryCommand({
|
|
76
|
-
TableName: requireMainTableName(),
|
|
77
|
-
IndexName: "gsi1",
|
|
78
|
-
KeyConditionExpression: "gsi1pk = :pk",
|
|
79
|
-
ExpressionAttributeValues: { ":pk": clipListPk(owner) },
|
|
80
|
-
ScanIndexForward: false,
|
|
81
|
-
Limit: limit
|
|
82
|
-
}));
|
|
83
|
-
return (res.Items ?? []).map(stripKeys);
|
|
84
|
-
}
|
|
85
|
-
/** All embeddings for a user's clips — feeds the brute-force vector search. */
|
|
86
|
-
async listClipsWithEmbeddings(owner) {
|
|
87
|
-
const clips = await this.listClips(owner, { limit: 5000 });
|
|
88
|
-
return clips.filter((c) => Array.isArray(c.embedding) && c.embedding.length > 0);
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* The embedding model the library was built with (from the newest embedded
|
|
92
|
-
* clip). A match query must be embedded with the SAME model to be comparable.
|
|
93
|
-
*/
|
|
94
|
-
async getLibraryEmbeddingModel(owner) {
|
|
95
|
-
const clips = await this.listClips(owner, { limit: 20 });
|
|
96
|
-
for (const c of clips) {
|
|
97
|
-
if (c.embedding_model && Array.isArray(c.embedding) && c.embedding.length > 0)
|
|
98
|
-
return c.embedding_model;
|
|
99
|
-
}
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
async deleteClip(owner, clipId) {
|
|
103
|
-
await dynamodb.send(new DeleteCommand({ TableName: requireMainTableName(), Key: { pk: clipPk(owner), sk: clipSk(clipId) } }));
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Move a clip between virtual Raws folders. Partial update (doesn't rewrite
|
|
107
|
-
* the embedding vector). `folderPath` is stored normalized; "" = raws root.
|
|
108
|
-
*/
|
|
109
|
-
async updateClipFolder(owner, clipId, folderPath) {
|
|
110
|
-
await dynamodb.send(new UpdateCommand({
|
|
111
|
-
TableName: requireMainTableName(),
|
|
112
|
-
Key: { pk: clipPk(owner), sk: clipSk(clipId) },
|
|
113
|
-
UpdateExpression: "SET folder_path = :fp",
|
|
114
|
-
ConditionExpression: "attribute_exists(pk)",
|
|
115
|
-
ExpressionAttributeValues: { ":fp": folderPath }
|
|
116
|
-
}));
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Rename/move a whole Raws folder: re-point every clip whose folder_path is
|
|
120
|
-
* `fromFolder` OR nested beneath it onto `toFolder` (prefix-preserving), so a
|
|
121
|
-
* folder rename carries its subfolders too. Returns the count moved.
|
|
122
|
-
* Paths are compared normalized (trimmed, no leading/trailing slashes).
|
|
123
|
-
*/
|
|
124
|
-
async moveClipsFolder(owner, fromFolder, toFolder) {
|
|
125
|
-
const norm = (v) => String(v ?? "").split("/").map((s) => s.trim()).filter(Boolean).join("/");
|
|
126
|
-
const from = norm(fromFolder);
|
|
127
|
-
const to = norm(toFolder);
|
|
128
|
-
if (!from || from === to)
|
|
129
|
-
return 0;
|
|
130
|
-
const clips = await this.listClips(owner, { limit: 5000 });
|
|
131
|
-
let moved = 0;
|
|
132
|
-
for (const clip of clips) {
|
|
133
|
-
const fp = norm(clip.folder_path);
|
|
134
|
-
let next = null;
|
|
135
|
-
if (fp === from)
|
|
136
|
-
next = to;
|
|
137
|
-
else if (fp.startsWith(from + "/"))
|
|
138
|
-
next = to + fp.slice(from.length);
|
|
139
|
-
if (next != null) {
|
|
140
|
-
await this.updateClipFolder(owner, clip.clip_id, next);
|
|
141
|
-
moved += 1;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return moved;
|
|
145
|
-
}
|
|
146
|
-
// ── Presets ──────────────────────────────────────────────────────────────
|
|
147
|
-
async listPresets(owner) {
|
|
148
|
-
const res = await dynamodb.send(new QueryCommand({
|
|
149
|
-
TableName: requireMainTableName(),
|
|
150
|
-
KeyConditionExpression: "pk = :pk AND begins_with(sk, :sk)",
|
|
151
|
-
ExpressionAttributeValues: { ":pk": clipPk(owner), ":sk": "CLIPPRESET#" }
|
|
152
|
-
}));
|
|
153
|
-
const saved = (res.Items ?? []).map((i) => stripKeys(i));
|
|
154
|
-
return [...BUILTIN_PRESETS, ...saved];
|
|
155
|
-
}
|
|
156
|
-
async putPreset(owner, preset) {
|
|
157
|
-
await dynamodb.send(new PutCommand({
|
|
158
|
-
TableName: requireMainTableName(),
|
|
159
|
-
Item: {
|
|
160
|
-
pk: clipPk(owner),
|
|
161
|
-
sk: presetSk(preset.preset_id),
|
|
162
|
-
entity_type: ENTITY_PRESET,
|
|
163
|
-
gsi1pk: presetListPk(owner),
|
|
164
|
-
gsi1sk: preset.created_at,
|
|
165
|
-
...preset,
|
|
166
|
-
owner_id: owner
|
|
167
|
-
}
|
|
168
|
-
}));
|
|
169
|
-
}
|
|
170
|
-
async deletePreset(owner, presetId) {
|
|
171
|
-
await dynamodb.send(new DeleteCommand({ TableName: requireMainTableName(), Key: { pk: clipPk(owner), sk: presetSk(presetId) } }));
|
|
172
|
-
}
|
|
173
|
-
// ── Source videos ──────────────────────────────────────────────────────────
|
|
174
|
-
async putSource(record) {
|
|
175
|
-
await dynamodb.send(new PutCommand({
|
|
176
|
-
TableName: requireMainTableName(),
|
|
177
|
-
Item: {
|
|
178
|
-
pk: clipPk(record.owner_id),
|
|
179
|
-
sk: sourceSk(record.source_video_id),
|
|
180
|
-
entity_type: ENTITY_SOURCE,
|
|
181
|
-
gsi1pk: sourceListPk(record.owner_id),
|
|
182
|
-
gsi1sk: record.created_at,
|
|
183
|
-
...record
|
|
184
|
-
}
|
|
185
|
-
}));
|
|
186
|
-
}
|
|
187
|
-
async getSource(owner, sourceId) {
|
|
188
|
-
const res = await dynamodb.send(new GetCommand({ TableName: requireMainTableName(), Key: { pk: clipPk(owner), sk: sourceSk(sourceId) } }));
|
|
189
|
-
return res.Item ? stripKeys(res.Item) : null;
|
|
190
|
-
}
|
|
191
|
-
async listSources(owner) {
|
|
192
|
-
const res = await dynamodb.send(new QueryCommand({
|
|
193
|
-
TableName: requireMainTableName(),
|
|
194
|
-
IndexName: "gsi1",
|
|
195
|
-
KeyConditionExpression: "gsi1pk = :pk",
|
|
196
|
-
ExpressionAttributeValues: { ":pk": sourceListPk(owner) },
|
|
197
|
-
ScanIndexForward: false
|
|
198
|
-
}));
|
|
199
|
-
return (res.Items ?? []).map((i) => stripKeys(i));
|
|
200
|
-
}
|
|
201
|
-
// ── Scan jobs ──────────────────────────────────────────────────────────────
|
|
202
|
-
async putScan(record) {
|
|
203
|
-
await dynamodb.send(new PutCommand({
|
|
204
|
-
TableName: requireMainTableName(),
|
|
205
|
-
Item: {
|
|
206
|
-
pk: clipPk(record.owner_id),
|
|
207
|
-
sk: scanSk(record.scan_id),
|
|
208
|
-
entity_type: ENTITY_SCAN,
|
|
209
|
-
...record
|
|
210
|
-
}
|
|
211
|
-
}));
|
|
212
|
-
}
|
|
213
|
-
async getScan(owner, scanId) {
|
|
214
|
-
const res = await dynamodb.send(new GetCommand({ TableName: requireMainTableName(), Key: { pk: clipPk(owner), sk: scanSk(scanId) } }));
|
|
215
|
-
return res.Item ? stripKeys(res.Item) : null;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
export const clipRecords = new ClipRecordsService();
|
|
219
|
-
/** Build a user-saved preset record from raw criteria. */
|
|
220
|
-
export function makeUserPreset(input) {
|
|
221
|
-
return {
|
|
222
|
-
preset_id: `preset_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`,
|
|
223
|
-
name: input.name,
|
|
224
|
-
description: input.description,
|
|
225
|
-
criteria: input.criteria,
|
|
226
|
-
builtin: false,
|
|
227
|
-
owner_id: input.owner,
|
|
228
|
-
created_at: new Date().toISOString()
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
function requireOwner(owner) {
|
|
232
|
-
if (!owner)
|
|
233
|
-
throw new Error("Clip records require an owner_id.");
|
|
234
|
-
return owner;
|
|
235
|
-
}
|
|
236
|
-
// Drop the DynamoDB key/index attributes so callers get back a clean record.
|
|
237
|
-
function stripKeys(item) {
|
|
238
|
-
const { pk, sk, entity_type, gsi1pk, gsi1sk, gsi2pk, gsi2sk, gsi3pk, gsi3sk, ...rest } = item;
|
|
239
|
-
void pk;
|
|
240
|
-
void sk;
|
|
241
|
-
void entity_type;
|
|
242
|
-
void gsi1pk;
|
|
243
|
-
void gsi1sk;
|
|
244
|
-
void gsi2pk;
|
|
245
|
-
void gsi2sk;
|
|
246
|
-
void gsi3pk;
|
|
247
|
-
void gsi3sk;
|
|
248
|
-
return rest;
|
|
249
|
-
}
|
|
250
|
-
//# sourceMappingURL=clip-records.js.map
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// Cloud clip search — the two-stage flow (structured filter → semantic re-rank)
|
|
2
|
-
// wired over DynamoDB records + the configured vector index. Same result shape
|
|
3
|
-
// as the devcli store so the API mirrors the CLI (success criterion #4).
|
|
4
|
-
import { matchesCriteria, rankHits, scoreClip, searchClips } from "./clip-curation/index.js";
|
|
5
|
-
import { clipRecords } from "./clip-records.js";
|
|
6
|
-
import { resolveClipVectorIndex } from "./clip-vectors.js";
|
|
7
|
-
const vectorIndex = resolveClipVectorIndex((owner) => clipRecords.listClipsWithEmbeddings(owner));
|
|
8
|
-
export async function searchUserClips(owner, input) {
|
|
9
|
-
const limit = input.limit ?? 20;
|
|
10
|
-
const all = await clipRecords.listClips(owner, { limit: 5000 });
|
|
11
|
-
// ── No semantic signal → pure structured (hard) filter. ──────────────────
|
|
12
|
-
if (!input.queryEmbedding) {
|
|
13
|
-
const structured = all.filter((c) => matchesCriteria(c, input.criteria));
|
|
14
|
-
return rankHits(structured.map((c) => scoreClip(c, input.criteria, undefined, false)), limit);
|
|
15
|
-
}
|
|
16
|
-
// ── Semantic query → SEMANTIC-FIRST hybrid. ──────────────────────────────
|
|
17
|
-
// The old flow hard-AND-filtered by the (often auto-derived) categorical
|
|
18
|
-
// criteria BEFORE looking at embeddings, so the true nearest-neighbor clips
|
|
19
|
-
// were silently excluded whenever the NL→criteria step over-constrained —
|
|
20
|
-
// the "search doesn't work" symptom. Now: retrieve the nearest neighbors over
|
|
21
|
-
// the WHOLE library from the vector index, and only UNION in exact structured
|
|
22
|
-
// matches when the criteria actually constrains something. Everything is
|
|
23
|
-
// scored with the soft structured boost (`relaxed`), so an exact facet match
|
|
24
|
-
// floats a clip up without ever gating a strong semantic match out.
|
|
25
|
-
const knn = await vectorIndex.query(owner, input.queryEmbedding, Math.max(limit * 5, 50));
|
|
26
|
-
const byId = new Map(all.map((c) => [c.clip_id, c]));
|
|
27
|
-
const pool = new Map();
|
|
28
|
-
for (const h of knn) {
|
|
29
|
-
const c = byId.get(h.clip_id);
|
|
30
|
-
if (c)
|
|
31
|
-
pool.set(c.clip_id, c);
|
|
32
|
-
}
|
|
33
|
-
if (hasStructuredConstraints(input.criteria)) {
|
|
34
|
-
for (const c of all)
|
|
35
|
-
if (matchesCriteria(c, input.criteria))
|
|
36
|
-
pool.set(c.clip_id, c);
|
|
37
|
-
}
|
|
38
|
-
// Fallback: an empty vector index (e.g. clips scanned without an embedding
|
|
39
|
-
// provider) yields no neighbors — degrade to the whole library so keyword /
|
|
40
|
-
// structured scoring still returns something rather than nothing.
|
|
41
|
-
const candidates = pool.size > 0 ? [...pool.values()] : all;
|
|
42
|
-
const hits = candidates.map((c) => scoreClip(c, input.criteria, input.queryEmbedding, true));
|
|
43
|
-
return rankHits(hits, limit);
|
|
44
|
-
}
|
|
45
|
-
/** True when the criteria constrains any categorical/duration/boolean facet (not just free-text). */
|
|
46
|
-
function hasStructuredConstraints(criteria) {
|
|
47
|
-
for (const [key, value] of Object.entries(criteria)) {
|
|
48
|
-
if (key === "semantic_text")
|
|
49
|
-
continue;
|
|
50
|
-
if (value == null)
|
|
51
|
-
continue;
|
|
52
|
-
if (Array.isArray(value) ? value.length > 0 : true)
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
export { vectorIndex };
|
|
58
|
-
/**
|
|
59
|
-
* Match many scene annotations against the owner's clip library at once. Loads
|
|
60
|
-
* the library ONCE (embeddings ride on the records) and runs each scene's
|
|
61
|
-
* structured filter + semantic re-rank in memory — so a whole decomposed video
|
|
62
|
-
* costs one library read plus M cheap in-memory ranks.
|
|
63
|
-
*/
|
|
64
|
-
export async function matchScenesToClips(owner, queries) {
|
|
65
|
-
const all = await clipRecords.listClips(owner, { limit: 5000 });
|
|
66
|
-
return queries.map((q) => ({
|
|
67
|
-
id: q.id,
|
|
68
|
-
meta: q.meta,
|
|
69
|
-
matches: searchClips(all, {
|
|
70
|
-
criteria: q.criteria,
|
|
71
|
-
queryEmbedding: q.queryEmbedding,
|
|
72
|
-
limit: q.limit ?? 5,
|
|
73
|
-
relaxWhenEmpty: true
|
|
74
|
-
})
|
|
75
|
-
}));
|
|
76
|
-
}
|
|
77
|
-
//# sourceMappingURL=clip-search.js.map
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
// Vector index abstraction for cloud clip search. Two backends behind one
|
|
2
|
-
// interface so the pipeline is portable and cloud infra cost stays deferrable:
|
|
3
|
-
//
|
|
4
|
-
// • "dynamo" (default) — embeddings live on the clip records in DynamoDB;
|
|
5
|
-
// search brute-forces cosine in memory. Zero extra infra, works with the
|
|
6
|
-
// local-dynamo shim in `serve` mode. Fine to thousands of clips per user.
|
|
7
|
-
// • "s3vectors" — Amazon S3 Vectors index (handoff target). Best-effort:
|
|
8
|
-
// dynamically imports @aws-sdk/client-s3vectors and errors clearly if the
|
|
9
|
-
// dep/config is absent, so selecting it is opt-in.
|
|
10
|
-
//
|
|
11
|
-
// Both return the same {clip_id, similarity} shape; the caller applies the
|
|
12
|
-
// structured filter + final scoring with the shared query builder.
|
|
13
|
-
import { cosineSimilarity } from "./clip-curation/index.js";
|
|
14
|
-
import { config } from "../config.js";
|
|
15
|
-
/** Default backend: brute-force cosine over the owner's clip embeddings in DynamoDB. */
|
|
16
|
-
export class DynamoVectorIndex {
|
|
17
|
-
loadEmbeddings;
|
|
18
|
-
backend = "dynamo";
|
|
19
|
-
constructor(loadEmbeddings) {
|
|
20
|
-
this.loadEmbeddings = loadEmbeddings;
|
|
21
|
-
}
|
|
22
|
-
async upsert() {
|
|
23
|
-
/* embedding is stored on the clip record itself */
|
|
24
|
-
}
|
|
25
|
-
async delete() {
|
|
26
|
-
/* removed with the clip record */
|
|
27
|
-
}
|
|
28
|
-
async query(owner, queryEmbedding, k) {
|
|
29
|
-
const clips = await this.loadEmbeddings(owner);
|
|
30
|
-
return clips
|
|
31
|
-
.map((c) => ({ clip_id: c.clip_id, similarity: cosineSimilarity(c.embedding, queryEmbedding) }))
|
|
32
|
-
.sort((a, b) => b.similarity - a.similarity)
|
|
33
|
-
.slice(0, k);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Amazon S3 Vectors backend. Vectors are keyed by clip_id within a per-owner
|
|
38
|
-
* partition; metadata carries source_video_id for filtering. Requires
|
|
39
|
-
* VIDFARM_CLIP_VECTORS_BUCKET + VIDFARM_CLIP_VECTORS_INDEX and the
|
|
40
|
-
* @aws-sdk/client-s3vectors package.
|
|
41
|
-
*/
|
|
42
|
-
export class S3VectorsIndex {
|
|
43
|
-
bucket;
|
|
44
|
-
indexName;
|
|
45
|
-
backend = "s3vectors";
|
|
46
|
-
clientPromise = null;
|
|
47
|
-
constructor(bucket, indexName) {
|
|
48
|
-
this.bucket = bucket;
|
|
49
|
-
this.indexName = indexName;
|
|
50
|
-
}
|
|
51
|
-
async client() {
|
|
52
|
-
if (!this.clientPromise) {
|
|
53
|
-
this.clientPromise = import("@aws-sdk/client-s3vectors").catch(() => {
|
|
54
|
-
throw new Error("S3 Vectors backend selected but @aws-sdk/client-s3vectors is not installed. `npm i @aws-sdk/client-s3vectors` or set VIDFARM_CLIP_VECTOR_BACKEND=dynamo.");
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
const mod = await this.clientPromise;
|
|
58
|
-
return { mod, client: new mod.S3VectorsClient({}) };
|
|
59
|
-
}
|
|
60
|
-
async upsert(owner, clip) {
|
|
61
|
-
if (!clip.embedding?.length)
|
|
62
|
-
return;
|
|
63
|
-
const { mod, client } = await this.client();
|
|
64
|
-
await client.send(new mod.PutVectorsCommand({
|
|
65
|
-
vectorBucketName: this.bucket,
|
|
66
|
-
indexName: this.indexName,
|
|
67
|
-
vectors: [
|
|
68
|
-
{
|
|
69
|
-
key: vectorKey(owner, clip.clip_id),
|
|
70
|
-
data: { float32: clip.embedding },
|
|
71
|
-
metadata: { owner, source_video_id: clip.source_video_id, clip_id: clip.clip_id }
|
|
72
|
-
}
|
|
73
|
-
]
|
|
74
|
-
}));
|
|
75
|
-
}
|
|
76
|
-
async delete(owner, clipId) {
|
|
77
|
-
const { mod, client } = await this.client();
|
|
78
|
-
await client.send(new mod.DeleteVectorsCommand({ vectorBucketName: this.bucket, indexName: this.indexName, keys: [vectorKey(owner, clipId)] }));
|
|
79
|
-
}
|
|
80
|
-
async query(owner, queryEmbedding, k) {
|
|
81
|
-
const { mod, client } = await this.client();
|
|
82
|
-
const res = await client.send(new mod.QueryVectorsCommand({
|
|
83
|
-
vectorBucketName: this.bucket,
|
|
84
|
-
indexName: this.indexName,
|
|
85
|
-
topK: k,
|
|
86
|
-
queryVector: { float32: queryEmbedding },
|
|
87
|
-
filter: { owner },
|
|
88
|
-
returnMetadata: true,
|
|
89
|
-
returnDistance: true
|
|
90
|
-
}));
|
|
91
|
-
const vectors = res.vectors ?? [];
|
|
92
|
-
return vectors.map((v) => ({
|
|
93
|
-
clip_id: v.metadata?.clip_id ?? stripVectorKey(v.key),
|
|
94
|
-
// S3 Vectors returns cosine DISTANCE (0..2); convert to similarity.
|
|
95
|
-
similarity: 1 - Number(v.distance ?? 0)
|
|
96
|
-
}));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function vectorKey(owner, clipId) {
|
|
100
|
-
return `${owner}/${clipId}`;
|
|
101
|
-
}
|
|
102
|
-
function stripVectorKey(key) {
|
|
103
|
-
const idx = key.lastIndexOf("/");
|
|
104
|
-
return idx >= 0 ? key.slice(idx + 1) : key;
|
|
105
|
-
}
|
|
106
|
-
let cached = null;
|
|
107
|
-
/** Resolve the configured vector index (default: dynamo brute-force). */
|
|
108
|
-
export function resolveClipVectorIndex(loadEmbeddings) {
|
|
109
|
-
if (cached)
|
|
110
|
-
return cached;
|
|
111
|
-
const backend = (process.env.VIDFARM_CLIP_VECTOR_BACKEND ?? "dynamo").toLowerCase();
|
|
112
|
-
if (backend === "s3vectors") {
|
|
113
|
-
const bucket = process.env.VIDFARM_CLIP_VECTORS_BUCKET;
|
|
114
|
-
const index = process.env.VIDFARM_CLIP_VECTORS_INDEX;
|
|
115
|
-
if (bucket && index) {
|
|
116
|
-
cached = new S3VectorsIndex(bucket, index);
|
|
117
|
-
return cached;
|
|
118
|
-
}
|
|
119
|
-
// Missing config → fall back to the always-available dynamo backend.
|
|
120
|
-
}
|
|
121
|
-
cached = new DynamoVectorIndex(loadEmbeddings);
|
|
122
|
-
void config; // config imported for parity with sibling services / future use
|
|
123
|
-
return cached;
|
|
124
|
-
}
|
|
125
|
-
//# sourceMappingURL=clip-vectors.js.map
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// Sanitizes composition.html before it is stored in a fork's S3 namespace.
|
|
2
|
-
//
|
|
3
|
-
// Fork content lives under a public-read S3 prefix and is rendered SAME-ORIGIN
|
|
4
|
-
// inside the editor iframe (the editor needs same-origin DOM access to morph the
|
|
5
|
-
// timeline). That means any <script> or inline handler an author embeds would
|
|
6
|
-
// execute in a *viewer's* vidfarm.cc origin when a fork is shared — a stored-XSS
|
|
7
|
-
// vector against other users. Compositions are declarative (data-* attributes
|
|
8
|
-
// drive the injected runtime), so scripts are never needed in stored content:
|
|
9
|
-
// the trusted runtime is (re)injected at serve time by
|
|
10
|
-
// rewriteHyperframesDraftCompositionHtml. We therefore strip all author scripts,
|
|
11
|
-
// inline on*= handlers, and javascript:/vbscript: URLs.
|
|
12
|
-
//
|
|
13
|
-
// This is a pragmatic regex sanitizer (the Lambda has no DOM). It errs toward
|
|
14
|
-
// removing anything script-like; it is a hardening layer, not the only defense.
|
|
15
|
-
const SCRIPT_TAG_RE = /<script\b[^>]*>[\s\S]*?<\/script\s*>/gi;
|
|
16
|
-
// Void/self-closed or unterminated <script ...> with no closing tag.
|
|
17
|
-
const LONE_SCRIPT_OPEN_RE = /<script\b[^>]*\/?>/gi;
|
|
18
|
-
// Inline event handlers: on-word = "..." | '...' | bare token. Requires a
|
|
19
|
-
// preceding whitespace so we don't match attributes like `data-on=""`.
|
|
20
|
-
const INLINE_HANDLER_RE = /\son[a-z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi;
|
|
21
|
-
// Dangerous URL schemes in a navigable attribute value. Matches the whole
|
|
22
|
-
// value — quoted (single/double) or bare — so the closing quote is consumed and
|
|
23
|
-
// the replacement stays well-formed.
|
|
24
|
-
const DANGEROUS_SCHEME = String.raw `(?:javascript\s*:|vbscript\s*:|data\s*:\s*text\/html)`;
|
|
25
|
-
const DANGEROUS_URL_RE = new RegExp(String.raw `((?:href|src|xlink:href|action|formaction)\s*=\s*)` +
|
|
26
|
-
String.raw `(?:"\s*${DANGEROUS_SCHEME}[^"]*"|'\s*${DANGEROUS_SCHEME}[^']*'|${DANGEROUS_SCHEME}[^\s>]*)`, "gi");
|
|
27
|
-
export function sanitizeCompositionHtml(html) {
|
|
28
|
-
let scripts = 0;
|
|
29
|
-
let handlers = 0;
|
|
30
|
-
let urls = 0;
|
|
31
|
-
let out = html.replace(SCRIPT_TAG_RE, () => {
|
|
32
|
-
scripts += 1;
|
|
33
|
-
return "";
|
|
34
|
-
});
|
|
35
|
-
out = out.replace(LONE_SCRIPT_OPEN_RE, () => {
|
|
36
|
-
scripts += 1;
|
|
37
|
-
return "";
|
|
38
|
-
});
|
|
39
|
-
out = out.replace(INLINE_HANDLER_RE, () => {
|
|
40
|
-
handlers += 1;
|
|
41
|
-
return "";
|
|
42
|
-
});
|
|
43
|
-
out = out.replace(DANGEROUS_URL_RE, (_match, prefix) => {
|
|
44
|
-
urls += 1;
|
|
45
|
-
// Neutralize to a harmless anchor while preserving the attribute shape.
|
|
46
|
-
return `${prefix}"#"`;
|
|
47
|
-
});
|
|
48
|
-
const changed = scripts > 0 || handlers > 0 || urls > 0;
|
|
49
|
-
return { html: out, changed, removed: { scripts, handlers, urls } };
|
|
50
|
-
}
|
|
51
|
-
// Re-derives inline z-index from data-track-index on every open tag carrying
|
|
52
|
-
// the attribute. The render pipeline (normalizePublishHtml) enforces
|
|
53
|
-
// z-index = track — higher track draws in front — but the preview runtime
|
|
54
|
-
// trusts whatever inline z is stored, so any stored html with stale or missing
|
|
55
|
-
// z previews with the wrong stacking. Applying this at serve time keeps the
|
|
56
|
-
// preview identical to the render for every composition, including ones never
|
|
57
|
-
// touched by the current editor. String-only pass for the same reason as the
|
|
58
|
-
// sanitizer above (no DOM in the Lambda; and DOM serialization re-quotes
|
|
59
|
-
// font-family values, breaking the render's font resolver).
|
|
60
|
-
const OPEN_TAG_RE = /<([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[^\s"'>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'`=<>]+))?)*)\s*(\/?)>/g;
|
|
61
|
-
// Walks real attribute boundaries. A flat regex over the attr blob can match
|
|
62
|
-
// `style=` or `data-track-index` text INSIDE another attribute's value
|
|
63
|
-
// (e.g. a data-label containing markup), corrupting user content.
|
|
64
|
-
const ATTR_RE = /\s+([^\s"'>/=]+)(?:\s*=\s*("[^"]*"|'[^']*'|[^\s"'`=<>]+))?/g;
|
|
65
|
-
// KEEP IN SYNC with normalizeCompositionZIndex in
|
|
66
|
-
// demo/src/HyperframesStudioEditor.tsx (client editor copy).
|
|
67
|
-
export function normalizeCompositionZIndexHtml(html) {
|
|
68
|
-
if (!html || !html.includes("data-track-index"))
|
|
69
|
-
return html;
|
|
70
|
-
return html.replace(OPEN_TAG_RE, (match, tag, rawAttrs, selfClose) => {
|
|
71
|
-
let track = null;
|
|
72
|
-
let styleQuote = '"';
|
|
73
|
-
let styleValue = null;
|
|
74
|
-
let styleStart = -1;
|
|
75
|
-
let styleEnd = -1;
|
|
76
|
-
ATTR_RE.lastIndex = 0;
|
|
77
|
-
let attr;
|
|
78
|
-
while ((attr = ATTR_RE.exec(rawAttrs))) {
|
|
79
|
-
const name = attr[1].toLowerCase();
|
|
80
|
-
const rawValue = attr[2];
|
|
81
|
-
if (rawValue === undefined)
|
|
82
|
-
continue;
|
|
83
|
-
const quoted = rawValue.startsWith('"') || rawValue.startsWith("'");
|
|
84
|
-
const value = quoted ? rawValue.slice(1, -1) : rawValue;
|
|
85
|
-
if (name === "data-track-index") {
|
|
86
|
-
const parsed = Number.parseInt(value, 10);
|
|
87
|
-
if (Number.isFinite(parsed))
|
|
88
|
-
track = parsed;
|
|
89
|
-
}
|
|
90
|
-
else if (name === "style") {
|
|
91
|
-
styleQuote = quoted ? rawValue[0] : '"';
|
|
92
|
-
styleValue = value;
|
|
93
|
-
styleStart = attr.index;
|
|
94
|
-
styleEnd = attr.index + attr[0].length;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (track === null)
|
|
98
|
-
return match;
|
|
99
|
-
const desired = String(track);
|
|
100
|
-
if (styleValue !== null) {
|
|
101
|
-
let nextStyle;
|
|
102
|
-
if (/(^|;)\s*z-index\s*:/i.test(styleValue)) {
|
|
103
|
-
if (new RegExp(`(^|;)\\s*z-index\\s*:\\s*${desired}\\s*(;|$)`, "i").test(styleValue)) {
|
|
104
|
-
return match;
|
|
105
|
-
}
|
|
106
|
-
// Replacer function: a plain replacement string would interpret $-
|
|
107
|
-
// sequences inside the surrounding style (e.g. content:'20$') as
|
|
108
|
-
// substitution metacharacters and corrupt the attribute.
|
|
109
|
-
nextStyle = styleValue.replace(/(^|;)\s*z-index\s*:\s*[^;]*/i, (_zMatch, lead) => `${lead}z-index:${desired}`);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
nextStyle = styleValue.endsWith(";") || styleValue.length === 0
|
|
113
|
-
? `${styleValue}z-index:${desired}`
|
|
114
|
-
: `${styleValue};z-index:${desired}`;
|
|
115
|
-
}
|
|
116
|
-
// Rebuild by slicing around the style attribute — never String.replace
|
|
117
|
-
// with user-controlled content.
|
|
118
|
-
const rebuiltAttrs = `${rawAttrs.slice(0, styleStart)} style=${styleQuote}${nextStyle}${styleQuote}${rawAttrs.slice(styleEnd)}`;
|
|
119
|
-
return `<${tag}${rebuiltAttrs}${selfClose ? "/" : ""}>`;
|
|
120
|
-
}
|
|
121
|
-
return `<${tag}${rawAttrs} style="z-index:${desired}"${selfClose ? "/" : ""}>`;
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
//# sourceMappingURL=composition-sanitize.js.map
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, watch } from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { config } from "../config.js";
|
|
4
|
-
// fs.watch on macOS emits 1–3 events per save; coalesce a burst into one
|
|
5
|
-
// broadcast. Also drop events for a fork the server itself just wrote (the
|
|
6
|
-
// editor's own PUT) so a save doesn't echo back as a reload.
|
|
7
|
-
const COALESCE_MS = 80;
|
|
8
|
-
const SELF_WRITE_SUPPRESS_MS = 500;
|
|
9
|
-
class CompositionWatch {
|
|
10
|
-
listeners = new Set();
|
|
11
|
-
suppressUntil = new Map();
|
|
12
|
-
started = false;
|
|
13
|
-
pending = null;
|
|
14
|
-
timer = null;
|
|
15
|
-
forksDir() {
|
|
16
|
-
return path.join(config.VIDFARM_DATA_DIR, "storage", "compositions", "forks");
|
|
17
|
-
}
|
|
18
|
-
start() {
|
|
19
|
-
if (this.started || config.STORAGE_DRIVER !== "local")
|
|
20
|
-
return;
|
|
21
|
-
this.started = true;
|
|
22
|
-
const dir = this.forksDir();
|
|
23
|
-
mkdirSync(dir, { recursive: true });
|
|
24
|
-
try {
|
|
25
|
-
watch(dir, { recursive: true }, (_event, filename) => {
|
|
26
|
-
if (filename)
|
|
27
|
-
this.onFsEvent(String(filename));
|
|
28
|
-
});
|
|
29
|
-
console.log(`[vidfarm] live-reload watching ${dir}`);
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
console.warn(`[vidfarm] live-reload watch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
// Call right before the server writes a working composition file so the
|
|
36
|
-
// editor's own save doesn't bounce back as an external reload.
|
|
37
|
-
suppressFork(forkId) {
|
|
38
|
-
this.suppressUntil.set(forkId, Date.now() + SELF_WRITE_SUPPRESS_MS);
|
|
39
|
-
}
|
|
40
|
-
subscribe(listener) {
|
|
41
|
-
this.listeners.add(listener);
|
|
42
|
-
return () => this.listeners.delete(listener);
|
|
43
|
-
}
|
|
44
|
-
onFsEvent(filename) {
|
|
45
|
-
// filename is relative to forksDir, e.g. "<forkId>/working/composition.html".
|
|
46
|
-
const parts = filename.split(/[\\/]/).filter(Boolean);
|
|
47
|
-
if (parts.length < 3 || parts[1] !== "working")
|
|
48
|
-
return;
|
|
49
|
-
const file = parts[2];
|
|
50
|
-
if (file !== "composition.html" && file !== "composition.json")
|
|
51
|
-
return;
|
|
52
|
-
const forkId = parts[0];
|
|
53
|
-
const until = this.suppressUntil.get(forkId);
|
|
54
|
-
if (until && Date.now() < until)
|
|
55
|
-
return;
|
|
56
|
-
this.pending = { forkId, file, at: Date.now() };
|
|
57
|
-
if (this.timer)
|
|
58
|
-
clearTimeout(this.timer);
|
|
59
|
-
this.timer = setTimeout(() => {
|
|
60
|
-
this.timer = null;
|
|
61
|
-
const change = this.pending;
|
|
62
|
-
this.pending = null;
|
|
63
|
-
if (change)
|
|
64
|
-
this.broadcast(change);
|
|
65
|
-
}, COALESCE_MS);
|
|
66
|
-
}
|
|
67
|
-
broadcast(change) {
|
|
68
|
-
for (const listener of [...this.listeners]) {
|
|
69
|
-
try {
|
|
70
|
-
listener(change);
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// A dead SSE writer is dropped on its own onAbort; ignore here.
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
export const compositionWatch = new CompositionWatch();
|
|
79
|
-
//# sourceMappingURL=composition-watch.js.map
|