Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
ec59ebc
Make SeNamesList.Load async
ivandrofly Oct 28, 2026
b2f208a
Italian: YTT pen strings as formatting options, fix Right
niksedk Oct 29, 2026
09a9f68
Merge pull request #16460 from SubtitleEdit/fix/italian-ytt-pen-strings
niksedk Oct 29, 2026
5a5ebeb
Load names off the UI thread and share in-flight loads
niksedk Oct 29, 2026
24d06be
Merge pull request #16451 from ivandrofly/names-list-load-async
niksedk Oct 29, 2026
5aa46ac
Update CrispASR to v0.8.48
niksedk Oct 29, 2026
8a54361
Merge pull request #16461 from SubtitleEdit/claude/crisp-asr-v0848
niksedk Oct 29, 2026
0a1ea0d
Use Silero VAD for Crisp ASR Granite by default
niksedk Oct 29, 2026
d013b5b
Merge pull request #16462 from SubtitleEdit/claude/granite-force-vad
niksedk Oct 29, 2026
aed87bc
Add "move start/end X ms" shortcuts that push the neighbour
niksedk Oct 29, 2026
df169a7
Update German Translation
FunkyKnilch Oct 29, 2026
64dd8b1
Update Korean.json
12si27 Oct 29, 2026
b28e22b
Merge pull request #16464 from SubtitleEdit/claude/move-xms-push
niksedk Oct 29, 2026
fd48ddc
Merge pull request #16466 from FunkyKnilch/main
niksedk Oct 29, 2026
9d11506
Merge pull request #16467 from 12si27/patch-10
niksedk Oct 29, 2026
e32fd25
Merge pull request #16465 from pdjdev/fix/waveform-original-cue-drag
niksedk Oct 29, 2026
4ea119d
Keep read-only original blocks still in the timeline while retiming
niksedk Oct 29, 2026
84ebb31
Merge pull request #16468 from SubtitleEdit/claude/timeline-readonly-…
niksedk Oct 29, 2026
f046b2f
Fix uneven 500 ms / custom video steps on quick presses (#16469)
niksedk Oct 29, 2026
3f22483
Merge pull request #16470 from SubtitleEdit/claude/subtitleedit-issue…
niksedk Oct 29, 2026
fed4ab3
Word lists: move focus with type-ahead so arrow keys continue from th…
niksedk Oct 29, 2026
9aeb375
Merge pull request #16473 from SubtitleEdit/claude/subtitleedit-issue…
niksedk Oct 29, 2026
f52e602
Spell check: Undo of "Add to names list" shows the word again (#16472)
niksedk Oct 29, 2026
7df1505
Merge pull request #16475 from SubtitleEdit/claude/subtitleedit-issue…
niksedk Oct 29, 2026
f92171b
Japanese for 5.4.0 Beta11
hisui3393 Oct 29, 2026
65f37d5
Merge branch 'main' into update-for5.4-Beta11
hisui3393 Oct 29, 2026
94b5931
ASSA styles: preview attached [Fonts] fonts
niksedk Oct 29, 2026
07f47a3
Merge pull request #16477 from SubtitleEdit/claude/assa-embedded-font…
niksedk Oct 29, 2026
d793c0d
SSA styles: preview attached [Fonts] fonts
niksedk Oct 29, 2026
cfdd431
Merge pull request #16478 from SubtitleEdit/claude/ssa-embedded-font-…
niksedk Oct 29, 2026
f1ba31d
Format picker: treat the pick as a user format change
niksedk Oct 29, 2026
4b4c3b5
macOS: suggest the save name with the format's extension
niksedk Oct 29, 2026
92398bf
Merge pull request #16476 from hisui3393/update-for5.4-Beta11
niksedk Oct 29, 2026
df663f4
Merge pull request #16479 from SubtitleEdit/claude/sub-station-alpha-…
niksedk Oct 29, 2026
c5db6f4
Second subtitle: add a font name picker
niksedk Oct 30, 2026
53cac6a
Merge pull request #16484 from SubtitleEdit/claude/subtitle-propertie…
niksedk Oct 30, 2026
b95d8c8
Second subtitle: auto-detect if the video offset applies
niksedk Oct 30, 2026
f4dadb2
Merge pull request #16485 from SubtitleEdit/claude/auto-detect-offset…
niksedk Oct 30, 2026
346ff76
Multiple replace: edit rules in their row, drop the Edit rule panel
niksedk Oct 30, 2026
e9340c6
Docs: Multiple replace double-click edits the rule in its row
niksedk Oct 30, 2026
b21ff17
Docs: new Multiple replace screenshot and window layout
niksedk Oct 30, 2026
e67682f
Merge pull request #16486 from SubtitleEdit/claude/multiple-replace-c…
niksedk Oct 30, 2026
0861365
v5.4.0-beta11: version bump + change log
niksedk Oct 30, 2026
1a62466
Merge pull request #16487 from SubtitleEdit/claude/beta11-release-prep
niksedk Oct 30, 2026
9714ed6
FFmpeg player overlay: italic/bold runs, right-to-left text, no drawings
niksedk Oct 30, 2026
a9da230
FFmpeg player overlay: per-line alignment, secondary style, box, just…
niksedk Oct 30, 2026
9c8a3b4
Merge pull request #16490 from SubtitleEdit/claude/ffmpeg-overlay-tex…
niksedk Oct 30, 2026
ed4ba5b
llama.cpp: pick CPU or Vulkan on Linux, switch builds from engine set…
niksedk Oct 30, 2026
b24cb15
Merge pull request #16491 from SubtitleEdit/claude/llama-cpp-linux-vu…
niksedk Oct 30, 2026
2ca282e
Fix Source View editor frame clipping
pdjdev Oct 30, 2026
aa7b57c
Match AutoCompleteBox corner radius to editor text fields
pdjdev Oct 30, 2026
0ae5c4a
Use rounded border for Source View syntax editor
pdjdev Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions change-log.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@ Subtitle Edit Changelog

-----------------------------------------------------------------------------------------------------

v5.4.0-beta11 (30th of October 2026)

* Add "Move start/end X ms" shortcuts that push the neighbour line (no default key) - thx doom9 forum
* Second subtitle: add font name picker
* Second subtitle: auto-detect if the video offset applies - thx Jamestankw
* Multiple replace: edit rules directly in the list (Enter/F2/double-click), more compact window
* ASSA/SSA styles: preview fonts attached in [Fonts]
* Use Silero VAD for CrispASR Granite by default (better accuracy)
* Fix spell check undo of "Add to names list" - thx bptrsn
* Fix arrow keys after type-ahead in Options > Word lists - thx FunkyKnilch
* Fix uneven 500 ms / custom video steps on quick presses - thx vitonique
* Fix read-only original moving in waveform/timeline while retiming translation - thx pdjdev
* Fix "Save as" ignoring format picked via format picker (e.g. Sub Station Alpha/.ssa)
* Update Japanese translation - thx hisui3393
* Update German translation - thx FunkyKnilch
* Update Italian translation - thx bovirus
* Update Korean translation - thx 12si27
* Update CrispASR to v0.8.48

-----------------------------------------------------------------------------------------------------

v5.4.0-beta10 (29th of October 2026)

* Add full support for YouTube timed text (.ytt/srv3): pens, positions, preview + styles window
Expand Down
10 changes: 9 additions & 1 deletion docs/features/auto-translate-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,12 @@ For the local llama.cpp engines, the gear button opens an info dialog about the
- **Release** — The llama.cpp release Subtitle Edit is pinned to
- **Install folder** — Where the server binaries live

Use **Download** / **Update** / **Re-download...** to (re)install the server build, for example after a graphics driver change. The same dialog is used by [AI Review](ai-review.md) and the [AI Assistant](ai-assistant.md).
Use **Download** / **Update** / **Re-download...** to (re)install the server build, for example after a graphics driver change. On Windows and Linux you pick the build each time, so this is also how to switch between CPU and GPU. The same dialog is used by [AI Review](ai-review.md) and the [AI Assistant](ai-assistant.md).

### GPU acceleration

Pick **Vulkan (GPU)** instead of **CPU** to run llama.cpp on the GPU. Vulkan works with NVIDIA, AMD and Intel GPUs that have Vulkan drivers installed. On Windows, **CUDA (NVIDIA GPU)** is also available.

If llama.cpp is already installed as a CPU build, click the gear next to the engine, click **Re-download...** in the engine settings, and pick the GPU build. **Backend** shows which build is installed. The **Re-download...** button next to the model only downloads the model again.

Subtitle Edit offloads all model layers to the GPU, so the model should fit in the GPU memory. For a model that doesn't fit, add `-ngl N` under **Advanced...** to offload only N layers. If **Use only these parameters** is on, your parameters decide this.
24 changes: 17 additions & 7 deletions docs/features/edit.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,13 @@ Apply multiple find-and-replace rules at once, organized into named categories.

### Window layout

The window is split into two resizable panels:
The window is split into resizable panels:

| Panel | Description |
|---|---|
| **Left — Rules** | Tree of categories and their rules. Each rule shows its type icon, find pattern, replacement, and an optional description. The buttons above the tree add a category, add a rule, open **Add rules** (a whole list in one go), import rules, and collapse or expand all categories. |
| **Right — Fixes** | Preview of all lines that will be changed. The **Before** column highlights removed characters in red and the **After** column highlights added characters in green. Selecting a row reveals an **Applied rules** detail panel at the bottom listing every rule that matched that line. |
| **Left — Categories** | The categories, each with an on/off checkbox and its number of rules. The buttons below the list add, rename, delete and import categories. |
| **Top right — Rules** | The rules of the selected category: on/off, type icon, **Find what**, **Replace with**, **Whole word** and description. The row above the list adds a rule as you type it (`Enter` adds it); **Newest first** and the filter box change what the list shows. The buttons below remove, edit, move and reorder the selected rule, and open **Add rules** (a whole list in one go). |
| **Bottom right — Preview** | All lines that will be changed. The **Before** column highlights removed characters in red and the **After** column highlights added characters in green. Selecting a row shows **Applied rules** under it, listing every rule that matched that line. |

### Choosing which fixes to apply

Expand All @@ -105,7 +106,7 @@ Editing a rule regenerates the preview, which ticks every row again.

### Rule types

Each rule has one of three match types, shown as an icon in the tree:
Each rule has one of three match types, shown as an icon in front of **Find what**. Click the icon to pick another type:

| Type | Description |
|---|---|
Expand Down Expand Up @@ -137,7 +138,7 @@ Right-click a category node to open its context menu:

Right-click a rule node to open its context menu:

- **Edit rule** — change find/replace text, type, and description
- **Edit rule** — open the **Edit rule** dialog for find/replace text, type, whole word, and description
- **Duplicate** — insert a copy of the rule above the current one
- **Insert before / Insert after** — add a new rule relative to this one
- **Add rules...** — add a list of rules right after this one (see [Adding many rules at once](#adding-many-rules-at-once))
Expand All @@ -146,7 +147,15 @@ Right-click a rule node to open its context menu:
- **Move to category** — move the rule to the end of another category
- **Delete** — remove the rule

Double-clicking a rule also opens the **Edit rule** dialog. A new rule is selected and scrolled into view, also at the end of a long category.
Double-click a rule (or press `Enter` or `F2` on it) to edit it in its own row: **Find what**, **Replace with** and **Description** turn into text boxes, with the cursor in the cell you double-clicked. `Tab` moves between the boxes and the preview follows while you type.

- `Enter` keeps the change
- `Escape` puts the texts back
- Selecting another rule or clicking outside the list also keeps the change

**Whole word** is the checkbox in its own column, and the type is the icon in front of **Find what**. The **Edit...** button below the list and **Edit rule** in the context menu still open the full dialog.

A new rule is selected and scrolled into view, also at the end of a long category.

### Adding many rules at once

Expand All @@ -165,7 +174,8 @@ The rules are added in the order they were entered, which is the order they run
|---|---|
| `Ctrl+N` | Add a new rule to the selected category, or insert after the selected rule |
| `Ctrl+Shift+N` | Open **Add rules** for the selected category or rule |
| `F2` | Edit the selected rule, or rename the selected category |
| `F2` / `Enter` | Edit the selected rule in its row (`F2` renames the selected category) |
| `Enter` / `Escape` while editing a row | Keep the change / put the texts back |
| `Ctrl+D` | Duplicate the selected rule |
| `Ctrl+F` | Find a rule by name / text |
| `Ctrl+Up` / `Ctrl+Down` | Move the selected category or rule up / down (`Cmd` on macOS) |
Expand Down
Binary file modified docs/screenshots/multiple-replace.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 48 additions & 11 deletions src/libse/Dictionaries/NameList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ public class NameList
private readonly HashSet<string> _namesMultiListUppercase;
private readonly HashSet<string> _blackList;

// Names from the shipped lists (not the user file) - only these need a blacklist entry to
// stay removed; a name that only lives in the user file is gone once its node is deleted.
private readonly HashSet<string> _shippedNames;

// Lazy reverse index for IsInNamesMultiWordList, see GetMultiNamePartIndex.
private Dictionary<string, List<string>> _namesMultiListParts;
public string LanguageName { get; private set; }
Expand All @@ -28,10 +32,11 @@ public NameList(string dictionaryFolder, string languageName, bool useOnlineName
_namesMultiList = new HashSet<string>();
_namesMultiListUppercase = new HashSet<string>();
_blackList = new HashSet<string>();
_shippedNames = new HashSet<string>();

LoadNamesList(GetLocalNamesUserFileName()); // e.g: en_names_user.xml (culture sensitive)
LoadNamesList(GetLocalNamesFileName()); // e.g: en_names.xml (culture sensitive)
LoadNamesList(GetLocalCultureNamesFileName()); // e.g: pt_BR_names.xml (region specific)
LoadNamesList(GetLocalNamesUserFileName(), null); // e.g: en_names_user.xml (culture sensitive)
LoadNamesList(GetLocalNamesFileName(), _shippedNames); // e.g: en_names.xml (culture sensitive)
LoadNamesList(GetLocalCultureNamesFileName(), _shippedNames); // e.g: pt_BR_names.xml (region specific)
if (useOnlineNameList && !string.IsNullOrEmpty(namesUrl))
{
try
Expand All @@ -45,7 +50,7 @@ public NameList(string dictionaryFolder, string languageName, bool useOnlineName
}
else
{
LoadNamesList(Path.Combine(_dictionaryFolder, "names.xml"));
LoadNamesList(Path.Combine(_dictionaryFolder, "names.xml"), _shippedNames);
}

foreach (var name in _blackList)
Expand Down Expand Up @@ -121,7 +126,7 @@ private string GetLocalCultureNamesFileName()
return Path.Combine(_dictionaryFolder, LanguageName + "_names.xml");
}

private void LoadNamesList(string fileNameOrUrl)
private void LoadNamesList(string fileNameOrUrl, HashSet<string> loadedNames)
{
if (string.IsNullOrEmpty(fileNameOrUrl) ||
!File.Exists(fileNameOrUrl) &&
Expand All @@ -145,6 +150,7 @@ private void LoadNamesList(string fileNameOrUrl)
var name = reader.ReadElementContentAsString().Trim();
if (name.Length > 0)
{
loadedNames?.Add(name);
if (name.Contains(' '))
{
_namesMultiList.Add(name);
Expand Down Expand Up @@ -197,12 +203,18 @@ public bool Remove(string name)
var fileName = GetLocalNamesUserFileName();
var nameListXml = CreateDocument(fileName);

// Add removed name to blacklist
var nameNode = nameListXml.CreateElement("name");
nameNode.InnerText = name;
if (nameListXml.DocumentElement != null)
{
nameListXml.DocumentElement.SelectSingleNode("blacklist")?.AppendChild(nameNode);
// Blacklist only a shipped name: one the user added (e.g. spell-check "Add to
// names list" followed by Undo) just loses its node - a blacklist entry would
// make adding it again later silently fail to save. (#16472)
if (_shippedNames.Contains(name))
{
var nameNode = nameListXml.CreateElement("name");
nameNode.InnerText = name;
nameListXml.DocumentElement.SelectSingleNode("blacklist")?.AppendChild(nameNode);
}

var nodeToRemove = default(XmlNode);

// Remove remove-name from name-list
Expand Down Expand Up @@ -242,23 +254,48 @@ public bool Remove(string name)
public bool Add(string name)
{
name = name.RemoveControlCharacters().Trim();
if (name.Length == 0 || _blackList.Contains(name) || !name.ContainsLetter())
if (name.Length == 0 || !name.ContainsLetter())
{
return false;
}

// An explicit add overrides an earlier removal: blacklisted names are left out of the
// lists at load, so TryAdd succeeds and the blacklist entry is dropped below. Refusing
// here left the name in the caller's session but never saved it. (#16472)
var wasBlacklisted = _blackList.Remove(name);
if (!TryAdd(name))
{
return false;
}

// <two-letter-iso-code>_names.xml, e.g "en_names.xml"
// <two-letter-iso-code>_names_user.xml, e.g "en_names_user.xml"
var fileName = GetLocalNamesUserFileName();
var nameListXml = CreateDocument(fileName);

var de = nameListXml.DocumentElement;
if (de != null)
{
if (wasBlacklisted)
{
var blacklistNodes = de.SelectNodes("blacklist/name");
var nodesToRemove = new List<XmlNode>();
if (blacklistNodes != null)
{
foreach (XmlNode blacklistNode in blacklistNodes)
{
if (blacklistNode.InnerText.Trim().Equals(name, StringComparison.Ordinal))
{
nodesToRemove.Add(blacklistNode);
}
}
}

foreach (var blacklistNode in nodesToRemove)
{
blacklistNode.ParentNode?.RemoveChild(blacklistNode);
}
}

var node = nameListXml.CreateElement("name");
node.InnerText = name;
de.AppendChild(node);
Expand Down
50 changes: 50 additions & 0 deletions src/libuilogic/Media/SecondarySubtitleOffset.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Nikse.SubtitleEdit.Core.Common;

namespace Nikse.SubtitleEdit.UiLogic.Media;

/// <summary>
/// Decides whether the video offset applies to a second subtitle on the video (#16434). With an
/// offset like 10:00:00:00 the main file is shown minus the offset, but a second file may be
/// either a sibling written with the same 10-hour times or one already timed from zero. Each way
/// of reading it is scored by how many of its lines start inside the reference window (the video,
/// or else the main subtitle on the video timeline), and the better fit wins.
/// </summary>
public static class SecondarySubtitleOffset
{
private const double ToleranceMs = 1000;

/// <param name="secondary">The second subtitle with its file time codes.</param>
/// <param name="offsetMs">The video offset, subtracted from file times to get video times.</param>
/// <param name="windowStartMs">Start of the reference window on the video timeline.</param>
/// <param name="windowEndMs">End of the reference window; <see cref="double.MaxValue"/> when unknown.</param>
/// <returns>True when the offset should be subtracted; also on a tie, so a file that fits
/// neither way keeps the main file's split.</returns>
public static bool ShouldApply(Subtitle secondary, double offsetMs, double windowStartMs, double windowEndMs)
{
if (offsetMs == 0 || secondary.Paragraphs.Count == 0)
{
return offsetMs != 0;
}

var low = windowStartMs - ToleranceMs;
var high = windowEndMs == double.MaxValue ? double.MaxValue : windowEndMs + ToleranceMs;
var asIs = 0;
var shifted = 0;
foreach (var p in secondary.Paragraphs)
{
var start = p.StartTime.TotalMilliseconds;
if (start >= low && start <= high)
{
asIs++;
}

var shiftedStart = start - offsetMs;
if (shiftedStart >= low && shiftedStart <= high)
{
shifted++;
}
}

return shifted >= asIs;
}
}
35 changes: 34 additions & 1 deletion src/libuilogic/SpellCheck/SpellCheckWordLists.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,26 @@ private void AddWordWithDashesOrPeriods(string word)
}
}

private void RemoveWordWithDashesOrPeriods(string word)
{
if (!_wordsWithDashesOrPeriods.Remove(word))
{
return;
}

foreach (var part in word.Split(PeriodAndDash, StringSplitOptions.RemoveEmptyEntries))
{
if (_wordsWithDashesOrPeriodsByPart.TryGetValue(part, out var words))
{
words.Remove(word);
if (words.Count == 0)
{
_wordsWithDashesOrPeriodsByPart.Remove(part);
}
}
}
}

private IEnumerable<string> GetSingleUnifiedCollection(IEnumerable<string> namesMultiWordList)
{
return namesMultiWordList
Expand Down Expand Up @@ -277,6 +297,15 @@ public void RemoveName(string word)
{
_namesListWithApostrophe.Remove(word + "'");
}

// AddName used to index every name for the dash/period part lookup - a plain name like
// "Boris" as its own single part - and nothing took it out again, so HasNameExtended stayed
// true and spell-check Undo of "Add to names list" never showed the word again. (#16472)
// The lookup is shared with the user dictionary, so keep a word that is still in there.
if (!_userWordList.Contains(word) && !_userPhraseList.Contains(word))
{
RemoveWordWithDashesOrPeriods(word);
}
}

/// <summary>
Expand Down Expand Up @@ -369,7 +398,11 @@ public bool AddName(string word)
_namesListWithApostrophe.Add(word + "'");
}

AddWordWithDashesOrPeriods(word);
// Same rule as the start-up load: only names with '.' or '-' get split into parts.
if (word.Contains(PeriodAndDash))
{
AddWordWithDashesOrPeriods(word);
}

var namesList = new NameList(_dictionaryFolder, _languageName, false, string.Empty);
namesList.Add(word);
Expand Down
Loading
Loading