using System.Collections.ObjectModel;
using Nikse.SubtitleEdit.UiLogic.AutoTranslate;
using Nikse.SubtitleEdit.Core.Common;
using Nikse.SubtitleEdit.UiLogic.Translate;
namespace Nikse.SubtitleEdit.UiLogic.Translate;
public class DoAutoTranslate
{
///
/// Force one-line-at-a-time translation from the start. The UI wires this to the
/// per-engine "translate each line separately" user setting; headless callers
/// (seconv) leave it off and rely on the built-in per-engine defaults below.
///
public bool TranslateEachLineSeparately { get; set; }
/// Optional progress callback: (lines done, total lines). Invoked as translation advances.
public Action? Progress { get; set; }
public async Task> DoTranslate(Subtitle subtitle, TranslationPair sourceLanguage, TranslationPair targetLanguage, IAutoTranslator translator, CancellationToken cancellationToken)
{
try
{
translator.Initialize();
var start = 0;
// Single-line mode forced by the user setting or the engine itself is permanent;
// forceSingleLineMode may additionally be turned on temporarily after errors and
// is only allowed to fall back to merged mode when the permanent flag is off.
var alwaysSingleLineMode = TranslateEachLineSeparately ||
translator.Name ==
NoLanguageLeftBehindApi.StaticName || // NLLB seems to miss some text...
translator.Name == NoLanguageLeftBehindServe.StaticName ||
translator.Name == CrispAsrMadladTranslate.StaticName; // one CLI process per line
var forceSingleLineMode = alwaysSingleLineMode;
var index = start;
var linesTranslated = 0;
var errorCount = 0;
var noErrorCount = 0;
var noProgressCount = 0;
var rows = new ObservableCollection();
foreach (var p in subtitle.Paragraphs)
{
rows.Add(new TranslateRow { Number = p.Number, Show = p.StartTime.TimeSpan, Hide = p.EndTime.TimeSpan, Duration = p.Duration.ToShortDisplayString(), Text = p.Text });
}
// The "advanced" local-LLM engines run their own batch loop: numbered batches with a
// schema-forced JSON reply and rolling context, so MergeAndSplitHelper's merge/split
// heuristics are not needed (and would break the line alignment the schema guarantees).
if (translator is IBatchContextTranslator batchTranslator)
{
while (index < subtitle.Paragraphs.Count && !cancellationToken.IsCancellationRequested)
{
index += await batchTranslator.TranslateBatchAsync(rows, index, sourceLanguage.Code, targetLanguage.Code, cancellationToken);
Progress?.Invoke(Math.Min(index, subtitle.Paragraphs.Count), subtitle.Paragraphs.Count);
}
return rows.ToList();
}
while (index < subtitle.Paragraphs.Count)
{
if (cancellationToken.IsCancellationRequested)
{
break;
}
var linesMergedAndTranslated = 0;
linesMergedAndTranslated = await MergeAndSplitHelper.MergeAndTranslateIfPossible(rows, sourceLanguage, targetLanguage, index, translator, forceSingleLineMode,
cancellationToken);
if (linesMergedAndTranslated > 0)
{
noErrorCount++;
index += linesMergedAndTranslated;
Progress?.Invoke(Math.Min(index, subtitle.Paragraphs.Count), subtitle.Paragraphs.Count);
var index1 = index;
linesTranslated += linesMergedAndTranslated;
errorCount = 0;
noProgressCount = 0;
if (noErrorCount > 7 && !alwaysSingleLineMode)
{
forceSingleLineMode = false;
}
continue;
}
errorCount++;
noErrorCount = 0;
if (errorCount > 3)
{
forceSingleLineMode = true;
}
var translateCount = await MergeAndSplitHelper.MergeAndTranslateIfPossible(
rows,
sourceLanguage,
targetLanguage,
index,
translator,
forceSingleLineMode,
cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return rows.ToList();
}
if (translateCount > 0)
{
index += translateCount;
noProgressCount = 0;
Progress?.Invoke(Math.Min(index, subtitle.Paragraphs.Count), subtitle.Paragraphs.Count);
}
else
{
forceSingleLineMode = true;
// The engine keeps returning nothing for this line without throwing -
// without a cap the loop would retry the same line forever.
noProgressCount++;
if (noProgressCount > 3)
{
throw new Exception($"Translation engine {translator.Name} returned no translation for line {index + 1} after {noProgressCount} attempts");
}
}
}
return rows.ToList();
}
catch (Exception exception)
{
SeLogger.Error(exception, "Auto-translate failed");
throw;
}
}
}