@maravilla-labs/platform 0.13.0 → 0.15.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/CHANGELOG.md +16 -0
- package/dist/config.d.ts +42 -3
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +16 -7
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -1
- package/dist/{transforms-CbM38kf7.d.ts → transforms-BPrlJoYd.d.ts} +158 -1
- package/package.json +1 -1
- package/src/config.ts +40 -0
- package/src/remote-client.ts +70 -7
- package/src/transforms.ts +162 -0
- package/src/types.ts +21 -12
|
@@ -32,6 +32,15 @@ interface TranscodeOpts {
|
|
|
32
32
|
max_height?: number;
|
|
33
33
|
audio_codec?: string;
|
|
34
34
|
bitrate_kbps?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Force a constant output frame rate (fps). Pass this for variable-frame-rate
|
|
37
|
+
* sources (e.g. MediaRecorder WebM) so the MP4 stays frame-accurately
|
|
38
|
+
* seekable for the frames pipeline — without it, a near-static VFR segment
|
|
39
|
+
* can collapse to a handful of frames that a fixed-rate renderer stalls on.
|
|
40
|
+
* When omitted, the worker auto-applies CFR only if it probes the source as
|
|
41
|
+
* VFR; a determinate-rate source is left untouched.
|
|
42
|
+
*/
|
|
43
|
+
fps?: number;
|
|
35
44
|
}
|
|
36
45
|
/** Options for `transforms.thumbnail` — extract a single video frame. */
|
|
37
46
|
interface ThumbnailOpts {
|
|
@@ -62,6 +71,43 @@ interface ExtractFramesOpts {
|
|
|
62
71
|
seek_ms?: number;
|
|
63
72
|
/** How much of the clip to extract from `seek_ms`, ms (default: to end). */
|
|
64
73
|
duration_ms?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Also extract the source's audio track to `detach_audio.target` in the SAME
|
|
76
|
+
* job — one call yields both the frame set and a detachable audio asset
|
|
77
|
+
* (folds the upload-time "detach audio" step into frame extraction). The
|
|
78
|
+
* frame-set manifest is still the job's primary output; the audio is written
|
|
79
|
+
* to the caller-chosen `target` key.
|
|
80
|
+
*/
|
|
81
|
+
detach_audio?: DetachAudioOpts;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Bundle a detached-audio side-output into an {@link ExtractFramesOpts} job.
|
|
85
|
+
* The worker runs one extra audio-extraction pass on the same source and writes
|
|
86
|
+
* the AAC/m4a result to `target` (a caller-chosen, tenant-scoped storage key).
|
|
87
|
+
*/
|
|
88
|
+
interface DetachAudioOpts {
|
|
89
|
+
/** Storage key to write the extracted audio (AAC/m4a) to. */
|
|
90
|
+
target: string;
|
|
91
|
+
/** Output bitrate, kbps. Server picks a sensible AAC bitrate when omitted. */
|
|
92
|
+
bitrate_kbps?: number;
|
|
93
|
+
/** Start offset into the source, ms (default 0). */
|
|
94
|
+
seek_ms?: number;
|
|
95
|
+
/** How much of the source to extract from `seek_ms`, ms (default: to end). */
|
|
96
|
+
duration_ms?: number;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Options for `transforms.extractAudio` — split a video's audio track off into
|
|
100
|
+
* a standalone AAC/m4a asset. Used to detach the mic/system audio recorded
|
|
101
|
+
* alongside a camera/screen clip so it can be placed + ducked independently on
|
|
102
|
+
* the frames timeline.
|
|
103
|
+
*/
|
|
104
|
+
interface ExtractAudioOpts {
|
|
105
|
+
/** Output bitrate, kbps. Server picks a sensible AAC bitrate when omitted. */
|
|
106
|
+
bitrate_kbps?: number;
|
|
107
|
+
/** Start offset into the source, ms (default 0). */
|
|
108
|
+
seek_ms?: number;
|
|
109
|
+
/** How much of the source to extract from `seek_ms`, ms (default: to end). */
|
|
110
|
+
duration_ms?: number;
|
|
65
111
|
}
|
|
66
112
|
/**
|
|
67
113
|
* Result of an `extractFrames` job: a JSON manifest stored at the job's
|
|
@@ -87,12 +133,80 @@ interface ResizeOpts {
|
|
|
87
133
|
format: ImageFormat;
|
|
88
134
|
quality?: number;
|
|
89
135
|
strip_metadata?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Crop strategy when BOTH `width` and `height` are given (the output is
|
|
138
|
+
* cropped to fill the exact box rather than letterboxed):
|
|
139
|
+
* - `'center'` — crop around the geometric center.
|
|
140
|
+
* - `'focal'` — crop around the source's focal point. The server injects
|
|
141
|
+
* the focal point from the persisted {@link FocalPointRecord} (set via
|
|
142
|
+
* `detectFaces` or `setFocalPoint`); if none exists it falls back to
|
|
143
|
+
* center.
|
|
144
|
+
* Omit for the default aspect-preserving fit (no crop).
|
|
145
|
+
*/
|
|
146
|
+
crop?: 'focal' | 'center';
|
|
147
|
+
/**
|
|
148
|
+
* Explicit focal point for `crop: 'focal'`, normalized `0..=1`. Server-
|
|
149
|
+
* injected — callers normally omit this and let the platform resolve the
|
|
150
|
+
* stored focal point for `src_key`. Present here so the wire type mirrors
|
|
151
|
+
* the Rust `ResizeOpts` exactly.
|
|
152
|
+
*/
|
|
153
|
+
focal?: FocalPoint;
|
|
90
154
|
}
|
|
91
155
|
/** Options for `transforms.ocr`. */
|
|
92
156
|
interface OcrOpts {
|
|
93
157
|
/** Tesseract language code(s). Defaults to `"eng"` server-side when omitted. */
|
|
94
158
|
lang?: string;
|
|
95
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Options for `transforms.detectFaces` — run face detection over an image and
|
|
162
|
+
* persist the resulting focal point (the center of the highest-scoring face,
|
|
163
|
+
* or the centroid of all faces) as a {@link FocalPointRecord} keyed on the
|
|
164
|
+
* source. The derived output is a JSON artifact listing the detected boxes.
|
|
165
|
+
*/
|
|
166
|
+
interface DetectFacesOpts {
|
|
167
|
+
/** Minimum detector confidence to keep a face, `0..=1`. Defaults to `0.6` server-side. */
|
|
168
|
+
min_score?: number;
|
|
169
|
+
/** Cap on the number of faces retained (highest-scoring first). Defaults to `32` server-side. */
|
|
170
|
+
max_faces?: number;
|
|
171
|
+
/** Trade accuracy for latency (smaller detector input). Defaults to `false` server-side. */
|
|
172
|
+
fast?: boolean;
|
|
173
|
+
}
|
|
174
|
+
/** A focal point in normalized image coordinates, each component `0..=1`. */
|
|
175
|
+
interface FocalPoint {
|
|
176
|
+
x: number;
|
|
177
|
+
y: number;
|
|
178
|
+
}
|
|
179
|
+
/** A single detected face box, normalized to `0..1` of the image dimensions. */
|
|
180
|
+
interface FaceBox {
|
|
181
|
+
x: number;
|
|
182
|
+
y: number;
|
|
183
|
+
w: number;
|
|
184
|
+
h: number;
|
|
185
|
+
/** Detector confidence, `0..1`. */
|
|
186
|
+
score: number;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Persisted focal-point record for a source image — the result of
|
|
190
|
+
* `detectFaces` or a manual `setFocalPoint`. Returned by `getFocalPoint`.
|
|
191
|
+
*/
|
|
192
|
+
interface FocalPointRecord {
|
|
193
|
+
/** Storage key of the source image this focal point belongs to. */
|
|
194
|
+
src_key: string;
|
|
195
|
+
/** The resolved focal point, normalized `0..=1`. */
|
|
196
|
+
focal: FocalPoint;
|
|
197
|
+
/** Whether the point came from face detection or a manual override. */
|
|
198
|
+
source: 'faces' | 'manual';
|
|
199
|
+
/** Number of faces detected (`null` until a detection has run for this key). */
|
|
200
|
+
face_count: number | null;
|
|
201
|
+
/** The detected face boxes, when `source === 'faces'`. */
|
|
202
|
+
faces?: FaceBox[];
|
|
203
|
+
/** Content hash of the source bytes the detection ran against. */
|
|
204
|
+
src_hash?: string;
|
|
205
|
+
/** Version tag of the detector that produced this record. */
|
|
206
|
+
detector_version?: string;
|
|
207
|
+
/** Unix-epoch milliseconds the record was last written. */
|
|
208
|
+
updated_at: number;
|
|
209
|
+
}
|
|
96
210
|
/**
|
|
97
211
|
* LibreOffice-supported output container for `doc_convert`. Each value
|
|
98
212
|
* maps to a `--convert-to` filter and a derived-key extension.
|
|
@@ -213,8 +327,12 @@ type TransformSpec = ({
|
|
|
213
327
|
} & ThumbnailOpts) | ({
|
|
214
328
|
kind: 'extract_frames';
|
|
215
329
|
} & ExtractFramesOpts) | ({
|
|
330
|
+
kind: 'extract_audio';
|
|
331
|
+
} & ExtractAudioOpts) | ({
|
|
216
332
|
kind: 'resize';
|
|
217
333
|
} & ResizeOpts) | ({
|
|
334
|
+
kind: 'detect_faces';
|
|
335
|
+
} & DetectFacesOpts) | ({
|
|
218
336
|
kind: 'ocr';
|
|
219
337
|
} & OcrOpts) | ({
|
|
220
338
|
kind: 'doc_to_pdf';
|
|
@@ -264,9 +382,38 @@ interface TransformsService {
|
|
|
264
382
|
* complete; frames live under `${manifest.prefix}/frame_NNNNNN.<ext>`.
|
|
265
383
|
*/
|
|
266
384
|
extractFrames(srcKey: string, opts: ExtractFramesOpts): Promise<JobHandle>;
|
|
385
|
+
/**
|
|
386
|
+
* Extract a video's audio track to a standalone AAC/m4a asset. Resolves to a
|
|
387
|
+
* `JobHandle` whose `output_key` holds the audio file once complete.
|
|
388
|
+
*/
|
|
389
|
+
extractAudio(srcKey: string, opts?: ExtractAudioOpts | null): Promise<JobHandle>;
|
|
267
390
|
resize(srcKey: string, opts: ResizeOpts): Promise<JobHandle>;
|
|
268
391
|
probe(srcKey: string): Promise<MediaInfo>;
|
|
269
392
|
ocr(srcKey: string, opts?: OcrOpts | null): Promise<JobHandle>;
|
|
393
|
+
/**
|
|
394
|
+
* Run face detection over an image and persist the resulting focal point as
|
|
395
|
+
* a {@link FocalPointRecord}. Resolves to a `JobHandle` whose `output_key`
|
|
396
|
+
* holds a JSON artifact of the detected {@link FaceBox | boxes} once
|
|
397
|
+
* complete. Subsequent `resize` calls with `crop: 'focal'` on the same
|
|
398
|
+
* source pick up the stored focal point automatically.
|
|
399
|
+
*/
|
|
400
|
+
detectFaces(srcKey: string, opts?: DetectFacesOpts | null): Promise<JobHandle>;
|
|
401
|
+
/**
|
|
402
|
+
* Manually set (or override) the focal point for a source image, normalized
|
|
403
|
+
* `0..=1`. Persists a {@link FocalPointRecord} with `source: 'manual'`.
|
|
404
|
+
* Resolves to `null`.
|
|
405
|
+
*/
|
|
406
|
+
setFocalPoint(srcKey: string, point: FocalPoint): Promise<null>;
|
|
407
|
+
/**
|
|
408
|
+
* Read the persisted {@link FocalPointRecord} for a source, or `null` when
|
|
409
|
+
* none has been set (neither `detectFaces` nor `setFocalPoint` has run).
|
|
410
|
+
*/
|
|
411
|
+
getFocalPoint(srcKey: string): Promise<FocalPointRecord | null>;
|
|
412
|
+
/**
|
|
413
|
+
* Remove the persisted focal point for a source. Resolves to `true` when a
|
|
414
|
+
* record was deleted, `false` when there was nothing to clear.
|
|
415
|
+
*/
|
|
416
|
+
clearFocalPoint(srcKey: string): Promise<boolean>;
|
|
270
417
|
/** Convert a LibreOffice-supported document to PDF. */
|
|
271
418
|
docToPdf(srcKey: string, opts?: DocToPdfOpts | null): Promise<JobHandle>;
|
|
272
419
|
/** Render a single page of a document as a raster thumbnail. */
|
|
@@ -314,6 +461,16 @@ interface TransformsPatternSpec {
|
|
|
314
461
|
/** Sugar for `resize` arrays — same shape, same semantics. */
|
|
315
462
|
variants?: ResizeOpts[];
|
|
316
463
|
ocr?: OcrOpts | OcrOpts[];
|
|
464
|
+
/**
|
|
465
|
+
* Run face detection on matching uploads and persist a focal point so
|
|
466
|
+
* `resize`/`variants` entries with `crop: 'focal'` crop around faces. Pass
|
|
467
|
+
* `true` for detector defaults, or an object to tune the detection.
|
|
468
|
+
*/
|
|
469
|
+
detectFaces?: boolean | {
|
|
470
|
+
min_score?: number;
|
|
471
|
+
max_faces?: number;
|
|
472
|
+
fast?: boolean;
|
|
473
|
+
};
|
|
317
474
|
}
|
|
318
475
|
/**
|
|
319
476
|
* Top-level `transforms` block. Keys are glob path patterns matched against
|
|
@@ -343,4 +500,4 @@ interface TransformsPatternSpec {
|
|
|
343
500
|
*/
|
|
344
501
|
type TransformsConfig = Record<string, TransformsPatternSpec>;
|
|
345
502
|
|
|
346
|
-
export { type
|
|
503
|
+
export { type DetachAudioOpts as D, type ExtractFramesOpts as E, type FramesetManifest as F, type ImageFormat as I, type JobStatus as J, type MediaInfo as M, type OcrOpts as O, type QrCodeSpec as Q, type ResizeOpts as R, type TransformsConfig as T, type VideoFormat as V, type TransformsPatternSpec as a, type TranscodeOpts as b, type ThumbnailOpts as c, type ExtractAudioOpts as d, type DetectFacesOpts as e, type FocalPoint as f, type FaceBox as g, type FocalPointRecord as h, type DocFormat as i, type DocToPdfOpts as j, type DocThumbnailOpts as k, type DocConvertOpts as l, type DocToMarkdownOpts as m, type DocToHtmlOpts as n, type ImageRef as o, type DocReplaceImagesOpts as p, type DocInsertQrCodeOpts as q, type QrPayload as r, type DocTemplateMergeOpts as s, type TransformSpec as t, type JobHandle as u, type JobStatusResponse as v, type TransformsService as w, keyFor as x, transforms as y };
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -393,6 +393,46 @@ export interface SessionConfigDefinition {
|
|
|
393
393
|
export interface SecurityConfig {
|
|
394
394
|
password_policy?: PasswordPolicyDefinition;
|
|
395
395
|
session?: SessionConfigDefinition;
|
|
396
|
+
signup?: SignupProtectionConfig;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Bot protection for the registration endpoints, applied to both the hosted
|
|
401
|
+
* `/_auth/register` form and the `POST /api/platform/auth/register` JSON API.
|
|
402
|
+
*
|
|
403
|
+
* These layers are not equally strong:
|
|
404
|
+
*
|
|
405
|
+
* - `honeypot` (and its submit-timing check) stops untargeted form spam — bots
|
|
406
|
+
* that scrape the HTML and fill every field. Anyone who looks at the page
|
|
407
|
+
* defeats it. It is free, so it is on by default, but it is a filter, not a
|
|
408
|
+
* gate.
|
|
409
|
+
* - `rate_limit` bounds how fast anyone can register. On by default.
|
|
410
|
+
* - `challenge` is the only layer that imposes a real per-attempt cost. `"pow"`
|
|
411
|
+
* makes the client burn CPU; `"turnstile"` / `"hcaptcha"` outsource the
|
|
412
|
+
* judgement to a third party. Off by default.
|
|
413
|
+
*
|
|
414
|
+
* A server-to-server caller cannot solve a browser challenge, so it presents
|
|
415
|
+
* `signup_secret` in the `x-maravilla-signup-secret` header instead. Without
|
|
416
|
+
* that header the JSON endpoint enforces exactly the same challenge as the
|
|
417
|
+
* form — otherwise a bot would simply POST the JSON endpoint to escape.
|
|
418
|
+
*/
|
|
419
|
+
export interface SignupProtectionConfig {
|
|
420
|
+
/** Default `"none"`. */
|
|
421
|
+
challenge?: 'none' | 'pow' | 'turnstile' | 'hcaptcha';
|
|
422
|
+
/** Leading zero bits a proof-of-work solution must have. Default 18. */
|
|
423
|
+
pow_difficulty?: number;
|
|
424
|
+
/** Default `true`. */
|
|
425
|
+
honeypot?: boolean;
|
|
426
|
+
/** Default `true`. */
|
|
427
|
+
rate_limit?: boolean;
|
|
428
|
+
/** Prefer `{ env: "VAR_NAME" }` or `"${env.VAR_NAME}"`. */
|
|
429
|
+
signup_secret?: SecretRef;
|
|
430
|
+
/** Required when `challenge` is `"turnstile"`. */
|
|
431
|
+
turnstile_secret?: SecretRef;
|
|
432
|
+
/** Required when `challenge` is `"hcaptcha"`. */
|
|
433
|
+
hcaptcha_secret?: SecretRef;
|
|
434
|
+
/** Public widget key, rendered into the page. Not a secret. */
|
|
435
|
+
captcha_site_key?: string;
|
|
396
436
|
}
|
|
397
437
|
|
|
398
438
|
// ── Branding ──
|
package/src/remote-client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { KvNamespace, KvListResult, Database, DbDocument, DbFindOptions, Storage, RealtimeService, PresenceService, AuthService, AuthCaller, AuthUser, AuthSession, AuthField, RegisterOptions, LoginOptions, CreateManagedUserOptions, UserListFilter, UserListResponse, UpdateUserOptions, Relation, AddRelationOptions, ListRelationsOptions, PolicyExplain, CanCheck, ConnectedClient, ApiKeyInfo, LoginSession, PolicyService, VectorIndexSpec, VectorIndexDescriptor, VectorQueryWithFilter, VectorSearchHit, IndexSpec, IndexDescriptor, Workflows, WorkflowHandle, WorkflowRun, WorkflowStepRecord, BrowserClient, FramesClient, BrowserJobHandle, ScreenshotRequest, PdfRequest, FramesRenderRequest } from './types.js';
|
|
2
|
-
import type { TransformsService, TranscodeOpts, ThumbnailOpts, ResizeOpts, OcrOpts, DocToPdfOpts, DocThumbnailOpts, DocConvertOpts, DocToMarkdownOpts, DocToHtmlOpts, DocReplaceImagesOpts, DocInsertQrCodeOpts, DocTemplateMergeOpts, JobHandle, JobStatusResponse, MediaInfo } from './transforms.js';
|
|
2
|
+
import type { TransformsService, TranscodeOpts, ExtractFramesOpts, ExtractAudioOpts, ThumbnailOpts, ResizeOpts, OcrOpts, DetectFacesOpts, FocalPoint, FocalPointRecord, DocToPdfOpts, DocThumbnailOpts, DocConvertOpts, DocToMarkdownOpts, DocToHtmlOpts, DocReplaceImagesOpts, DocInsertQrCodeOpts, DocTemplateMergeOpts, JobHandle, JobStatusResponse, MediaInfo } from './transforms.js';
|
|
3
3
|
import { RemoteMediaService } from './media.js';
|
|
4
4
|
import { getRequestAuthHeader } from './request-scope.js';
|
|
5
5
|
|
|
@@ -133,20 +133,32 @@ class RemoteDatabase implements Database {
|
|
|
133
133
|
return result.id;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
async updateOne(
|
|
136
|
+
async updateOne(
|
|
137
|
+
collection: string,
|
|
138
|
+
filter: any,
|
|
139
|
+
update: any,
|
|
140
|
+
): Promise<{ modified: number; matched: number; matchedCount: number; modifiedCount: number }> {
|
|
137
141
|
const response = await this.fetch(`${this.baseUrl}/api/db/${collection}/update`, {
|
|
138
142
|
method: 'POST',
|
|
139
143
|
body: JSON.stringify({ filter, update }),
|
|
140
144
|
});
|
|
141
|
-
return response.json() as Promise<{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
return response.json() as Promise<{
|
|
146
|
+
modified: number;
|
|
147
|
+
matched: number;
|
|
148
|
+
matchedCount: number;
|
|
149
|
+
modifiedCount: number;
|
|
150
|
+
}>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async deleteOne(
|
|
154
|
+
collection: string,
|
|
155
|
+
filter: any,
|
|
156
|
+
): Promise<{ deleted: number; deletedCount: number }> {
|
|
145
157
|
const response = await this.fetch(`${this.baseUrl}/api/db/${collection}/delete`, {
|
|
146
158
|
method: 'DELETE',
|
|
147
159
|
body: JSON.stringify(filter),
|
|
148
160
|
});
|
|
149
|
-
return response.json() as Promise<{ deleted: number }>;
|
|
161
|
+
return response.json() as Promise<{ deleted: number; deletedCount: number }>;
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
async deleteMany(collection: string, filter: any): Promise<{ deleted: number }> {
|
|
@@ -1280,6 +1292,14 @@ class RemoteTransformsService implements TransformsService {
|
|
|
1280
1292
|
return this.post<JobHandle>('/transcode', { srcKey, opts });
|
|
1281
1293
|
}
|
|
1282
1294
|
|
|
1295
|
+
extractFrames(srcKey: string, opts: ExtractFramesOpts): Promise<JobHandle> {
|
|
1296
|
+
return this.post<JobHandle>('/extract_frames', { srcKey, opts });
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
extractAudio(srcKey: string, opts?: ExtractAudioOpts | null): Promise<JobHandle> {
|
|
1300
|
+
return this.post<JobHandle>('/extract_audio', { srcKey, opts: opts ?? {} });
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1283
1303
|
thumbnail(srcKey: string, opts: ThumbnailOpts): Promise<JobHandle> {
|
|
1284
1304
|
return this.post<JobHandle>('/thumbnail', { srcKey, opts });
|
|
1285
1305
|
}
|
|
@@ -1292,6 +1312,49 @@ class RemoteTransformsService implements TransformsService {
|
|
|
1292
1312
|
return this.post<JobHandle>('/ocr', { srcKey, opts: opts ?? {} });
|
|
1293
1313
|
}
|
|
1294
1314
|
|
|
1315
|
+
detectFaces(srcKey: string, opts?: DetectFacesOpts | null): Promise<JobHandle> {
|
|
1316
|
+
return this.post<JobHandle>('/detect-faces', { srcKey, opts: opts ?? {} });
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
async setFocalPoint(srcKey: string, point: FocalPoint): Promise<null> {
|
|
1320
|
+
const res = await fetch(`${this.baseUrl}/api/media/transforms/focal-point`, {
|
|
1321
|
+
method: 'PUT',
|
|
1322
|
+
headers: { 'Content-Type': 'application/json', ...this.headers, ...getRequestAuthHeader() },
|
|
1323
|
+
body: JSON.stringify({ srcKey, x: point.x, y: point.y }),
|
|
1324
|
+
});
|
|
1325
|
+
if (!res.ok) {
|
|
1326
|
+
const text = await res.text();
|
|
1327
|
+
throw new Error(`Focal-point set error (${res.status}): ${text}`);
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
async getFocalPoint(srcKey: string): Promise<FocalPointRecord | null> {
|
|
1333
|
+
const res = await fetch(
|
|
1334
|
+
`${this.baseUrl}/api/media/transforms/focal-point?srcKey=${encodeURIComponent(srcKey)}`,
|
|
1335
|
+
{ method: 'GET', headers: { ...this.headers, ...getRequestAuthHeader() } },
|
|
1336
|
+
);
|
|
1337
|
+
if (res.status === 404) return null;
|
|
1338
|
+
if (!res.ok) {
|
|
1339
|
+
const text = await res.text();
|
|
1340
|
+
throw new Error(`Focal-point lookup error (${res.status}): ${text}`);
|
|
1341
|
+
}
|
|
1342
|
+
return res.json() as Promise<FocalPointRecord>;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
async clearFocalPoint(srcKey: string): Promise<boolean> {
|
|
1346
|
+
const res = await fetch(
|
|
1347
|
+
`${this.baseUrl}/api/media/transforms/focal-point?srcKey=${encodeURIComponent(srcKey)}`,
|
|
1348
|
+
{ method: 'DELETE', headers: { ...this.headers, ...getRequestAuthHeader() } },
|
|
1349
|
+
);
|
|
1350
|
+
if (!res.ok) {
|
|
1351
|
+
const text = await res.text();
|
|
1352
|
+
throw new Error(`Focal-point clear error (${res.status}): ${text}`);
|
|
1353
|
+
}
|
|
1354
|
+
const data = (await res.json()) as { cleared: boolean };
|
|
1355
|
+
return data.cleared;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1295
1358
|
docToPdf(srcKey: string, opts?: DocToPdfOpts | null): Promise<JobHandle> {
|
|
1296
1359
|
return this.post<JobHandle>('/doc_to_pdf', { srcKey, opts: opts ?? {} });
|
|
1297
1360
|
}
|
package/src/transforms.ts
CHANGED
|
@@ -80,6 +80,15 @@ export interface TranscodeOpts {
|
|
|
80
80
|
max_height?: number;
|
|
81
81
|
audio_codec?: string;
|
|
82
82
|
bitrate_kbps?: number;
|
|
83
|
+
/**
|
|
84
|
+
* Force a constant output frame rate (fps). Pass this for variable-frame-rate
|
|
85
|
+
* sources (e.g. MediaRecorder WebM) so the MP4 stays frame-accurately
|
|
86
|
+
* seekable for the frames pipeline — without it, a near-static VFR segment
|
|
87
|
+
* can collapse to a handful of frames that a fixed-rate renderer stalls on.
|
|
88
|
+
* When omitted, the worker auto-applies CFR only if it probes the source as
|
|
89
|
+
* VFR; a determinate-rate source is left untouched.
|
|
90
|
+
*/
|
|
91
|
+
fps?: number;
|
|
83
92
|
}
|
|
84
93
|
|
|
85
94
|
/** Options for `transforms.thumbnail` — extract a single video frame. */
|
|
@@ -112,6 +121,45 @@ export interface ExtractFramesOpts {
|
|
|
112
121
|
seek_ms?: number;
|
|
113
122
|
/** How much of the clip to extract from `seek_ms`, ms (default: to end). */
|
|
114
123
|
duration_ms?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Also extract the source's audio track to `detach_audio.target` in the SAME
|
|
126
|
+
* job — one call yields both the frame set and a detachable audio asset
|
|
127
|
+
* (folds the upload-time "detach audio" step into frame extraction). The
|
|
128
|
+
* frame-set manifest is still the job's primary output; the audio is written
|
|
129
|
+
* to the caller-chosen `target` key.
|
|
130
|
+
*/
|
|
131
|
+
detach_audio?: DetachAudioOpts;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Bundle a detached-audio side-output into an {@link ExtractFramesOpts} job.
|
|
136
|
+
* The worker runs one extra audio-extraction pass on the same source and writes
|
|
137
|
+
* the AAC/m4a result to `target` (a caller-chosen, tenant-scoped storage key).
|
|
138
|
+
*/
|
|
139
|
+
export interface DetachAudioOpts {
|
|
140
|
+
/** Storage key to write the extracted audio (AAC/m4a) to. */
|
|
141
|
+
target: string;
|
|
142
|
+
/** Output bitrate, kbps. Server picks a sensible AAC bitrate when omitted. */
|
|
143
|
+
bitrate_kbps?: number;
|
|
144
|
+
/** Start offset into the source, ms (default 0). */
|
|
145
|
+
seek_ms?: number;
|
|
146
|
+
/** How much of the source to extract from `seek_ms`, ms (default: to end). */
|
|
147
|
+
duration_ms?: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Options for `transforms.extractAudio` — split a video's audio track off into
|
|
152
|
+
* a standalone AAC/m4a asset. Used to detach the mic/system audio recorded
|
|
153
|
+
* alongside a camera/screen clip so it can be placed + ducked independently on
|
|
154
|
+
* the frames timeline.
|
|
155
|
+
*/
|
|
156
|
+
export interface ExtractAudioOpts {
|
|
157
|
+
/** Output bitrate, kbps. Server picks a sensible AAC bitrate when omitted. */
|
|
158
|
+
bitrate_kbps?: number;
|
|
159
|
+
/** Start offset into the source, ms (default 0). */
|
|
160
|
+
seek_ms?: number;
|
|
161
|
+
/** How much of the source to extract from `seek_ms`, ms (default: to end). */
|
|
162
|
+
duration_ms?: number;
|
|
115
163
|
}
|
|
116
164
|
|
|
117
165
|
/**
|
|
@@ -139,6 +187,24 @@ export interface ResizeOpts {
|
|
|
139
187
|
format: ImageFormat;
|
|
140
188
|
quality?: number;
|
|
141
189
|
strip_metadata?: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Crop strategy when BOTH `width` and `height` are given (the output is
|
|
192
|
+
* cropped to fill the exact box rather than letterboxed):
|
|
193
|
+
* - `'center'` — crop around the geometric center.
|
|
194
|
+
* - `'focal'` — crop around the source's focal point. The server injects
|
|
195
|
+
* the focal point from the persisted {@link FocalPointRecord} (set via
|
|
196
|
+
* `detectFaces` or `setFocalPoint`); if none exists it falls back to
|
|
197
|
+
* center.
|
|
198
|
+
* Omit for the default aspect-preserving fit (no crop).
|
|
199
|
+
*/
|
|
200
|
+
crop?: 'focal' | 'center';
|
|
201
|
+
/**
|
|
202
|
+
* Explicit focal point for `crop: 'focal'`, normalized `0..=1`. Server-
|
|
203
|
+
* injected — callers normally omit this and let the platform resolve the
|
|
204
|
+
* stored focal point for `src_key`. Present here so the wire type mirrors
|
|
205
|
+
* the Rust `ResizeOpts` exactly.
|
|
206
|
+
*/
|
|
207
|
+
focal?: FocalPoint;
|
|
142
208
|
}
|
|
143
209
|
|
|
144
210
|
/** Options for `transforms.ocr`. */
|
|
@@ -147,6 +213,60 @@ export interface OcrOpts {
|
|
|
147
213
|
lang?: string;
|
|
148
214
|
}
|
|
149
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Options for `transforms.detectFaces` — run face detection over an image and
|
|
218
|
+
* persist the resulting focal point (the center of the highest-scoring face,
|
|
219
|
+
* or the centroid of all faces) as a {@link FocalPointRecord} keyed on the
|
|
220
|
+
* source. The derived output is a JSON artifact listing the detected boxes.
|
|
221
|
+
*/
|
|
222
|
+
export interface DetectFacesOpts {
|
|
223
|
+
/** Minimum detector confidence to keep a face, `0..=1`. Defaults to `0.6` server-side. */
|
|
224
|
+
min_score?: number;
|
|
225
|
+
/** Cap on the number of faces retained (highest-scoring first). Defaults to `32` server-side. */
|
|
226
|
+
max_faces?: number;
|
|
227
|
+
/** Trade accuracy for latency (smaller detector input). Defaults to `false` server-side. */
|
|
228
|
+
fast?: boolean;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** A focal point in normalized image coordinates, each component `0..=1`. */
|
|
232
|
+
export interface FocalPoint {
|
|
233
|
+
x: number;
|
|
234
|
+
y: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** A single detected face box, normalized to `0..1` of the image dimensions. */
|
|
238
|
+
export interface FaceBox {
|
|
239
|
+
x: number;
|
|
240
|
+
y: number;
|
|
241
|
+
w: number;
|
|
242
|
+
h: number;
|
|
243
|
+
/** Detector confidence, `0..1`. */
|
|
244
|
+
score: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Persisted focal-point record for a source image — the result of
|
|
249
|
+
* `detectFaces` or a manual `setFocalPoint`. Returned by `getFocalPoint`.
|
|
250
|
+
*/
|
|
251
|
+
export interface FocalPointRecord {
|
|
252
|
+
/** Storage key of the source image this focal point belongs to. */
|
|
253
|
+
src_key: string;
|
|
254
|
+
/** The resolved focal point, normalized `0..=1`. */
|
|
255
|
+
focal: FocalPoint;
|
|
256
|
+
/** Whether the point came from face detection or a manual override. */
|
|
257
|
+
source: 'faces' | 'manual';
|
|
258
|
+
/** Number of faces detected (`null` until a detection has run for this key). */
|
|
259
|
+
face_count: number | null;
|
|
260
|
+
/** The detected face boxes, when `source === 'faces'`. */
|
|
261
|
+
faces?: FaceBox[];
|
|
262
|
+
/** Content hash of the source bytes the detection ran against. */
|
|
263
|
+
src_hash?: string;
|
|
264
|
+
/** Version tag of the detector that produced this record. */
|
|
265
|
+
detector_version?: string;
|
|
266
|
+
/** Unix-epoch milliseconds the record was last written. */
|
|
267
|
+
updated_at: number;
|
|
268
|
+
}
|
|
269
|
+
|
|
150
270
|
/**
|
|
151
271
|
* LibreOffice-supported output container for `doc_convert`. Each value
|
|
152
272
|
* maps to a `--convert-to` filter and a derived-key extension.
|
|
@@ -276,7 +396,9 @@ export type TransformSpec =
|
|
|
276
396
|
| ({ kind: 'transcode' } & TranscodeOpts)
|
|
277
397
|
| ({ kind: 'thumbnail' } & ThumbnailOpts)
|
|
278
398
|
| ({ kind: 'extract_frames' } & ExtractFramesOpts)
|
|
399
|
+
| ({ kind: 'extract_audio' } & ExtractAudioOpts)
|
|
279
400
|
| ({ kind: 'resize' } & ResizeOpts)
|
|
401
|
+
| ({ kind: 'detect_faces' } & DetectFacesOpts)
|
|
280
402
|
| ({ kind: 'ocr' } & OcrOpts)
|
|
281
403
|
| ({ kind: 'doc_to_pdf' } & DocToPdfOpts)
|
|
282
404
|
| ({ kind: 'doc_thumbnail' } & DocThumbnailOpts)
|
|
@@ -323,9 +445,38 @@ export interface TransformsService {
|
|
|
323
445
|
* complete; frames live under `${manifest.prefix}/frame_NNNNNN.<ext>`.
|
|
324
446
|
*/
|
|
325
447
|
extractFrames(srcKey: string, opts: ExtractFramesOpts): Promise<JobHandle>;
|
|
448
|
+
/**
|
|
449
|
+
* Extract a video's audio track to a standalone AAC/m4a asset. Resolves to a
|
|
450
|
+
* `JobHandle` whose `output_key` holds the audio file once complete.
|
|
451
|
+
*/
|
|
452
|
+
extractAudio(srcKey: string, opts?: ExtractAudioOpts | null): Promise<JobHandle>;
|
|
326
453
|
resize(srcKey: string, opts: ResizeOpts): Promise<JobHandle>;
|
|
327
454
|
probe(srcKey: string): Promise<MediaInfo>;
|
|
328
455
|
ocr(srcKey: string, opts?: OcrOpts | null): Promise<JobHandle>;
|
|
456
|
+
/**
|
|
457
|
+
* Run face detection over an image and persist the resulting focal point as
|
|
458
|
+
* a {@link FocalPointRecord}. Resolves to a `JobHandle` whose `output_key`
|
|
459
|
+
* holds a JSON artifact of the detected {@link FaceBox | boxes} once
|
|
460
|
+
* complete. Subsequent `resize` calls with `crop: 'focal'` on the same
|
|
461
|
+
* source pick up the stored focal point automatically.
|
|
462
|
+
*/
|
|
463
|
+
detectFaces(srcKey: string, opts?: DetectFacesOpts | null): Promise<JobHandle>;
|
|
464
|
+
/**
|
|
465
|
+
* Manually set (or override) the focal point for a source image, normalized
|
|
466
|
+
* `0..=1`. Persists a {@link FocalPointRecord} with `source: 'manual'`.
|
|
467
|
+
* Resolves to `null`.
|
|
468
|
+
*/
|
|
469
|
+
setFocalPoint(srcKey: string, point: FocalPoint): Promise<null>;
|
|
470
|
+
/**
|
|
471
|
+
* Read the persisted {@link FocalPointRecord} for a source, or `null` when
|
|
472
|
+
* none has been set (neither `detectFaces` nor `setFocalPoint` has run).
|
|
473
|
+
*/
|
|
474
|
+
getFocalPoint(srcKey: string): Promise<FocalPointRecord | null>;
|
|
475
|
+
/**
|
|
476
|
+
* Remove the persisted focal point for a source. Resolves to `true` when a
|
|
477
|
+
* record was deleted, `false` when there was nothing to clear.
|
|
478
|
+
*/
|
|
479
|
+
clearFocalPoint(srcKey: string): Promise<boolean>;
|
|
329
480
|
/** Convert a LibreOffice-supported document to PDF. */
|
|
330
481
|
docToPdf(srcKey: string, opts?: DocToPdfOpts | null): Promise<JobHandle>;
|
|
331
482
|
/** Render a single page of a document as a raster thumbnail. */
|
|
@@ -389,6 +540,8 @@ function outputExtension(spec: TransformSpec): string {
|
|
|
389
540
|
case 'extract_frames':
|
|
390
541
|
// Output key is the JSON frameset manifest; frames live under its base.
|
|
391
542
|
return 'json';
|
|
543
|
+
case 'extract_audio':
|
|
544
|
+
return 'm4a';
|
|
392
545
|
case 'thumbnail':
|
|
393
546
|
case 'resize': {
|
|
394
547
|
// thumbnail's `format` is optional in TS (defaults to jpg server-side);
|
|
@@ -396,6 +549,9 @@ function outputExtension(spec: TransformSpec): string {
|
|
|
396
549
|
const fmt = (spec as { format?: ImageFormat }).format ?? 'jpg';
|
|
397
550
|
return fmt;
|
|
398
551
|
}
|
|
552
|
+
case 'detect_faces':
|
|
553
|
+
// Output key is the JSON artifact listing the detected face boxes.
|
|
554
|
+
return 'json';
|
|
399
555
|
case 'ocr':
|
|
400
556
|
return 'txt';
|
|
401
557
|
case 'doc_to_pdf':
|
|
@@ -477,6 +633,12 @@ export interface TransformsPatternSpec {
|
|
|
477
633
|
/** Sugar for `resize` arrays — same shape, same semantics. */
|
|
478
634
|
variants?: ResizeOpts[];
|
|
479
635
|
ocr?: OcrOpts | OcrOpts[];
|
|
636
|
+
/**
|
|
637
|
+
* Run face detection on matching uploads and persist a focal point so
|
|
638
|
+
* `resize`/`variants` entries with `crop: 'focal'` crop around faces. Pass
|
|
639
|
+
* `true` for detector defaults, or an object to tune the detection.
|
|
640
|
+
*/
|
|
641
|
+
detectFaces?: boolean | { min_score?: number; max_faces?: number; fast?: boolean };
|
|
480
642
|
}
|
|
481
643
|
|
|
482
644
|
/**
|
package/src/types.ts
CHANGED
|
@@ -206,37 +206,46 @@ export interface Database {
|
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* Update a single document in a collection.
|
|
209
|
-
*
|
|
209
|
+
*
|
|
210
210
|
* @param collection - The collection name
|
|
211
211
|
* @param filter - Query filter to match the document to update
|
|
212
212
|
* @param update - Update operations (MongoDB-style with $set, $inc, etc.)
|
|
213
|
-
* @returns Promise that resolves to
|
|
214
|
-
*
|
|
213
|
+
* @returns Promise that resolves to real matched/modified counts — `modified`
|
|
214
|
+
* is 0 when the filter matched nothing, so a conditional filter plus an
|
|
215
|
+
* atomic operator is safe for enforcement (quotas, redemption caps, seats)
|
|
216
|
+
*
|
|
215
217
|
* @example
|
|
216
218
|
* ```typescript
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
219
|
+
* // Atomic redemption-cap enforcement: only one of N concurrent checkouts
|
|
220
|
+
* // can win each remaining slot — no read-then-act needed.
|
|
221
|
+
* const result = await db.updateOne('discount_codes',
|
|
222
|
+
* { id, redeemedCount: { $lt: maxRedemptions } },
|
|
223
|
+
* { $inc: { redeemedCount: 1 } }
|
|
220
224
|
* );
|
|
221
|
-
*
|
|
225
|
+
* if (result.modified === 0) throw new Error('code exhausted');
|
|
222
226
|
* ```
|
|
223
227
|
*/
|
|
224
|
-
updateOne(
|
|
228
|
+
updateOne(
|
|
229
|
+
collection: string,
|
|
230
|
+
filter: any,
|
|
231
|
+
update: any,
|
|
232
|
+
): Promise<{ modified: number; matched: number; matchedCount: number; modifiedCount: number }>;
|
|
225
233
|
|
|
226
234
|
/**
|
|
227
235
|
* Delete a single document from a collection.
|
|
228
|
-
*
|
|
236
|
+
*
|
|
229
237
|
* @param collection - The collection name
|
|
230
238
|
* @param filter - Query filter to match the document to delete
|
|
231
|
-
* @returns Promise that resolves to
|
|
232
|
-
*
|
|
239
|
+
* @returns Promise that resolves to the real number of deleted documents
|
|
240
|
+
* (0 when the filter matched nothing)
|
|
241
|
+
*
|
|
233
242
|
* @example
|
|
234
243
|
* ```typescript
|
|
235
244
|
* const result = await db.deleteOne('posts', { _id: postId });
|
|
236
245
|
* console.log(`Deleted ${result.deleted} documents`);
|
|
237
246
|
* ```
|
|
238
247
|
*/
|
|
239
|
-
deleteOne(collection: string, filter: any): Promise<{ deleted: number }>;
|
|
248
|
+
deleteOne(collection: string, filter: any): Promise<{ deleted: number; deletedCount: number }>;
|
|
240
249
|
|
|
241
250
|
/**
|
|
242
251
|
* Delete multiple documents from a collection.
|