@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.
Files changed (154) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
  13. package/SKILL.director.md +521 -261
  14. package/dist/src/cli.js +404 -96
  15. package/dist/src/devcli/clips.js +15 -10
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +10 -0
  20. package/package.json +30 -10
  21. package/SKILL.platform.md +0 -432
  22. package/demo/README.md +0 -28
  23. package/demo/dist/app.css +0 -1
  24. package/demo/dist/app.js +0 -1850
  25. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  26. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  27. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  28. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  29. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  30. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  31. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  32. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  33. package/demo/dist/favicon.ico +0 -0
  34. package/demo/dist/icons/timeline/audio.svg +0 -7
  35. package/demo/dist/icons/timeline/captions.svg +0 -5
  36. package/demo/dist/icons/timeline/composition.svg +0 -12
  37. package/demo/dist/icons/timeline/image.svg +0 -18
  38. package/demo/dist/icons/timeline/music.svg +0 -10
  39. package/demo/dist/icons/timeline/text.svg +0 -3
  40. package/demo/dist/index.html +0 -15
  41. package/dist/src/account-pages-legacy.js +0 -9396
  42. package/dist/src/account-pages.js +0 -61
  43. package/dist/src/app.js +0 -21602
  44. package/dist/src/composition-runtime.js +0 -1053
  45. package/dist/src/config.js +0 -217
  46. package/dist/src/context.js +0 -447
  47. package/dist/src/dev-app-legacy.js +0 -739
  48. package/dist/src/dev-app.js +0 -6
  49. package/dist/src/devcli/migrate-local.js +0 -140
  50. package/dist/src/devcli/sync.js +0 -368
  51. package/dist/src/domain.js +0 -5
  52. package/dist/src/editor-chat-history.js +0 -82
  53. package/dist/src/editor-chat.js +0 -756
  54. package/dist/src/editor-dark-theme.js +0 -1128
  55. package/dist/src/frontend/debug.js +0 -71
  56. package/dist/src/frontend/discover-client.js +0 -130
  57. package/dist/src/frontend/discover-store.js +0 -23
  58. package/dist/src/frontend/file-directory.js +0 -1018
  59. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  60. package/dist/src/frontend/homepage-client.js +0 -446
  61. package/dist/src/frontend/homepage-shared.js +0 -201
  62. package/dist/src/frontend/homepage-store.js +0 -66
  63. package/dist/src/frontend/homepage-view.js +0 -705
  64. package/dist/src/frontend/page-runtime-client.js +0 -132
  65. package/dist/src/frontend/page-runtime-store.js +0 -9
  66. package/dist/src/frontend/sentry.js +0 -42
  67. package/dist/src/frontend/template-editor-chat.js +0 -4181
  68. package/dist/src/help-page.js +0 -346
  69. package/dist/src/homepage.js +0 -1458
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/instrument.js +0 -30
  72. package/dist/src/landing-page.js +0 -384
  73. package/dist/src/page-runtime.js +0 -2
  74. package/dist/src/page-shell.js +0 -1452
  75. package/dist/src/primitive-context.js +0 -416
  76. package/dist/src/primitive-registry.js +0 -3940
  77. package/dist/src/primitive-sdk.js +0 -4
  78. package/dist/src/primitives/hyperframes-media.js +0 -108
  79. package/dist/src/react-page-shell.js +0 -35
  80. package/dist/src/ready-post-schedule-component.js +0 -1540
  81. package/dist/src/registry.js +0 -296
  82. package/dist/src/reskin/agency-page.js +0 -299
  83. package/dist/src/reskin/calendar-page.js +0 -568
  84. package/dist/src/reskin/chat-page.js +0 -942
  85. package/dist/src/reskin/discover-page.js +0 -1788
  86. package/dist/src/reskin/document.js +0 -1587
  87. package/dist/src/reskin/help-page.js +0 -357
  88. package/dist/src/reskin/index-page.js +0 -62
  89. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  90. package/dist/src/reskin/inpaint-page.js +0 -2554
  91. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  92. package/dist/src/reskin/job-runs-page.js +0 -477
  93. package/dist/src/reskin/library-page.js +0 -1634
  94. package/dist/src/reskin/login-page.js +0 -262
  95. package/dist/src/reskin/portfolio-page.js +0 -687
  96. package/dist/src/reskin/pricing-page.js +0 -390
  97. package/dist/src/reskin/settings-page.js +0 -732
  98. package/dist/src/reskin/theme.js +0 -711
  99. package/dist/src/runtime.js +0 -35
  100. package/dist/src/services/api-call-history.js +0 -249
  101. package/dist/src/services/auth.js +0 -152
  102. package/dist/src/services/billing-pricing.js +0 -39
  103. package/dist/src/services/billing.js +0 -241
  104. package/dist/src/services/cast.js +0 -127
  105. package/dist/src/services/chat-threads.js +0 -92
  106. package/dist/src/services/clip-records.js +0 -250
  107. package/dist/src/services/clip-search.js +0 -77
  108. package/dist/src/services/clip-vectors.js +0 -125
  109. package/dist/src/services/composition-sanitize.js +0 -124
  110. package/dist/src/services/composition-watch.js +0 -79
  111. package/dist/src/services/elevenlabs.js +0 -222
  112. package/dist/src/services/file-directory.js +0 -117
  113. package/dist/src/services/fork-access.js +0 -93
  114. package/dist/src/services/fork-manifest.js +0 -42
  115. package/dist/src/services/ghostcut.js +0 -179
  116. package/dist/src/services/hyperframes.js +0 -3517
  117. package/dist/src/services/job-capacity.js +0 -14
  118. package/dist/src/services/job-logs.js +0 -197
  119. package/dist/src/services/jobs.js +0 -136
  120. package/dist/src/services/local-dynamo.js +0 -0
  121. package/dist/src/services/media-processing.js +0 -766
  122. package/dist/src/services/primitive-media-lambda.js +0 -280
  123. package/dist/src/services/providers.js +0 -2748
  124. package/dist/src/services/rate-limits.js +0 -262
  125. package/dist/src/services/scene-annotations.js +0 -32
  126. package/dist/src/services/serverless-auth.js +0 -382
  127. package/dist/src/services/serverless-jobs.js +0 -1084
  128. package/dist/src/services/serverless-provider-keys.js +0 -409
  129. package/dist/src/services/serverless-records.js +0 -1515
  130. package/dist/src/services/serverless-template-configs.js +0 -75
  131. package/dist/src/services/storage.js +0 -461
  132. package/dist/src/services/swipe-customize.js +0 -437
  133. package/dist/src/services/template-certification.js +0 -413
  134. package/dist/src/services/template-loader.js +0 -99
  135. package/dist/src/services/template-runtime-bundles.js +0 -217
  136. package/dist/src/services/template-sources.js +0 -1017
  137. package/dist/src/services/upstream.js +0 -248
  138. package/dist/src/services/video-normalization.js +0 -2
  139. package/dist/src/services/webhooks.js +0 -62
  140. package/dist/src/template-editor-pages.js +0 -2576
  141. package/dist/src/template-editor-shell.js +0 -2893
  142. package/dist/src/template-sdk.js +0 -4
  143. package/dist/src/worker.js +0 -17
  144. package/public/assets/discover-client-app.js +0 -1
  145. package/public/assets/file-directory-app.js +0 -3
  146. package/public/assets/homepage-app.js +0 -54
  147. package/public/assets/homepage-client-app.js +0 -80
  148. package/public/assets/page-runtime-client-app.js +0 -94
  149. package/public/assets/placeholders/scene-placeholder.png +0 -0
  150. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  151. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  152. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  153. package/src/assets/favicon.ico +0 -0
  154. 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