Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion src/ui/Assets/Languages/English.json
Original file line number Diff line number Diff line change
Expand Up @@ -2170,7 +2170,11 @@
"unableToReadVideoTitle": "Unable to read video",
"unableToReadVideoMessage": "Could not read video info from: {0}",
"abortOcrTitle": "Abort OCR?",
"abortOcrMessage": "Do you want to abort the running OCR?"
"abortOcrMessage": "Do you want to abort the running OCR?",
"testOcr": "Test current frame",
"testOcrRunning": "Testing OCR on current frame...",
"testOcrResultX": "Test result: {0}",
"testOcrNoTextFound": "Test: no text found in the scan area"
},
"goToVideoPosition": "Go to video position",
"generateBlankVideoDotDotDot": "Generate blank video...",
Expand Down
167 changes: 162 additions & 5 deletions src/ui/Features/Video/VideoOcr/VideoOcrViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,126 @@ private void SetScanAreaFullFrame()
CropSelector?.SetSelectionVideoRect(0, 0, VideoWidth, VideoHeight);
}

/// <summary>
/// OCRs only the frame at the current preview position so the user can validate the scan
/// area, engine, and settings without scanning the whole video. The result is shown in the
/// status text.
/// </summary>
[RelayCommand]
private async Task TestOcr()
{
if (IsRunning || string.IsNullOrEmpty(_videoFileName) || VideoWidth <= 0 || VideoHeight <= 0)
{
return;
}

var engineOk = await EnsureEngineIsAvailable();
if (!engineOk)
{
return;
}

ClampSelection();

_cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _cancellationTokenSource.Token;

IsRunning = true;
ProgressText = Se.Language.Video.VideoOcr.TestOcrRunning;

var frameFileName = Path.Combine(Path.GetTempPath(), "se_video_ocr_test_" + Guid.NewGuid() + ".jpg");
try
{
await ExtractSingleFrame(frameFileName, PreviewPositionSeconds, cancellationToken);
if (!File.Exists(frameFileName) || new FileInfo(frameFileName).Length == 0)
{
throw new Exception("Could not extract the current frame - see log for the ffmpeg command line.");
}

var group = new VideoOcrFrameGroup { RepresentativeFileName = frameFileName };
await OcrGroups(new List<VideoOcrFrameGroup> { group }, () => { }, _ => { }, cancellationToken);

ProgressText = string.IsNullOrWhiteSpace(group.Text)
? Se.Language.Video.VideoOcr.TestOcrNoTextFound
: string.Format(Se.Language.Video.VideoOcr.TestOcrResultX, group.Text.ReplaceLineEndings(" | "));
}
catch (OperationCanceledException)
{
ProgressText = string.Empty;
}
catch (Exception exception)
{
Se.LogError(exception, "Video OCR: test on current frame failed");
ProgressText = string.Empty;

await MessageBox.Show(
Window!,
Se.Language.General.Error,
exception.Message,
MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
finally
{
IsRunning = false;

try
{
File.Delete(frameFileName);
}
catch
{
// ignore
}
}
}

private async Task ExtractSingleFrame(string outputFileName, double positionSeconds, CancellationToken cancellationToken)
{
var scale = string.Empty;
var maxImageWidth = Se.Settings.Video.VideoOcr.MaxImageWidth;
if (maxImageWidth > 0 && SelectionWidth > maxImageWidth)
{
scale = $",scale={maxImageWidth}:-2";
}

// -ss before -i: seek in the demuxer, so a test frame late in a long video is still fast.
var arguments = $"-nostdin -y -ss {positionSeconds.ToString("0.###", CultureInfo.InvariantCulture)} " +
$"-i \"{_videoFileName}\" " +
$"-vf \"crop={SelectionWidth}:{SelectionHeight}:{SelectionX}:{SelectionY}{scale}\" " +
$"-frames:v 1 -q:v 2 \"{outputFileName}\"";

Se.WriteToolsLog("Video OCR: extracting test frame - ffmpeg " + arguments);
var process = FfmpegGenerator.GetProcess(arguments, (_, _) => { });

#pragma warning disable CA1416 // Validate platform compatibility
process.Start();
#pragma warning restore CA1416
process.BeginOutputReadLine();
process.BeginErrorReadLine();

try
{
await process.WaitForExitAsync(cancellationToken);
}
catch (OperationCanceledException)
{
try
{
if (!process.HasExited)
{
process.Kill(true);
}
}
catch
{
// ignore
}

throw;
}
}

[RelayCommand]
private async Task StartOcr()
{
Expand Down Expand Up @@ -692,6 +812,15 @@ void AddPreviewLine(VideoOcrFrameGroup group)
ProgressText = string.Format(Se.Language.Video.VideoOcr.RunningOcrXY, 0, ocrGroups.Count);
});

await OcrGroups(ocrGroups, ReportOcrProgress, AddPreviewLine, cancellationToken);
}

/// <summary>
/// Runs the selected OCR engine over the given frame groups, storing each result in
/// <see cref="VideoOcrFrameGroup.Text"/>. Shared by the full scan and the single-frame test.
/// </summary>
private async Task OcrGroups(List<VideoOcrFrameGroup> ocrGroups, Action reportProgress, Action<VideoOcrFrameGroup> addPreviewLine, CancellationToken cancellationToken)
{
var engineType = SelectedEngine.EngineType;
if (engineType is OcrEngineType.PaddleOcrStandalone or OcrEngineType.PaddleOcrPython)
{
Expand All @@ -708,8 +837,8 @@ void AddPreviewLine(VideoOcrFrameGroup group)
if (group != null)
{
group.Text = VideoOcrLineBuilder.CleanOcrResult(p.Text);
ReportOcrProgress();
AddPreviewLine(group);
reportProgress();
addPreviewLine(group);
}
});

Expand All @@ -731,14 +860,14 @@ void AddPreviewLine(VideoOcrFrameGroup group)
var ollamaOcr = new OllamaOcr(Se.Settings.Ocr.OllamaOcrTimeoutMinutes);
await RunLlmOcr(ocrGroups, group => OcrWithBitmap(group, bitmap =>
ollamaOcr.Ocr(bitmap, OllamaUrl, OllamaModel, OllamaLanguage, cancellationToken)),
() => ollamaOcr.Error, ReportOcrProgress, AddPreviewLine, cancellationToken);
() => ollamaOcr.Error, reportProgress, addPreviewLine, cancellationToken);
}
else if (engineType == OcrEngineType.Glm)
{
var glmOcr = new GlmOcr(GlmApiKey);
await RunLlmOcr(ocrGroups, group =>
glmOcr.Ocr(group.RepresentativeFileName, GlmUrl, GlmModel, GlmLanguage, cancellationToken),
() => glmOcr.Error, ReportOcrProgress, AddPreviewLine, cancellationToken);
() => glmOcr.Error, reportProgress, addPreviewLine, cancellationToken);
}
else if (engineType == OcrEngineType.LlamaCpp)
{
Expand All @@ -750,7 +879,7 @@ await RunLlmOcr(ocrGroups, group =>
var prompt = Se.Settings.Ocr.LlamaCppOcrPrompt;
await RunLlmOcr(ocrGroups, group => OcrWithBitmap(group, bitmap =>
llamaCppOcr.Ocr(bitmap, url, modelName, LlamaCppLanguage, prompt, cancellationToken)),
() => llamaCppOcr.Error, ReportOcrProgress, AddPreviewLine, cancellationToken);
() => llamaCppOcr.Error, reportProgress, addPreviewLine, cancellationToken);
}
}

Expand Down Expand Up @@ -915,6 +1044,34 @@ private void Ok()
Window?.Close();
}

/// <summary>Removes the given result lines and renumbers the rest (Delete key in the grid).</summary>
internal void DeleteLines(List<VideoOcrLineItem> items)
{
if (IsRunning || items.Count == 0)
{
return;
}

foreach (var item in items)
{
Lines.Remove(item);
}

var number = 1;
foreach (var line in Lines)
{
line.Number = number++;
}

IsOkEnabled = Lines.Count > 0;
}

/// <summary>Moves the preview to the given line's start time (double-click in the grid).</summary>
internal void SeekPreview(VideoOcrLineItem item)
{
PreviewPositionSeconds = Math.Clamp(item.StartTime.TotalSeconds, 0, DurationSeconds);
}

[RelayCommand]
private async Task Cancel()
{
Expand Down
27 changes: 25 additions & 2 deletions src/ui/Features/Video/VideoOcr/VideoOcrWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Nikse.SubtitleEdit.Logic.Config;
using Nikse.SubtitleEdit.Logic.LlamaCpp;
using Nikse.SubtitleEdit.Logic.ValueConverters;
using System.Linq;

namespace Nikse.SubtitleEdit.Features.Video.VideoOcr;

Expand Down Expand Up @@ -157,6 +158,9 @@ private static Border MakePreviewView(VideoOcrViewModel vm)
UiUtil.MakeButton(Se.Language.Video.VideoOcr.BottomThird, vm.SetScanAreaBottomThirdCommand),
UiUtil.MakeButton(Se.Language.Video.VideoOcr.BottomHalf, vm.SetScanAreaBottomHalfCommand),
UiUtil.MakeButton(Se.Language.Video.VideoOcr.FullFrame, vm.SetScanAreaFullFrameCommand),
UiUtil.MakeButton(Se.Language.Video.VideoOcr.TestOcr, vm.TestOcrCommand)
.WithBindEnabled(nameof(vm.IsRunning), new InverseBooleanConverter())
.WithMarginLeft(10),
scanAreaText,
},
};
Expand Down Expand Up @@ -366,7 +370,7 @@ private static Border MakeLinesView(VideoOcrViewModel vm)
CanUserResizeColumns = true,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
IsReadOnly = true,
IsReadOnly = false, // the text column is editable so OCR mistakes can be fixed in place
DataContext = vm,
ItemsSource = vm.Lines,
Columns =
Expand Down Expand Up @@ -404,12 +408,31 @@ private static Border MakeLinesView(VideoOcrViewModel vm)
Header = Se.Language.General.Text,
CellTheme = UiUtil.DataGridNoBorderNoPaddingCellTheme,
Binding = new Binding(nameof(VideoOcrLineItem.Text)),
IsReadOnly = true,
IsReadOnly = false,
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
},
},
};

// Double-click a line to see the frame it came from in the preview.
dataGrid.DoubleTapped += (s, e) =>
{
if (dataGrid.SelectedItem is VideoOcrLineItem item)
{
vm.SeekPreview(item);
}
};

// Delete removes the selected lines (unless a cell edit is in progress).
dataGrid.KeyDown += (s, e) =>
{
if (e.Key == Avalonia.Input.Key.Delete && e.Source is not TextBox)
{
vm.DeleteLines(dataGrid.SelectedItems.OfType<VideoOcrLineItem>().ToList());
e.Handled = true;
}
};

return UiUtil.MakeBorderForControl(dataGrid);
}

Expand Down
8 changes: 8 additions & 0 deletions src/ui/Logic/Config/Language/Video/LanguageVideoOcr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ public class LanguageVideoOcr
public string UnableToReadVideoMessage { get; set; }
public string AbortOcrTitle { get; set; }
public string AbortOcrMessage { get; set; }
public string TestOcr { get; set; }
public string TestOcrRunning { get; set; }
public string TestOcrResultX { get; set; }
public string TestOcrNoTextFound { get; set; }

public LanguageVideoOcr()
{
Expand Down Expand Up @@ -67,5 +71,9 @@ public LanguageVideoOcr()
UnableToReadVideoMessage = "Could not read video info from: {0}";
AbortOcrTitle = "Abort OCR?";
AbortOcrMessage = "Do you want to abort the running OCR?";
TestOcr = "Test current frame";
TestOcrRunning = "Testing OCR on current frame...";
TestOcrResultX = "Test result: {0}";
TestOcrNoTextFound = "Test: no text found in the scan area";
}
}