Skip to content

Add FixType property to IFixCommonError - #23

Open
ivandrofly wants to merge 700 commits into
mainfrom
feature/fix-common-errors-fixtype
Open

Add FixType property to IFixCommonError#23
ivandrofly wants to merge 700 commits into
mainfrom
feature/fix-common-errors-fixtype

Conversation

@ivandrofly

Copy link
Copy Markdown
Owner

Every fix rule now declares the kind of fix it performs via a new
FixType enum (Time, Formatting, Dialog, Punctuation, Casing, Spacing,
Characters, Ocr), so the UI/CLI can group, filter or sort rules by
category later. The member is abstract on the interface so the
compiler enforces it on all 40 implementations.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01TTVPifejhQqpeh1mkQHeRg


Stack created with GitHub Stacks CLIGive Feedback 💬

niksedk and others added 30 commits August 29, 2026 21:04
…se-language-file-update-6ef75f

Update Japanese.json for v5.2.0-beta28
…rewind

Two things left over from the beta 28 fix, both reported on the issue.

The waveform drew no paragraphs at all after Options/OK - no boxes, no text,
no number/duration/CPS, just the time ruler and the cursor - until playback was
resumed, which brought everything back at once.

The paragraph sets the waveform draws are rebuilt on the 50 ms position timer
into plain fields, so rebuilding them asks for no repaint (AllSelectedParagraphs
is an AffectsRender property, but only its identity is, and the reload mutates
the list in place). While the video is paused nothing else moves an AffectsRender
property either, so the last painted frame simply stays on screen. Options/OK
produced exactly that: the rebuilt video player reports 0 for a moment, "center
video position" scrolls the waveform to the start on the 16 ms cursor timer, the
paragraph reload - on the lower-priority 50 ms timer, competing with the rebuild -
empties the set, and the frame painted when the player lands back on the real
position shows the right time range with nothing in it. The reload a tick later
fixed the set but not the picture. LoadParagraphs now repaints when the set it
draws actually changed (reference comparison, so an unchanged tick still costs
nothing).

The occasional rewind survived in two holes in the PositionForRestore guard:

- The undocked window announced nothing before its open. It publishes the new
  control synchronously and opens it from a posted continuation 100 ms later, so
  a rebuild landing in between (Settings -> Apply followed by OK) read the live 0
  of a player that had not been given a file yet. It announces the target up
  front now, like the docked rebuild does.
- EndPositionRestore ran unconditionally at the end of every restore sequence.
  Those sequences are bounded (a ready wait plus a fixed number of seeks), so one
  that runs out while mpv is still loading handed PositionForRestore back to a
  player still reporting 0 - the very rewind the guard exists to prevent. The
  three restore paths use EndPositionRestoreIfArrived instead, which keeps the
  target until the player really is there; the arrival check in the position tick
  drops it as soon as it lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-preamble-cap

Read the whole Manzanita preamble, not the first 200 KB
…Edit#14245)

mpv's `frame-step` is a real un-pause: it sets pause=no, plays until the
next video frame, then sets pause=yes. The observed pause flag therefore
flickers, and the playhead estimate read that as a play->pause cycle -
the edge cleared _playheadPausedSettled on the very tick mpv reported
the stepped-to frame, so the follow-a-paused-seek branch skipped it, and
settling deliberately never snaps (SubtitleEdit#12740). The frame was dropped from
the cursor for good and stepping forward drifted a frame per press until
the 0.5 s discontinuity snap caught up.

Whether it bit on a given press depended on how long mpv's un-pause
window lasted - i.e. on the audio period, which any other application
starting or stopping audio can change (the reporter tracked it to the
PipeWire quantum): a long window is seen by the cursor timer, a short
one is coalesced by mpv into a single playloop iteration and never
emits a pause change at all. Slow motion always lost, since one frame
then takes several times longer to play out. Stepping backwards was
never affected - `frame-back-step` seeks and stays paused - which is
why it snapped the cursor back onto the right spot.

Open a short window around a native frame step in which the estimate
treats the core as paused and keeps the cursor authoritative, so the
stepped-to frame is followed exactly like a paused seek. The window
closes as soon as the step lands, so a real Play() right after a step
is not swallowed - and a frame-step that mpv ignores (no video track)
closes it on the first tick, since a moving clock means no step is in
flight.
SE5's Compare built two independent TableViews, so the wheel and the
scroll bars moved one side only. With larger files the two views ended up
showing completely different line ranges - even comparing a file with
itself - which is what the report shows. SE4 kept them together by
copying the active list view's TopItem index to the other side
(Compare.SelectLinesInBothListViews).

Selection was already mirrored, but it scrolled the other side with
ScrollIntoView, which only promises the row is somewhere in view: with
rows of different heights the two sides could keep the same row selected
and still show different ranges, so clicking did not recover the
alignment either.

TableViewScrollSync links the two grids' ScrollViewers and, on every
scroll, puts the source's top row at the other side's viewport top. It
works in row indices rather than pixels on purpose: the two grids hold
different text, a row wraps to one or two lines depending on content, and
TableView's VirtualizingStackPanel estimates its extent from the average
realized row height - so equal offsets do not mean equal rows, and the
mapping drifts as rows realize. Placing the row uses the same
PrePositionScroll + settle loop as TableViewScrollAnchor and
TableViewIndexScrollBar, suspends any anchor on the target while it
moves, and guards against the mirrored scroll bouncing back.

Rows below the top can still diverge where one side wraps to two lines
and its twin does not; aligning those too means giving matched rows a
shared height, which is a change to the compare rows rather than to
scrolling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…leedit-issue-14218-followup

Repaint the waveform when its paragraphs change, and close two rewind holes (SubtitleEdit#14218)
…leedit-issue-14245-d925ec

Keep the waveform cursor on the frame when stepping forward (SubtitleEdit#14245)
…leedit-issue-13504-43a6cd

Compare: restore synchronized vertical scrolling (SubtitleEdit#13504)
…4231)

Follow-up to SubtitleEdit#14236, which taught the six go-to-previous/next-line commands
and the grid's arrow/page keys to re-center on every step with "subtitle grid,
center when selecting prev/next row" on. Three more ways of stepping a line at
a time were left on the old behaviour - they only scrolled once the target row
had walked off the bottom of the view, and then jumped half a screen:

- "Play next/previous line (and stop/loop)". These move the selection
  synchronously (a posted selection would be nulled by the SelectionChanged
  handler's ResetPlaySelection and break stop/loop), so they set the grid's
  SelectedItem themselves and never reached the centering in
  SelectAndScrollToRow. This is also what SE 4's Alt+Down default runs with a
  video open - "go to next subtitle (play translate)" (SubtitleEdit#14167) - i.e. the
  line-by-line translate/sync loop, where the setting matters most.
- "Go to next/previous time code", which walks the cue boundaries.
- "Set end time and go to next", from the position timer's key-up handling.

The play commands get the same posted follow-up SelectAndScrollToRow does -
center when the setting is on, otherwise just nudge the row fully into view;
the other two ask for the centering variant.

Regression net: three cases added to SubtitleGridCenterSelectedRowTests, each
measuring the selected row's middle against the viewport's middle over 8-16
steps. All three drift by a row height per step without the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three new cases assigned a second VideoPlayerControl to the view model,
but MainView's layout already builds one - so each test left an extra
unparented control (with its own content presenter and brushes) behind on a
headless session that keeps the application alive across the whole assembly.
CI then failed with the known contamination cascade ("the calling thread
cannot access this object" out of the compositor) in unrelated windows, a
different victim per attempt, while main stayed green.

The commands only need GetVideoPlayerControl() to return something, which the
layout's control already does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-grid-remaining-paths

Center the grid for the last prev/next-line paths too (SubtitleEdit#14231)
LibMpvEventLoopTests.Pause_RightAfterSeek_KeepsReportingTheSeekTarget
failed under full-suite load on any machine with a loadable libmpv, and
the diagnosis was not what it looked like: Pause() was keeping
_pausedValue exactly as intended (traced: pausedValue=1.5,
seekInFlight=true).

The Position getter serves that cached seek target only while IsPaused,
and IsPaused answers from _observedPause - which only the mpv event
thread writes, when the pause property-change event is dequeued. The
pause commands are synchronous (mpv_command returns after the core has
applied them), so for the moment until that event is processed the cache
says the opposite of the truth, the getter skips the cached target and
serves mpv's pre-seek time-pos instead. That is the SubtitleEdit#14187 waveform-click
window: seek, Pause(), read Position. The test only passed in isolation
because mpv usually finished the seek fast enough that the observed
time-pos already was the target; under load it was still the old one.

Write the commanded pause state through into the cache after a
successful synchronous pause command (Pause/Play/Stop/LoadFile);
PlayOrPause's "cycle pause" re-reads the property instead of predicting
the core's decision.

Also harden the restart bookkeeping the SubtitleEdit#14187 fix leans on. A restart
was stamped when the event thread processed it, not when mpv emitted it,
so a restart queued earlier (opening a file fires one) could be stamped
after a seek issued in the meantime and pass for that seek's own restart
- which would clear a live seek target. Each async seek now carries a
generation id in its mpv_command_async reply_userdata; the event loop
records which generation every restart followed, and
HasPlaybackRestartedSince answers for the outstanding seek rather than
for the clock alone. mpv's event queue is FIFO, so this is an ordering
guarantee rather than a timing assumption.

Both invariants from SubtitleEdit#14187 are unchanged: an in-flight seek target
survives Pause(), a long-settled one does not get resurrected.

Regression tests, both of which fail without their fix even in isolation
(the click test only failed by luck):
- PlayAndPause_UpdateTheObservedPauseCacheImmediately
- HasPlaybackRestartedSince_AnswersForTheOutstandingSeekNotAnOlderRestart

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…use-cache-and-seek-generation

Fix mpv observed-pause staleness that dropped the clicked seek target
The blip was faked with a play-selection window, which the 50 ms position
timer stops by comparing the *cursor estimate* against the window end. A
one-frame window is 33-40 ms, so it always overshot: mpv played 50-150 ms
past the frame with the cursor gliding along, and the stop then parked back
one frame - a visible forward run and snap back on every single press. How
far it ran depended on where the timer tick happened to fall, so it looked
random.

The next press then computed its target from vp.Position, a styled property
refreshed from mpv by another 50 ms timer, which right after a blip holds
mpv's overshoot rather than the frame the cursor is parked on. A held-down
shortcut advanced a random one to four frames per press.

Give the blip its own small state machine instead:

- It ends from the 16 ms cursor timer, measured against mpv's own clock, and
  waits for MPV_EVENT_PLAYBACK_RESTART first so the async seek onto the frame
  has landed (a backward step's pre-seek clock is already past the stop
  point). 700 ms safety cap.
- UpdatePlayheadEstimate holds the cursor on the stepped-to frame for the
  duration, so it no longer glides forward and snaps back. Every other
  play/pause/seek path cancels the blip, so it can never hold the cursor
  against playback the user started, nor pause it.
- Each press chains off the frame the previous press stepped to, so the step
  is exactly one frame every time.

Also adds VideoPlayerControl.SeekTo: assigning Position silently no-ops when
it equals the value the property already holds, which the park-back-onto-the-
frame seek can hit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new frame-step-with-play tests assign their test-built VideoPlayerControl
to the view model so the command under test can find it - but an unparented
control left reachable on the per-assembly headless application is the
documented contamination-cascade trigger (PR SubtitleEdit#14258): unrelated windows fail
out of the compositor on CI, a different victim per attempt, while the suite
passes locally. Detach it (and restore the global frame rate) after each test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hortcut-jerkiness-6d0c0f

Make "one frame forward/back (with play)" step smoothly
Findings from a responsiveness/accuracy review of the waveform seek paths:

1. SMPTE drop-frame seeks landed 0.1% early. While SMPTE timing is enabled
   every position READ from the player is compressed by 1000/1001 (the
   control's position timer, the playhead estimator, the waveform time axis
   via UseSmpteDropFrameTime), so all UI position values live on the
   drop-frame clock - but seeks passed those values to the player as real
   seconds. Every seek therefore landed early in proportion to the absolute
   position (~1 s per 17 minutes), and past ~2.5 minutes the error exceeds
   the playhead pin's 0.15 s arrive tolerance: a waveform click pinned the
   cursor to the click, timed out after 600 ms, and snapped back seconds.
   Seeks (NotifyPositionChanged and SeekTo) now expand UI values by
   1001/1000 back to the player's real clock.

2. Keyboard nudges never pinned the cursor. SetVideoPositionSeconds (the
   +/-100 ms and custom-ms shortcuts, and snap-to-frames frame stepping)
   scrolled the view to the target immediately but left the cursor waiting
   the 100-200 ms the seek takes, so nudges read as laggy next to a waveform
   click or wheel scrub, which always pinned. It now seeks via SeekTo (the
   Position-property assignment silently no-ops when equal to the displayed
   value) and pins like every other seek path.

3. The frame-with-play blip compared clocks across spaces in SMPTE mode:
   its stop point is UI (drop-frame) time but it read the player's real
   clock, so far into a file the check tripped immediately and the blip
   degenerated to a silent step. The read is now compressed like the
   estimator's.

Tests: VideoPlayerControlSmpteSeekTests (expansion on/off), and a
PlayheadFrameStepTests case asserting the nudge pin. Full UITests suite
clean (the one failure is the known flaky LibMpv live-core test, fixed
separately on main).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same-time-code merge from SubtitleEdit#10447 guards on `region:` only. Files that
position cues with inline `line:` settings instead - Netflix WebVTT among
them - leave `Region` null on both cues, so a caption pinned to the top of
the screen and the dialogue underneath it are merged into one paragraph.
The two placements then collapse into one: whichever alignment survives is
applied to the whole event, leaving one of the two lines in the wrong half
of the screen.

Compare the vertical band each cue sits in as well. Cues split horizontally
across one line still share a band and still merge, so SubtitleEdit#10444 - the case the
merge was added for - keeps working; its two halves differ in `position:`,
not in `line:`. The explicit --merge-same-time-codes operation is untouched
and still merges across bands when asked for.

The band is read by a new GetVerticalAlignment, extracted from the `line:`
half of GetPositionInfo so both callers share one definition.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rm-seek-accuracy

Waveform seek review: SMPTE seek drift, unpinned nudges, blip clock mix
The vertical bands the check borrowed from GetPositionInfo exist to pick an
{\an} tag, and the 25%/75% boundaries they draw fall in the middle of ordinary
caption blocks. On the very grid the reported file uses - rows 5.33% apart -
a three-row bottom caption sits at 74.00%/79.33%/84.67%, so the top row landed
in the "middle" band and split off into its own paragraph with a stray {\an5}
on it. Tall captions near the top broke the same way against the 25% boundary.

What separates two rows of one caption from a caption over unrelated dialogue
is how far apart they sit, not which band they fall in: consecutive rows are a
few percent apart, the reported top caption and its dialogue 69%. So compare
the distance instead, and read "line:" as a percentage down the video for it.

Negative line numbers count rows from the bottom, and NumberStyles.
AllowDecimalPoint alone never parsed them, so a top caption written "line:-16"
fell through to "bottom" and merged anyway. Allow the leading sign and count
from the bottom for it.

Only a cue that says where it sits can now rule a merge out, so a file whose
cues carry no usable "line:" merges exactly as it did before the check existed.

That leaves GetPositionInfo with no second caller, so it goes back untouched
and the change is confined to the merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SE 4 had an Edit-menu action (MainEditToggleTranslationOriginalInPreviews)
that swapped the video preview and the waveform over to the original text
while a translation was being edited. SE 5 had no equivalent (SubtitleEdit#14252).

Adds the command, its shortcut registration (General / Translate group) and
the SE 4 importer mapping so an imported SE 4 key binding keeps working.

The video preview builds a throw-away subtitle from the rows' original text
rather than going through GetUpdateSubtitleOriginal - that one owns the
instance that gets saved and re-stamps every row's reference id, far too much
for something the preview timer calls. Rows the original has no line for are
left out. The waveform draws from the rows, so it gets a plain flag instead.

The state is session-only, not a saved setting: it only means anything while
an original is loaded, and a remembered "on" would start the next session with
a blank preview and no visible way back. Hiding the original column clears it
for the same reason. The paragraph footer keeps showing the row's own CPS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "Update video offset" window had to be reopened for every value, and every
value had to be typed in full - so switching between the two or three offsets a
job actually uses (09:59:40.000 and back, say) was retyping a twelve-character
time code each way.

The offsets that have been applied are now kept, most recent first, in a
drop-down next to the time code box: picking one fills the box. The list holds
ten and is stored in settings, so it survives a restart (SE 4 parity, which had
the same list behind a pick button); an empty list starts out offering 1 hour and
10 hours. The drop-down also follows the box, so an entry typed over can be
picked again.

The window no longer closes on every apply. Apply updates the offset and stays
open, OK updates and closes, Cancel closes and keeps what was applied - so
several offsets can be tried against the video without reopening the dialog.
Reset stays open too, since resetting is now just another value to try.

Applying hands the offset to the main window through a callback instead of the
caller reading it off a closed dialog. The time codes are still shifted against
the offset in force at that moment, so applying twice lands where applying the
second value straight away would.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Waveform paragraph text was always shaped with a left to right paragraph
direction, so in an Arabic or Hebrew line the neutral characters - the
dialogue dash, a trailing ellipsis, brackets - took the paragraph
direction instead of the direction of the letters around them and landed
on the wrong side of the line. SE 4 rendered the same lines correctly
(issue SubtitleEdit#14262, matter H).

The direction is now taken from the whole paragraph (after tags are
stripped), through the same TextToFlowDirectionConverter helper the grid
and the compare view use, and applied to every line of it - a second line
holding only neutrals or a Latin name belongs to the same block as the
first. Placement is unchanged: Avalonia lays a right to left line out
from the draw origin as long as no finite paragraph width is set, so the
text stays left aligned in its region like SE 4 has it. The shaped-text
cache is keyed on the direction as well, because the footer labels ask
for the left to right form of strings a paragraph may draw the other way.

Chapter titles get the same treatment, with the alignment pinned to the
left: that layout does set a finite MaxTextWidth, which would otherwise
push a right to left title to the far end of it, outside the flag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…cross-vertical-bands

Do not merge WebVTT cues placed in different vertical bands
"Google it" was the only way to look up selected text, and its search engine
was hard-coded. Five "search via" slots now sit next to it: each holds a name
and a URL template, configured in Options > Shortcuts the same way the
"surround with" slots are (select the shortcut, press Configure).

The slots ship with SE 4's defaults - The Free Dictionary, Wikipedia,
DuckDuckGo - and appear in a "Search via" submenu in the text box context
menu; slots without a URL stay hidden, and the submenu disappears when none
is configured. Searching uses the selected text, or the whole text of the
focused text box when nothing is selected (SE 4 parity), with the text put
where "{0}" is - appended when the template has no placeholder.

Importing SE 4 shortcuts now carries MainTranslateCustomSearch1-5 along with
the name/URL pairs they fire (VideoControls/CustomSearchTextN + UrlN), so an
imported key searches the site it searched in SE 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-h-rtl-support-dac64e

Waveform: give right-to-left text the direction its letters ask for
…ut-request-ca1b23

Add "toggle translation and original in video/audio preview" shortcut
…offset-dialog-90fcc5

Video offset: recently used offsets and an Apply that keeps the window open
The footer kept showing the row's own characters-per-second while the
paragraph drew the original text, so the number read as the original's and
was wrong. It now counts whatever is on screen.

OriginalCharactersPerSecond is memoized like CharactersPerSecond - the footer
reads it for every visible paragraph on every painted frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
niksedk and others added 29 commits September 2, 2026 14:37
…ubtitleEdit#14420)

Ports SE 4's two text-box shortcuts: one break at the first space at (or right
before) the caret, existing breaks flattened first, on whichever text box has
focus. The plain variant leaves the caret at the end of the new first line; the
"and go to next" variant moves to the next row. Both are unbound by default, as
in SE 4, and the SE 4 settings import now maps MainTextBoxBreakAtPosition and
MainTextBoxBreakAtPositionAndGoToNext to them. Utilities.ReSplit gets its first
unit tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…leedit-issue-14420-4f1bf1

Add "Break at first space from cursor position" text box shortcuts
…leedit-issue-14418-7028b7

Fix recalculate duration landing just over max CPS, and CPS readouts disagreeing
"Clone from video (voice of each line)" needs an engine that takes its
reference per synthesis call. IndexTTS 2.5, Higgs Audio v3 and Fish Audio
S2 Pro on audio.cpp already send `voice_ref` as a per-request path, so
enabling it is the OmniVoice shape: the line's voice points straight at
the cut clip, nothing is staged, nothing to clear between runs. audio.cpp
resamples the reference itself, so the 24 kHz clips are used as cut.

Fish refuses a reference without `reference_text`, so a clip with no
transcript sidecar is not handed out as a voice there; the line falls
back to an ordinary voice instead of failing the run. Higgs and IndexTTS
clone from the audio alone and take every clip.

Fixes SubtitleEdit#14425 (Higgs and Fish; Confucius4 needs the CrispASR adapter to
re-apply the voice per request first).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Updates LlamaCppDownloadService.ReleaseTag and all 17 LlamaCpp hash lists
(12 archives, 2 cudart redistributables, 5 unpacked llama-server binaries).

Archive hashes come from GitHub's per-asset digest field and were re-verified
by downloading and hashing each of the ten engine archives. The llama-server
hashes were extracted from those archives.

Both cudart-*.zip redistributables are still byte-identical to earlier
releases (GitHub digest matches the committed hash), so those lists repeat
the previous hash. The "one executable key per OS" invariant still holds at
b10760: the Windows CPU/Vulkan/CUDA 12.4/CUDA 13.3 builds ship the same
llama-server.exe, and the Linux x64 and ARM64 CPU/Vulkan builds each ship the
same llama-server. Both macOS llama-server binaries are byte-identical to
b10625, so those two lists repeat the previous hash with a note.

Smoke-tested the real b10760 macOS ARM64 llama-server with the flags
LlamaCppServerManager passes: TranslateGemma 4B (--no-jinja --chat-template
gemma) produces the same Danish translation as b10625, and Qwen3.5 4B
(--reasoning off) returns a well-formed json_schema response.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…leedit-issue-14425-4a4a0b

TTS: per-line voice cloning on the audio.cpp engines (IndexTTS 2.5, Higgs, Fish)
…cpp-latest-8fa65e

Update llama.cpp to b10760
…ing consent dialog

VoiceCloningConsent.IsCloneVoice is a hand-kept list of every cloning engine's
voice type, matched on a non-empty FilePath. Confucius4TtsVoice and PocketTtsVoice
were never added, so a reference imported for either engine - or a WAV dropped
into its voices folder by hand - was synthesised without the consent dialog ever
showing at Generate/Test voice. Import still asked; synthesis did not.

Add both types with the same rule, and add a catalog-driven test that walks
TtsEngineCatalog.CreateVoiceCloningEngines() and asserts each engine's clone
voice type is recognised, so the next cloning engine cannot ship with the same
gap. Also add the two types (and DotsTtsVoice, which the switch already had but
the theory data did not) to the existing clone/preset theory data.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…clone-consent-confucius4-pocket

Gate Confucius4-TTS and Pocket TTS clone voices behind the voice-cloning consent dialog
…e menu

A recent-video entry is only validated when clicked, so a moved or deleted
file now drops out of the list (and is saved) instead of staying there behind
an error box. The error uses the existing "X not found" string.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The set-end-at-key-up guard only rewrites the end once the playhead is
100 ms past the new start, so a tap shorter than that keeps the old end.
Assigning StartTime alone leaves the end in place, which inverts a line
whose old end lies before the new start. Move the whole line instead,
matching SE4's SetStartTime(adjustEndTime: true).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… timing filter

The zero/negative-duration filter does not change what libass shows: a
zero-duration event never matches the render window, and libass parses
the negative time codes SE writes as real negative times, so a line
from -0.5 s to 1.5 s already shows from 0 s. Verified by rendering
through mpv 0.17.4/libass. The clamp and the removal are dropped.

The dedupe stays: the 1.001 stretch the mpv and VLC reloaders each
carried now lives in SmptePreviewStretch, and the secondary subtitle
gets it too - on a copy, since it is shared between refreshes. Without
SMPTE the secondary paragraphs are still added by reference, so the
normal refresh allocates nothing extra.

VlcReloader gets its deep copy back: RefreshVlc receives the live
GetUpdateSubtitle() instance, and mutating that stretched or filtered
the working subtitle on every refresh.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cent-video-menu

feat(video): add open recent video menu option
…-set-end-at-keyup

fix(shortcut): set end time and advance to next subtitle on key up
…e-preview

Share the SMPTE preview stretch between the mpv and VLC reloaders and the secondary subtitle
…ffmpeg is missing

Waveform extraction was the one ffmpeg consumer that bailed out silently
when ffmpeg was not installed, leaving an empty waveform with no hint,
status line or dialog (SubtitleEdit#14390). Every other ffmpeg feature already goes
through RequireFfmpegOk.

- Auto-generate on video open: when ffmpeg is missing, show the existing
  click-to-generate hint over the empty waveform plus a status-bar line,
  instead of a modal (video opens also run from session restore, drag and
  drop and the command line).
- Clicking the empty waveform now runs RequireFfmpegOk first, so the user
  gets the "Download FFmpeg?" prompt on Windows/macOS and extraction
  starts right after the download.
- Linux (no ffmpeg download): RequireFfmpegOk now shows a message saying
  ffmpeg was not found and how to install/configure it, instead of
  returning false silently.

Fixes SubtitleEdit#14390

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-install-prompt-waveform-5856fe

Waveform: offer the ffmpeg download instead of failing silently when ffmpeg is missing
Zonos is not language-agnostic: CrispASR's zonos-tts backend phonemises
the input with eSpeak in whatever language it was started with and
defaults to en-us. SE never sent one, so every non-English line was read
with English G2P (the "Language ?" half of SubtitleEdit#14433).

The main window now shows the language combo for Zonos, populated from
the GGUF's own zonos.language_codes table (eSpeak codes, English (US)
leading as the backend default - there is no honest "Auto", the model
cannot detect a language). The pick is persisted like the other CrispASR
engines and passed as the server's startup -l flag; a language change
restarts the server the same way a voice change already does, because
the zonos backend reads -l once at init. Verified against v0.8.31: the
request body's `language` / `target_lang` fields leave zonos on en-us,
while -l cs / fr-fr / cmn all reach G2P. Czech sample through parakeet
v3: "Dobri Dentoto G Okushka Chaskahol Jessica" before, "Dobrý den
tohoto je skôska Českého jazyka" after.

Fixes the Zonos part of SubtitleEdit#14433.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…14434)

With "Allow edit of original subtitle" on, splitting a line split the
translation and the timing but copied the complete original text onto
both halves, so every split desynchronized the two columns.

SplitManager now takes an optional OriginalSplit and runs the same text
split (line break, weighted line count, auto-break, dialog dashes,
continuation style, tag balancing) on OriginalText. "Split at cursor"
takes the caret from whichever text box has focus: the original box
splits the original at its caret and auto-splits the translation, and
the translation box does the reverse, so the two languages never share
one split position. Read-only references are left alone - the file is
authoritative there and the sticky refresh re-syncs them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…leedit-issue-14433-52730d

TTS: language picker for Zonos (CrispASR)
…leedit-issue-14434-0c8d3b

Split the editable original along with the translation (SubtitleEdit#14434)
…titleEdit#14421)

ApplySettings rebuilds the layout, and the fresh subtitle grid silently
auto-selects row 0. Re-filling the subtitle format list afterwards fired
the format-combo handler several times, each reading that row 0 back and
scrolling to it - and because the scroll helper keeps one pending index
(last writer wins), the restore SetLayout had posted for the user's row
was overridden. Apply, and OK after Apply, jumped to the first line.

- ApplySettings captures the selected index up front and restores it at
  the very end, after the format list refresh; the extra restore in the
  settings command is folded into it.
- The format-combo handler only re-selects a row when the grid's selection
  is the view model's, so a silent row-0 auto-pick can no longer hijack a
  pending scroll.
- Headless regression test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ubtitleEdit#14434)

With "show all original lines" and an editable original, merging two rows
merged their original texts but then brought the second row's original line
back as an extra display-only row under the merged line, showing that text
twice. WithoutReferenceOnlyRows refreshed the reference against the capture
taken before the merge, where that line was still its own paragraph.

Capture the original from the rows again before the refresh. For an editable
original the merged line is consumed, not orphaned; a read-only original is
never captured, so its lines still come back as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…merge-editable-original

Merge with an editable original no longer resurrects the merged line (SubtitleEdit#14434)
…leedit-issue-14421-fce488

Settings: keep the selected line when settings are applied (SubtitleEdit#14421)
Every fix rule now declares the kind of fix it performs via a new
FixType enum (Time, Formatting, Dialog, Punctuation, Casing, Spacing,
Characters, Ocr), so the UI/CLI can group, filter or sort rules by
category later. The member is abstract on the interface so the
compiler enforces it on all 40 implementations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TTVPifejhQqpeh1mkQHeRg
@ivandrofly
ivandrofly marked this pull request as ready for review September 2, 2026 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants