-
feat(dsl): standardize the asset manifest and converge the export paths (#1007 part 3) (#1117)
发布于
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
下载附件