@skyphusion-labs/vivijure-core 1.23.0 → 1.25.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/dist/assemble-admission.d.ts +163 -0
- package/dist/assemble-admission.d.ts.map +1 -0
- package/dist/assemble-admission.js +189 -0
- package/dist/assemble-admission.js.map +1 -0
- package/dist/clip-content-validate.d.ts +39 -3
- package/dist/clip-content-validate.d.ts.map +1 -1
- package/dist/clip-content-validate.js +96 -9
- package/dist/clip-content-validate.js.map +1 -1
- package/dist/clip-job-model.d.ts +18 -0
- package/dist/clip-job-model.d.ts.map +1 -1
- package/dist/clip-job-model.js.map +1 -1
- package/dist/clip-validate.d.ts +19 -0
- package/dist/clip-validate.d.ts.map +1 -1
- package/dist/clip-validate.js +112 -14
- package/dist/clip-validate.js.map +1 -1
- package/dist/film-model.d.ts +75 -0
- package/dist/film-model.d.ts.map +1 -1
- package/dist/film-model.js +88 -2
- package/dist/film-model.js.map +1 -1
- package/dist/film-orchestrator.d.ts +34 -1
- package/dist/film-orchestrator.d.ts.map +1 -1
- package/dist/film-orchestrator.js +260 -45
- package/dist/film-orchestrator.js.map +1 -1
- package/dist/film-output-key.d.ts +24 -1
- package/dist/film-output-key.d.ts.map +1 -1
- package/dist/film-output-key.js +31 -6
- package/dist/film-output-key.js.map +1 -1
- package/dist/modules/conformance.d.ts +16 -1
- package/dist/modules/conformance.d.ts.map +1 -1
- package/dist/modules/conformance.js +31 -5
- package/dist/modules/conformance.js.map +1 -1
- package/dist/modules/types.d.ts +137 -2
- package/dist/modules/types.d.ts.map +1 -1
- package/dist/modules/types.js +53 -4
- package/dist/modules/types.js.map +1 -1
- package/dist/render-orchestrator.d.ts +23 -1
- package/dist/render-orchestrator.d.ts.map +1 -1
- package/dist/render-orchestrator.js +46 -5
- package/dist/render-orchestrator.js.map +1 -1
- package/dist/render-output-payload.d.ts.map +1 -1
- package/dist/render-output-payload.js +23 -1
- package/dist/render-output-payload.js.map +1 -1
- package/dist/runpod-job-log.d.ts.map +1 -1
- package/dist/runpod-job-log.js +26 -2
- package/dist/runpod-job-log.js.map +1 -1
- package/dist/video-finish-assemble.d.ts +6 -1
- package/dist/video-finish-assemble.d.ts.map +1 -1
- package/dist/video-finish-assemble.js +41 -11
- package/dist/video-finish-assemble.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight input-bytes admission check for a chunked assemble (cf#815).
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS BOUNDS, AND WHY IT IS NOT AN OUTPUT CAP. The container writes the assembled film to
|
|
5
|
+
* local disk before streaming it to R2, and nothing bounds that write. Chunked assemble (cf#784)
|
|
6
|
+
* flattened peak disk for the BATCH stage -- each batch dir is removed on flush and every partial
|
|
7
|
+
* goes straight to R2 -- but it left the FINALIZE stage untouched:
|
|
8
|
+
*
|
|
9
|
+
* containers/video-finish/app.py:315 `_silent.mp4` <- the joined film
|
|
10
|
+
* containers/video-finish/app.py:786 `final.mp4` <- _mux_bed_onto, written FROM _silent.mp4
|
|
11
|
+
*
|
|
12
|
+
* `_silent.mp4` is never removed (the only `os.remove` calls in the chunked path are the two that
|
|
13
|
+
* reap batch partials, app.py:266 and :270), and the no-bed branch still writes `final.mp4` with
|
|
14
|
+
* `-c copy` rather than renaming. So finalize holds TWO full-length copies of the film at once.
|
|
15
|
+
* That is the term this check bounds. NOTE: the container's own comment at app.py:57-59 says
|
|
16
|
+
* "peak disk at that stage is one film"; the code above says two, and this file follows the code.
|
|
17
|
+
*
|
|
18
|
+
* WHY THE THRESHOLD IS NOT `MAX_CLIPS x MAX_CLIP_BYTES`. cf#813 measures that product (80 x 256 MB
|
|
19
|
+
* = 20.0 GB) as 23x to 120x above what a real film weighs, because `MAX_CLIP_BYTES` is the size at
|
|
20
|
+
* which a DOWNLOAD is refused, not a payload. A gate written against it could never fire, which is
|
|
21
|
+
* worse than no gate because it reads as protection. It is also the wrong QUANTITY: cf#813 measures
|
|
22
|
+
* the normalized-to-source byte ratio at 0.98x to 5.09x, so source bytes do not predict the film.
|
|
23
|
+
*
|
|
24
|
+
* WHAT IT IS WRITTEN AGAINST INSTEAD: bitrate x duration, from cf#813's measured encodes through
|
|
25
|
+
* this container's exact normalize command. See NORMALIZED_BITS_PER_PIXEL_FRAME.
|
|
26
|
+
*
|
|
27
|
+
* WHERE IT RUNS. On the Worker, before the first presign is minted. The Worker has the clip list
|
|
28
|
+
* and the per-clip measured durations for free; the container learns a clip's length only by
|
|
29
|
+
* downloading it. Refusing here costs nothing, refusing there costs the whole download.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Normalized video bits per pixel per frame at the container's encode settings
|
|
33
|
+
* (`libx264 -crf 18 -preset medium`, containers/video-finish/app.py:612).
|
|
34
|
+
*
|
|
35
|
+
* DERIVED FROM cf#813's MEASUREMENT, not picked. Its 1080p24 band tops out at 11.16 Mbps:
|
|
36
|
+
*
|
|
37
|
+
* 11.16e6 bits/s / (1920 x 1080 x 24 px/s) = 0.2243 bits/px/frame
|
|
38
|
+
*
|
|
39
|
+
* Rounded UP to 0.225. Cross-checked against the same measurement's 720p24 ceiling, which is an
|
|
40
|
+
* independent row: 0.225 x 1280 x 720 x 24 = 4.98 Mbps against a measured 4.82 Mbps, i.e. 3.3%
|
|
41
|
+
* conservative. Bits-per-pixel being near-constant across the two resolutions is what lets one
|
|
42
|
+
* number serve any delivery target instead of a per-resolution table that would rot.
|
|
43
|
+
*
|
|
44
|
+
* THE CEILING OF THE BAND, DELIBERATELY, and the direction of error is the point. This predicts
|
|
45
|
+
* the LARGEST film a given input can normalize to, so the refusal comes early rather than after
|
|
46
|
+
* the disk is already full. cf#813 also warns that its pan-based proxies UNDERESTIMATE real
|
|
47
|
+
* generated video (no subject deformation, no per-frame diffusion shimmer), which argues for the
|
|
48
|
+
* ceiling rather than the middle of the band as well.
|
|
49
|
+
*
|
|
50
|
+
* A film's predicted bytes are therefore an upper bound on its size and a lower bound on nothing:
|
|
51
|
+
* a legitimate film lands well under, which is what keeps the CONTROL case admitted.
|
|
52
|
+
*/
|
|
53
|
+
export declare const NORMALIZED_BITS_PER_PIXEL_FRAME = 0.225;
|
|
54
|
+
/**
|
|
55
|
+
* Ephemeral disk of the container instance the finish tier runs on.
|
|
56
|
+
*
|
|
57
|
+
* `instance_type = "standard-4"` (vivijure-cf wrangler.toml.example:238), documented there at
|
|
58
|
+
* :201 as "the LARGEST Cloudflare offers (4 vCPU / 12 GiB / 20 GB disk)". Read as DECIMAL GB, the
|
|
59
|
+
* smaller of the two readings, because guessing GiB here would silently widen the budget by 7%.
|
|
60
|
+
*/
|
|
61
|
+
export declare const CONTAINER_DISK_BYTES = 20000000000;
|
|
62
|
+
/**
|
|
63
|
+
* Full-length copies of the film resident on disk at once during finalize.
|
|
64
|
+
*
|
|
65
|
+
* TWO, not one: `_silent.mp4` then `final.mp4`, with no remove between them. See the file header
|
|
66
|
+
* for the exact lines. This is the multiplier the whole check turns on, so it is a named constant
|
|
67
|
+
* with its evidence rather than a `* 2` inline.
|
|
68
|
+
*/
|
|
69
|
+
export declare const FINALIZE_FILM_COPIES = 2;
|
|
70
|
+
/**
|
|
71
|
+
* Peak disk the BATCH stage may occupy, mirroring the container's own budget at
|
|
72
|
+
* containers/video-finish/app.py:49-50: `3 x (MAX_BATCH_BYTES + MAX_CLIP_BYTES)` -- a batch's
|
|
73
|
+
* sources, their normalized copies, and the batch partial.
|
|
74
|
+
*
|
|
75
|
+
* Reserved even though the batch dirs are removed before the final join, so the two stages do not
|
|
76
|
+
* in fact coexist. cf#815 asks for headroom for "the assembled film plus one batch working set",
|
|
77
|
+
* and a reserve that survives a future reordering of those stages is worth 4 GB of a 20 GB disk.
|
|
78
|
+
*
|
|
79
|
+
* cf#813 argues this multiplier should be ~5x rather than 3x against SOURCE bytes, because the
|
|
80
|
+
* normalized copy is roughly 2x its source while the fixed 1920x1080 target is still upscaling.
|
|
81
|
+
* Correcting the container's constant is cf#813's acceptance item, not this one's; this reserve
|
|
82
|
+
* deliberately quotes the container's CURRENT number so the two cannot silently disagree about
|
|
83
|
+
* what is reserved today.
|
|
84
|
+
*/
|
|
85
|
+
export declare const CONTAINER_BATCH_PEAK_BYTES: number;
|
|
86
|
+
/** `MAX_AUDIO_BYTES`, containers/video-finish/app.py:41. The bed lands on disk beside the film. */
|
|
87
|
+
export declare const CONTAINER_AUDIO_BED_BYTES: number;
|
|
88
|
+
/**
|
|
89
|
+
* Disk the image, the OS and ffmpeg's own scratch occupy, which the film must not be allowed to
|
|
90
|
+
* claim. A RESERVE, stated as one: it is not a measurement of the image, and it is deliberately
|
|
91
|
+
* generous, because the cost of over-reserving is a lower film ceiling and the cost of
|
|
92
|
+
* under-reserving is the ENOSPC this check exists to prevent.
|
|
93
|
+
*/
|
|
94
|
+
export declare const CONTAINER_IMAGE_RESERVE_BYTES = 2000000000;
|
|
95
|
+
/**
|
|
96
|
+
* Frames per second the container normalizes to when the payload omits `fps`.
|
|
97
|
+
*
|
|
98
|
+
* `int(body.get("fps", 24))`, containers/video-finish/app.py:391 (and :205, :1107).
|
|
99
|
+
*
|
|
100
|
+
* THREE NUMBERS HAVE TO AGREE HERE and a comment cannot make them: the container's fallback, core's
|
|
101
|
+
* `DEFAULT_DELIVERY_FPS` (film-model.ts:1458), and this prediction. `assemble-admission.test.ts`
|
|
102
|
+
* asserts this constant equals `DEFAULT_DELIVERY_FPS`, so a future edit to either one fails a test
|
|
103
|
+
* rather than silently mispredicting every film by the ratio between them.
|
|
104
|
+
*/
|
|
105
|
+
export declare const CONTAINER_DEFAULT_FPS = 24;
|
|
106
|
+
/** The largest predicted film the container can finalize on its ephemeral disk. */
|
|
107
|
+
export declare function assembleFilmByteCeiling(): number;
|
|
108
|
+
/** Predicted normalized bytes for `seconds` of video at a delivery target. */
|
|
109
|
+
export declare function predictNormalizedBytes(width: number, height: number, fps: number, seconds: number): number;
|
|
110
|
+
/** Seconds of film this assemble will produce, and how much of that is EVIDENCE. */
|
|
111
|
+
export interface AssembleSecondsBasis {
|
|
112
|
+
/** Summed seconds of every clip whose length is known. */
|
|
113
|
+
seconds: number;
|
|
114
|
+
/** Clips whose length came from the container's own probe (delivered_frames / delivered_fps). */
|
|
115
|
+
measured: number;
|
|
116
|
+
/** Clips whose length came from the plan (bundle target_seconds, else the authored scene). */
|
|
117
|
+
planned: number;
|
|
118
|
+
/** Clips with neither. They contribute ZERO seconds, so `seconds` is a LOWER bound. */
|
|
119
|
+
unknown: number;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the film's length from what the Worker already holds.
|
|
123
|
+
*
|
|
124
|
+
* MEASURED WINS, PLAN FILLS, ABSENCE IS NAMED. This mirrors the #697 duration gate's discipline
|
|
125
|
+
* (`findClipDurationShortfalls`): the check fires on EVIDENCE and never on absence, so a clip with
|
|
126
|
+
* no known length cannot by itself refuse a film. It contributes zero seconds and is COUNTED, so
|
|
127
|
+
* the caller can say the prediction is a lower bound instead of implying it is complete.
|
|
128
|
+
*
|
|
129
|
+
* A lower bound is still enough to refuse on: if the clips we CAN size already exceed the disk,
|
|
130
|
+
* the ones we cannot size do not make the film smaller.
|
|
131
|
+
*/
|
|
132
|
+
export declare function resolveAssembleSeconds(finalClips: {
|
|
133
|
+
shot_id: string;
|
|
134
|
+
}[], measuredSeconds: Map<string, number> | ReadonlyMap<string, number>, plannedSeconds: Record<string, number>): AssembleSecondsBasis;
|
|
135
|
+
export interface AssembleAdmissionInput {
|
|
136
|
+
clipCount: number;
|
|
137
|
+
width: number;
|
|
138
|
+
height: number;
|
|
139
|
+
fps?: number;
|
|
140
|
+
basis: AssembleSecondsBasis;
|
|
141
|
+
}
|
|
142
|
+
export interface AssembleAdmission {
|
|
143
|
+
admitted: boolean;
|
|
144
|
+
predictedBytes: number;
|
|
145
|
+
ceilingBytes: number;
|
|
146
|
+
/** Seconds of film the ceiling allows at this delivery target. The actionable number. */
|
|
147
|
+
allowedSeconds: number;
|
|
148
|
+
basis: AssembleSecondsBasis;
|
|
149
|
+
/** Loud, arithmetic-bearing refusal. Set only when `admitted` is false. */
|
|
150
|
+
reason?: string;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Admit or refuse an assemble before a single byte is fetched.
|
|
154
|
+
*
|
|
155
|
+
* PURE. Every number it uses is either passed in or a constant above with its provenance, so the
|
|
156
|
+
* arithmetic in the refusal is reproducible from the message alone.
|
|
157
|
+
*
|
|
158
|
+
* REFUSAL IS LOUD AND TERMINAL, never a degrade (cf#815, the #249/#77 discipline). A film that
|
|
159
|
+
* cannot fit is not silently truncated to one that does: the caller is told what was asked, what
|
|
160
|
+
* fits, by how much it is over, and what length would be accepted.
|
|
161
|
+
*/
|
|
162
|
+
export declare function admitAssemble(input: AssembleAdmissionInput): AssembleAdmission;
|
|
163
|
+
//# sourceMappingURL=assemble-admission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assemble-admission.d.ts","sourceRoot":"","sources":["../src/assemble-admission.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,+BAA+B,QAAQ,CAAC;AAErD;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,cAAiB,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,0BAA0B,QAA+C,CAAC;AAEvF,mGAAmG;AACnG,eAAO,MAAM,yBAAyB,QAAoB,CAAC;AAE3D;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,aAAgB,CAAC;AAE3D;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC;AAExC,mFAAmF;AACnF,wBAAgB,uBAAuB,IAAI,MAAM,CAIhD;AAED,8EAA8E;AAC9E,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,GACd,MAAM,CAGR;AAED,oFAAoF;AACpF,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,QAAQ,EAAE,MAAM,CAAC;IACjB,8FAA8F;IAC9F,OAAO,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,EACjC,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAClE,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACrC,oBAAoB,CAsBtB;AAED,MAAM,WAAW,sBAAsB;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,oBAAoB,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,yFAAyF;IACzF,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,oBAAoB,CAAC;IAC5B,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAMD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,sBAAsB,GAAG,iBAAiB,CAwB9E"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight input-bytes admission check for a chunked assemble (cf#815).
|
|
3
|
+
*
|
|
4
|
+
* WHAT THIS BOUNDS, AND WHY IT IS NOT AN OUTPUT CAP. The container writes the assembled film to
|
|
5
|
+
* local disk before streaming it to R2, and nothing bounds that write. Chunked assemble (cf#784)
|
|
6
|
+
* flattened peak disk for the BATCH stage -- each batch dir is removed on flush and every partial
|
|
7
|
+
* goes straight to R2 -- but it left the FINALIZE stage untouched:
|
|
8
|
+
*
|
|
9
|
+
* containers/video-finish/app.py:315 `_silent.mp4` <- the joined film
|
|
10
|
+
* containers/video-finish/app.py:786 `final.mp4` <- _mux_bed_onto, written FROM _silent.mp4
|
|
11
|
+
*
|
|
12
|
+
* `_silent.mp4` is never removed (the only `os.remove` calls in the chunked path are the two that
|
|
13
|
+
* reap batch partials, app.py:266 and :270), and the no-bed branch still writes `final.mp4` with
|
|
14
|
+
* `-c copy` rather than renaming. So finalize holds TWO full-length copies of the film at once.
|
|
15
|
+
* That is the term this check bounds. NOTE: the container's own comment at app.py:57-59 says
|
|
16
|
+
* "peak disk at that stage is one film"; the code above says two, and this file follows the code.
|
|
17
|
+
*
|
|
18
|
+
* WHY THE THRESHOLD IS NOT `MAX_CLIPS x MAX_CLIP_BYTES`. cf#813 measures that product (80 x 256 MB
|
|
19
|
+
* = 20.0 GB) as 23x to 120x above what a real film weighs, because `MAX_CLIP_BYTES` is the size at
|
|
20
|
+
* which a DOWNLOAD is refused, not a payload. A gate written against it could never fire, which is
|
|
21
|
+
* worse than no gate because it reads as protection. It is also the wrong QUANTITY: cf#813 measures
|
|
22
|
+
* the normalized-to-source byte ratio at 0.98x to 5.09x, so source bytes do not predict the film.
|
|
23
|
+
*
|
|
24
|
+
* WHAT IT IS WRITTEN AGAINST INSTEAD: bitrate x duration, from cf#813's measured encodes through
|
|
25
|
+
* this container's exact normalize command. See NORMALIZED_BITS_PER_PIXEL_FRAME.
|
|
26
|
+
*
|
|
27
|
+
* WHERE IT RUNS. On the Worker, before the first presign is minted. The Worker has the clip list
|
|
28
|
+
* and the per-clip measured durations for free; the container learns a clip's length only by
|
|
29
|
+
* downloading it. Refusing here costs nothing, refusing there costs the whole download.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Normalized video bits per pixel per frame at the container's encode settings
|
|
33
|
+
* (`libx264 -crf 18 -preset medium`, containers/video-finish/app.py:612).
|
|
34
|
+
*
|
|
35
|
+
* DERIVED FROM cf#813's MEASUREMENT, not picked. Its 1080p24 band tops out at 11.16 Mbps:
|
|
36
|
+
*
|
|
37
|
+
* 11.16e6 bits/s / (1920 x 1080 x 24 px/s) = 0.2243 bits/px/frame
|
|
38
|
+
*
|
|
39
|
+
* Rounded UP to 0.225. Cross-checked against the same measurement's 720p24 ceiling, which is an
|
|
40
|
+
* independent row: 0.225 x 1280 x 720 x 24 = 4.98 Mbps against a measured 4.82 Mbps, i.e. 3.3%
|
|
41
|
+
* conservative. Bits-per-pixel being near-constant across the two resolutions is what lets one
|
|
42
|
+
* number serve any delivery target instead of a per-resolution table that would rot.
|
|
43
|
+
*
|
|
44
|
+
* THE CEILING OF THE BAND, DELIBERATELY, and the direction of error is the point. This predicts
|
|
45
|
+
* the LARGEST film a given input can normalize to, so the refusal comes early rather than after
|
|
46
|
+
* the disk is already full. cf#813 also warns that its pan-based proxies UNDERESTIMATE real
|
|
47
|
+
* generated video (no subject deformation, no per-frame diffusion shimmer), which argues for the
|
|
48
|
+
* ceiling rather than the middle of the band as well.
|
|
49
|
+
*
|
|
50
|
+
* A film's predicted bytes are therefore an upper bound on its size and a lower bound on nothing:
|
|
51
|
+
* a legitimate film lands well under, which is what keeps the CONTROL case admitted.
|
|
52
|
+
*/
|
|
53
|
+
export const NORMALIZED_BITS_PER_PIXEL_FRAME = 0.225;
|
|
54
|
+
/**
|
|
55
|
+
* Ephemeral disk of the container instance the finish tier runs on.
|
|
56
|
+
*
|
|
57
|
+
* `instance_type = "standard-4"` (vivijure-cf wrangler.toml.example:238), documented there at
|
|
58
|
+
* :201 as "the LARGEST Cloudflare offers (4 vCPU / 12 GiB / 20 GB disk)". Read as DECIMAL GB, the
|
|
59
|
+
* smaller of the two readings, because guessing GiB here would silently widen the budget by 7%.
|
|
60
|
+
*/
|
|
61
|
+
export const CONTAINER_DISK_BYTES = 20_000_000_000;
|
|
62
|
+
/**
|
|
63
|
+
* Full-length copies of the film resident on disk at once during finalize.
|
|
64
|
+
*
|
|
65
|
+
* TWO, not one: `_silent.mp4` then `final.mp4`, with no remove between them. See the file header
|
|
66
|
+
* for the exact lines. This is the multiplier the whole check turns on, so it is a named constant
|
|
67
|
+
* with its evidence rather than a `* 2` inline.
|
|
68
|
+
*/
|
|
69
|
+
export const FINALIZE_FILM_COPIES = 2;
|
|
70
|
+
/**
|
|
71
|
+
* Peak disk the BATCH stage may occupy, mirroring the container's own budget at
|
|
72
|
+
* containers/video-finish/app.py:49-50: `3 x (MAX_BATCH_BYTES + MAX_CLIP_BYTES)` -- a batch's
|
|
73
|
+
* sources, their normalized copies, and the batch partial.
|
|
74
|
+
*
|
|
75
|
+
* Reserved even though the batch dirs are removed before the final join, so the two stages do not
|
|
76
|
+
* in fact coexist. cf#815 asks for headroom for "the assembled film plus one batch working set",
|
|
77
|
+
* and a reserve that survives a future reordering of those stages is worth 4 GB of a 20 GB disk.
|
|
78
|
+
*
|
|
79
|
+
* cf#813 argues this multiplier should be ~5x rather than 3x against SOURCE bytes, because the
|
|
80
|
+
* normalized copy is roughly 2x its source while the fixed 1920x1080 target is still upscaling.
|
|
81
|
+
* Correcting the container's constant is cf#813's acceptance item, not this one's; this reserve
|
|
82
|
+
* deliberately quotes the container's CURRENT number so the two cannot silently disagree about
|
|
83
|
+
* what is reserved today.
|
|
84
|
+
*/
|
|
85
|
+
export const CONTAINER_BATCH_PEAK_BYTES = 3 * (1024 * 1024 * 1024 + 256 * 1024 * 1024);
|
|
86
|
+
/** `MAX_AUDIO_BYTES`, containers/video-finish/app.py:41. The bed lands on disk beside the film. */
|
|
87
|
+
export const CONTAINER_AUDIO_BED_BYTES = 256 * 1024 * 1024;
|
|
88
|
+
/**
|
|
89
|
+
* Disk the image, the OS and ffmpeg's own scratch occupy, which the film must not be allowed to
|
|
90
|
+
* claim. A RESERVE, stated as one: it is not a measurement of the image, and it is deliberately
|
|
91
|
+
* generous, because the cost of over-reserving is a lower film ceiling and the cost of
|
|
92
|
+
* under-reserving is the ENOSPC this check exists to prevent.
|
|
93
|
+
*/
|
|
94
|
+
export const CONTAINER_IMAGE_RESERVE_BYTES = 2_000_000_000;
|
|
95
|
+
/**
|
|
96
|
+
* Frames per second the container normalizes to when the payload omits `fps`.
|
|
97
|
+
*
|
|
98
|
+
* `int(body.get("fps", 24))`, containers/video-finish/app.py:391 (and :205, :1107).
|
|
99
|
+
*
|
|
100
|
+
* THREE NUMBERS HAVE TO AGREE HERE and a comment cannot make them: the container's fallback, core's
|
|
101
|
+
* `DEFAULT_DELIVERY_FPS` (film-model.ts:1458), and this prediction. `assemble-admission.test.ts`
|
|
102
|
+
* asserts this constant equals `DEFAULT_DELIVERY_FPS`, so a future edit to either one fails a test
|
|
103
|
+
* rather than silently mispredicting every film by the ratio between them.
|
|
104
|
+
*/
|
|
105
|
+
export const CONTAINER_DEFAULT_FPS = 24;
|
|
106
|
+
/** The largest predicted film the container can finalize on its ephemeral disk. */
|
|
107
|
+
export function assembleFilmByteCeiling() {
|
|
108
|
+
const reserved = CONTAINER_BATCH_PEAK_BYTES + CONTAINER_AUDIO_BED_BYTES + CONTAINER_IMAGE_RESERVE_BYTES;
|
|
109
|
+
return Math.max(0, Math.floor((CONTAINER_DISK_BYTES - reserved) / FINALIZE_FILM_COPIES));
|
|
110
|
+
}
|
|
111
|
+
/** Predicted normalized bytes for `seconds` of video at a delivery target. */
|
|
112
|
+
export function predictNormalizedBytes(width, height, fps, seconds) {
|
|
113
|
+
if (!(width > 0) || !(height > 0) || !(fps > 0) || !(seconds > 0))
|
|
114
|
+
return 0;
|
|
115
|
+
return Math.ceil((NORMALIZED_BITS_PER_PIXEL_FRAME * width * height * fps * seconds) / 8);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Resolve the film's length from what the Worker already holds.
|
|
119
|
+
*
|
|
120
|
+
* MEASURED WINS, PLAN FILLS, ABSENCE IS NAMED. This mirrors the #697 duration gate's discipline
|
|
121
|
+
* (`findClipDurationShortfalls`): the check fires on EVIDENCE and never on absence, so a clip with
|
|
122
|
+
* no known length cannot by itself refuse a film. It contributes zero seconds and is COUNTED, so
|
|
123
|
+
* the caller can say the prediction is a lower bound instead of implying it is complete.
|
|
124
|
+
*
|
|
125
|
+
* A lower bound is still enough to refuse on: if the clips we CAN size already exceed the disk,
|
|
126
|
+
* the ones we cannot size do not make the film smaller.
|
|
127
|
+
*/
|
|
128
|
+
export function resolveAssembleSeconds(finalClips, measuredSeconds, plannedSeconds) {
|
|
129
|
+
let seconds = 0;
|
|
130
|
+
let measured = 0;
|
|
131
|
+
let planned = 0;
|
|
132
|
+
let unknown = 0;
|
|
133
|
+
for (const c of finalClips ?? []) {
|
|
134
|
+
if (!c || typeof c.shot_id !== "string")
|
|
135
|
+
continue;
|
|
136
|
+
const m = measuredSeconds?.get(c.shot_id);
|
|
137
|
+
if (typeof m === "number" && Number.isFinite(m) && m > 0) {
|
|
138
|
+
seconds += m;
|
|
139
|
+
measured += 1;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const p = plannedSeconds?.[c.shot_id];
|
|
143
|
+
if (typeof p === "number" && Number.isFinite(p) && p > 0) {
|
|
144
|
+
seconds += p;
|
|
145
|
+
planned += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
unknown += 1;
|
|
149
|
+
}
|
|
150
|
+
return { seconds, measured, planned, unknown };
|
|
151
|
+
}
|
|
152
|
+
function gb(bytes) {
|
|
153
|
+
return `${(bytes / 1_000_000_000).toFixed(2)} GB`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Admit or refuse an assemble before a single byte is fetched.
|
|
157
|
+
*
|
|
158
|
+
* PURE. Every number it uses is either passed in or a constant above with its provenance, so the
|
|
159
|
+
* arithmetic in the refusal is reproducible from the message alone.
|
|
160
|
+
*
|
|
161
|
+
* REFUSAL IS LOUD AND TERMINAL, never a degrade (cf#815, the #249/#77 discipline). A film that
|
|
162
|
+
* cannot fit is not silently truncated to one that does: the caller is told what was asked, what
|
|
163
|
+
* fits, by how much it is over, and what length would be accepted.
|
|
164
|
+
*/
|
|
165
|
+
export function admitAssemble(input) {
|
|
166
|
+
const fps = input.fps && input.fps > 0 ? input.fps : CONTAINER_DEFAULT_FPS;
|
|
167
|
+
const { width, height, basis, clipCount } = input;
|
|
168
|
+
const ceilingBytes = assembleFilmByteCeiling();
|
|
169
|
+
const predictedBytes = predictNormalizedBytes(width, height, fps, basis.seconds);
|
|
170
|
+
const perSecond = predictNormalizedBytes(width, height, fps, 1);
|
|
171
|
+
const allowedSeconds = perSecond > 0 ? Math.floor(ceilingBytes / perSecond) : 0;
|
|
172
|
+
if (predictedBytes <= ceilingBytes) {
|
|
173
|
+
return { admitted: true, predictedBytes, ceilingBytes, allowedSeconds, basis };
|
|
174
|
+
}
|
|
175
|
+
const over = predictedBytes - ceilingBytes;
|
|
176
|
+
const bound = basis.unknown > 0 ? " (a LOWER bound: " + basis.unknown + " clip(s) of unknown length contributed 0s)" : "";
|
|
177
|
+
const reason = `assemble refused before any clip was fetched: ${clipCount} clip(s) totalling ` +
|
|
178
|
+
`${basis.seconds.toFixed(1)}s at ${width}x${height}@${fps} predict ${gb(predictedBytes)} of ` +
|
|
179
|
+
`normalized video${bound}, and the container can finalize at most ${gb(ceilingBytes)}. ` +
|
|
180
|
+
`Over by ${gb(over)}. Budget: ${gb(CONTAINER_DISK_BYTES)} ephemeral disk (standard-4), less ` +
|
|
181
|
+
`${gb(CONTAINER_BATCH_PEAK_BYTES)} batch working set, ${gb(CONTAINER_AUDIO_BED_BYTES)} audio ` +
|
|
182
|
+
`bed and ${gb(CONTAINER_IMAGE_RESERVE_BYTES)} image reserve, divided by ` +
|
|
183
|
+
`${FINALIZE_FILM_COPIES} full-length copies resident at finalize. ` +
|
|
184
|
+
`Basis: ${basis.measured} measured, ${basis.planned} planned, ${basis.unknown} unknown. ` +
|
|
185
|
+
`This delivery target admits at most ${allowedSeconds}s; reduce the film, or lower ` +
|
|
186
|
+
`delivery_width/delivery_height.`;
|
|
187
|
+
return { admitted: false, predictedBytes, ceilingBytes, allowedSeconds, basis, reason };
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=assemble-admission.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assemble-admission.js","sourceRoot":"","sources":["../src/assemble-admission.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,17 +15,38 @@ export interface InspectResult {
|
|
|
15
15
|
error?: string;
|
|
16
16
|
}
|
|
17
17
|
/** Call the video-finish container's POST /inspect, retrying the transient gateway statuses (503/504) the
|
|
18
|
-
* way callVideoFinish does for /finish. backoffMs is injectable so tests do not wait.
|
|
19
|
-
*
|
|
18
|
+
* way callVideoFinish does for /finish. backoffMs is injectable so tests do not wait.
|
|
19
|
+
*
|
|
20
|
+
* Returns the parsed result, or an UNREACHABLE marker the caller can act on (core#321). The
|
|
21
|
+
* distinction is not cosmetic: contentValidateDoneClips walks shots SEQUENTIALLY, so a container
|
|
22
|
+
* that is down costs its whole retry budget once per shot.
|
|
23
|
+
*
|
|
24
|
+
* A THROWN fetch (NXDOMAIN, connection refused, a container that died before binding 8000, which is
|
|
25
|
+
* cf#851's exact signature) is NOT the gateway-busy condition the backoff was written for, and one
|
|
26
|
+
* attempt establishes it. It used to retry anyway, because the loop's break tested
|
|
27
|
+
* `resp && resp.status !== 503 ...` and a throw leaves resp null, which is falsy, which is not a
|
|
28
|
+
* break. Three attempts at 1500ms, per shot, sequentially: about 3s a shot, roughly 51s of a finish
|
|
29
|
+
* pass on a 17-shot film, re-paid on later ticks because core#30 says not to persist the skip.
|
|
30
|
+
*
|
|
31
|
+
* The 503/504 retry is UNCHANGED and deliberately so: that one is the container saying "busy, come
|
|
32
|
+
* back", and cutting it would trade this latency problem for a coverage problem, i.e. more false
|
|
33
|
+
* `unmeasured` records, which is the state cf#856 exists to make visible. */
|
|
20
34
|
export declare function callVideoFinishInspect(env: Env, payload: {
|
|
21
35
|
clipUrl: string;
|
|
22
36
|
keyframeUrl?: string;
|
|
23
37
|
}, opts?: {
|
|
24
38
|
retries?: number;
|
|
25
39
|
backoffMs?: number;
|
|
26
|
-
}): Promise<InspectResult |
|
|
40
|
+
}): Promise<InspectResult | {
|
|
41
|
+
unreachable: true;
|
|
42
|
+
} | null>;
|
|
27
43
|
export interface ContentVerdict {
|
|
28
44
|
verdict: "ok" | "suspect" | "corrupt" | "skip";
|
|
45
|
+
/** core#321: this skip means "the container is not serving", not "this one clip could not be
|
|
46
|
+
* inspected". The per-pass breaker trips on THIS FIELD, never on a reason string: a string match
|
|
47
|
+
* is not a relationship, and a reason is prose that someone will reword without thinking about a
|
|
48
|
+
* breaker. */
|
|
49
|
+
unreachable?: true;
|
|
29
50
|
reason?: string;
|
|
30
51
|
metrics?: InspectResult["metrics"];
|
|
31
52
|
keyframe_similarity?: number | null;
|
|
@@ -40,4 +61,19 @@ export declare function contentValidateClip(env: Env, clipKey: string, keyframeK
|
|
|
40
61
|
* `clip.content_validate` structured event per shot. Returns true iff it changed any shot; the CALLER owns
|
|
41
62
|
* the job-doc write. A no-op (returns false immediately) when the video-finish tier is not installed. */
|
|
42
63
|
export declare function contentValidateDoneClips(env: Env, job: ClipJob, inspect?: (env: Env, clipKey: string, keyframeKey?: string) => Promise<ContentVerdict>): Promise<boolean>;
|
|
64
|
+
/** cf#856: what Layer 2 actually MEASURED on this clip job, for the render record.
|
|
65
|
+
*
|
|
66
|
+
* THE LADDER, the same three states cf#836 established for the per-stage degrades, because the
|
|
67
|
+
* failure they both prevent is the same one (two states reported as one):
|
|
68
|
+
* undefined -> Layer 2 never ran on this job at all. NOT MEASURED. (Self-host with no
|
|
69
|
+
* VIDEO_FINISH_URL is exactly this, and it is honest: the tier is not installed.)
|
|
70
|
+
* unmeasured 0 -> it ran on every done clip and every one got a verdict.
|
|
71
|
+
* unmeasured n -> it could not measure n of them, and `reasons` says why, verbatim.
|
|
72
|
+
*
|
|
73
|
+
* Counts the population Layer 2 covers: DONE shots that produced a clip. */
|
|
74
|
+
export declare function contentValidationView(job: ClipJob): {
|
|
75
|
+
checked: number;
|
|
76
|
+
unmeasured: number;
|
|
77
|
+
reasons: string[];
|
|
78
|
+
} | undefined;
|
|
43
79
|
//# sourceMappingURL=clip-content-validate.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clip-content-validate.d.ts","sourceRoot":"","sources":["../src/clip-content-validate.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oCAAoC,CAAC;AAE9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAMnD,8GAA8G;AAC9G,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,IAAI,GAAG,SAAS,GAAG,SAAS,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,sBAAsB,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACtG,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED
|
|
1
|
+
{"version":3,"file":"clip-content-validate.d.ts","sourceRoot":"","sources":["../src/clip-content-validate.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oCAAoC,CAAC;AAE9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAMnD,8GAA8G;AAC9G,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,IAAI,GAAG,SAAS,GAAG,SAAS,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,sBAAsB,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACtG,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;8EAgB8E;AAC9E,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,GAAG,EACR,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,EAClD,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,OAAO,CAAC,aAAa,GAAG;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG,IAAI,CAAC,CAsCvD;AAOD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,IAAI,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;IAC/C;;;mBAGe;IACf,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACnC,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED;kFACkF;AAClF,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAgBlH;AAED;;;;;0GAK0G;AAC1G,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAuB,GAC1G,OAAO,CAAC,OAAO,CAAC,CAuDlB;AAED;;;;;;;;;6EAS6E;AAC7E,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,OAAO,GACX;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CAgBxE"}
|
|
@@ -22,8 +22,22 @@ import { presignR2Get } from "./presign.js";
|
|
|
22
22
|
import { emitStructuredEvent } from "./structured-events.js";
|
|
23
23
|
const INSPECT_TTL_SECONDS = 1800;
|
|
24
24
|
/** Call the video-finish container's POST /inspect, retrying the transient gateway statuses (503/504) the
|
|
25
|
-
* way callVideoFinish does for /finish. backoffMs is injectable so tests do not wait.
|
|
26
|
-
*
|
|
25
|
+
* way callVideoFinish does for /finish. backoffMs is injectable so tests do not wait.
|
|
26
|
+
*
|
|
27
|
+
* Returns the parsed result, or an UNREACHABLE marker the caller can act on (core#321). The
|
|
28
|
+
* distinction is not cosmetic: contentValidateDoneClips walks shots SEQUENTIALLY, so a container
|
|
29
|
+
* that is down costs its whole retry budget once per shot.
|
|
30
|
+
*
|
|
31
|
+
* A THROWN fetch (NXDOMAIN, connection refused, a container that died before binding 8000, which is
|
|
32
|
+
* cf#851's exact signature) is NOT the gateway-busy condition the backoff was written for, and one
|
|
33
|
+
* attempt establishes it. It used to retry anyway, because the loop's break tested
|
|
34
|
+
* `resp && resp.status !== 503 ...` and a throw leaves resp null, which is falsy, which is not a
|
|
35
|
+
* break. Three attempts at 1500ms, per shot, sequentially: about 3s a shot, roughly 51s of a finish
|
|
36
|
+
* pass on a 17-shot film, re-paid on later ticks because core#30 says not to persist the skip.
|
|
37
|
+
*
|
|
38
|
+
* The 503/504 retry is UNCHANGED and deliberately so: that one is the container saying "busy, come
|
|
39
|
+
* back", and cutting it would trade this latency problem for a coverage problem, i.e. more false
|
|
40
|
+
* `unmeasured` records, which is the state cf#856 exists to make visible. */
|
|
27
41
|
export async function callVideoFinishInspect(env, payload, opts = {}) {
|
|
28
42
|
if (!videoFinishReachable(env))
|
|
29
43
|
return null;
|
|
@@ -34,30 +48,47 @@ export async function callVideoFinishInspect(env, payload, opts = {}) {
|
|
|
34
48
|
headers: await mediaFinishHeaders(env),
|
|
35
49
|
body: JSON.stringify(payload),
|
|
36
50
|
};
|
|
37
|
-
let
|
|
51
|
+
let last;
|
|
38
52
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
53
|
+
let resp;
|
|
39
54
|
try {
|
|
40
55
|
resp = await videoFinishFetch(env, "/inspect", init);
|
|
41
56
|
}
|
|
42
57
|
catch (e) {
|
|
43
58
|
if (isMediaFinishAuthError(e))
|
|
44
59
|
throw e;
|
|
45
|
-
|
|
60
|
+
// The transport itself failed. No amount of waiting makes a name resolve or a dead process
|
|
61
|
+
// bind, so stop here instead of sleeping twice to ask the same question.
|
|
62
|
+
return { unreachable: true };
|
|
46
63
|
}
|
|
47
|
-
|
|
64
|
+
// videoFinishFetch RESOLVES to null when the door is unset or could not be reached at all.
|
|
65
|
+
// Same class as a throw, equally unhelped by waiting, and it used to sit in the retry loop too
|
|
66
|
+
// because the old break condition (`resp && ...`) read null as "keep trying".
|
|
67
|
+
if (!resp)
|
|
68
|
+
return { unreachable: true };
|
|
69
|
+
last = resp;
|
|
70
|
+
if (resp.status !== 503 && resp.status !== 504)
|
|
48
71
|
break;
|
|
49
72
|
if (attempt < retries - 1)
|
|
50
73
|
await new Promise((r) => setTimeout(r, backoffMs));
|
|
51
74
|
}
|
|
52
|
-
|
|
53
|
-
|
|
75
|
+
// Exhausted the 503/504 budget, or a non-2xx answer: the tier is up but not serving this call.
|
|
76
|
+
if (!last || !last.ok)
|
|
77
|
+
return { unreachable: true };
|
|
54
78
|
try {
|
|
55
|
-
return (await
|
|
79
|
+
return (await last.json());
|
|
56
80
|
}
|
|
57
81
|
catch {
|
|
82
|
+
// It answered 2xx with a body that will not parse. That is the container misbehaving on THIS
|
|
83
|
+
// clip, not the tier being down, so it is not an unreachable marker and must not trip the
|
|
84
|
+
// per-pass breaker.
|
|
58
85
|
return null;
|
|
59
86
|
}
|
|
60
87
|
}
|
|
88
|
+
/** True when the inspect call could not reach a serving container at all (core#321). */
|
|
89
|
+
function isUnreachable(r) {
|
|
90
|
+
return r !== null && r.unreachable === true;
|
|
91
|
+
}
|
|
61
92
|
/** Presign the clip (and its keyframe, when known) and ask the container for a content verdict. Skips
|
|
62
93
|
* honestly (never throws, never fails a render) when the tier is unavailable. */
|
|
63
94
|
export async function contentValidateClip(env, clipKey, keyframeKey) {
|
|
@@ -74,6 +105,9 @@ export async function contentValidateClip(env, clipKey, keyframeKey) {
|
|
|
74
105
|
return { verdict: "skip", reason: `presign failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
75
106
|
}
|
|
76
107
|
const r = await callVideoFinishInspect(env, { clipUrl, keyframeUrl });
|
|
108
|
+
if (isUnreachable(r)) {
|
|
109
|
+
return { verdict: "skip", reason: "video-finish /inspect unreachable or errored", unreachable: true };
|
|
110
|
+
}
|
|
77
111
|
if (!r || !r.ok || !r.verdict)
|
|
78
112
|
return { verdict: "skip", reason: "video-finish /inspect unreachable or errored" };
|
|
79
113
|
return { verdict: r.verdict, reason: r.reason, metrics: r.metrics, keyframe_similarity: r.keyframe_similarity };
|
|
@@ -88,6 +122,12 @@ export async function contentValidateDoneClips(env, job, inspect = contentValida
|
|
|
88
122
|
if (!videoFinishReachable(env))
|
|
89
123
|
return false;
|
|
90
124
|
let changed = false;
|
|
125
|
+
// core#321: once ONE shot has established that the container is not serving, the remaining shots
|
|
126
|
+
// in this pass do not re-probe it. They still get the cf#856 unmeasured record, because the film
|
|
127
|
+
// is equally unvouched-for either way and a record that depended on probe ORDER would be a worse
|
|
128
|
+
// lie than no record at all. O(shots) transport failures become O(1) per pass, and nothing is
|
|
129
|
+
// latched beyond this pass: the next tick starts clean and tries again.
|
|
130
|
+
let tierDown;
|
|
91
131
|
for (const shot of job.shots) {
|
|
92
132
|
// #30: "skip" (a transient /inspect outage or the tier being unavailable) is NOT a validation result, so
|
|
93
133
|
// it must not short-circuit re-inspection -- only a terminal verdict (ok / suspect / corrupt) counts as
|
|
@@ -95,10 +135,30 @@ export async function contentValidateDoneClips(env, job, inspect = contentValida
|
|
|
95
135
|
// (the pixel/noise gate) for the whole pass and the clip ships ungated.
|
|
96
136
|
if (shot.status !== "done" || !shot.clip_key || (shot.content_validated && shot.content_validated !== "skip"))
|
|
97
137
|
continue;
|
|
98
|
-
const v =
|
|
138
|
+
const v = tierDown
|
|
139
|
+
? { verdict: "skip", reason: tierDown, unreachable: true }
|
|
140
|
+
: await inspect(env, shot.clip_key, shot.keyframe_key);
|
|
141
|
+
if (v.unreachable && !tierDown)
|
|
142
|
+
tierDown = v.reason ?? "video-finish /inspect unreachable";
|
|
99
143
|
// Don't persist "skip" as a verdict: leave it unset so a later tick re-inspects.
|
|
100
144
|
if (v.verdict !== "skip")
|
|
101
145
|
shot.content_validated = v.verdict;
|
|
146
|
+
// cf#856: but DO say that it could not run. Measured on a live film while the container was
|
|
147
|
+
// crashing: 5 skips, 0 passes, and the film advanced with nothing anywhere recording that the
|
|
148
|
+
// pixel gate never looked at a single clip. A skip left only in a log line is indistinguishable
|
|
149
|
+
// from a pass to every reader except one who happened to have `wrangler tail` attached.
|
|
150
|
+
// SEPARATE from content_validated on purpose (see #30 above): the verdict stays unset so
|
|
151
|
+
// re-inspection still happens, and this carries the reason meanwhile. Written only when it
|
|
152
|
+
// CHANGES, so a down inspector does not rewrite the doc every tick, and cleared the moment a
|
|
153
|
+
// terminal verdict lands, so it cannot go stale.
|
|
154
|
+
const unmeasured = v.verdict === "skip" ? (v.reason ?? "content validation could not run") : undefined;
|
|
155
|
+
if (shot.content_unmeasured !== unmeasured) {
|
|
156
|
+
if (unmeasured)
|
|
157
|
+
shot.content_unmeasured = unmeasured;
|
|
158
|
+
else
|
|
159
|
+
delete shot.content_unmeasured;
|
|
160
|
+
changed = true;
|
|
161
|
+
}
|
|
102
162
|
emitStructuredEvent({
|
|
103
163
|
ev: "clip.content_validate",
|
|
104
164
|
job_id: job.job_id,
|
|
@@ -121,4 +181,31 @@ export async function contentValidateDoneClips(env, job, inspect = contentValida
|
|
|
121
181
|
}
|
|
122
182
|
return changed;
|
|
123
183
|
}
|
|
184
|
+
/** cf#856: what Layer 2 actually MEASURED on this clip job, for the render record.
|
|
185
|
+
*
|
|
186
|
+
* THE LADDER, the same three states cf#836 established for the per-stage degrades, because the
|
|
187
|
+
* failure they both prevent is the same one (two states reported as one):
|
|
188
|
+
* undefined -> Layer 2 never ran on this job at all. NOT MEASURED. (Self-host with no
|
|
189
|
+
* VIDEO_FINISH_URL is exactly this, and it is honest: the tier is not installed.)
|
|
190
|
+
* unmeasured 0 -> it ran on every done clip and every one got a verdict.
|
|
191
|
+
* unmeasured n -> it could not measure n of them, and `reasons` says why, verbatim.
|
|
192
|
+
*
|
|
193
|
+
* Counts the population Layer 2 covers: DONE shots that produced a clip. */
|
|
194
|
+
export function contentValidationView(job) {
|
|
195
|
+
const covered = job.shots.filter((s) => s.status === "done" && s.clip_key);
|
|
196
|
+
const checked = covered.filter((s) => s.content_validated && s.content_validated !== "skip").length;
|
|
197
|
+
const blocked = covered.filter((s) => typeof s.content_unmeasured === "string" && s.content_unmeasured.length > 0);
|
|
198
|
+
if (checked === 0 && blocked.length === 0)
|
|
199
|
+
return undefined;
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
const reasons = [];
|
|
202
|
+
for (const s of blocked) {
|
|
203
|
+
const r = s.content_unmeasured;
|
|
204
|
+
if (seen.has(r))
|
|
205
|
+
continue;
|
|
206
|
+
seen.add(r);
|
|
207
|
+
reasons.push(r);
|
|
208
|
+
}
|
|
209
|
+
return { checked, unmeasured: blocked.length, reasons };
|
|
210
|
+
}
|
|
124
211
|
//# sourceMappingURL=clip-content-validate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clip-content-validate.js","sourceRoot":"","sources":["../src/clip-content-validate.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,EAAE;AACF,wGAAwG;AACxG,uGAAuG;AACvG,qGAAqG;AACrG,qGAAqG;AACrG,yGAAyG;AACzG,8CAA8C;AAC9C,EAAE;AACF,0GAA0G;AAC1G,uGAAuG;AACvG,uGAAuG;AACvG,qGAAqG;AACrG,0FAA0F;AAC1F,0GAA0G;AAC1G,gFAAgF;AAChF,EAAE;AACF,sGAAsG;AACtG,oEAAoE;AAGpE,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAE5H,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAYjC
|
|
1
|
+
{"version":3,"file":"clip-content-validate.js","sourceRoot":"","sources":["../src/clip-content-validate.ts"],"names":[],"mappings":"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"}
|