Skip to content

Releases: francozanardi/tscaps

web-v0.6.0

Choose a tag to compare

@github-actions github-actions released this 14 Oct 16:24

Added

  • Export the animated captions as a video of their own, to use them as a layer in another video editor. Choose "Export captions instead" at the bottom of the export dialog, then "Captions as a video". The background can be transparent, in a WebM file (VP9) with transparency, or a solid color to remove with a chroma key. The file has the same size, frame rate and length as your video, and the audio is optional. Some video editors need a plugin to read transparency in WebM. If a style in your project uses the video, such as a blur or a blend, the dialog warns you that it will look different. The parts of those styles that show your video stay transparent, and blended words are exported without the blend, so you can apply it again in your video editor.
  • Change any keyboard shortcut. Press ?, or use the keyboard button in the editor's toolbar, to see every shortcut and set your own keys. A key another shortcut already uses asks before it moves over. Your keys are kept in this browser, and tooltips show them.
  • Correct a transcript without leaving the keyboard. While typing in a scene, Alt+↑ and Alt+↓ move to the previous or next scene and take the playhead with it, Alt+Shift+↑ and Alt+Shift+↓ join the scene with its neighbour, Alt+Enter plays from the start of the scene, and Alt+K plays or pauses. On a Mac, Alt is ⌥.
  • An Effects tab for a single element, in the panel that opens when you click a word, an emoji or a scene in the preview. It is separate from the sheet's Effects tab. Blend the element with the video, which inverts the video behind it so it reads on any background. Or put a single word or emoji behind the person while the rest of its scene stays in front. The two work together, and a blend over the person inverts the person too.

Changed

  • New keys to step through the video: ← and → move one frame, Alt+← and Alt+→ one word, Shift+← and Shift+→ one scene. They replace holding F, W or S while pressing an arrow. The step buttons in the player's settings menu now show only on phones.
  • "Hide behind person" for a scene moved from the scene's menu to the element panel's Effects tab.
  • The subtitle file export moved behind "Export captions instead" at the bottom of the export dialog, next to "Captions as a video".

Fixed

  • In an exported video, the person passing in front of the captions moves smoothly. With a style that reads the video, such as the frosted ones, they held still for a moment and then jumped.

engine-v0.7.0

Choose a tag to compare

@github-actions github-actions released this 14 Oct 16:22

Added

  • Caption layers. A segment or element override can carry stacking (CaptionStacking.UNDER_TOP_LAYER or OVER_TOP_LAYER) and blendMode (CaptionBlendMode, the blend modes CSS and the 2D canvas share under the same names). Whatever an element leaves out follows its segment, and whatever a segment leaves out is the base layer: under the top layer, plain. Each layer is rasterized apart and LayeredFrameCompositor draws it with its blend against everything already composited, the top layer included. A blend therefore holds whatever stacking contexts the stylesheet creates, which mix-blend-mode written on the element does not, and one word can sit behind a top layer such as an actor cutout while the rest of its caption stays in front. A word or glyph still in its line is painted by a copy of its segment's subtree in which only it is visible. A blended raster holds nothing of the video, so a blended element that does not move reuses one tile.
  • RenderJob.background paints the captions on something other than the input's frames: { kind: 'color', color } for a solid colour, or { kind: 'transparent' } for an output with an alpha channel (WebM through VP9 or VP8; any other format is rejected). The input still sets the output's size, frame rate, duration and cuts. A top layer follows the background: it is cut out of the colour, or, on a transparent output, erases the captions under it instead of painting over them.
  • RenderJob.audio: 'discard' leaves the output without audio.
  • A render whose background is not the video no longer decodes it: each frame's time comes from the packets alone (PacketTimingVideoFrameDecoder). Measured on an 18 s 1080×1920 source in headless Chromium, a solid-colour render went from about 12 s to 9 s.
  • SegmentSubtreeDecomposer and SubtreeIsolationMarks are exported, so a live preview can split a segment into the same subtrees and copies the export paints, and stamp the same classes on them.

Changed

  • CaptionsOverlayFramePainterFactory takes a SubtitleLayerSourceFactory and a CaptionLayerDemand in place of one SubtitleLayerSource. It opens one source per layer the document paints in, each with only the styles that paint there, so a run with no stacking or blendMode opens the base layer alone, as before. BrowserSubtitleLayerSourceFactory builds the default source per layer, and BrowserSubtitleFrameRenderer.create takes the layer it paints through options.layer.
  • CaptionsOverlayFramePainterFactory.create takes the FrameBackdrop to paint on (VideoFrameBackdrop for the previous behaviour), and its top layer is no longer optional. MediaBunnyTranscodeRequest gains alpha and audio, VideoTrackEncoderFactoryRequest gains alpha, AudioTrackBridgeRequest gains wanted, and VideoFrameDecoderRequest gains pixels. MediaBunnyTranscodeRequest gains sourcePixels, FrameBackdrop has showsFramePixels, and VideoFrameDecoderKind gains 'packet-timing'. SubtitleLayerSourceFactory.create takes framesHaveAlpha, FrameBackdrop has sampledByCaptions, and FrameCompositionRequest has blendCaptions. On a background that is not the video, captions blend with nothing: blended layers are composited plainly, and a caption that samples the video gets an empty frame, as PNG with its alpha instead of JPEG, so it stays transparent rather than black or tinted by the background.
  • FrameCompositionRequest.captions is a list of CaptionLayerFrame in paint order instead of one SubtitleFrame.
  • ComposedSubtitleLayerSource takes a SubtitleStyleFrameDemand as its third argument, the one place that decides whether a style reads the video frame.
  • BaselineNeeds gains isolation, the rules a copy of a subtree needs, so composeOptional callers have to say whether they want them.

Fixed

  • A caption that reads the video frame no longer erases the overlay beneath it. It was handed the bare frame and painted it back over the overlay wherever it drew, so a watermark disappeared inside a frosted caption's box. Caption sources are now handed the frame with the overlay drawn on it; the top layer still gets the bare frame.

web-v0.5.0

Choose a tag to compare

@github-actions github-actions released this 01 Oct 18:07

Added

  • Edit the timeline by scenes. "Edit by" in the timeline's eye menu switches between words and scenes. At the scene level each scene is a block you can move or trim, and carry along with every scene before or after it from its menu or with Shift-drag. Double-click a scene to edit its words without leaving the scene level.
  • Start a video from your own subtitle file instead of transcribing it. "Use subtitle file instead" in the start dialog takes a SubRip (.srt) or WebVTT (.vtt) file. Each caption keeps the file's timing; when each word appears inside it is estimated, unless the file times every word.

Changed

  • The timeline's waveform shows up right away after a transcription, because it reuses the audio the transcription already read. While a waveform is still being read, its strip says so instead of staying empty.
  • Ezra's words scale in subtly instead of fading in, and its large word slides in for longer.

Fixed

  • In-browser transcription puts words on time. They appeared about a quarter of a second after being said, and the last word could run past the end of the video.
  • In-browser transcription no longer drops whole sentences next to a pause. In videos longer than about half a minute, the speech around a pause could vanish from the transcript without any notice.
  • A scene's start or end dragged by hand on the timeline stays where you put it. Changing a style or a sheet setting used to snap it back.
  • A scene moved to another sheet takes that sheet's timing right away, instead of keeping the old one until the next edit. Ezra's large word also accounts for a caption from another sheet that takes over sooner.

engine-v0.6.0

Choose a tag to compare

@github-actions github-actions released this 01 Oct 18:05

Changed

  • RenderPipeline splits lines after effects, so a line splitter reads each segment's time as it will be shown. The order is now segment splitting → semantic tagging → effects → line splitting → structural tagging → rendering. runSplittingStep is replaced by runSegmentSplittingStep and runLineSplittingStep, and the splitting progress stage by splitting-segments and splitting-lines.
  • IsolatedWordLineSplitter measures a word's time on screen up to its segment's time.end, so an effect that holds a segment, or a time set by hand, counts. IsolatedWordLineSplitterConfig.holdAfterLastWordSeconds is removed.
  • PauseTagger measures the silence between words, so a segment held on screen past its last word no longer hides the pause after it.

Fixed

  • WhisperTranscriber word times no longer run late. transformers.js times a token from the cross-attention row that reads it, which lands about a token late (0.2 to 0.3 s in English), and it aligns a short final window over its zero padding, so the last word could end after the audio. Each token now takes the midpoint of the row that reads it and the row that predicts it, and the frame count is halved to encoder frames. Measured against audio of known timing, starts land within ±50 ms on base and small.
  • WhisperTranscriber no longer drops speech where two windows meet next to a pause. transformers.js merges the two windows' takes by matching tokens, accepting a match however much later the second take places it, so two unrelated sentences could be spliced on a shared "the" and lose every word between them. A match must now start within 1 s on either side. Both fixes patch private transformers.js methods on the loaded model and tokenizer, so a transformers.js upgrade has to be checked against them.

web-v0.4.1

Choose a tag to compare

@github-actions github-actions released this 27 Sep 22:03

Fixed

  • Ezra's smaller lines wrap inside the frame instead of running off its edge, and more of its captions get a word on a line of their own.

web-v0.4.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 21:12

Added

  • Edit several scenes or words at once from the desktop transcript. Change their style, move scenes to another sheet, place them behind the person, edit word tags, or delete the selection.
  • Choose separate fonts for the alphabets used in your captions. The preview and export use the same choices, and old projects are upgraded when they open.
  • Five caption templates: Sol, with handwritten accents and a coloured glow; Nova, with bold two-line captions and six colour variants; Ada, a karaoke pill on a light plate; Rhea, two lines where the bold weight moves to the line being spoken; and Ezra, which lifts one word of each caption onto its own line as a large serif. A new Hook sheet now starts on Ezra.
  • The template gallery marks templates added in the last two weeks as New.

New templates

Ezra Rhea Sol
ezra.mp4
rhea.mp4
sol.mp4

Changed

  • A scene's timeline menu opens on the first press and now includes Redistribute. The timeline also works when the transcript is empty.
  • The word tag menu shows only tags the current template styles.
  • Exports with long cuts are much faster because removed sections of the video are skipped instead of decoded.

Removed

  • "Group scenes by rule" from the transcript menu.

Fixed

  • Exports no longer fail on iPhone or iPad when the browser stops reading the originally selected file. The app keeps another copy during the edit and asks for the file again only when no readable copy remains.
  • The start dialog no longer waits forever on a video the browser never finishes reading. After three seconds it shows the same notice as an unreadable file. The "Analyzing video" line is now a spinner on the Start button.
  • A playable video is no longer rejected just because the browser cannot create the optional fast preview. If the browser cannot export it either, the export dialog says so before rendering begins.
  • Videos are no longer rejected as damaged just because the player could not read their duration. Files the browser cannot access now get a separate message.
  • Opening a saved project with missing video data now asks you to select the file again. A project that is genuinely absent says so instead of suggesting a reload.
  • A failed export shows its error on the first attempt instead of requiring a second press of Export.
  • Picking a template no longer leaves a caption on one line past its maximum width until the next edit. The lines were measured before the template's font had loaded.
  • Adding or joining scenes around cuts no longer places them inside removed video, deletes scenes hidden by a cut, or fails when every existing scene is cut.
  • Undo brings back the line breaks of the scene you are editing in the transcript.
  • First-run transcription no longer depends on an external host for its runtime files. Model downloads can also use a configured VITE_MODEL_RELAY when their normal host is blocked.
  • A caption written entirely in English inside an Arabic or Persian project no longer starts with its closing full stop. One reading This is a test. used to paint as [.] [This] [is] [a] [test].
  • A word-by-word template no longer paints a second highlight over a stray full stop. When a caption ends on an English phrase inside a right-to-left project, the full stop paints at the opposite end of the line from the word it follows, so two highlights appeared at once while a single word was spoken.
  • Firefox no longer offers Theo because its exports render with artifacts there.

engine-v0.5.1

Choose a tag to compare

@github-actions github-actions released this 27 Sep 22:01

Added

  • IsolatedWordLineSplitterConfig.fallbackMinSecondsOnScreen, a looser time on screen tried only when no word reaches minSecondsOnScreen. Absent, the splitter behaves as before.
  • LINE_NO_WRAP_CSS, the rule that keeps a line on one row, exported so a consumer can emit it in its own framework layer and let a stylesheet override white-space on .line.

engine-v0.5.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 21:10

Added

  • Model downloads are configurable as one ModelAssetSources value: the persistent file cache, the request strategy, and the URLs of the ONNX WebAssembly runtime. DirectModelFileFetcher names transport failures, while RelayFallbackModelFileFetcher retries an unreachable host through a relay without changing the cache key and remembers that host for the rest of the run. WhisperTranscriber now takes this value as its third constructor argument instead of a bare ModelFileCache.
  • CSS resource embedding has an explicit outbound-request policy. BrowserCssResourceEmbedder now requires a CssResourceUrlPolicy; AnyOriginCssResourceUrlPolicy preserves the browser-rendering behaviour, and SameMachineCssResourceUrlPolicy limits a hosted renderer to blob: and same-origin URLs. The default RenderPipelineBuilder remains permissive.
  • IsolatedWordLineSplitter puts one word of each segment alone on the second line, so a stylesheet can size it as that line. The word is the latest one with at least minLetters letters that stays on screen for minSecondsOnScreen, counted from when it is said until the next segment starts or holdAfterLastWordSeconds after the last word, whichever comes first. Words after it take a third line; when no word qualifies, the segment stays on one line.
  • --previous-line-width-em on every line after the first: the measured width of the line above, in the same unit as --line-width-em, so a stylesheet can align two lines against each other.
  • FixedTailLineSplitterConfig.minWordsToSplit, the fewest words a segment needs before it is split. Defaults to 3, the value the splitter always used.
  • VideoEncoderSelectionFailedError identifies a render that cannot start because the browser supports none of the codecs accepted by the output format. Its stable name and ERROR_NAME survive worker and browser-automation boundaries.

Changed

  • An export decodes only the video it keeps. A cut of three seconds or more is seeked past rather than decoded and thrown away frame by frame, so decode cost follows the output's length instead of the source's. On a 60 s source with 54 s cut, 1439 decoded frames become 145: 3387 ms to 636 ms at 4K, and 1565 ms to 219 ms at 1080p. Shorter cuts are still read through, because a seek resumes at the key frame before it and measured h264 sources key every 1.0 s to 3.5 s. VideoFrameDecoder gains canSeek() and an optional span on samples(); a decoder answering false still receives the whole track.
  • SubtitleStyle.wordOverrides is now subtreeOverrides. The broader name reflects that overrides can target lines and decorations as well as words. This is a breaking rename for consumers that construct subtitle styles.

Fixed

  • A line written entirely against the declared direction is laid out on its own base. RenderingConfig.textDirection states the language of the captions, so a caption holding one whole English sentence inside a right-to-left document painted as [.] [This] [is] [a] [test]: the full stop is neutral, its right-hand neighbour is the paragraph boundary, and UAX #9 resolves that tie toward the paragraph direction. LineBaseDirectionResolver overrides the base for a line carrying strong characters of one direction only, where the base moves the neutrals at the edges and can reorder nothing. A line mixing both keeps the declared base. WordFragmenter takes the resolver as its third constructor argument.
  • A fragment of a word that paints away from the rest of it no longer carries the word's decorations. A word split across embedding levels is emitted as one element per piece, and a stylesheet drawing a background on .word-being-narrated painted it twice, at opposite ends of a mixed line: a right-to-left line closing on this is a test. paints as [.] [this] [is] [a] [test], with the right-to-left words further right, so the pill landed on test and again on the full stop with three words between them. WordFragment.carriesWordBody is false on such a piece and the renderers add the detached-word-fragment class, which a rule that points at a word excludes. Pieces that paint flush against each other are all body, so a word whose punctuation lands beside it still highlights as one box. The state class and the timing variables stay on every piece: a stray is read at the same moment as the rest of its word. WordFragmenter takes a BidiCharacterClassifier as its fourth constructor argument.
  • Segment and line inline styles land on the element they describe instead of being merged into an outer wrapper. Template rules can no longer override a chosen element font merely because the font was inherited, and line-level overrides now reach exported frames.

web-v0.3.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 15:44

Added

  • Five more templates: Enzo, Hugo, Sara, Lewis and Pastor. Thirty-six in the gallery now. Pastor is the first template that asks for its captions to sit behind the speaker.
  • The template gallery is grouped into families: Modern, Key moments, Viral, Classic and Lab. Two of them show a short clip playing over real footage instead of a still swatch, because their look either composes against the video or lives in what it does with the words over time.
  • Colour presets on most templates. The Preset field in the Style tab used to appear on four templates and now appears on twenty-four. The first preset is the look the template already ships with, so nothing you picked before looks different until you choose another.
  • Add a Hook or a Peak sheet from the "+" beside the sheet chips. Each one is offered only when your transcript holds words that belong on it, so the button never creates an empty sheet.
  • Reset a whole sheet to its template default, from the sheet chip's menu. One step reseeds every tab at once, typography, style, layout, effects, motion and your own CSS, and clears the edits you made on individual words and scenes under that sheet.
  • Captions that sit behind the speaker are measured while you work, with no dialog to wait behind. The measurement covers only the stretches captions occupy, runs a piece at a time starting from where you are, and fills in a stretch it had never looked at when you move a caption there. Playback pauses only when it reaches one of those, and a finished pass reports how many scenes it will lift.
  • The language list in the start dialog is ordered by what you are likely to want: the ones you pick most, the one you picked last, the ones your browser and your timezone suggest, then a popular set, then the rest alphabetically.
  • A warning before a long video is transcribed in the browser, past ten minutes on a computer and past two on a phone. Speed and accuracy there depend on the device, and a long video on a weak one is a long wait.
  • The projects list takes a video dropped anywhere on it, and each card carries the video's length and a menu for renaming and deleting.
  • A Support panel behind the heart button in the toolbar, listing the ways to help the project: share it, star the repository, leave a review, send feedback, or donate.

Changed

  • Captions break at the width they really render at. The arithmetic behind the line breaks counted a space between every pair of words that captions never have, spent letter spacing everywhere except after the last letter, and read a single typeface for every word whatever size the template gave it. Measured against the same words laid out by a browser, it was running eight to eleven percent over. A line now holds about a tenth more, and every template's width was retuned against the corrected number. The default width is narrower as well, so a full line stays clear of the action rail that the short-form players draw down the right edge.
  • Changing a template clears every hand edit under that sheet. Edits on words and decorations used to survive, keyed to values the new template knows nothing about, so they rendered as nothing at all or as noise you could not find and undo. Returning one scene to the automatic decision about sitting behind the speaker is part of that same step now, and the scene menu keeps one switch rather than two that read as two ways to do the same thing.
  • Preparing the preview stops once it is no longer worth the wait. How long a video takes to prepare depends on the machine and on how the video was recorded, and the old limit tried to predict that from the file alone. It turned away long videos that would have been quick, and it kept you waiting on short ones that were not. The preparation is now timed while it runs and stopped when it passes what the video earns: around twenty seconds for a one minute video, rising to ninety for ten minutes or longer. One that is clearly going to run over is dropped in the first few seconds instead of at the end. When it stops, your video plays right away, and you can ask for the precise preview whenever you want from the timeline.
  • Opening a saved project no longer prepares its preview again. A project you had opened without one used to repeat the whole preparation every single time.
  • Exports redraw the captions only when they change. Three things stood in the way. Any style that draws its outline with an SVG filter was redrawn from scratch on every frame, including the long stretches where nothing on screen moves. The drawing was done in fixed chunks of frames, so the same caption came back around to be drawn again a few frames later even when nothing about it had changed. And the styles that read the video's own pixels, the frosted ones and the ones that blend, were redrawn once per frame no matter what. A caption that holds still is now drawn once and reused for as long as it holds still, which is where the saving is largest: exports at high resolution, where each drawing is the expensive part, and phones, where one of those pixel-reading styles took 160 seconds and now takes 59. Styles whose effect genuinely changes every frame, like the one that flickers, redraw exactly as before.
  • Exporting no longer waits for the project to be saved. The save ran before anything appeared on screen, so over a slow connection that was twenty seconds of nothing at all. It runs alongside the render now, and it is skipped when nothing has changed since the last one.
  • The Highlight tag is now called Peak. Fourteen templates already use "highlight" for the word being spoken right now, and the tag means something else: the closed phrases a video is built around. A template styling .highlight for the tag needs .peak, and kel's tag-bg-color control is now peak-bg-color. Projects saved before this are converted when you open them.

Fixed

  • Safari opens videos that have sound. Safari before version 26 ships only the video half of the toolkit the app checks for, so every video with an audio track was refused a moment after you dropped it. The check now asks for the audio half only on the paths that actually decode audio, and transcription falls back to the browser's older audio decoder where the newer one is missing. That fallback also handles long videos: it used to read the whole soundtrack at once, which ran a phone out of memory on anything past a few minutes, and now works through it a minute at a time.
  • A video the browser genuinely cannot open no longer suggests converting a file that is already MP4. When both attempts at reading the audio failed, only the first was reported, so the advice named the wrong problem.
  • A caption whose movement lives in a decoration is no longer exported as a still image. The export decided whether a caption was moving by reading the caption itself, which cannot see an animation attached to a shape drawn beside it. Pepper's growing pill is the one that showed it: it played in the preview and was a still image in the exported file.
  • Typing your first caption into an empty transcript paints it on the video. A video with no speech opens with nothing in the transcript, and the first scene added to it landed under no style sheet at all, so the words read back correctly in the transcript panel while the video stayed bare.
  • A full disk no longer costs you the project. Reading a project was also writing a timestamp, and a browser with no room left refuses every write, so opening any project failed with no message and sent you back to the list. Alongside that: a video you re-select is attached even when it cannot be kept, and a full disk now reports once instead of twice with two notices that said nearly the same thing.
  • A preset survives a template change. Twenty-four of the thirty-six templates ship presets and the rest ship none, so passing through one of those collapsed your choice permanently, with no error and no way back.
  • A control a template gained after you saved a project reaches that project. The value was missing rather than set to its default, so the control did nothing at all. Fonts are where it showed: the exported video fell back to a system typeface while the preview looked right.
  • Fixing a typo no longer removes the gap-free padding from the scene.
  • Sentences and clauses break correctly in Arabic, Persian, Urdu, Chinese, Japanese and Korean. The splitter looked only for the punctuation Latin scripts write.
  • A Hook or Peak sheet reads in the project's own direction. Both arrived left to right inside a right-to-left project, which puts the closing full stop at the wrong end of the line.
  • Cancelling the prepare-video dialog takes the template back. It used to leave the sheet on a template that needs captions behind the speaker with no measurement to support it, so the effect you picked never appeared. And hiding one scene behind the speaker by hand no longer removes the offer to measure the whole video, which it did permanently, reloads included.
  • Captions in Russian, Ukrainian, Serbian, Greek or Hindi land on a typeface the style chose. Most of the heavy display faces caption work starts from carry no letters for those alphabets, so those captions were drawn in whatever your device happened to supply, which differs from one machine to the next and matches nothing the style intended. Each style now names a stand in for them that carries its own voice, the way the Arabic and Hebrew ones already did.
  • The fast preview notice claimed everything worked without a prepared preview. Styles that place captions behind the speaker cannot play until it is ready, and the notice says so now.
  • Transcription in the Docker image runs on every core instead of one. The image was serving the app without the two headers a browser wants before it will hand a page shared memory, and the speech model falls back to a single thread without them. On one machine that was 20 seconds of inference on a video that takes 13. Chrome and Firefox pic...
Read more

engine-v0.4.0

Choose a tag to compare

@github-actions github-actions released this 30 Aug 15:43

Added

  • Stylesheets can read how wide an element renders: --segment-width-em, --line-width-em, --word-width-em. The engine mounts the subtree under the consumer's own stylesheet and reads the boxes back, so the widths account for the classes the element actually carries. They are multiples of the font size, which is what lets a rule derive the size from the width. Measured once per caption per style, and skipped for a stylesheet that reads none of them.
  • --segment-anchor-y and --segment-anchor-origin-y report where placement put the caption and which part of the box landed there. A rule can position a caption absolutely instead of assuming an anchor the consumer may have moved.
  • --line-char-count, beside the segment and word counts already published.
  • Audio decoding without WebCodecs. WebAudioAudioDecoder decodes through AudioContext.decodeAudioData, FallbackAudioDecoder retries a failed primary decoder through a second one, and MediaBunnyAudioOnlyRemuxer copies a container's audio packets out so neither has to load the video bytes. Safari before 26 ships WebCodecs with no AudioDecoder.
  • WebAudioAudioDecoder decodes in bounded segments rather than in one call, so peak memory is one segment plus the output whatever the source's length. decodeAudioData holds its input and its output at once, and a 38-minute recording died 5.3 s in on iOS 18.7. Measured on a 2290 s source in Chromium: 760 MB peak against 2884 MB, at the same wall clock. AudioSegmentLayout places the segments and gives each a margin of context at both ends, which the codec needs to reconstruct the frames at a seam.
  • Document.language carries the ISO 639-1 two-letter code the document was transcribed in, null when unknown.
  • WebCodecsOnlyVideoFrameDecoderFactory returns the WebCodecs decoder or throws VideoFrameDecoderSelectionFailedError. The default factory falls back to a hidden <video> element, which runs at playback speed. A host that would rather rewrite the source needs to see that refusal instead of a slow path taken in silence.
  • RenderJob reports the decoder the run picked, through VideoFrameDecoderKind and VideoFrameDecoderSelection. Both are null when the run ended before a decoder was chosen.
  • MediaBunnyTranscodeRequest.signal aborts a run. Checked between frames, so it stops cleanly, and rejects with an AbortError.
  • SpriteSheetProbeObserver reports a host that can hold only one tile per sprite sheet. One tile per sheet means one rasterization per frame, and the capacity alone never said whether that was the host refusing or a legitimate answer. SpriteSheetRasterProbe backs the size walk and returns why a raster was refused rather than a boolean.
  • DocumentEditor.insertFirstSegment(doc, time, sectionKind), for a Document with no segments. kind is opaque to the engine, so the consumer names the Section.
  • Word.getTagCssClasses() returns the classes a word's tags contribute, without asking for a timestamp.
  • BalancedLineSplitter takes a minCharsPerLine floor. It uses one line fewer instead of leaving a short one.

Changed

  • A batch covers as much video as the captions hold still for, not a fixed number of frames. getMaxBatchSize is now getMaxTilesPerBatch: how many distinct pictures one sprite sheet can hold. getFrames returns a prefix of the timestamps it was given, ending where those pictures run out, so a caller offers a whole look-ahead and advances by the length of the answer. Passing at most getMaxTilesPerBatch() timestamps still comes back whole, so a caller on the old contract sees only the rename. The old shape assumed every timestamp was a distinct picture, so the sheet count was frames / capacity however well the frames deduplicated, and every sheet costs the same whatever it holds. Simulated over ten seconds of captions at 30 fps: 15 sheets become 1 at 21 tiles per sheet, and 300 become 20 at one.
  • A caption bound to the video's own pixels batches too. Its picture only exists once the frame beneath it does, so it used to rasterize one sheet per output frame. FramePainter.lookAhead() tells the coordinator how many frames to gather, and FramePainter and SubtitleLayerSource now take runs of frames rather than single ones. At lookAhead() === 1 the loop is the old one, with no copy and no retention. On a phone, a style doing this took 160 s and now takes 58.6 s.
  • A style with SVG filters no longer rasterizes a tile per frame just for having them. Only a filter reading a value from the render context varies with time, typically a feTurbulence seed driven by a tick, and the cache treated every filter set that way. The planner now compares the filter markup each timestamp would paint, rendered through the same SvgFilterDefsRenderer the tile uses. A var() the scope leaves unresolved still forces separate frames: the document's CSS decides what it becomes, and the planner cannot see that. Measured on an export of 1702 frames at 720×1280 with an outline filter: 1636 tiles became 471.
  • SvgFilter reports the custom property names its body reads (variableNames), and SvgFilterDefinitions the union across its filters. A caller can describe the scope a filter set is sensitive to without materializing it.
  • A SubtitleFrame's raster belongs to the batch that produced it, not to the renderer, and stays valid until close. Frames from different batches can be held and painted together, which a caller gathering a run across a batch boundary needs.
  • TextMeasurer.measure takes the CSS classes the run will carry. It used to read one typography from a probe wearing no tags, so a rule sizing .emphasis differently was invisible and every word was measured as a plain one.
  • The default maxWidthRatio is 0.72 rather than 0.8. The short-form players draw an action rail down the right edge, and the widest of them takes about a ninth of the width. 0.72 is also what 0.8 produced before the measurement fix below, so a template on the default looks as it did.
  • insertSegmentAt handles only the anchored case and refuses a Document with no segments. Use insertFirstSegment for that one.
  • SpriteSheetSizeProber is now SpriteSheetSizeProbe, and BrowserSubtitleFrameRenderer.create accepts one through options.sizeProbe. The size walk decodes rasters up to the whole pixel budget for an answer that depends only on the output size and the host, so renderers working at the same size should share it. Both were paying for it separately.

Removed

  • SubtitleFrameRenderer.getFrame. getFrames with one timestamp is the same call, and keeping both meant two raster lifetimes to document.
  • TextMeasurer.spaceWidth. The renderer emits words as adjacent elements with nothing between them, so the gap is each word's margin and a space character was never part of a line.

Fixed

  • An animation on a pseudo-element no longer exports as a still image. The planner decided whether a caption was moving by mounting a chain of divs and reading getComputedStyle off the leaf, which never asks about pseudo-elements, so the tile was shared across the run and burned in while the preview played it. The question now goes through AnimationStateFingerprint, picked through BrowserSubtitleFrameRenderer.create's animationStateFingerprint option. keyframe-scan, the default, reads the style's CSS once, including the inline styles per-element overrides set. subtree-mount mounts the subtree and asks getAnimations({ subtree: true }), which reaches pseudo-elements at any depth and resolves calc() delays.
  • A line is measured at the width it renders at. The arithmetic was wrong three ways and came out 8% to 11.5% over the same words laid out by a browser: it counted a space between every pair of words, it spent letter spacing between a word's letters but not after the last, and it read one typography for every word. The corrections pull in opposite directions and the result now agrees with a real layout to within half a percent. Lines hold about a tenth more, so a template's width wants revisiting against what it was tuned to.
  • Document.withSegments and withMetadata no longer drop narrationPace. Neither has a caller in this repo, so the loss was silent for anyone using the published package. Both clone paths now carry every field.
  • FallbackAudioDecoder reports both failures rather than the primary's alone. Blaming the WebCodecs path on a runtime where it can never work is what told a reader to convert a video that was already MP4/H.264/AAC. The two travel as the errors of an AggregateError, since neither caused the other.
  • RenderProgress.totalFrames carries a number. It was hard-coded to 0 at the only place the progress event is built, so a consumer showing "frame N of M" had nothing to put in M. It now comes from the output duration and the source's frame rate, both of which the coordinator already resolves, and the field documents that it is an estimate: a variable-frame-rate source has no single rate to read.
  • A tagger keeps what the line it touches was already carrying. All five rebuilt their lines field by field, dropping anything tagging has no opinion about, and WordlistTagger and RegexTagger also handed back a fresh id, which is what per-line state is keyed by.