-
feat(media): allocate generated assets through the registry (#1007 part 2, step b) (#1039)
发布于
2026-08-06 10:49:34 +00:00 - feat(media): establish shared asset ownership primitives
Introduce global browser asset-pool ownership, asset-reference collection, stage reclamation planning, and lease-based URL access. Carry allocated media identity through storage and generation boundaries with regression coverage.
- fix(media): enforce safe resolution across every consumer
Route image, video, thumbnail, presentation, and video-export consumers through one resolution state machine. Prevent opaque allocated or generated references from reaching render and export sinks, with fallback and ownership tests.
- fix(media): protect document-owned assets across mutations
Allocate pool bytes before compatibility writes and document commits, then roll back uncommitted generations safely. Preserve document ownership across edits, retries, imports, speech generation, scene changes, and stage deletion.
- fix(media): scope retries and tighten ownership guard
Scope retries to the target scene and slide and refuse ambiguous shared-reference mutations. Expand retry rendering coverage and keep direct pool URL resolution behind the shared lease owner.
- fix(storage): avoid nested lock during stage cleanup
Execute prepared reclamation plans against an explicitly deleted document so the compatibility cascade cannot re-enter the per-document lock. Cover deletion of stage-owned media rows even when the document has no references.
- refactor(media): confine reclamation to stage deletion
Remove inline pool and compatibility-row cleanup from element, speech, scene, and audio replacement flows. Keep whole-stage reclamation behind explicit stage ownership, preserve stage-less legacy audio rows, and document deferred document-truth sweeping.
- fix(media): close retry and resolution gaps
Restore shared source tasks after successful forks, scope retries across both whiteboard locations, and wait for parallel TTS workers before rollback. Resolve background media through import, rendering, and PPTX export paths while keeping retry controls visible over last-good bytes.
- test(media): execute the consumer safety matrix
Replace source-substring checks with resolver seam execution across all six UI consumers, stage hydration, and both exporters. Pin each rollback layer independently and harden the ownership guard against aliased pool imports.
- chore(packages): publish the additive DSL field
Bump the DSL patch version for the optional speech-action field. Keep the transitional reclamation policy app-owned and document the legitimate transaction rollback removals there.
- fix(media): make retry rollback task-safe
Delay allocation task re-keying until final document reconciliation succeeds, restore shared source tasks on failed forks, and require exact stage-whiteboard targets before falling through from a missed scene.
- fix(media): clear private assets and refresh leases
Delete the asset-pool database during the confirmed local-data wipe. Notify the app lease layer after same-id replacement so mounted consumers re-resolve current bytes without reaching into storage internals.
- test(media): pin closure safety guards
Exercise the CSS allocation boundary, unfiltered legacy-row ownership, delete-and-undo byte survival, and real distinct consumer seams. Restore the video-only manifest overwrite condition and share the direct video resolution hook across both element variants.
- test(media): narrow rollback element assertion
Narrow the reconciled slide element to an image before checking its source so the rollback regression remains type-safe under the full root compiler configuration.
- fix(media): close lease refresh races
Gate the first batch publication by unique resolved refs, then publish every replacement snapshot without mutating the prior React state object. Serialize invalidation behind pending releases, evict rejected refreshes, and register replacement observation at the pool boundary.
- fix(media): reopen pool after clear failures
Always evict the singleton once its store has been closed, including blocked and failed database deletion paths. Report blocked deletion as deferred and prove a later write uses a fresh live store.
- fix(media): isolate shared retry progress
Track forked regeneration under the selected element until it receives a fresh asset identity, leaving the shared source task and bytes untouched. Surface targeted failures through renderer task lookup and skip the redundant fork reconciliation lock.
- fix(media): close final asset retry gaps
Keep blocked asset clears fail-loud until a successful retry, with actionable settings guidance.
Clear failed shared-fork state across durable and live key spaces, and pin renderer lookups, lease publication identity, and committed-ref rollback protection.
- fix: preserve actionable retry failures
Localize the blocked cache-clear recovery hint across every supported locale and pin the deferred-error mapping.
Retain durable fork failure rows while retries run, deleting them only after successful generation so unstructured failures survive reload.
- fix(media): hydrate legacy stored video thumbnails
Home-page recent-video thumbnails regressed for legacy Dexie mediaFiles
rows keyed by gen_vid placeholders: the reworked hydration resolved the
row's bytes through the sealed resolver but never surfaced the stored
blob (and its poster) as object URLs for the preview card, so the CI
recent-video-thumbnail e2e specs found no visible element.Hydration now materializes legacy stored video rows into blob URLs for
both the element src and poster while keeping the resolver invariants:
opaque refs still never reach a DOM src, and concrete addresses are
never blanked. Unit pins cover the seam so the vitest suite catches this
class without a browser.- fix(media): resolve sole restored legacy video
Classroom playback restored tasks by exact document media references. Legacy gen_vid references can outlive the key used by the one persisted video row, leaving the player on a placeholder even though bytes were restored.
Select the sole completed stage video only for legacy sequential refs after exact and reconciled matches. Keep exact failures authoritative, refuse ambiguous candidates, and cover success, ambiguity, and failure precedence in unit tests.
- fix(media): scope legacy video recovery
Decide restored legacy video recovery once from the complete document and record it through the shared task lookup consumed by playback, editing, and resolved slides.
Keep ambiguous documents as placeholders, apply the same decision to thumbnail hydration, and preserve exact failure precedence.
- fix(media): exclude claimed video recovery tasks
Model restored legacy recovery as a two-pass match across document video elements and task rows. Remove tasks claimed by exact, targeted, or placeholder lookup before applying the sole-candidate fallback, and cover the ownership/cardinality matrix.
- fix(media): unify video element resolution
Centralize source, task, poster, and legacy recovery decisions for every video consumer. Ensure direct URLs win over opaque refs and play_video waits on element-targeted retry tasks.
- refactor(media): route legacy recovery through resolver
Let document-aware consumers request legacy video recovery through the unified element binding API, keeping thumbnail hydration on a single decision path.
- fix(media): preserve import refs and prefer pool bytes
Recognize unambiguous extensionless relative media addresses during classroom import. Resolve allocated export and thumbnail assets from the shared pool before falling back to lagging compatibility rows.
- fix(export): preserve concrete video sources
Route PPTX video elements through the shared media binding resolver so an unresolved opaque reference cannot replace a playable source. Complete browser and persisted-store cleanup when asset-pool deletion is deferred, while retaining distinct hard-failure behavior.
- fix(media): fork retries without exclusive ownership
Enumerate logical asset owners across every persisted document before allowing global pool replacement. Thread explicit targets through fresh-id rewrites and cover cross-document aliases plus unreadable ownership.
- fix(media): guard global asset reclamation
Share a fail-closed persisted-document liveness check between stage deletion and retry replacement. Preserve cross-document pool aliases while deleting stage-owned compatibility rows and cover enumeration failures.
- fix(media): cover complete slide asset references
Route slide media traversal through a shared mutable slot contract so backgrounds participate in export, thumbnail hydration, collection, and rewrite lifecycles. Snapshot complete surviving-document refs once per reclamation and preserve manifest-only owners while retaining fail-closed behavior.
- fix(media): preserve exclusive retry asset ids
Allow targeted retries to replace exclusively owned pool assets in place. Keep shared and unprovable ownership paths on fresh allocations, and pin production-shaped retries plus compatibility-row cleanup.
- fix(media): revalidate asset bindings at completion
Recheck repository-wide ownership before replacing generated media and fork scoped retries when exclusivity changed. Route video export selection through the unified resolver and keep concrete posters independent of task state.
- fix(media): count unflushed owners and broadcast replacements
The completion-time exclusivity proof read only the persisted document, but
slide duplication updates the Zustand aggregate synchronously and schedules
persistence behind a debounce. A retry finishing inside that window saw a
single persisted owner and replaced the bytes behind a reference the
duplicate also held. The proof now also counts owners in the live stage
snapshot when that snapshot represents the stage being retried, so an
unflushed duplicate forks instead.Same-id replacement notifications were realm-local, so a second tab showing
the same classroom kept its lease pinned to the superseded blob URL. The
notification now travels over a BroadcastChannel; each receiving realm runs
its own observers against its own pool, so a spoofed message can at most
force a re-resolve. A missing or failing channel never fails the replacement.Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- fix(media): bind replacement listeners and spare shared audio rows
A realm that only renders never sends a replacement, so binding the channel
from the sender path left passive tabs deaf to peers. Binding now happens
where the observer is registered, when the asset-pool module loads, and the
receiving realm resolves its own pool lazily so a cleared or unavailable
pool degrades to the next resolve instead of throwing.Observer notifications ran under Promise.all, so a rejection surfaced after
BrowserAssetStore.replace had already committed and turned a durable success
into a reported failure. They are settled individually now; the callback is
wrapped because a synchronous throw would otherwise escape before allSettled
sees the array.audioFiles rows are keyed globally by audioId, so deleting a stage removed
the sole row for an id a surviving document still referenced — playback and
both export paths read that table directly and cannot fall back to the
preserved pool blob. Rows are now filtered against surviving references,
while a failed enumeration still withholds only the irreversible pool
removal.Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- fix(media): serve replaced bytes from the pool everywhere
Unknown survivor liveness deleted every planned audioFiles row. Those rows
are keyed globally by audioId, and playback plus both export paths read the
table directly, so losing one is as irreversible for them as removing the
pool entry. Unknown liveness now preserves the rows too, leaving bounded
garbage for a later pass that can prove exclusivity.The earlier pool-first change covered PPTX, video collection and thumbnail
hydration but missed classroom ZIP export, which still serialized the stale
compatibility row after a lagged same-id replacement, shipping media the
classroom no longer renders.Auditing every direct reader of the media and audio tables surfaced the same
gap in playback: speech regeneration also replaces bytes under a stable id
and does not roll the pool back when the compatibility write fails, so the
player kept serving superseded narration. It now resolves the pool first and
falls back to stored rows for legacy and imported audio.Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- fix(audio): replace exclusively owned speech clips in place
regenerateSpeechAudio accepted the action's audioId but always passed
undefined as replaceAssetId, so regenerating an exclusively owned
pool-backed clip allocated a new asset every time, rewrote the action and
orphaned the previous pool entry and compatibility row until stage
reclamation — contradicting the stable-id path generateAndStoreTTS already
implements for media.Ownership is now established before synthesis, and the rule itself moved to
the shared reference module so media retries, poster replacement and speech
regeneration consume one implementation instead of restating it. An
exclusively owned clip keeps its id and has its bytes replaced; a shared
clip, a legacy id with no pool entry, or unprovable ownership still gets a
fresh allocation so other holders keep their audio.Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- chore(media): drop the import left behind by the ownership move
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- fix(media): resolve audio pool-first and fence peer realms
Stable-id TTS regeneration commits replaced narration to the pool before the
audioFiles mirror write, so a failed mirror leaves the row stale. AudioPlayer
already resolved pool-first, but audioObjectUrl, collectAudioFiles and the
video timeline dependencies still read Dexie directly and would serve the
superseded clip. All allocated-audio readers now share one resolver, with
Dexie kept as the fallback for legacy and imported rows.The exclusivity proof modelled unflushed owners in the active realm only, so
another tab duplicating the same asset during its save debounce could still
be observed as a single owner and have its bytes replaced globally. A peer's
pending state cannot be read across realms, so presence is probed instead:
any realm holding the stage forces the fork path.A deferred asset-pool deletion no longer reloads the page. The database is
still on disk and the guidance asks the user to close the other tab and
retry, which the reload discarded. The decision moved into a shared helper so
the rule is pinned rather than living inline in the component.Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- fix(media): fail closed when presence cannot be probed
The presence helper documented that an unanswerable probe must count as a
peer, but every unavailable path — no BroadcastChannel, a constructor that
threw, a send that failed, and the window before the pool's asynchronous
binding completes — returned false, so the ownership proof cleared a single
local owner and replaced globally shared bytes in place.Probing now returns present, absent or unknown, and only a probe that was
actually sent and went unanswered is absent; the ownership decision treats
unknown exactly like present. The pool declares its binding intent
synchronously so a probe issued during the load-time window waits for the
bind instead of concluding that presence is unavailable, and releases that
gate if the import fails.Coverage reaches the write boundary: with presence unknown, a
production-shaped targeted retry forks to a fresh id instead of calling
replace, and the original bytes stay intact for a peer's unflushed owner.Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- chore(dsl): bump to 0.6.3 after the release dedupe took 0.6.2
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
- fix(i18n): add the deferred-clear guidance to fr-FR
The locale landed on main after this branch added the key, so the alignment
check flagged it as the one missing translation.Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Co-authored-by: Claude Opus 5 noreply@anthropic.com
Co-authored-by: 杨慎 117187635+cosarah@users.noreply.github.com下载附件