mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
* fix(producer): mix audio into a container that can record encoder delay Every rendered composition's audio landed 1024 samples (21.33 ms at 48 kHz) after its authored `data-start`, against a frame-accurate video track. The mix is AAC-encoded, and AAC encoders emit ~1024 priming samples. The mix was written to a raw ADTS `.aac` file, which has nowhere to record that delay, so it decoded as real leading silence and every stage downstream preserved it faithfully. Measuring each intermediate localises it precisely: the source WAV is exact, the mixer's own output is already 21.33 ms late, and the pad/trim and mux stages inherit it unchanged. The filter graph itself is correct - run by hand to PCM it lands on the authored start. Switch the artifact to an MP4-family container, which stores the delay as an edit list that decoders strip. Same codec, same bitrate, so no size or quality change. The filename is a contract shared by three consumers - the mux input, the distributed plan artifact, and the PNG-sequence sidecar handed to users for NLE ingest - and its extension is what selects the muxer. Give it one owner in the engine rather than five literals, so those consumers cannot drift onto different containers. Note for reviewers: this renames the distributed plan's audio artifact, which is an on-disk contract between the plan writer and the assembler. Both move together here, but a plan written by an older build would not be found by a newer assembler. Flagging in case that mixed-version window matters for how these are deployed. * fix(cloud): read the plan audio artifact name from the producer contract The aws-lambda and gcp-cloud-run adapters each restated the plan's audio filename in five places, so renaming it in the producer left them looking for a file that is no longer written. CI caught it: the gcp dispatch test asserting a plan has no audio artifact started seeing one. Export the name from `@hyperframes/producer/distributed` and consume it in both adapters. This is the same failure the constant exists to prevent, one package boundary further out: a literal that drifts from the writer's is a silently missing audio track rather than a loud error, because both call sites only ever ask whether the file exists. * fix(cloud): accept a legacy plan's audio artifact name for one release Review raised a rolling-deploy window I had flagged but left undecided: `plan` and `assemble` are separate invocations bridged by object storage, so a pre-rollout planner can be paired with a post-rollout assembler. Both readers locate the artifact by existence alone, which makes that pairing a silently muted video rather than an error. That is reachable enough to be worth two lines, so reads now accept the old name while writes only ever emit the new one. Give the fallback one owner (`resolvePlanAudioPath` / `isPlanAudioArtifactPath`) rather than four call sites, marked for deletion one release out. Also fixes a hole in the first pass of this: the plan-v2 materializer matched either name but then joined the CURRENT one, so a legacy plan resolved to a path that was never written. It now joins the artifact's own name. Review nits in the same pass: correct the pad-branch docstring, which still described a concat-copy shape the pad branch stopped using when it moved to apad + re-encode, and fix the Windows fixture's stale `.aac` output extension so it cannot model a shape that reintroduces the priming delay. * test(producer): rebake the missing-host-comp-id golden without the audio delay The pinned reference was rendered before this branch, so it carries the 1024 sample encoder-priming delay in its audio. With the delay gone the correct audio now sits ahead of the reference and the harness's envelope correlation drops below its floor. Cross-correlating the old and new references at native 48 kHz gives a lag of exactly 1024 samples (21.33 ms) at a correlation of 0.99985: same audio, moved by exactly the amount this branch removes. Regenerated inside the CI container (Dockerfile.test, ffmpeg 5.1.9) rather than natively, so the reference matches the encoder CI will compare against - the container reproduced CI's failure to the digit (correlation 0.3938764027803616, lagWindows -12) before the rebake and passes at correlation 1.0 after it. Note for archaeology: the new reference is also 3 dB louder than the old one. That gap is not from this branch - `main` and this branch render the fixture at the same level - it is pre-existing drift the reference had accumulated, which a scale-invariant correlator could never see. The rebake absorbs it. Only output.mp4 is updated. `--update` also rewrites compiled.html, but that diff is embedded-font churn with no bearing on the comparison, which reports "Failed at compilation: 0" either way. * test(producer): rebake the variables-prod golden without the audio delay Same cause as the missing-host-comp-id rebake, caught by shard-8 once the earlier shard stopped failing and the rest of the matrix could run: this reference also carries the encoder-priming delay this branch removes. Reproduced in the CI container to the digit (correlation 0.42704173048439215, lagWindows -12), rebaked there, and it now passes at correlation 1.0. Worth recording: the shift here is 2048 samples (42.67 ms) at correlation 0.99983, exactly twice the 1024 of the other fixture. The delay compounds once per un-compensated AAC generation, and this fixture's audio needs its duration normalized, so it takes the pad/trim branch's re-encode and picks up a second frame of priming on top of the mixer's. So the pre-fix error was not a fixed 21 ms - it grew with the number of times the audio was re-encoded. All nine shards ran in that CI round with only this one failing, so the matrix has now covered every fixture against this change.
182 lines
7.2 KiB
TypeScript
182 lines
7.2 KiB
TypeScript
/**
|
|
* `@hyperframes/producer/distributed` — the distributed render primitives.
|
|
*
|
|
* The distributed activities are pure functions over local file paths;
|
|
* networking + orchestration live in adapters. New integrations should use
|
|
* Plan v2; the v1 functions remain available for compatibility.
|
|
*
|
|
* Adopters (AWS Lambda, Cloud Run Jobs, Temporal, K8s Jobs, plain SSH):
|
|
*
|
|
* ```ts
|
|
* import {
|
|
* planV2,
|
|
* renderChunkV2,
|
|
* assembleV2,
|
|
* } from "@hyperframes/producer/distributed";
|
|
*
|
|
* // Controller-side: publish a content-addressed Plan v2 manifest + CAS.
|
|
* const planResult = await planV2(projectDir, config, planV2Dir);
|
|
*
|
|
* // Worker-side: render one chunk. Byte-identical retries on the same
|
|
* // (planV2Dir, chunkIndex) — Temporal / Step Functions retry policies are
|
|
* // safe to point at this.
|
|
* const chunk = await renderChunkV2(planV2Dir, chunkIndex, outputChunkPath);
|
|
*
|
|
* // Controller-side: stitch chunks into the final deliverable.
|
|
* await assembleV2(planV2Dir, chunkPaths, outputPath);
|
|
* ```
|
|
*
|
|
* No networking, no AWS SDK, no Temporal SDK — those live in adapter
|
|
* packages. This module is library code only.
|
|
*/
|
|
|
|
// ── Plan (Activity A) ───────────────────────────────────────────────────────
|
|
export {
|
|
// Functions
|
|
buildChunkSlices,
|
|
measurePlanDirBytes,
|
|
plan,
|
|
rejectUnsupportedDistributedFormat,
|
|
resolveChunkPlan,
|
|
// Types
|
|
type DistributedRenderConfig,
|
|
type PlanResult,
|
|
// Constants
|
|
DEFAULT_CHUNK_SIZE,
|
|
DEFAULT_MAX_PARALLEL_CHUNKS,
|
|
MIN_CHUNK_SIZE,
|
|
PLAN_DIR_SIZE_LIMIT_BYTES,
|
|
PLAN_PROJECT_DIR_SKIP_SEGMENTS,
|
|
// Error codes + classes
|
|
FORMAT_NOT_SUPPORTED_IN_DISTRIBUTED,
|
|
FormatNotSupportedInDistributedError,
|
|
PLAN_TOO_LARGE,
|
|
PlanTooLargeError,
|
|
} from "./services/distributed/plan.js";
|
|
|
|
// ── Plan v2 content-addressed transport ────────────────────────────────────
|
|
export {
|
|
createPlanV2FromExecutionPlan,
|
|
createPlanV2FromV1,
|
|
getPlanV2ExecutionPlanHash,
|
|
listPlanV2ArtifactsForTarget,
|
|
materializePlanV2Target,
|
|
planV2,
|
|
planV2WithPublisher,
|
|
publishPlanV2FromExecutionPlan,
|
|
publishPlanV2FromV1,
|
|
readPlanV2Manifest,
|
|
validatePlanV2MaterializedTarget,
|
|
PLAN_V2_INTEGRITY_UNRECOVERABLE,
|
|
PLAN_V2_MATERIALIZATION_MARKER,
|
|
PlanV2IntegrityError,
|
|
type PlanV2Artifact,
|
|
type PlanV2Limitations,
|
|
type PlanV2Manifest,
|
|
type PlanV2MaterializationResult,
|
|
type PlanV2MaterializationTarget,
|
|
type PlanV2Result,
|
|
type PlanV2WithPublisherOptions,
|
|
} from "./services/distributed/planV2.js";
|
|
export {
|
|
LocalPlanV2ArtifactPublisher,
|
|
type LocalPlanV2ArtifactPublisherOptions,
|
|
type PlanV2ArtifactPublisher,
|
|
type PlanV2PublishBlob,
|
|
} from "./services/distributed/planV2Publisher.js";
|
|
export { assembleV2, renderChunkV2 } from "./services/distributed/planV2Execution.js";
|
|
|
|
// ── RenderChunk (Activity B) ────────────────────────────────────────────────
|
|
export {
|
|
applyRuntimeEnvSnapshot,
|
|
readWebGlVendorInfoFromCanvas,
|
|
renderChunk,
|
|
// Types
|
|
type ChunkRenderer,
|
|
type ChunkResult,
|
|
type EffectiveChunkResult,
|
|
// Error codes + classes
|
|
FFMPEG_VERSION_MISMATCH,
|
|
INVALID_VIDEO_METADATA,
|
|
PLAN_HASH_MISMATCH,
|
|
RenderChunkValidationError,
|
|
} from "./services/distributed/renderChunk.js";
|
|
|
|
// ── Assemble (Activity C) ───────────────────────────────────────────────────
|
|
export { assemble, type AssembleResult } from "./services/distributed/assemble.js";
|
|
|
|
// ── Cloud-agnostic adapter helpers ──────────────────────────────────────────
|
|
// Shared by the distributed-render adapters (aws-lambda, gcp-cloud-run, …) so
|
|
// the config-shape validator lives in one place; each adapter layers only its
|
|
// own wire-format size cap on top.
|
|
export {
|
|
InvalidConfigError,
|
|
type SerializableDistributedRenderConfig,
|
|
validateDistributedRenderConfig,
|
|
validateVariablesPayload,
|
|
} from "./services/distributed/renderConfigValidation.js";
|
|
export { hashProjectDir } from "./services/distributed/projectHash.js";
|
|
|
|
// ── Plan protocol compatibility ────────────────────────────────────────────
|
|
// Workers validate this descriptor before consuming layout-specific
|
|
// artifacts. Missing descriptors remain compatible with legacy v1 plans.
|
|
export {
|
|
CURRENT_PLAN_PROTOCOL,
|
|
DISTRIBUTED_RENDER_CAPABILITIES,
|
|
getDistributedRenderCapabilities,
|
|
PLAN_ARTIFACT_LAYOUT,
|
|
PLAN_HASH_SCHEMA,
|
|
PLAN_PROTOCOL_V1,
|
|
PLAN_PROTOCOL_V2,
|
|
PLAN_PROTOCOL_UNSUPPORTED,
|
|
PLAN_SCHEMA_VERSION,
|
|
PLAN_V2_ARTIFACT_LAYOUT,
|
|
PLAN_V2_HASH_SCHEMA,
|
|
PLAN_V2_SCHEMA_VERSION,
|
|
PlanProtocolUnsupportedError,
|
|
readPlanProtocol,
|
|
readPlanProtocolV1,
|
|
type DistributedRenderCapabilities,
|
|
type PlanProtocolConsumerCapabilities,
|
|
type PlanProtocolDescriptor,
|
|
type PlanProtocolV1Descriptor,
|
|
type PlanProtocolV2Descriptor,
|
|
type SupportedPlanProtocolDescriptor,
|
|
} from "./services/distributed/planProtocol.js";
|
|
|
|
// ── Format union ────────────────────────────────────────────────────────────
|
|
// Canonical output-format type. The aws-lambda package re-exports it so
|
|
// CLI / adopter SDKs can derive runtime allowlists from one source.
|
|
export { PlanVideosMetadataError, type DistributedFormat } from "./services/distributed/shared.js";
|
|
|
|
// ── Plan artifact names ─────────────────────────────────────────────────────
|
|
// The cloud adapters locate and publish the plan's audio artifact by name. Its
|
|
// extension selects the container, so they must read it from here rather than
|
|
// restate it: a literal that drifts from the writer's is a silently missing
|
|
// audio track, not a loud failure.
|
|
export {
|
|
isPlanAudioArtifactPath,
|
|
PLAN_AUDIO_LEGACY_RELATIVE_PATH,
|
|
PLAN_AUDIO_RELATIVE_PATH,
|
|
resolvePlanAudioPath,
|
|
} from "./services/distributed/shared.js";
|
|
|
|
// ── Plan-time shared types from `freezePlan` ───────────────────────────────
|
|
// Re-exported so adopters that deserialize a planDir's `meta/encoder.json`
|
|
// or `meta/chunks.json` see the same shapes the producer wrote them as.
|
|
export type {
|
|
ChunkSliceJson,
|
|
CompositionMetadataJson,
|
|
LockedRenderConfig,
|
|
} from "./services/render/stages/freezePlan.js";
|
|
|
|
// ── Plan-time validation errors ────────────────────────────────────────────
|
|
// Export typed deterministic validation codes so orchestration adapters can
|
|
// mark authoring/configuration failures as terminal while still retrying real
|
|
// infrastructure faults.
|
|
export {
|
|
DISTRIBUTED_DURATION_OUT_OF_RANGE,
|
|
MAX_DISTRIBUTED_DURATION_SECONDS,
|
|
PlanValidationError,
|
|
} from "./services/render/planValidation.js";
|