mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-17 05:29:46 +08:00
* fix(video-export): bridge play_video element id → media ref Generated-media records are keyed by the element's media ref (gen_vid_…), but a play_video action targets the slide element by its .id. The app-side compiler deps looked media/durations up by the raw element id, so every generated video missed → present:false → no <video> emitted, no bytes collected → the clip vanished from the export (only its static first frame, baked into the base PNG, remained). Build an elementId → mediaRef bridge across all slide scenes in createVideoTimelineDeps (mirroring the live engine's resolveMediaPlaceholderId) and resolve both media() and videoDurationMs() through it. Refs #867. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): re-measure KaTeX fit-scale after fonts settle KaTeX sizes large delimiters (\left\{, \begin{cases}) from its KaTeX_Size faces, which load asynchronously. KatexContent measured its shrink-to-fit scale once on mount, against the fallback font, and never recomputed — so a cold slide snapshot baked a stale scale and the big brace desynced from the piecewise body ("大括号后面的分段函数与大括号错位"). - Re-run the fit-scale measurement on document.fonts.ready and on each `loadingdone`, keeping the shrink-only (cap-at-1) behavior. - slideToPng waits two extra frames before capture so the font-triggered React re-measure commits into the DOM html2canvas reads. Refs #867. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): place spotlight/laser/video on the rendered content box The pure geometry helper resolves an element's authored outer box against a fixed 1000×562.5 base, but the live overlays — and the exported frame PNG — measure the element's `.element-content` box, which for horizontal text is auto-height plus 10px content padding. So a spotlight/laser sat offset from where the text actually renders ("spotlight 与对应元素的位置有偏差", #867 item 5). Add an optional GeometryProbe to the compiler's DI boundary: when supplied, the geometry pass prefers each element's measured content-box geometry (video keeps its rotation from the authored element), degrading to the pure authored-box calc on a miss so the compiler stays deterministic in unit contexts. App side: measureSlideElementGeometry (new, in @openmaic/renderer/snapshot) mounts the slide off-screen and reads the same `.element-content`-against- container box the live SpotlightOverlay uses; createVideoTimelineDeps pre-measures every spotlight/laser/play_video target per scene and serves the probe as a synchronous table lookup. Tests: geometry.test.ts quantifies the outer-box vs content-box delta and proves probe-wins / fallback / rotation-preservation; timeline-deps.test.ts covers the pre-measure wiring. Refs #867. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): decode a first-frame poster for posterless videos A play_video clip only shows during its play window; the rest of the time the static base-frame PNG backs that spot. But html2canvas can't draw a <video>, so the snapshot swaps it for its poster <img> — and generated videos usually carry no poster, leaving the video area blank whenever the clip isn't playing. When a video record has no stored poster bytes, decode the video blob's first frame (seek slightly off 0, draw to a canvas) and set element.poster before the base snapshot, so that spot shows the first frame — the "paused on frame one" look — instead of blank. Falls back to no poster on decode failure / timeout / CORS taint, never failing the export. Refs #867. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(video-export): split subtitles, make burn-in optional, centered modal Burned-in captions used one cue per whole narration paragraph, so a long cue became a 4–6 line block covering the lower half of the slide, and there was no way to turn it off. - Split cues (pure, compiler layer): lib/video-export/split-cue.ts splits each cue on sentence → clause → hard-wrap to ≤~40 CJK-equiv units, distributing the parent window by character weight (last piece pinned to the parent end, sub-1.2s slivers merged). Wired into the timeline pass, so the burned-in overlay and the SRT/VTT sidecar share one split track and can't drift. - Burn-in optional (default off, #867 item 2): emitHyperframes gains burnInSubtitles; off → clean video + sidecar SRT/VTT only. Caption layout hard- clamped to 2 lines so an outlier can't grow tall again. - Download subtitles without rendering: compileSubtitles() runs only the compiler (no assets/zip/service); new useDownloadSubtitles saves an .srt. - Centered modal: video-export-dialog.tsx replaces the cramped dropdown section, grouping Output / Subtitles / Actions / Progress with a stable lifecycle for multi-minute renders. header-controls opens it from an "Export Video" entry; old video-export-menu.tsx removed. i18n keys added across all 8 locales. Refs #867. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style: prettier format video-export fidelity changes Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: drop unused playVideo helper in timeline-deps test The media-bridge tests assert via deps.assets.media(...) directly, so the playVideo helper (and its PlayVideoAction import) were dead. Flagged by the code-quality bot on #952. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): resolve cross-review findings + lower subtitle band - emit-hyperframes: drop duplicate `display:-webkit-box` on subtitle cues so they start truly `display:none` (a second declaration overrode the `none`, showing every cue stacked at t=0 when burn-in was enabled). - geometry: don't feed the measured content-box AABB to a rotated video clip — it already encloses the rotation, so re-applying `rotate` doubled it. Rotated elements fall back to the authored box (one, un-doubled source of truth). - build-export-zip: extract shared `compileStageIr` so buildExportZip and compileSubtitles can't drift on the timing/assets/geometry wiring. - split-cue: join merged CJK cue pieces without a spurious ASCII space. - subtitles: lower the caption band (bottomRatio 0.055 → 0.01) and hoist the band's magic numbers into a named SUBTITLE constants block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): stop the snapshot onclone override from reshaping KaTeX The export snapshot injects a neutral-kerning / geometricPrecision style over `.slide-renderer-prose *` to keep CJK prose+table glyphs from mis-advancing under html2canvas-pro. KaTeX formulas (output:'html') also live inside `.slide-renderer-prose`, so the override was reshaping their math glyph advances — a burned-in formula rendered subtly differently from the live canvas, which applies no such override. Empirical check (Playwright + system Chrome, cases+fraction formula, native paint vs html2canvas): the override raised the formula's mean per-pixel delta vs the live paint from 120.1 to 139.2 (~16%) and moved ~75% of inked pixels. Restoring native font-kerning/text-rendering for `.katex` subtrees brings the formula's delta back to 120.1 (bit-identical to the no-override capture) while prose and table text keep the override. The residual 120.1 is the deeper html2canvas rasterization gap (vlist/frac-line/delimiter), a separate track. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): render slides via native paint, bundle KaTeX font embed Root-fix the exported-vs-live formula distortion (and the filter/mask/video gaps) by rasterizing slides with native Chrome paint instead of html2canvas's reimplementation. - slideToPng now captures via html-to-image (foreignObject → the same Chrome engine that paints the live classroom rasterizes the same DOM). KaTeX formulas, CSS filter, soft-edge masks, and mixed CJK/Latin text come out as the classroom shows them. html2canvas-pro stays as a fallback (with the filter/mask bakes + KaTeX text-rendering reset) for when native paint can't run — e.g. a cross-origin image taints the canvas. - A foreignObject SVG can't reach the document font registry, so fonts must be inlined. KaTeX math faces are prepended from the bundled woff2 (KATEX_FONT_EMBED_CSS, generated by scripts/generate-katex-fonts.mjs), NOT left to runtime getFontEmbedCSS — which reads cssRules and silently drops a cross-origin KaTeX stylesheet, collapsing a large brace to a fallback glyph (the observed "formula render failure" on e.g. a \begin{cases} with \text{}). A per-face guard verifies every KaTeX face the formula references is embedded, falling back to html2canvas otherwise. Verified in a Playwright spike (native vs html2canvas vs html-to-image): formula inked-delta 143→53; filter/mask/video match native; and with getFontEmbedCSS forced empty, the bundled embed alone still renders the failing formula correctly. Renderer package tests: 226 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): prettier-format the generated KaTeX font-embed module The generator emitted an unformatted TS file (double quotes, long unwrapped CSS literal) that failed CI `prettier --check`. Run the generated output through the repo's Prettier config before writing, so every rebuild produces CI-clean, stable output. Regenerated the committed file to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lint): rename reserved `module` var in katex-fonts generator; ignore .scratch - generate-katex-fonts.mjs assigned to a variable named `module`, tripping @next/next/no-assign-module-variable (the one hard eslint error). Renamed to `fileContents`; generated output is byte-identical (deterministic). - Add `.scratch/**` to the eslint globalIgnores (alongside .claude/.worktrees) so local throwaway verification scaffolding doesn't pollute `pnpm lint`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): address PR #952 review — scene-scoped media bridge, safer cue splitting Resolves the two blocking correctness issues and three follow-ups from the cross-review on #952. - Scope the elementId→mediaRef bridge by scene (was deck-wide/last-writer-wins): slide-local ids like `video_001` now resolve within their own scene. `assets.media` uses its scene arg; `timing.videoDurationMs` resolves via action object identity. Adds a cross-scene collision test. - Stop treating every ASCII period as a sentence boundary: a lone `.` splits only when not between word chars (3.14, v1.2) and not an abbreviation dot (e.g.), collapsing repeated-punctuation runs. Adds decimal/version/abbrev/ellipsis tests. - Share the module-level in-flight guard across ZIP export and subtitle download (extracted to export-in-flight.ts); dialog `busy` folds in `downloading`. - Skip the off-screen geometry render for the subtitles-only path (skipGeometry); duration probes are kept so sidecar cue timing stays in sync with the video. - Align measure.ts font settling with slideToPng: force-load each used face before fonts.ready so measured content-boxes don't bake fallback advance widths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(video-export): prettier-format split-cue test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(video-export): enable beginFrame capture via PRODUCER_BROWSER_GPU_MODE=hardware The producer defaults to `software` GPU mode, which silently force-enables the CPU-bound `Page.captureScreenshot` fallback: SwiftShader pins every core on rasterization and caps frame capture at ~10 fps. Selecting `hardware` keeps Chromium's `HeadlessExperimental.beginFrame` capture active instead. Measured on a 900-frame (30s) clip, no real GPU on the host: - capture ~10 -> ~19 fps, total 100s -> 62s (~38% faster) - CPU peak ~810% -> ~98%; peak mem 2.2 GiB -> 0.9 GiB - static frames pixel-identical; animation frames differ only in sub-pixel edge antialiasing (same-path re-render is deterministic/inf PSNR) No real GPU is required — the win is the faster capture API, not hardware rasterization; beginFrame falls back gracefully if the host lacks a GPU. Also eases the RENDER_JOB_DEADLINE_MS timeout risk on long videos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(video-export): sentence splitting via Intl.Segmenter + abbreviation merge Addresses the PR #952 follow-up: separate sentence-boundary detection from cue shaping and stop growing a custom punctuation heuristic. - splitSentences now delegates boundary detection to Intl.Segmenter (locale- agnostic ICU), the multilingual/CJK baseline. It already preserves 3.14, v1.2, e.g., U.S., ellipses, and CJK 。!?… without any hand-rolled period scan. - ICU still ends a sentence after a titlecase abbreviation before a capitalized word (Dr. Smith, Fig. 3) — the edge case the reviewer flagged. A small abbreviation-aware post-merge re-joins those, driven by an ABBREVIATIONS set. - Semicolons are no longer sentence ends (they join clauses); moved ;; into the clause-level SECONDARY splitter so over-budget sentences still break there. - Cue shaping (budget / clause / hard-wrap / weighted timing) is unchanged, so detection and shaping are now distinct stages. Not using `sbd` as suggested: lib/video-export/*.ts sits behind the machine- enforced eslint purity boundary (imports limited to @openmaic/dsl, zod, ../choreography, siblings), so a new npm dep is rejected. Intl.Segmenter is a pure Node >=20.9 builtin, matching the reviewer's "small abbreviation-aware post-merge layer" alternative. Tests: add Dr. Smith / Fig. 3 / U.S. coverage; the rhetorical ellipsis now stays one under-budget cue (was split); add a semicolon over-budget case. 22/22 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(video-export): prettier-format split-cue sentence refactor Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n(es-MX): add 7 subtitle/video export keys to align with en-US main added the Spanish (Mexico) locale (#942) after this branch's subtitle keys landed, so es-MX was the only locale missing export.subtitles* and export.videoBurnSubtitles* — which failed the i18n key-alignment CI check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(video-export): restrict title post-merge so it never deletes real boundaries Addresses the blocking re-review on the Intl.Segmenter change: the abbreviation post-merge unconditionally rejoined the next ICU segment whenever the previous one ended in a listed abbreviation, which erased genuine sentence boundaries — `It was made by Acme Inc. Next point.` collapsed into one cue, and even an explicit newline (`Acme Inc.\nNext topic`) was rejoined. The merge now fires only for the case ICU actually mis-splits — a title (Mr/Mrs/Ms/Dr/Prof/Sr/Jr) before a capitalized proper noun — and only when: - the previous segment does not end at a newline (hard boundary), and - the next segment starts with a capital letter. Sentence-trailing abbreviations (Inc./Ltd./etc./vs./No.) are dropped from the set entirely: they legitimately end sentences, and ICU already keeps a title before a number (No. 5, Fig. 3) or a lowercase continuation (etc. and…) whole without any merge, so nothing there needs rejoining. Regression tests: Inc./etc. boundary preserved, newline-after-title preserved, lowercase continuation kept whole, plus the retained Dr. Smith / Fig. 3 / U.S. cases. 26 split-cue tests pass; full video-export suite 123 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>