From 88ee1a991298b490b90d35cc981fa0f82d11c8dc Mon Sep 17 00:00:00 2001 From: niksedk Date: Wed, 14 Oct 2026 19:04:56 +0200 Subject: [PATCH] SplitDialogs: support Subtitle Edit Batch convert - plugin.json "batch": true - batch request: split every dialog line with the saved options (all rows checked, the window's default), no UI; no dialogs = ok without subtitle - configure request: options-only window, settings returned without subtitle - settings load/save moved to SplitSettings so window and batch share them - new SplitDialogs.Tests project, run by split-dialogs.yml Co-Authored-By: Claude Opus 5.5 --- .github/workflows/split-dialogs.yml | 18 ++- se5/SplitDialogs.Tests/BatchModeTests.cs | 108 ++++++++++++++++++ .../SplitDialogs.Tests.csproj | 27 +++++ se5/SplitDialogs/BatchMode.cs | 54 +++++++++ se5/SplitDialogs/MainWindow.axaml | 21 +++- se5/SplitDialogs/MainWindow.axaml.cs | 79 ++++++------- se5/SplitDialogs/Program.cs | 2 +- se5/SplitDialogs/README.md | 7 ++ se5/SplitDialogs/SplitSettings.cs | 44 +++++++ se5/SplitDialogs/plugin.json | 1 + 10 files changed, 314 insertions(+), 47 deletions(-) create mode 100644 se5/SplitDialogs.Tests/BatchModeTests.cs create mode 100644 se5/SplitDialogs.Tests/SplitDialogs.Tests.csproj create mode 100644 se5/SplitDialogs/BatchMode.cs create mode 100644 se5/SplitDialogs/SplitSettings.cs diff --git a/.github/workflows/split-dialogs.yml b/.github/workflows/split-dialogs.yml index aeb86ce2..39e27d98 100644 --- a/.github/workflows/split-dialogs.yml +++ b/.github/workflows/split-dialogs.yml @@ -5,11 +5,13 @@ on: branches: [main] paths: - 'se5/SplitDialogs/**' + - 'se5/SplitDialogs.Tests/**' - 'se5/Plugin-Shared/**' - '.github/workflows/split-dialogs.yml' pull_request: paths: - 'se5/SplitDialogs/**' + - 'se5/SplitDialogs.Tests/**' - 'se5/Plugin-Shared/**' - '.github/workflows/split-dialogs.yml' workflow_dispatch: @@ -90,8 +92,22 @@ jobs: echo "ref=$ref" >> "$GITHUB_OUTPUT" echo "release=$release_flag" >> "$GITHUB_OUTPUT" + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET 8 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Test + run: dotnet test se5/SplitDialogs.Tests/SplitDialogs.Tests.csproj -c Release + build: - needs: prepare + needs: [prepare, test] runs-on: ubuntu-latest strategy: fail-fast: false diff --git a/se5/SplitDialogs.Tests/BatchModeTests.cs b/se5/SplitDialogs.Tests/BatchModeTests.cs new file mode 100644 index 00000000..03c41d77 --- /dev/null +++ b/se5/SplitDialogs.Tests/BatchModeTests.cs @@ -0,0 +1,108 @@ +using SubtitleEdit.Plugins.Shared; +using SubtitleEdit.Plugins.SplitDialogs; +using System.Text.Json; + +namespace SplitDialogs.Tests; + +/// Subtitle Edit Batch convert: "batch" splits every dialog headless, "configure" returns the options only. +public class BatchModeTests +{ + private const string DialogSrt = + "1\n00:00:01,000 --> 00:00:04,000\n- Andro!\n- Ben, I've been oozed.\n\n" + + "2\n00:00:05,000 --> 00:00:07,000\nPlain line.\n\n" + + "3\n00:00:08,000 --> 00:00:10,000\n- How are you\n- Fine.\n"; + + private static PluginRequest Request(string requestType, string srt, string? settingsJson = null) => new() + { + RequestType = requestType, + Subtitle = new PluginSubtitle { Format = "SubRip", SubRip = srt, Native = srt }, + Settings = settingsJson == null ? null : JsonDocument.Parse(settingsJson).RootElement.Clone(), + SettingsVersion = settingsJson == null ? null : 1, + }; + + private static List Texts(PluginResponse response) => + new SrtCueDocument(response.Subtitle!.Native).Cues.Select(c => string.Join("\n", c.Lines)).ToList(); + + [Fact] + public void Batch_SplitsAllDialogs_WithDefaultSettings() + { + var response = BatchMode.BuildBatchResponse(Request(PluginRequestType.Batch, DialogSrt)); + + Assert.Equal(PluginStatus.Ok, response.Status); + Assert.Equal("SubRip", response.Subtitle!.Format); + // Line 3 is not split by default: "How are you" does not end a sentence. + Assert.Equal(new[] { "Andro!", "Ben, I've been oozed.", "Plain line.", "- How are you\n- Fine." }, Texts(response)); + Assert.Null(response.Settings); + Assert.Null(response.SettingsVersion); + } + + [Fact] + public void Batch_UsesSavedSettings() + { + var response = BatchMode.BuildBatchResponse( + Request(PluginRequestType.Batch, DialogSrt, "{\"requireSentenceEnding\":false,\"gapMs\":100}")); + + var cues = new SrtCueDocument(response.Subtitle!.Native).Cues; + Assert.Equal(new[] { "Andro!", "Ben, I've been oozed.", "Plain line.", "How are you", "Fine." }, + cues.Select(c => string.Join("\n", c.Lines))); + Assert.Equal(100, cues[4].StartMs - cues[3].EndMs); + Assert.Equal(1000, cues[0].StartMs); + Assert.Equal(10000, cues[4].EndMs); + } + + [Fact] + public void Batch_NoDialogs_ReturnsOkWithoutSubtitle() + { + var response = BatchMode.BuildBatchResponse( + Request(PluginRequestType.Batch, "1\n00:00:01,000 --> 00:00:02,000\nHello.\n")); + + Assert.Equal(PluginStatus.Ok, response.Status); + Assert.Null(response.Subtitle); + } + + [Fact] + public void Batch_Assa_KeepsNativeFormat() + { + const string assa = + "[Script Info]\nScriptType: v4.00+\n\n[Events]\n" + + "Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n" + + "Dialogue: 0,0:00:01.00,0:00:04.00,Top,Bob,0,0,0,,- Hi!\\N- Hello.\n"; + var request = new PluginRequest + { + RequestType = PluginRequestType.Batch, + Subtitle = new PluginSubtitle { Format = "Advanced Sub Station Alpha", Native = assa }, + }; + + var response = BatchMode.BuildBatchResponse(request); + + Assert.Equal("Advanced Sub Station Alpha", response.Subtitle!.Format); + var dialogues = response.Subtitle.Native.Split('\n').Where(l => l.StartsWith("Dialogue:")).ToList(); + Assert.Equal(2, dialogues.Count); + Assert.All(dialogues, d => Assert.Contains(",Top,Bob,", d)); + Assert.EndsWith(",Hi!", dialogues[0]); + Assert.EndsWith(",Hello.", dialogues[1]); + } + + [Fact] + public void Configure_ReturnsSettingsWithoutSubtitle() + { + var response = BatchMode.BuildConfigureResponse(new SplitSettings(false, 80)); + + Assert.Equal(PluginStatus.Ok, response.Status); + Assert.Null(response.Subtitle); + Assert.Equal(1, response.SettingsVersion); + Assert.False(response.Settings!.Value.GetProperty("requireSentenceEnding").GetBoolean()); + Assert.Equal(80, response.Settings.Value.GetProperty("gapMs").GetInt32()); + } + + [Fact] + public void Settings_RoundTrip_AndDefaults() + { + var saved = new SplitSettings(false, 42); + var request = new PluginRequest { Settings = saved.ToJson(), SettingsVersion = SplitSettings.Version }; + + Assert.Equal(saved, SplitSettings.Load(request)); + Assert.Equal(SplitSettings.Default, SplitSettings.Load(new PluginRequest())); + Assert.Equal(SplitSettings.Default, SplitSettings.Load(new PluginRequest { Settings = saved.ToJson(), SettingsVersion = 99 })); + } +} diff --git a/se5/SplitDialogs.Tests/SplitDialogs.Tests.csproj b/se5/SplitDialogs.Tests/SplitDialogs.Tests.csproj new file mode 100644 index 00000000..ff6f6284 --- /dev/null +++ b/se5/SplitDialogs.Tests/SplitDialogs.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/se5/SplitDialogs/BatchMode.cs b/se5/SplitDialogs/BatchMode.cs new file mode 100644 index 00000000..9facaa1a --- /dev/null +++ b/se5/SplitDialogs/BatchMode.cs @@ -0,0 +1,54 @@ +using SubtitleEdit.Plugins.Shared; + +namespace SubtitleEdit.Plugins.SplitDialogs; + +/// Subtitle Edit's Batch convert: "batch" splits headless, "configure" returns the settings only. +public static class BatchMode +{ + /// + /// A "batch" request: split every dialog line in the file with the saved options, without + /// UI - what the window would do with its default "all rows checked". No dialogs is "ok" + /// without a subtitle. + /// + public static PluginResponse BuildBatchResponse(PluginRequest request) + { + var settings = SplitSettings.Load(request); + var document = CueDocument.Create(request.Subtitle); + var splits = new Dictionary>(); + foreach (var cue in document.Cues) + { + if (!cue.CanSplit) + { + continue; + } + + var texts = DialogSplitter.Split(cue.Lines, settings.RequireSentenceEnding); + if (texts != null) + { + splits[cue.Index] = SplitTiming.Distribute(cue.StartMs, cue.EndMs, texts, settings.GapMs); + } + } + + if (splits.Count == 0) + { + return new PluginResponse { Status = PluginStatus.Ok, Message = "Split dialogs: no dialog lines found." }; + } + + var lineCount = splits.Values.Sum(p => p.Count); + return new PluginResponse + { + Status = PluginStatus.Ok, + Message = $"Split {splits.Count} dialog line(s) into {lineCount} lines.", + UndoDescription = "Split dialogs", + Subtitle = new PluginSubtitle + { + Format = document.FormatName, + Native = document.Build(splits), + }, + }; + } + + /// A "configure" request: hand back the settings only, for the next Batch convert run. + public static PluginResponse BuildConfigureResponse(SplitSettings settings) => + new() { Status = PluginStatus.Ok, Settings = settings.ToJson(), SettingsVersion = SplitSettings.Version }; +} diff --git a/se5/SplitDialogs/MainWindow.axaml b/se5/SplitDialogs/MainWindow.axaml index e4ca4edb..7618d0a3 100644 --- a/se5/SplitDialogs/MainWindow.axaml +++ b/se5/SplitDialogs/MainWindow.axaml @@ -64,7 +64,8 @@ - @@ -72,6 +73,24 @@