@de-otio/trellis 0.11.0 → 0.12.1
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/dist/env.d.ts +168 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +155 -0
- package/dist/env.js.map +1 -1
- package/dist/lambda/media-completion-worker.d.ts +175 -0
- package/dist/lambda/media-completion-worker.d.ts.map +1 -0
- package/dist/lambda/media-completion-worker.js +373 -0
- package/dist/lambda/media-completion-worker.js.map +1 -0
- package/dist/lambda/media-processing-worker.d.ts +172 -1
- package/dist/lambda/media-processing-worker.d.ts.map +1 -1
- package/dist/lambda/media-processing-worker.js +343 -49
- package/dist/lambda/media-processing-worker.js.map +1 -1
- package/dist/lib/exif-stripper.d.ts +37 -22
- package/dist/lib/exif-stripper.d.ts.map +1 -1
- package/dist/lib/exif-stripper.js +101 -41
- package/dist/lib/exif-stripper.js.map +1 -1
- package/dist/lib/media/cas-keys.d.ts +63 -0
- package/dist/lib/media/cas-keys.d.ts.map +1 -0
- package/dist/lib/media/cas-keys.js +102 -0
- package/dist/lib/media/cas-keys.js.map +1 -0
- package/dist/lib/media/classify-worker-error.d.ts +48 -0
- package/dist/lib/media/classify-worker-error.d.ts.map +1 -0
- package/dist/lib/media/classify-worker-error.js +319 -0
- package/dist/lib/media/classify-worker-error.js.map +1 -0
- package/dist/lib/media/dedupe-key.d.ts +29 -0
- package/dist/lib/media/dedupe-key.d.ts.map +1 -0
- package/dist/lib/media/dedupe-key.js +49 -0
- package/dist/lib/media/dedupe-key.js.map +1 -0
- package/dist/lib/media/duration-cap.d.ts +30 -0
- package/dist/lib/media/duration-cap.d.ts.map +1 -0
- package/dist/lib/media/duration-cap.js +37 -0
- package/dist/lib/media/duration-cap.js.map +1 -0
- package/dist/lib/media/ffmpeg-args.d.ts +83 -0
- package/dist/lib/media/ffmpeg-args.d.ts.map +1 -0
- package/dist/lib/media/ffmpeg-args.js +119 -0
- package/dist/lib/media/ffmpeg-args.js.map +1 -0
- package/dist/lib/media/media-ports.d.ts +126 -0
- package/dist/lib/media/media-ports.d.ts.map +1 -0
- package/dist/lib/media/media-ports.js +129 -0
- package/dist/lib/media/media-ports.js.map +1 -0
- package/dist/lib/media/media-upsert.d.ts +55 -0
- package/dist/lib/media/media-upsert.d.ts.map +1 -0
- package/dist/lib/media/media-upsert.js +38 -0
- package/dist/lib/media/media-upsert.js.map +1 -0
- package/dist/lib/media/moderation-provider.d.ts +111 -0
- package/dist/lib/media/moderation-provider.d.ts.map +1 -0
- package/dist/lib/media/moderation-provider.js +130 -0
- package/dist/lib/media/moderation-provider.js.map +1 -0
- package/dist/lib/media/moderation-resolved-payload.d.ts +48 -0
- package/dist/lib/media/moderation-resolved-payload.d.ts.map +1 -0
- package/dist/lib/media/moderation-resolved-payload.js +37 -0
- package/dist/lib/media/moderation-resolved-payload.js.map +1 -0
- package/dist/lib/media/moderation-status.d.ts +98 -0
- package/dist/lib/media/moderation-status.d.ts.map +1 -0
- package/dist/lib/media/moderation-status.js +122 -0
- package/dist/lib/media/moderation-status.js.map +1 -0
- package/dist/lib/media/processing-types.d.ts +45 -0
- package/dist/lib/media/processing-types.d.ts.map +1 -0
- package/dist/lib/media/processing-types.js +9 -0
- package/dist/lib/media/processing-types.js.map +1 -0
- package/dist/lib/media/promote-decision.d.ts +64 -0
- package/dist/lib/media/promote-decision.d.ts.map +1 -0
- package/dist/lib/media/promote-decision.js +76 -0
- package/dist/lib/media/promote-decision.js.map +1 -0
- package/dist/lib/media/quota-check.d.ts +22 -0
- package/dist/lib/media/quota-check.d.ts.map +1 -0
- package/dist/lib/media/quota-check.js +42 -0
- package/dist/lib/media/quota-check.js.map +1 -0
- package/dist/lib/media/quota-types.d.ts +15 -0
- package/dist/lib/media/quota-types.d.ts.map +1 -0
- package/dist/lib/media/quota-types.js +9 -0
- package/dist/lib/media/quota-types.js.map +1 -0
- package/dist/lib/media/route-upload.d.ts +58 -0
- package/dist/lib/media/route-upload.d.ts.map +1 -0
- package/dist/lib/media/route-upload.js +80 -0
- package/dist/lib/media/route-upload.js.map +1 -0
- package/dist/lib/media/serve-gate.d.ts +51 -0
- package/dist/lib/media/serve-gate.d.ts.map +1 -0
- package/dist/lib/media/serve-gate.js +68 -0
- package/dist/lib/media/serve-gate.js.map +1 -0
- package/dist/lib/media/tenant-resolution.d.ts +42 -0
- package/dist/lib/media/tenant-resolution.d.ts.map +1 -0
- package/dist/lib/media/tenant-resolution.js +45 -0
- package/dist/lib/media/tenant-resolution.js.map +1 -0
- package/dist/lib/media/text-moderation.d.ts +28 -0
- package/dist/lib/media/text-moderation.d.ts.map +1 -0
- package/dist/lib/media/text-moderation.js +62 -0
- package/dist/lib/media/text-moderation.js.map +1 -0
- package/dist/lib/media/track-verdict.d.ts +45 -0
- package/dist/lib/media/track-verdict.d.ts.map +1 -0
- package/dist/lib/media/track-verdict.js +52 -0
- package/dist/lib/media/track-verdict.js.map +1 -0
- package/dist/lib/media/transcript-moderation.d.ts +47 -0
- package/dist/lib/media/transcript-moderation.d.ts.map +1 -0
- package/dist/lib/media/transcript-moderation.js +70 -0
- package/dist/lib/media/transcript-moderation.js.map +1 -0
- package/dist/lib/media-handler.d.ts.map +1 -1
- package/dist/lib/media-handler.js +15 -9
- package/dist/lib/media-handler.js.map +1 -1
- package/dist/lib/post-handler.d.ts.map +1 -1
- package/dist/lib/post-handler.js +4 -1
- package/dist/lib/post-handler.js.map +1 -1
- package/dist/lib/route-helpers.d.ts.map +1 -1
- package/dist/lib/route-helpers.js +9 -1
- package/dist/lib/route-helpers.js.map +1 -1
- package/dist/lib/routes/media.d.ts +21 -0
- package/dist/lib/routes/media.d.ts.map +1 -1
- package/dist/lib/routes/media.js +584 -483
- package/dist/lib/routes/media.js.map +1 -1
- package/dist/lib/services/image-normalizer.d.ts +64 -6
- package/dist/lib/services/image-normalizer.d.ts.map +1 -1
- package/dist/lib/services/image-normalizer.js +88 -6
- package/dist/lib/services/image-normalizer.js.map +1 -1
- package/dist/lib/services/media-upload-service.d.ts +2 -2
- package/dist/lib/services/media-upload-service.d.ts.map +1 -1
- package/dist/lib/services/media-upload-service.js +22 -21
- package/dist/lib/services/media-upload-service.js.map +1 -1
- package/dist/lib/tenant-scope.d.ts.map +1 -1
- package/dist/lib/tenant-scope.js +16 -1
- package/dist/lib/tenant-scope.js.map +1 -1
- package/package.json +2 -1
- package/prisma/migrations/20260625000000_media_tenant_scope_and_moderation_status/migration.sql +49 -0
- package/prisma/migrations/20260625000001_p0b_moderation_jobs/migration.sql +73 -0
- package/prisma/schema.prisma +95 -17
- package/src/lambda/media-completion-worker.ts +567 -0
- package/src/lambda/media-processing-worker.ts +508 -59
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure functional-core builder for ffmpeg argument arrays.
|
|
3
|
+
*
|
|
4
|
+
* Produces an argv ARRAY passed directly to the ffmpeg process — NEVER a shell
|
|
5
|
+
* string. No interpolation into a shell context means no command injection risk
|
|
6
|
+
* regardless of what inputPath / outputPath / posterPath contain.
|
|
7
|
+
*
|
|
8
|
+
* Hardening applied unconditionally on every invocation:
|
|
9
|
+
* - "-protocol_whitelist","file,pipe" blocks SSRF (no http/https/rtmp/etc.)
|
|
10
|
+
* - "-t", String(maxDurationSeconds) bounds processing time (from spec; never a literal)
|
|
11
|
+
* - "-dn" drop data tracks
|
|
12
|
+
* - "-sn" drop subtitle tracks
|
|
13
|
+
*
|
|
14
|
+
* Video additionally gets:
|
|
15
|
+
* - "-c:v","libx264","-c:a","aac" re-encode to safe codecs
|
|
16
|
+
* - "-movflags","+faststart" progressive streaming
|
|
17
|
+
*
|
|
18
|
+
* Audio-only gets:
|
|
19
|
+
* - "-c:a","aac" re-encode to safe codec
|
|
20
|
+
*
|
|
21
|
+
* Poster frame extraction is a SEPARATE argv (buildPosterArgs). Splitting the
|
|
22
|
+
* two operations keeps the main transcode deterministic and lets the poster be
|
|
23
|
+
* produced in a separate process without retranscoding. The poster job uses
|
|
24
|
+
* "-frames:v","1" to extract exactly one frame and inherits all hardening args.
|
|
25
|
+
*
|
|
26
|
+
* PURITY: no I/O, no AWS SDK, no fs, no Date.now, no Math.random. All
|
|
27
|
+
* operational parameters (maxDurationSeconds) arrive as function arguments
|
|
28
|
+
* sourced from Env.media — never as literals in this file. Ships in the PUBLIC
|
|
29
|
+
* npm tarball: no hard-coded operational numbers here.
|
|
30
|
+
*/
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// buildFfmpegArgs
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Build the argv for transcoding a media object.
|
|
36
|
+
*
|
|
37
|
+
* The returned array is passed to the child-process launcher directly — never
|
|
38
|
+
* joined into a shell string. The caller is responsible for prepending the
|
|
39
|
+
* ffmpeg binary path or using `execFile`/`spawn` with `shell: false`.
|
|
40
|
+
*
|
|
41
|
+
* The protocol whitelist, duration cap, and track-drop flags are always present
|
|
42
|
+
* in the output regardless of kind. The caller should verify these are present
|
|
43
|
+
* before launching (the test suite does so exhaustively).
|
|
44
|
+
*/
|
|
45
|
+
export function buildFfmpegArgs(spec) {
|
|
46
|
+
const args = [
|
|
47
|
+
// SSRF prevention: accept only file: and pipe: protocols. This blocks any
|
|
48
|
+
// attempt to use the ffmpeg process to reach http/https/rtmp/concat/subfile
|
|
49
|
+
// endpoints that an in-VPC worker could reach.
|
|
50
|
+
"-protocol_whitelist",
|
|
51
|
+
"file,pipe",
|
|
52
|
+
// Duration cap: sourced from spec (Env.media), never a compiled literal.
|
|
53
|
+
"-t",
|
|
54
|
+
String(spec.maxDurationSeconds),
|
|
55
|
+
// Input
|
|
56
|
+
"-i",
|
|
57
|
+
spec.inputPath,
|
|
58
|
+
// Drop data tracks (camera metadata, GPS, etc.) and subtitle tracks.
|
|
59
|
+
// Neither is needed for the re-encoded output and both can carry payloads.
|
|
60
|
+
"-dn",
|
|
61
|
+
"-sn",
|
|
62
|
+
];
|
|
63
|
+
if (spec.kind === "video") {
|
|
64
|
+
args.push("-c:v", "libx264", "-c:a", "aac",
|
|
65
|
+
// Enable progressive download (moov atom first in the container).
|
|
66
|
+
"-movflags", "+faststart");
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// audio-only
|
|
70
|
+
args.push("-c:a", "aac");
|
|
71
|
+
}
|
|
72
|
+
// Overwrite output without prompt — required for non-interactive subprocess.
|
|
73
|
+
args.push("-y", spec.outputPath);
|
|
74
|
+
return args;
|
|
75
|
+
}
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// buildPosterArgs
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Build the argv for extracting a poster frame from a video.
|
|
81
|
+
*
|
|
82
|
+
* This is a SEPARATE invocation from {@link buildFfmpegArgs}: splitting the two
|
|
83
|
+
* means the transcode and the poster extraction run independently, the poster
|
|
84
|
+
* can be regenerated without retranscoding, and the transcode argv stays clean.
|
|
85
|
+
*
|
|
86
|
+
* The poster job inherits all hardening (protocol whitelist, duration cap,
|
|
87
|
+
* track drops) and adds:
|
|
88
|
+
* - "-frames:v","1" — extract exactly one video frame
|
|
89
|
+
* - "-an" — no audio output (image output only)
|
|
90
|
+
*
|
|
91
|
+
* Callers should only invoke this when `spec.posterPath` is defined. The
|
|
92
|
+
* function documents this contract via its signature: if posterPath is absent,
|
|
93
|
+
* the output path would be undefined and the caller must guard before spawning.
|
|
94
|
+
* The implementation always uses spec.posterPath so TypeScript callers can call
|
|
95
|
+
* with a spec that has posterPath set and get a well-formed array.
|
|
96
|
+
*
|
|
97
|
+
* @param spec - must have `posterPath` set; calling without it is a caller
|
|
98
|
+
* contract violation (outputPath would be undefined).
|
|
99
|
+
*/
|
|
100
|
+
export function buildPosterArgs(spec) {
|
|
101
|
+
return [
|
|
102
|
+
"-protocol_whitelist",
|
|
103
|
+
"file,pipe",
|
|
104
|
+
"-t",
|
|
105
|
+
String(spec.maxDurationSeconds),
|
|
106
|
+
"-i",
|
|
107
|
+
spec.inputPath,
|
|
108
|
+
"-dn",
|
|
109
|
+
"-sn",
|
|
110
|
+
// Extract exactly one video frame.
|
|
111
|
+
"-frames:v",
|
|
112
|
+
"1",
|
|
113
|
+
// No audio stream in the poster image output.
|
|
114
|
+
"-an",
|
|
115
|
+
"-y",
|
|
116
|
+
spec.posterPath,
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=ffmpeg-args.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg-args.js","sourceRoot":"","sources":["../../../src/lib/media/ffmpeg-args.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAuBH,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,IAAmB;IACjD,MAAM,IAAI,GAAa;QACrB,0EAA0E;QAC1E,4EAA4E;QAC5E,+CAA+C;QAC/C,qBAAqB;QACrB,WAAW;QAEX,yEAAyE;QACzE,IAAI;QACJ,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC;QAE/B,QAAQ;QACR,IAAI;QACJ,IAAI,CAAC,SAAS;QAEd,qEAAqE;QACrE,2EAA2E;QAC3E,KAAK;QACL,KAAK;KACN,CAAC;IAEF,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC1B,IAAI,CAAC,IAAI,CACP,MAAM,EACN,SAAS,EACT,MAAM,EACN,KAAK;QACL,kEAAkE;QAClE,WAAW,EACX,YAAY,CACb,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,aAAa;QACb,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC3B,CAAC;IAED,6EAA6E;IAC7E,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAEjC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAqD;IAErD,OAAO;QACL,qBAAqB;QACrB,WAAW;QAEX,IAAI;QACJ,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC;QAE/B,IAAI;QACJ,IAAI,CAAC,SAAS;QAEd,KAAK;QACL,KAAK;QAEL,mCAAmC;QACnC,WAAW;QACX,GAAG;QAEH,8CAA8C;QAC9C,KAAK;QAEL,IAAI;QACJ,IAAI,CAAC,UAAU;KAChB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export interface TranscodeVideoInput {
|
|
2
|
+
readonly inputPath: string;
|
|
3
|
+
readonly outputPath: string;
|
|
4
|
+
readonly posterPath: string;
|
|
5
|
+
/** Hard cap on accepted duration; injected from Env.media (never a literal). */
|
|
6
|
+
readonly maxDurationSeconds: number;
|
|
7
|
+
}
|
|
8
|
+
export interface TranscodeVideoResult {
|
|
9
|
+
readonly cleanedPath: string;
|
|
10
|
+
readonly posterPath: string;
|
|
11
|
+
readonly durationSeconds: number;
|
|
12
|
+
}
|
|
13
|
+
export interface TranscodeAudioInput {
|
|
14
|
+
readonly inputPath: string;
|
|
15
|
+
readonly outputPath: string;
|
|
16
|
+
/** Hard cap on accepted duration; injected from Env.media (never a literal). */
|
|
17
|
+
readonly maxDurationSeconds: number;
|
|
18
|
+
}
|
|
19
|
+
export interface TranscodeAudioResult {
|
|
20
|
+
readonly cleanedPath: string;
|
|
21
|
+
readonly durationSeconds: number;
|
|
22
|
+
}
|
|
23
|
+
export interface TranscodePort {
|
|
24
|
+
/** Probe the duration of an input without transcoding it. */
|
|
25
|
+
probeDurationSeconds(inputPath: string): Promise<number>;
|
|
26
|
+
/** Re-encode a video to a clean form and emit a poster frame. */
|
|
27
|
+
transcodeVideo(input: TranscodeVideoInput): Promise<TranscodeVideoResult>;
|
|
28
|
+
/** Re-encode audio to a clean form. */
|
|
29
|
+
transcodeAudio(input: TranscodeAudioInput): Promise<TranscodeAudioResult>;
|
|
30
|
+
}
|
|
31
|
+
export interface StoragePort {
|
|
32
|
+
getObject(key: string): Promise<Buffer>;
|
|
33
|
+
putObject(key: string, body: Buffer, contentType: string): Promise<void>;
|
|
34
|
+
copyObject(fromKey: string, toKey: string): Promise<void>;
|
|
35
|
+
deleteObject(key: string): Promise<void>;
|
|
36
|
+
headObject(key: string): Promise<{
|
|
37
|
+
exists: boolean;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
export type TranscriptionStatus = "COMPLETED" | "FAILED" | "IN_PROGRESS";
|
|
41
|
+
export interface TranscribePort {
|
|
42
|
+
startTranscription(input: {
|
|
43
|
+
key: string;
|
|
44
|
+
jobName: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
jobId: string;
|
|
47
|
+
}>;
|
|
48
|
+
getTranscription(jobId: string): Promise<{
|
|
49
|
+
status: TranscriptionStatus;
|
|
50
|
+
transcript?: string;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* In-memory TranscodePort. Returns programmable durations and echoes the
|
|
55
|
+
* requested output/poster paths back, so the shell's path-plumbing can be
|
|
56
|
+
* asserted without invoking a real encoder.
|
|
57
|
+
*
|
|
58
|
+
* Determinism: a single `duration` (default 0) is returned by `probe` and by
|
|
59
|
+
* both transcode calls unless overridden. `transcodeVideo`/`transcodeAudio`
|
|
60
|
+
* never themselves enforce `maxDurationSeconds` — duration policy lives in the
|
|
61
|
+
* functional core (a separate caps unit), and the mock must not silently make
|
|
62
|
+
* that decision for it.
|
|
63
|
+
*/
|
|
64
|
+
export declare class MockTranscodePort implements TranscodePort {
|
|
65
|
+
private duration;
|
|
66
|
+
/** Records of each call, for assertions. */
|
|
67
|
+
readonly probeCalls: string[];
|
|
68
|
+
readonly videoCalls: TranscodeVideoInput[];
|
|
69
|
+
readonly audioCalls: TranscodeAudioInput[];
|
|
70
|
+
constructor(opts?: {
|
|
71
|
+
duration?: number;
|
|
72
|
+
});
|
|
73
|
+
/** Program the duration returned by subsequent calls. */
|
|
74
|
+
setDuration(seconds: number): void;
|
|
75
|
+
probeDurationSeconds(inputPath: string): Promise<number>;
|
|
76
|
+
transcodeVideo(input: TranscodeVideoInput): Promise<TranscodeVideoResult>;
|
|
77
|
+
transcodeAudio(input: TranscodeAudioInput): Promise<TranscodeAudioResult>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* In-memory StoragePort backed by a Map. `headObject` reports existence from the
|
|
81
|
+
* map; `getObject` throws on a miss (callers must handle the miss explicitly —
|
|
82
|
+
* a silent empty buffer would mask bugs).
|
|
83
|
+
*/
|
|
84
|
+
export declare class MockStoragePort implements StoragePort {
|
|
85
|
+
private readonly objects;
|
|
86
|
+
constructor(seed?: Record<string, Buffer>);
|
|
87
|
+
getObject(key: string): Promise<Buffer>;
|
|
88
|
+
putObject(key: string, body: Buffer, contentType: string): Promise<void>;
|
|
89
|
+
copyObject(fromKey: string, toKey: string): Promise<void>;
|
|
90
|
+
deleteObject(key: string): Promise<void>;
|
|
91
|
+
headObject(key: string): Promise<{
|
|
92
|
+
exists: boolean;
|
|
93
|
+
}>;
|
|
94
|
+
/** Test helper: read the content-type a key was stored with. */
|
|
95
|
+
contentTypeOf(key: string): string | undefined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* In-memory TranscribePort. By default a started job is immediately COMPLETED
|
|
99
|
+
* with an empty transcript; callers program per-job results via `setResult`.
|
|
100
|
+
* Job ids are a deterministic monotonic sequence.
|
|
101
|
+
*/
|
|
102
|
+
export declare class MockTranscribePort implements TranscribePort {
|
|
103
|
+
private seq;
|
|
104
|
+
private readonly results;
|
|
105
|
+
/** Records of each start call, for assertions. */
|
|
106
|
+
readonly startCalls: {
|
|
107
|
+
key: string;
|
|
108
|
+
jobName: string;
|
|
109
|
+
}[];
|
|
110
|
+
startTranscription(input: {
|
|
111
|
+
key: string;
|
|
112
|
+
jobName: string;
|
|
113
|
+
}): Promise<{
|
|
114
|
+
jobId: string;
|
|
115
|
+
}>;
|
|
116
|
+
/** Program the result a given job id will report. */
|
|
117
|
+
setResult(jobId: string, result: {
|
|
118
|
+
status: TranscriptionStatus;
|
|
119
|
+
transcript?: string;
|
|
120
|
+
}): void;
|
|
121
|
+
getTranscription(jobId: string): Promise<{
|
|
122
|
+
status: TranscriptionStatus;
|
|
123
|
+
transcript?: string;
|
|
124
|
+
}>;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=media-ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-ports.d.ts","sourceRoot":"","sources":["../../../src/lib/media/media-ports.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,iEAAiE;IACjE,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC1E,uCAAuC;IACvC,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAC3E;AAOD,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CACvD;AAQD,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,QAAQ,GAAG,aAAa,CAAC;AAEzE,MAAM,WAAW,cAAc;IAC7B,kBAAkB,CAAC,KAAK,EAAE;QACxB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/B,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QACvC,MAAM,EAAE,mBAAmB,CAAC;QAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC,CAAC;CACJ;AAMD;;;;;;;;;;GAUG;AACH,qBAAa,iBAAkB,YAAW,aAAa;IACrD,OAAO,CAAC,QAAQ,CAAS;IAEzB,4CAA4C;IAC5C,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,CAAM;IACnC,QAAQ,CAAC,UAAU,EAAE,mBAAmB,EAAE,CAAM;IAChD,QAAQ,CAAC,UAAU,EAAE,mBAAmB,EAAE,CAAM;gBAEpC,IAAI,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO;IAI5C,yDAAyD;IACzD,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAI5B,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKxD,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC;IASzE,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,oBAAoB,CAAC;CAOhF;AAED;;;;GAIG;AACH,qBAAa,eAAgB,YAAW,WAAW;IACjD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4D;gBAExE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM;IAMvC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAQvC,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxE,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQzD,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IAI3D,gEAAgE;IAChE,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAG/C;AAED;;;;GAIG;AACH,qBAAa,kBAAmB,YAAW,cAAc;IACvD,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAGpB;IAEJ,kDAAkD;IAClD,QAAQ,CAAC,UAAU,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,CAAM;IAEvD,kBAAkB,CAAC,KAAK,EAAE;QAC9B,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAU9B,qDAAqD;IACrD,SAAS,CACP,KAAK,EAAE,MAAM,EACb,MAAM,EAAE;QAAE,MAAM,EAAE,mBAAmB,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3D,IAAI;IAID,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAC7C,MAAM,EAAE,mBAAmB,CAAC;QAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;CAGH"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// CONTRACT: stable — coordinate changes. Shared P0b I/O SEAM definitions.
|
|
2
|
+
//
|
|
3
|
+
// These are the capability seams the media pipeline's imperative SHELL binds to,
|
|
4
|
+
// mirroring the MediaModerationProvider seam discipline (./moderation-provider.ts):
|
|
5
|
+
// core ships the *interfaces* plus test-only Mock implementations; the consuming
|
|
6
|
+
// app (Skybber) injects the concrete cloud adapters (ffmpeg/MediaConvert, S3,
|
|
7
|
+
// Transcribe) at startup. Core imports NO cloud SDK here.
|
|
8
|
+
//
|
|
9
|
+
// IMPORTANT: this file deliberately defines ONLY the seam interfaces and their
|
|
10
|
+
// in-memory Mocks. The mocks are deterministic and side-effect-free w.r.t. the
|
|
11
|
+
// outside world (they touch only their own in-process state) so functional-core
|
|
12
|
+
// units can be exercised against them in property tests. Operational parameters
|
|
13
|
+
// (e.g. maxDurationSeconds) are *arguments*, never literals baked into these
|
|
14
|
+
// interfaces — this file ships in the PUBLIC npm tarball.
|
|
15
|
+
// ===========================================================================
|
|
16
|
+
// Mock implementations (test-only). Deterministic, in-memory, no outside I/O.
|
|
17
|
+
// ===========================================================================
|
|
18
|
+
/**
|
|
19
|
+
* In-memory TranscodePort. Returns programmable durations and echoes the
|
|
20
|
+
* requested output/poster paths back, so the shell's path-plumbing can be
|
|
21
|
+
* asserted without invoking a real encoder.
|
|
22
|
+
*
|
|
23
|
+
* Determinism: a single `duration` (default 0) is returned by `probe` and by
|
|
24
|
+
* both transcode calls unless overridden. `transcodeVideo`/`transcodeAudio`
|
|
25
|
+
* never themselves enforce `maxDurationSeconds` — duration policy lives in the
|
|
26
|
+
* functional core (a separate caps unit), and the mock must not silently make
|
|
27
|
+
* that decision for it.
|
|
28
|
+
*/
|
|
29
|
+
export class MockTranscodePort {
|
|
30
|
+
duration;
|
|
31
|
+
/** Records of each call, for assertions. */
|
|
32
|
+
probeCalls = [];
|
|
33
|
+
videoCalls = [];
|
|
34
|
+
audioCalls = [];
|
|
35
|
+
constructor(opts = {}) {
|
|
36
|
+
this.duration = opts.duration ?? 0;
|
|
37
|
+
}
|
|
38
|
+
/** Program the duration returned by subsequent calls. */
|
|
39
|
+
setDuration(seconds) {
|
|
40
|
+
this.duration = seconds;
|
|
41
|
+
}
|
|
42
|
+
async probeDurationSeconds(inputPath) {
|
|
43
|
+
this.probeCalls.push(inputPath);
|
|
44
|
+
return this.duration;
|
|
45
|
+
}
|
|
46
|
+
async transcodeVideo(input) {
|
|
47
|
+
this.videoCalls.push(input);
|
|
48
|
+
return {
|
|
49
|
+
cleanedPath: input.outputPath,
|
|
50
|
+
posterPath: input.posterPath,
|
|
51
|
+
durationSeconds: this.duration,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async transcodeAudio(input) {
|
|
55
|
+
this.audioCalls.push(input);
|
|
56
|
+
return {
|
|
57
|
+
cleanedPath: input.outputPath,
|
|
58
|
+
durationSeconds: this.duration,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* In-memory StoragePort backed by a Map. `headObject` reports existence from the
|
|
64
|
+
* map; `getObject` throws on a miss (callers must handle the miss explicitly —
|
|
65
|
+
* a silent empty buffer would mask bugs).
|
|
66
|
+
*/
|
|
67
|
+
export class MockStoragePort {
|
|
68
|
+
objects = new Map();
|
|
69
|
+
constructor(seed = {}) {
|
|
70
|
+
for (const [key, body] of Object.entries(seed)) {
|
|
71
|
+
this.objects.set(key, { body, contentType: "application/octet-stream" });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async getObject(key) {
|
|
75
|
+
const obj = this.objects.get(key);
|
|
76
|
+
if (!obj) {
|
|
77
|
+
throw new Error(`MockStoragePort: no object at key "${key}"`);
|
|
78
|
+
}
|
|
79
|
+
return obj.body;
|
|
80
|
+
}
|
|
81
|
+
async putObject(key, body, contentType) {
|
|
82
|
+
this.objects.set(key, { body, contentType });
|
|
83
|
+
}
|
|
84
|
+
async copyObject(fromKey, toKey) {
|
|
85
|
+
const obj = this.objects.get(fromKey);
|
|
86
|
+
if (!obj) {
|
|
87
|
+
throw new Error(`MockStoragePort: no object at key "${fromKey}" to copy`);
|
|
88
|
+
}
|
|
89
|
+
this.objects.set(toKey, { body: obj.body, contentType: obj.contentType });
|
|
90
|
+
}
|
|
91
|
+
async deleteObject(key) {
|
|
92
|
+
this.objects.delete(key);
|
|
93
|
+
}
|
|
94
|
+
async headObject(key) {
|
|
95
|
+
return { exists: this.objects.has(key) };
|
|
96
|
+
}
|
|
97
|
+
/** Test helper: read the content-type a key was stored with. */
|
|
98
|
+
contentTypeOf(key) {
|
|
99
|
+
return this.objects.get(key)?.contentType;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* In-memory TranscribePort. By default a started job is immediately COMPLETED
|
|
104
|
+
* with an empty transcript; callers program per-job results via `setResult`.
|
|
105
|
+
* Job ids are a deterministic monotonic sequence.
|
|
106
|
+
*/
|
|
107
|
+
export class MockTranscribePort {
|
|
108
|
+
seq = 0;
|
|
109
|
+
results = new Map();
|
|
110
|
+
/** Records of each start call, for assertions. */
|
|
111
|
+
startCalls = [];
|
|
112
|
+
async startTranscription(input) {
|
|
113
|
+
this.startCalls.push(input);
|
|
114
|
+
this.seq += 1;
|
|
115
|
+
const jobId = `mock-transcribe-${this.seq}`;
|
|
116
|
+
if (!this.results.has(jobId)) {
|
|
117
|
+
this.results.set(jobId, { status: "COMPLETED", transcript: "" });
|
|
118
|
+
}
|
|
119
|
+
return { jobId };
|
|
120
|
+
}
|
|
121
|
+
/** Program the result a given job id will report. */
|
|
122
|
+
setResult(jobId, result) {
|
|
123
|
+
this.results.set(jobId, result);
|
|
124
|
+
}
|
|
125
|
+
async getTranscription(jobId) {
|
|
126
|
+
return this.results.get(jobId) ?? { status: "IN_PROGRESS" };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=media-ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-ports.js","sourceRoot":"","sources":["../../../src/lib/media/media-ports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure builder for the MediaFile upsert arguments (T9).
|
|
3
|
+
*
|
|
4
|
+
* Keeping this out of the route handler makes the dedup-safety invariant
|
|
5
|
+
* directly unit-testable: a within-tenant dedup hit (identical bytes
|
|
6
|
+
* re-uploaded) must NOT transfer ownership (`uploadedBy`) or de-publish the
|
|
7
|
+
* canonical row (`moderationStatus`). Subsequent uploaders get a *reference*
|
|
8
|
+
* (via the post→media relation), never a mutation of the shared row.
|
|
9
|
+
*
|
|
10
|
+
* The shell (media.ts) passes the result straight to
|
|
11
|
+
* `db.mediaFile.upsert(buildMediaUpsertArgs(...))`.
|
|
12
|
+
*/
|
|
13
|
+
export interface MediaUpsertInput {
|
|
14
|
+
tenantId: string;
|
|
15
|
+
contentHash: string;
|
|
16
|
+
originalKey: string;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
size: number;
|
|
19
|
+
uploadedBy: string;
|
|
20
|
+
width?: number;
|
|
21
|
+
height?: number;
|
|
22
|
+
duration?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The `create`/`update`/`where` payload for `db.mediaFile.upsert`.
|
|
26
|
+
*
|
|
27
|
+
* Typed structurally (not against the generated Prisma client) so this pure
|
|
28
|
+
* module never depends on a regenerated client across worktrees. The shell
|
|
29
|
+
* passes it to Prisma, which validates the shape.
|
|
30
|
+
*/
|
|
31
|
+
export interface MediaUpsertArgs {
|
|
32
|
+
where: {
|
|
33
|
+
tenantId_contentHash: {
|
|
34
|
+
tenantId: string;
|
|
35
|
+
contentHash: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
create: {
|
|
39
|
+
tenantId: string;
|
|
40
|
+
contentHash: string;
|
|
41
|
+
mimeType: string;
|
|
42
|
+
size: number;
|
|
43
|
+
originalKey: string;
|
|
44
|
+
uploadStatus: "COMPLETE";
|
|
45
|
+
uploadedBy: string;
|
|
46
|
+
width?: number;
|
|
47
|
+
height?: number;
|
|
48
|
+
duration?: number;
|
|
49
|
+
};
|
|
50
|
+
update: {
|
|
51
|
+
uploadStatus: "COMPLETE";
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export declare function buildMediaUpsertArgs(input: MediaUpsertInput): MediaUpsertArgs;
|
|
55
|
+
//# sourceMappingURL=media-upsert.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-upsert.d.ts","sourceRoot":"","sources":["../../../src/lib/media/media-upsert.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE;QAAE,oBAAoB,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IAC3E,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,UAAU,CAAC;QACzB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,MAAM,EAAE;QAIN,YAAY,EAAE,UAAU,CAAC;KAC1B,CAAC;CACH;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,gBAAgB,GACtB,eAAe,CAwBjB"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure builder for the MediaFile upsert arguments (T9).
|
|
3
|
+
*
|
|
4
|
+
* Keeping this out of the route handler makes the dedup-safety invariant
|
|
5
|
+
* directly unit-testable: a within-tenant dedup hit (identical bytes
|
|
6
|
+
* re-uploaded) must NOT transfer ownership (`uploadedBy`) or de-publish the
|
|
7
|
+
* canonical row (`moderationStatus`). Subsequent uploaders get a *reference*
|
|
8
|
+
* (via the post→media relation), never a mutation of the shared row.
|
|
9
|
+
*
|
|
10
|
+
* The shell (media.ts) passes the result straight to
|
|
11
|
+
* `db.mediaFile.upsert(buildMediaUpsertArgs(...))`.
|
|
12
|
+
*/
|
|
13
|
+
export function buildMediaUpsertArgs(input) {
|
|
14
|
+
return {
|
|
15
|
+
where: {
|
|
16
|
+
tenantId_contentHash: {
|
|
17
|
+
tenantId: input.tenantId,
|
|
18
|
+
contentHash: input.contentHash,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
create: {
|
|
22
|
+
tenantId: input.tenantId,
|
|
23
|
+
contentHash: input.contentHash,
|
|
24
|
+
mimeType: input.mimeType,
|
|
25
|
+
size: input.size,
|
|
26
|
+
originalKey: input.originalKey,
|
|
27
|
+
uploadStatus: "COMPLETE",
|
|
28
|
+
uploadedBy: input.uploadedBy,
|
|
29
|
+
width: input.width,
|
|
30
|
+
height: input.height,
|
|
31
|
+
duration: input.duration,
|
|
32
|
+
},
|
|
33
|
+
update: {
|
|
34
|
+
uploadStatus: "COMPLETE",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=media-upsert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-upsert.js","sourceRoot":"","sources":["../../../src/lib/media/media-upsert.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA2CH,MAAM,UAAU,oBAAoB,CAClC,KAAuB;IAEvB,OAAO;QACL,KAAK,EAAE;YACL,oBAAoB,EAAE;gBACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,WAAW,EAAE,KAAK,CAAC,WAAW;aAC/B;SACF;QACD,MAAM,EAAE;YACN,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,YAAY,EAAE,UAAU;YACxB,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,QAAQ,EAAE,KAAK,CAAC,QAAQ;SACzB;QACD,MAAM,EAAE;YACN,YAAY,EAAE,UAAU;SACzB;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ModerationDecision } from "./moderation-status.js";
|
|
2
|
+
export type { ModerationDecision };
|
|
3
|
+
/** An opaque reference to an already-stored image object (key + bucket handle). */
|
|
4
|
+
export interface ImageRef {
|
|
5
|
+
readonly bucket: string;
|
|
6
|
+
readonly key: string;
|
|
7
|
+
}
|
|
8
|
+
/** An opaque reference to an already-stored object in S3-compatible storage. */
|
|
9
|
+
export interface S3Ref {
|
|
10
|
+
readonly bucket: string;
|
|
11
|
+
readonly key: string;
|
|
12
|
+
}
|
|
13
|
+
/** A single classifier label. `category` is an OPAQUE token, never a real-category string. */
|
|
14
|
+
export interface ModerationLabel {
|
|
15
|
+
readonly category: string;
|
|
16
|
+
readonly confidence: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* ModerationVerdict — the RESULT object (hub name). The `decision` is the
|
|
20
|
+
* 3-value classifier verdict; `labels` are opaque category tokens with
|
|
21
|
+
* confidences; `provider` identifies which backend produced it.
|
|
22
|
+
*/
|
|
23
|
+
export interface ModerationVerdict {
|
|
24
|
+
readonly decision: ModerationDecision;
|
|
25
|
+
readonly labels: ReadonlyArray<ModerationLabel>;
|
|
26
|
+
readonly provider: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The one canonical moderation seam. Image moderation is sync-ish (resolves a
|
|
30
|
+
* verdict directly); video moderation is async (start → poll), mirroring the
|
|
31
|
+
* cloud provider's job model. Audio reuses the text-moderation path and adds no
|
|
32
|
+
* method here.
|
|
33
|
+
*/
|
|
34
|
+
export interface MediaModerationProvider {
|
|
35
|
+
/** Synchronous-style image moderation: resolves a verdict directly. */
|
|
36
|
+
moderateImage(input: ImageRef): Promise<ModerationVerdict>;
|
|
37
|
+
/** Kicks off async video moderation; returns a handle to poll. */
|
|
38
|
+
startVideoModeration(input: S3Ref): Promise<{
|
|
39
|
+
jobId: string;
|
|
40
|
+
}>;
|
|
41
|
+
/** Polls a previously-started video moderation job for its verdict. */
|
|
42
|
+
getVideoModeration(jobId: string): Promise<ModerationVerdict>;
|
|
43
|
+
}
|
|
44
|
+
export type WarnSink = (message: string, data?: unknown) => void;
|
|
45
|
+
/**
|
|
46
|
+
* A verdict that fails closed: every call resolves to `review` with no labels.
|
|
47
|
+
* Nothing this provider returns can ever auto-approve media. Used as the safe
|
|
48
|
+
* default before a concrete provider is injected (dev only — see the startup
|
|
49
|
+
* guard below).
|
|
50
|
+
*/
|
|
51
|
+
export declare class NullModerationProvider implements MediaModerationProvider {
|
|
52
|
+
private readonly warn;
|
|
53
|
+
constructor(warn?: WarnSink);
|
|
54
|
+
private failClosed;
|
|
55
|
+
moderateImage(_input: ImageRef): Promise<ModerationVerdict>;
|
|
56
|
+
startVideoModeration(_input: S3Ref): Promise<{
|
|
57
|
+
jobId: string;
|
|
58
|
+
}>;
|
|
59
|
+
getVideoModeration(_jobId: string): Promise<ModerationVerdict>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns true for the fail-closed Null provider. The startup guard uses this to
|
|
63
|
+
* reject Null outside dev.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isNullModerationProvider(provider: MediaModerationProvider): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* A test seam: returns canned verdicts on demand. Default is the fail-closed
|
|
68
|
+
* `review`. Labels use ONLY abstract category tokens (`category_a`,
|
|
69
|
+
* `category_b`); no real-category strings, no real imagery ever.
|
|
70
|
+
*/
|
|
71
|
+
export declare class MockModerationProvider implements MediaModerationProvider {
|
|
72
|
+
private imageVerdict;
|
|
73
|
+
private videoVerdict;
|
|
74
|
+
private jobIdSeq;
|
|
75
|
+
constructor(canned?: {
|
|
76
|
+
image?: ModerationVerdict;
|
|
77
|
+
video?: ModerationVerdict;
|
|
78
|
+
});
|
|
79
|
+
/** Program the verdict returned by `moderateImage`. */
|
|
80
|
+
setImageVerdict(verdict: ModerationVerdict): void;
|
|
81
|
+
/** Program the verdict returned by `getVideoModeration`. */
|
|
82
|
+
setVideoVerdict(verdict: ModerationVerdict): void;
|
|
83
|
+
moderateImage(_input: ImageRef): Promise<ModerationVerdict>;
|
|
84
|
+
startVideoModeration(_input: S3Ref): Promise<{
|
|
85
|
+
jobId: string;
|
|
86
|
+
}>;
|
|
87
|
+
getVideoModeration(_jobId: string): Promise<ModerationVerdict>;
|
|
88
|
+
}
|
|
89
|
+
/** Abstract category tokens for Mock verdicts — never real-category strings. */
|
|
90
|
+
export declare const MOCK_CATEGORY_A = "category_a";
|
|
91
|
+
export declare const MOCK_CATEGORY_B = "category_b";
|
|
92
|
+
/**
|
|
93
|
+
* Error raised by the startup guard when the fail-closed Null provider would run
|
|
94
|
+
* outside dev. Carrying a distinct type lets the wiring fail loudly and lets
|
|
95
|
+
* tests assert on it.
|
|
96
|
+
*/
|
|
97
|
+
export declare class NullProviderInProductionError extends Error {
|
|
98
|
+
constructor(environment: string);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Startup guard for the seam wiring. Validates that a non-Null provider is
|
|
102
|
+
* injected whenever `environment !== "dev"`, and throws loudly otherwise.
|
|
103
|
+
* Returns the provider unchanged when the check passes, so it can wrap the
|
|
104
|
+
* injection site directly:
|
|
105
|
+
*
|
|
106
|
+
* const provider = assertModerationProviderAllowed(injected, env.ENVIRONMENT);
|
|
107
|
+
*
|
|
108
|
+
* Fail loud, never silently run Null in prod.
|
|
109
|
+
*/
|
|
110
|
+
export declare function assertModerationProviderAllowed(provider: MediaModerationProvider, environment: string): MediaModerationProvider;
|
|
111
|
+
//# sourceMappingURL=moderation-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"moderation-provider.d.ts","sourceRoot":"","sources":["../../../src/lib/media/moderation-provider.ts"],"names":[],"mappings":"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"}
|