@mevdragon/vidfarm-devcli 0.7.1 → 0.9.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 +8 -0
- package/demo/dist/app.js +81 -53
- package/dist/src/app.js +1010 -8
- package/dist/src/cli.js +390 -53
- package/dist/src/config.js +6 -0
- package/dist/src/devcli/clip-store.js +335 -0
- package/dist/src/devcli/clips.js +803 -0
- package/dist/src/devcli/telemetry.js +236 -0
- package/dist/src/frontend/homepage-view.js +5 -0
- package/dist/src/frontend/template-editor-chat.js +212 -0
- package/dist/src/services/clip-curation/cost.js +112 -0
- package/dist/src/services/clip-curation/ffmpeg.js +258 -0
- package/dist/src/services/clip-curation/gemini.js +342 -0
- package/dist/src/services/clip-curation/index.js +15 -0
- package/dist/src/services/clip-curation/presets.js +20 -0
- package/dist/src/services/clip-curation/presets.v1.json +59 -0
- package/dist/src/services/clip-curation/query.js +163 -0
- package/dist/src/services/clip-curation/refine.js +136 -0
- package/dist/src/services/clip-curation/scan.js +137 -0
- package/dist/src/services/clip-curation/taxonomy.js +71 -0
- package/dist/src/services/clip-curation/taxonomy.v1.json +90 -0
- package/dist/src/services/clip-curation/types.js +7 -0
- package/dist/src/services/clip-records.js +209 -0
- package/dist/src/services/clip-search.js +47 -0
- package/dist/src/services/clip-vectors.js +125 -0
- package/dist/src/services/hyperframes.js +369 -9
- package/dist/src/services/scene-annotations.js +32 -0
- package/package.json +5 -1
- package/public/assets/homepage-client-app.js +17 -17
- package/public/assets/page-runtime-client-app.js +31 -31
|
@@ -0,0 +1,209 @@
|
|
|
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 } 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
|
+
// ── Presets ──────────────────────────────────────────────────────────────
|
|
106
|
+
async listPresets(owner) {
|
|
107
|
+
const res = await dynamodb.send(new QueryCommand({
|
|
108
|
+
TableName: requireMainTableName(),
|
|
109
|
+
KeyConditionExpression: "pk = :pk AND begins_with(sk, :sk)",
|
|
110
|
+
ExpressionAttributeValues: { ":pk": clipPk(owner), ":sk": "CLIPPRESET#" }
|
|
111
|
+
}));
|
|
112
|
+
const saved = (res.Items ?? []).map((i) => stripKeys(i));
|
|
113
|
+
return [...BUILTIN_PRESETS, ...saved];
|
|
114
|
+
}
|
|
115
|
+
async putPreset(owner, preset) {
|
|
116
|
+
await dynamodb.send(new PutCommand({
|
|
117
|
+
TableName: requireMainTableName(),
|
|
118
|
+
Item: {
|
|
119
|
+
pk: clipPk(owner),
|
|
120
|
+
sk: presetSk(preset.preset_id),
|
|
121
|
+
entity_type: ENTITY_PRESET,
|
|
122
|
+
gsi1pk: presetListPk(owner),
|
|
123
|
+
gsi1sk: preset.created_at,
|
|
124
|
+
...preset,
|
|
125
|
+
owner_id: owner
|
|
126
|
+
}
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
async deletePreset(owner, presetId) {
|
|
130
|
+
await dynamodb.send(new DeleteCommand({ TableName: requireMainTableName(), Key: { pk: clipPk(owner), sk: presetSk(presetId) } }));
|
|
131
|
+
}
|
|
132
|
+
// ── Source videos ──────────────────────────────────────────────────────────
|
|
133
|
+
async putSource(record) {
|
|
134
|
+
await dynamodb.send(new PutCommand({
|
|
135
|
+
TableName: requireMainTableName(),
|
|
136
|
+
Item: {
|
|
137
|
+
pk: clipPk(record.owner_id),
|
|
138
|
+
sk: sourceSk(record.source_video_id),
|
|
139
|
+
entity_type: ENTITY_SOURCE,
|
|
140
|
+
gsi1pk: sourceListPk(record.owner_id),
|
|
141
|
+
gsi1sk: record.created_at,
|
|
142
|
+
...record
|
|
143
|
+
}
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
async getSource(owner, sourceId) {
|
|
147
|
+
const res = await dynamodb.send(new GetCommand({ TableName: requireMainTableName(), Key: { pk: clipPk(owner), sk: sourceSk(sourceId) } }));
|
|
148
|
+
return res.Item ? stripKeys(res.Item) : null;
|
|
149
|
+
}
|
|
150
|
+
async listSources(owner) {
|
|
151
|
+
const res = await dynamodb.send(new QueryCommand({
|
|
152
|
+
TableName: requireMainTableName(),
|
|
153
|
+
IndexName: "gsi1",
|
|
154
|
+
KeyConditionExpression: "gsi1pk = :pk",
|
|
155
|
+
ExpressionAttributeValues: { ":pk": sourceListPk(owner) },
|
|
156
|
+
ScanIndexForward: false
|
|
157
|
+
}));
|
|
158
|
+
return (res.Items ?? []).map((i) => stripKeys(i));
|
|
159
|
+
}
|
|
160
|
+
// ── Scan jobs ──────────────────────────────────────────────────────────────
|
|
161
|
+
async putScan(record) {
|
|
162
|
+
await dynamodb.send(new PutCommand({
|
|
163
|
+
TableName: requireMainTableName(),
|
|
164
|
+
Item: {
|
|
165
|
+
pk: clipPk(record.owner_id),
|
|
166
|
+
sk: scanSk(record.scan_id),
|
|
167
|
+
entity_type: ENTITY_SCAN,
|
|
168
|
+
...record
|
|
169
|
+
}
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
async getScan(owner, scanId) {
|
|
173
|
+
const res = await dynamodb.send(new GetCommand({ TableName: requireMainTableName(), Key: { pk: clipPk(owner), sk: scanSk(scanId) } }));
|
|
174
|
+
return res.Item ? stripKeys(res.Item) : null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export const clipRecords = new ClipRecordsService();
|
|
178
|
+
/** Build a user-saved preset record from raw criteria. */
|
|
179
|
+
export function makeUserPreset(input) {
|
|
180
|
+
return {
|
|
181
|
+
preset_id: `preset_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`,
|
|
182
|
+
name: input.name,
|
|
183
|
+
description: input.description,
|
|
184
|
+
criteria: input.criteria,
|
|
185
|
+
builtin: false,
|
|
186
|
+
owner_id: input.owner,
|
|
187
|
+
created_at: new Date().toISOString()
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function requireOwner(owner) {
|
|
191
|
+
if (!owner)
|
|
192
|
+
throw new Error("Clip records require an owner_id.");
|
|
193
|
+
return owner;
|
|
194
|
+
}
|
|
195
|
+
// Drop the DynamoDB key/index attributes so callers get back a clean record.
|
|
196
|
+
function stripKeys(item) {
|
|
197
|
+
const { pk, sk, entity_type, gsi1pk, gsi1sk, gsi2pk, gsi2sk, gsi3pk, gsi3sk, ...rest } = item;
|
|
198
|
+
void pk;
|
|
199
|
+
void sk;
|
|
200
|
+
void entity_type;
|
|
201
|
+
void gsi1pk;
|
|
202
|
+
void gsi1sk;
|
|
203
|
+
void gsi2pk;
|
|
204
|
+
void gsi2sk;
|
|
205
|
+
void gsi3pk;
|
|
206
|
+
void gsi3sk;
|
|
207
|
+
return rest;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=clip-records.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
const structured = all.filter((c) => matchesCriteria(c, input.criteria));
|
|
12
|
+
if (structured.length > 0) {
|
|
13
|
+
// Semantic re-rank the survivors (embeddings already ride on the records).
|
|
14
|
+
const hits = structured.map((c) => scoreClip(c, input.criteria, input.queryEmbedding, false));
|
|
15
|
+
return rankHits(hits, limit);
|
|
16
|
+
}
|
|
17
|
+
// No structured match — relax to a semantic-only pass via the vector index.
|
|
18
|
+
if (input.queryEmbedding) {
|
|
19
|
+
const knn = await vectorIndex.query(owner, input.queryEmbedding, Math.max(limit * 5, 50));
|
|
20
|
+
const byId = new Map(all.map((c) => [c.clip_id, c]));
|
|
21
|
+
const pool = knn.map((h) => byId.get(h.clip_id)).filter((c) => Boolean(c));
|
|
22
|
+
const hits = pool.map((c) => scoreClip(c, input.criteria, input.queryEmbedding, true));
|
|
23
|
+
return rankHits(hits, limit);
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
export { vectorIndex };
|
|
28
|
+
/**
|
|
29
|
+
* Match many scene annotations against the owner's clip library at once. Loads
|
|
30
|
+
* the library ONCE (embeddings ride on the records) and runs each scene's
|
|
31
|
+
* structured filter + semantic re-rank in memory — so a whole decomposed video
|
|
32
|
+
* costs one library read plus M cheap in-memory ranks.
|
|
33
|
+
*/
|
|
34
|
+
export async function matchScenesToClips(owner, queries) {
|
|
35
|
+
const all = await clipRecords.listClips(owner, { limit: 5000 });
|
|
36
|
+
return queries.map((q) => ({
|
|
37
|
+
id: q.id,
|
|
38
|
+
meta: q.meta,
|
|
39
|
+
matches: searchClips(all, {
|
|
40
|
+
criteria: q.criteria,
|
|
41
|
+
queryEmbedding: q.queryEmbedding,
|
|
42
|
+
limit: q.limit ?? 5,
|
|
43
|
+
relaxWhenEmpty: true
|
|
44
|
+
})
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=clip-search.js.map
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|