发布

  • feat(dsl): standardize the asset manifest and converge the export paths (#1007 part 3) (#1117)

    frostbyte_neo 发布于 2026-08-19 12:43:14 +00:00

    • feat(dsl): standardize the document asset manifest

    Add asset-manifest.ts to @openmaic/dsl: the canonical AssetManifestEntry
    shape (ref, kind, and byteSize/mimeType/duration/voice/prompt metadata
    where available) plus enumerateAssetManifest, the pure document-to-manifest
    enumeration. An entry's ref is the reference exactly as the document holds
    it -- the manifest is the id-based reference enumeration with metadata,
    not a content hash and not a resolution result. The traversal walks the
    stage whiteboard, each scene's canvas/whiteboards/speech actions, and the
    stage video-manifest keys in document order, with logical-owner reference
    counts that match the accounting duplication-safe replacement uses.

    This settles the media-ref + asset-manifest schema question (#779 open
    question 4) on the side the asset-pool RFC already implied: the schema is
    a function of the id semantics decided there. The type lives in the dsl
    rather than a new @openmaic/exporter package because the enumeration is
    pure over document types the dsl already owns (Stage/Scene/Slide/Action),
    so a separate package would add a published artifact and release-workflow
    surface without adding a capability; the storage contract comment now
    points at the module.

    Refs #1007

    • refactor(export): drive the classroom ZIP from the asset manifest

    collectMediaFiles used to scan the whole mediaFiles table for the stage,
    so any row the document no longer references -- an orphan left by an edit
    or a superseded regeneration -- rode along into the archive. Both ZIP
    collectors now take their reference sets from the standardized asset
    manifest (buildStageAssetManifest wraps the dsl enumeration with the
    compatibility rows' metadata): only referenced assets are archived, and a
    referenced asset whose bytes exist only in the pool is still collected via
    a synthesized record. Byte resolution is unchanged: pool first through
    resolveStoredBytes / resolveAudioBlob, with the compatibility row kept as
    the legacy byte fallback and as the metadata source.

    mediaIndex is now a serialized view of the manifest, and the missing-audio
    report derives from the manifest's audio entries instead of a second
    action walk. The audioRef mapping and the legacy audioUrl fetch path
    (collectLegacyAudioForExport) are untouched.

    Refs #1007

    • refactor(video-export): take the timeline's reference sets from the manifest

    createVideoTimelineDeps scanned the whole mediaFiles table for the stage
    and derived its audio id set from its own action walk -- a third,
    independent answer to "which media does this course use?". Both record
    loads now key off the standardized asset manifest: media rows are read per
    manifest ref by compound key instead of by table scan, and the audio id
    set is the manifest's audio entries. Orphan rows were never reachable
    through the scene-scoped elementId-to-mediaRef bridge; now they are not
    even read.

    The bridge itself is untouched: element ids recur across scenes, so the
    elementId-to-mediaRef mapping stays scoped per scene, and the legacy
    audioUrl fallback keeps its own action walk because a URL is not a
    manifest ref. AssetPlan remains the video IR's view of the same
    references.

    Refs #1007

    • refactor(export): resolve PPTX media through the shared resolver only

    Each PPTX element branch carried its own resolution chain: a task-state
    renderable-URL lookup first, then -- gated on the legacy placeholder
    predicate -- a stored-bytes override, with the poster block repeating the
    pattern. One helper now owns resolution for backgrounds, images, video /
    audio sources, and posters: opaque refs (allocated ids and legacy
    placeholders alike, no placeholder-pattern gate) resolve pool-first
    through resolveStoredBytes and embed as data URLs, concrete addresses
    resolve through the media state machine and keep the caller's fetch path.
    exportMediaResolution and the resolveStoredMediaBlob wrapper fold into
    the helper; resolvePptxMediaBinding stays as the state-machine entry the
    resolution-surface test matrix drives.

    Refs #1007

    • refactor(export): retire the export-side Dexie byte fallbacks

    Export call sites no longer read bytes off compatibility rows directly.
    The ZIP collectors and the video timeline's audio load resolve bytes only
    through the shared resolvers (resolveStoredBytes / resolveAudioBlob),
    which answer pool-first and keep the compatibility row as their internal
    legacy fallback level; the row reads that remain at the call sites supply
    metadata (format/duration/voice/mime/size/prompt) only. The rows
    themselves stay for legacy and regeneration readers -- what goes is the
    export paths' own fallback logic.

    One observable tightening: a failed media row (error set, empty
    placeholder blob) no longer ships a 0-byte file into the classroom ZIP,
    and an evicted row no longer ships its empty local blob; referenced-but-
    byteless assets are simply absent from the archive, as they already were
    when no row existed.

    Refs #1007

    • test(media): cover the enriched stage asset manifest builder

    Pins the join between the pure dsl enumeration and the compatibility
    rows: metadata attaches by ref, rows no document reference names never
    appear, and a referenced asset with no row keeps a metadata-free entry.

    Refs #1007

    • fix(video-export): widen the deps stage input for the manifest enumeration

    enumerateAssetManifest reads the stage's whiteboard and videoManifest, so
    createVideoTimelineDeps declares them on its input instead of the bare id;
    callers pass only the id today and the optional fields stay absent. Also
    applies the repo prettier formatting to the files this branch touched.

    Refs #1007

    • fix(dsl): enumerate slide audio elements in the asset manifest

    Slide audio elements carry their own src, and the manifest skipped
    them, so a manifest-driven collector could never archive their bytes.
    The audio slot maps to kind 'audio' alongside narration ids.

    Refs #1007

    • fix(media): harden ref-keyed lookups against prototype-named asset refs

    AssetRef is an unconstrained string alias, so a media reference can legitimately be "proto", "constructor", or any other Object.prototype member. Plain objects keyed by such refs silently drop assignments or answer lookups with the prototype object, which rewrite paths then accept as a mapped id.

    Convert the remaining ref-keyed lookup tables introduced by the export convergence to prototype-safe structures: the classroom import media/poster alias maps and the legacy-conversion video-manifest reconstruction now use Map / null-prototype containers with explicit membership checks, and every consumed value is validated as a string before it is written into a src / mediaRef / audioId slot. The shared media-task lookup receives the same treatment: one centralized own-property-checked lookupMediaTask now serves the stored-bytes resolver, the PPTX embeddable-src path, the video collection path, and the element/background task resolution, so a prototype-named placeholderRef can no longer hide a re-keyed task from the fallback chain.

    Adversarial tests drive "proto" and "constructor" refs through the import round trip, the PPTX fallback path, the legacy conversion commit path, and the media-task fallback end to end, including a buildPptxBlob regression with a task re-keyed to an allocated id while retaining a prototype-named placeholderRef.

    • fix(export): use safe archive asset paths

    • refactor(dsl): centralize slide media slot roles

    • refactor(export): derive consumer refs from manifest

    • fix(export): sanitize classroom archive extensions

    • fix(video-export): preserve narration speech order

    • fix(export): enforce kind-coherent archive media

    • fix(export): define media coherence boundary

    • fix(export): carry task-owned poster binding for PPTX export

    A video element with no explicit poster falls back to its media task's
    generated poster URL, but resolveVideoMediaForElement left posterTask
    undefined for that case, so the PPTX manifest guard saw a foreign URL
    with no task-ownership exemption and dropped the video element instead
    of using the established runtime poster fallback.

    Carry the poster task binding whenever the task poster is the effective
    poster: the task-owned URL then satisfies the guard's objectUrl
    exemption end to end. A concrete explicit element poster still stays
    element-owned and never borrows the binding, and the guard's foreign-ref
    rejection is preserved (and exported as a directly testable predicate).

    Coverage: an element with no poster plus a task-provided poster embeds
    the task poster as the PPTX cover (red at the pre-fix head, green now),
    and a genuinely unrelated URL with no task ownership is still rejected
    by the guard.

    • fix(export): preserve legacy narration source refs in the media index

    The explicit sourceRef contract was partial: primary audio and generated
    media entries carried it, but legacy URL narration serialized no source
    ref. The legacy URL itself is the natural source ref — it is known at
    fetch time — so wire it through the collected blob into the mediaIndex
    entry. Import already registers serialized sourceRefs as aliases, so the
    URL now round-trips as an explicit mapping instead of being reconstructed
    only from the action's audioRef.

    Poster siblings are deliberately NOT given their own mediaIndex entry:
    a sibling poster (media/asset-.poster.) is a legacy byte copy
    written from the video record and is not an independently referenced
    document asset — when the poster is a real document asset it already has
    its own indexed entry with a sourceRef, and import reconstructs the
    sibling by path derivation from its parent video entry, reusing the
    poster's own indexed allocation when one exists. The PR description is
    narrowed to match; corrected paragraph:

    "Archive names never interpolate refs — sequential safe paths
    (media/asset-., audio/audio-.) with the original ref
    preserved through an explicit sourceRef mapping on every independently
    indexed media entry: generated media assets, poster assets, primary
    narration, and legacy URL narration (the legacy URL itself is the
    entry's sourceRef). Extensions are allowlisted per kind. The one
    exception is the legacy sibling poster byte copy
    (media/asset-.poster., written next to its video when the video
    record still carries the pre-pool poster bytes): it is not an
    independently referenced document asset, so it has no mediaIndex entry
    or sourceRef of its own — its identity is derivable from its parent
    video entry (same index), and import reconstructs it by sibling-path
    derivation from that video entry, reusing the poster's own indexed
    allocation when one exists."


    Co-authored-by: 杨慎 117187635+cosarah@users.noreply.github.com

    下载附件