This commit is contained in:
Nikolaj Olsson 2023-12-18 17:03:13 +01:00
commit b0217b3380
5 changed files with 197 additions and 14 deletions

View File

@ -425,7 +425,7 @@ namespace Test.Logic
[TestMethod] [TestMethod]
public void TestFirstWithin() public void TestFirstWithin()
{ {
var shotChangesFrames = new List<int>() { 1000, 10000, 20000 }; var shotChangesFrames = new List<int>() { 50, 1000, 3000, 5000, 10000, 11000, 14000, 15000, 16000, 20000, 22000 };
int? First(int start, int end) int? First(int start, int end)
{ {
@ -455,6 +455,47 @@ namespace Test.Logic
Assert.AreEqual(First(20001, 20001), shotChangesFrames.FirstWithin(20001, 20001)); Assert.AreEqual(First(20001, 20001), shotChangesFrames.FirstWithin(20001, 20001));
Assert.AreEqual(First(30000, 30000), shotChangesFrames.FirstWithin(30000, 30000)); Assert.AreEqual(First(30000, 30000), shotChangesFrames.FirstWithin(30000, 30000));
Assert.AreEqual(First(30000, 40000), shotChangesFrames.FirstWithin(30000, 40000)); Assert.AreEqual(First(30000, 40000), shotChangesFrames.FirstWithin(30000, 40000));
Assert.AreEqual(First(25000, 5000), shotChangesFrames.FirstWithin(25000, 5000));
Assert.AreEqual(First(50, 60), shotChangesFrames.FirstWithin(50, 60));
Assert.AreEqual(First(50, 1100), shotChangesFrames.FirstWithin(50, 1100));
}
[TestMethod]
public void TestLastWithin()
{
var shotChangesFrames = new List<int>() { 50, 1000, 3000, 5000, 10000, 11000, 14000, 15000, 16000, 20000, 22000 };
int? Last(int start, int end)
{
try
{
return shotChangesFrames.Last(x => x >= start && x <= end);
}
catch (InvalidOperationException)
{
return null;
}
}
Assert.AreEqual(Last(-100, 100000), shotChangesFrames.LastWithin(-100, 100000));
Assert.AreEqual(Last(0, 100), shotChangesFrames.LastWithin(0, 100));
Assert.AreEqual(Last(0, 1000), shotChangesFrames.LastWithin(0, 1000));
Assert.AreEqual(Last(1000, 1000), shotChangesFrames.LastWithin(1000, 1000));
Assert.AreEqual(Last(1000, 1100), shotChangesFrames.LastWithin(1000, 1100));
Assert.AreEqual(Last(1100, 900), shotChangesFrames.LastWithin(1100, 900));
Assert.AreEqual(Last(900, 15000), shotChangesFrames.LastWithin(900, 15000));
Assert.AreEqual(Last(900, 30000), shotChangesFrames.LastWithin(900, 30000));
Assert.AreEqual(Last(19999, 19999), shotChangesFrames.LastWithin(19999, 19999));
Assert.AreEqual(Last(19999, 20000), shotChangesFrames.LastWithin(19999, 20000));
Assert.AreEqual(Last(20000, 20000), shotChangesFrames.LastWithin(20000, 20000));
Assert.AreEqual(Last(20000, 20001), shotChangesFrames.LastWithin(20000, 20001));
Assert.AreEqual(Last(19999, 20001), shotChangesFrames.LastWithin(19999, 20001));
Assert.AreEqual(Last(20001, 20001), shotChangesFrames.LastWithin(20001, 20001));
Assert.AreEqual(Last(30000, 30000), shotChangesFrames.LastWithin(30000, 30000));
Assert.AreEqual(Last(30000, 40000), shotChangesFrames.LastWithin(30000, 40000));
Assert.AreEqual(Last(25000, 5000), shotChangesFrames.LastWithin(25000, 5000));
Assert.AreEqual(Last(50, 60), shotChangesFrames.LastWithin(50, 60));
Assert.AreEqual(Last(50, 1100), shotChangesFrames.LastWithin(50, 1100));
} }
} }
} }

View File

@ -153,7 +153,7 @@ namespace Nikse.SubtitleEdit.Core.Common
} }
// First within (int) // First / last within (int)
public static int? FirstWithin(this List<int> orderedList, int start, int end) public static int? FirstWithin(this List<int> orderedList, int start, int end)
{ {
@ -177,5 +177,28 @@ namespace Nikse.SubtitleEdit.Core.Common
return orderedList[startIndex]; return orderedList[startIndex];
} }
public static int? LastWithin(this List<int> orderedList, int start, int end)
{
int startIndex = orderedList.BinarySearch(start);
int endIndex = orderedList.BinarySearch(end);
if (startIndex < 0)
{
startIndex = ~startIndex;
}
if (endIndex < 0)
{
endIndex = ~endIndex - 1;
}
if (startIndex > endIndex || startIndex >= orderedList.Count || orderedList[startIndex] > end || endIndex >= orderedList.Count)
{
return null;
}
return orderedList[endIndex];
}
} }
} }

View File

@ -2927,6 +2927,7 @@ $HorzAlign = Center
public bool AlignTimeCodes { get; set; } public bool AlignTimeCodes { get; set; }
public bool ExtractExactTimeCodes { get; set; } public bool ExtractExactTimeCodes { get; set; }
public bool SnapToShotChanges { get; set; } public bool SnapToShotChanges { get; set; }
public int OverlapThreshold { get; set; }
public BeautifyTimeCodesProfile Profile { get; set; } public BeautifyTimeCodesProfile Profile { get; set; }
public BeautifyTimeCodesSettings() public BeautifyTimeCodesSettings()
@ -2934,6 +2935,7 @@ $HorzAlign = Center
AlignTimeCodes = true; AlignTimeCodes = true;
ExtractExactTimeCodes = false; ExtractExactTimeCodes = false;
SnapToShotChanges = true; SnapToShotChanges = true;
OverlapThreshold = 1000;
Profile = new BeautifyTimeCodesProfile(); Profile = new BeautifyTimeCodesProfile();
} }
@ -8757,6 +8759,12 @@ $HorzAlign = Center
settings.BeautifyTimeCodes.SnapToShotChanges = Convert.ToBoolean(subNode.InnerText, CultureInfo.InvariantCulture); settings.BeautifyTimeCodes.SnapToShotChanges = Convert.ToBoolean(subNode.InnerText, CultureInfo.InvariantCulture);
} }
subNode = node.SelectSingleNode("OverlapThreshold");
if (subNode != null)
{
settings.BeautifyTimeCodes.OverlapThreshold = Convert.ToInt32(subNode.InnerText, CultureInfo.InvariantCulture);
}
var profileNode = node.SelectSingleNode("Profile"); var profileNode = node.SelectSingleNode("Profile");
if (profileNode != null) if (profileNode != null)
{ {
@ -12327,6 +12335,7 @@ $HorzAlign = Center
textWriter.WriteElementString("AlignTimeCodes", settings.BeautifyTimeCodes.AlignTimeCodes.ToString(CultureInfo.InvariantCulture)); textWriter.WriteElementString("AlignTimeCodes", settings.BeautifyTimeCodes.AlignTimeCodes.ToString(CultureInfo.InvariantCulture));
textWriter.WriteElementString("ExtractExactTimeCodes", settings.BeautifyTimeCodes.ExtractExactTimeCodes.ToString(CultureInfo.InvariantCulture)); textWriter.WriteElementString("ExtractExactTimeCodes", settings.BeautifyTimeCodes.ExtractExactTimeCodes.ToString(CultureInfo.InvariantCulture));
textWriter.WriteElementString("SnapToShotChanges", settings.BeautifyTimeCodes.SnapToShotChanges.ToString(CultureInfo.InvariantCulture)); textWriter.WriteElementString("SnapToShotChanges", settings.BeautifyTimeCodes.SnapToShotChanges.ToString(CultureInfo.InvariantCulture));
textWriter.WriteElementString("OverlapThreshold", settings.BeautifyTimeCodes.OverlapThreshold.ToString(CultureInfo.InvariantCulture));
textWriter.WriteStartElement("Profile", string.Empty); textWriter.WriteStartElement("Profile", string.Empty);
textWriter.WriteElementString("Gap", settings.BeautifyTimeCodes.Profile.Gap.ToString(CultureInfo.InvariantCulture)); textWriter.WriteElementString("Gap", settings.BeautifyTimeCodes.Profile.Gap.ToString(CultureInfo.InvariantCulture));
textWriter.WriteElementString("InCuesGap", settings.BeautifyTimeCodes.Profile.InCuesGap.ToString(CultureInfo.InvariantCulture)); textWriter.WriteElementString("InCuesGap", settings.BeautifyTimeCodes.Profile.InCuesGap.ToString(CultureInfo.InvariantCulture));

View File

@ -108,6 +108,25 @@ namespace Nikse.SubtitleEdit.Core.Forms
} }
var distance = rightParagraph.StartTime.TotalMilliseconds - leftParagraph.EndTime.TotalMilliseconds; var distance = rightParagraph.StartTime.TotalMilliseconds - leftParagraph.EndTime.TotalMilliseconds;
// Check if there is an overlap
if (distance < 0)
{
// If an overlap threshold is set, don't connect if threshold exceeded
if (Configuration.Settings.BeautifyTimeCodes.OverlapThreshold > 0 && Math.Abs(distance) >= Configuration.Settings.BeautifyTimeCodes.OverlapThreshold)
{
return false;
}
else
{
// We are continuing, but there is still an overlap, so fix that first
leftParagraph.EndTime.TotalMilliseconds = rightParagraph.StartTime.TotalMilliseconds - 1;
// Re-calculate distance
distance = rightParagraph.StartTime.TotalMilliseconds - leftParagraph.EndTime.TotalMilliseconds;
}
}
var subtitlesAreConnected = distance < Configuration.Settings.BeautifyTimeCodes.Profile.ConnectedSubtitlesTreatConnected; var subtitlesAreConnected = distance < Configuration.Settings.BeautifyTimeCodes.Profile.ConnectedSubtitlesTreatConnected;
if (subtitlesAreConnected) if (subtitlesAreConnected)
@ -126,14 +145,14 @@ namespace Nikse.SubtitleEdit.Core.Forms
var leftInCueFrame = MillisecondsToFrames(leftParagraph.StartTime.TotalMilliseconds); var leftInCueFrame = MillisecondsToFrames(leftParagraph.StartTime.TotalMilliseconds);
var rightOutCueFrame = MillisecondsToFrames(rightParagraph.EndTime.TotalMilliseconds); var rightOutCueFrame = MillisecondsToFrames(rightParagraph.EndTime.TotalMilliseconds);
// Check result // Define align function for reusing
if (bestLeftOutCueFrameInfo.result == FindBestCueResult.SnappedToRedZone && bestRightInCueFrameInfo.result == FindBestCueResult.SnappedToRedZone) void AlignCuesAroundClosestShotChange(int leftShotChangeFrame, int rightShotChangeFrame)
{ {
var fixInfoForLeft = GetFixedConnectedSubtitlesCueFrames(leftParagraph, rightParagraph, bestLeftOutCueFrameInfo.cueFrame); var fixInfoForLeft = GetFixedConnectedSubtitlesCueFrames(leftParagraph, rightParagraph, leftShotChangeFrame);
var fixInfoForRight = GetFixedConnectedSubtitlesCueFrames(leftParagraph, rightParagraph, bestRightInCueFrameInfo.cueFrame); var fixInfoForRight = GetFixedConnectedSubtitlesCueFrames(leftParagraph, rightParagraph, rightShotChangeFrame);
// Both are in red zones! We will use the closest shot change to align the cues around // Calculate which shot change is closer
if (Math.Abs(newLeftOutCueFrame - bestLeftOutCueFrameInfo.cueFrame) <= Math.Abs(newRightInCueFrame - bestRightInCueFrameInfo.cueFrame)) if (Math.Abs(newLeftOutCueFrame - leftShotChangeFrame) <= Math.Abs(newRightInCueFrame - rightShotChangeFrame))
{ {
// Align around the left shot change // Align around the left shot change
// Except, when the left subtitle now becomes invalid (negative duration) and the right subtitle won't, we will use the right shot change anyway // Except, when the left subtitle now becomes invalid (negative duration) and the right subtitle won't, we will use the right shot change anyway
@ -168,6 +187,13 @@ namespace Nikse.SubtitleEdit.Core.Forms
} }
} }
} }
// Check result
if (bestLeftOutCueFrameInfo.result == FindBestCueResult.SnappedToRedZone && bestRightInCueFrameInfo.result == FindBestCueResult.SnappedToRedZone)
{
// Both are in red zones! We will use the closest shot change to align the cues around
AlignCuesAroundClosestShotChange(bestLeftOutCueFrameInfo.cueFrame, bestRightInCueFrameInfo.cueFrame);
}
else if ((bestLeftOutCueFrameInfo.result == FindBestCueResult.SnappedToLeftGreenZone || bestLeftOutCueFrameInfo.result == FindBestCueResult.SnappedToRightGreenZone) && else if ((bestLeftOutCueFrameInfo.result == FindBestCueResult.SnappedToLeftGreenZone || bestLeftOutCueFrameInfo.result == FindBestCueResult.SnappedToRightGreenZone) &&
(bestRightInCueFrameInfo.result == FindBestCueResult.SnappedToLeftGreenZone || bestRightInCueFrameInfo.result == FindBestCueResult.SnappedToRightGreenZone)) (bestRightInCueFrameInfo.result == FindBestCueResult.SnappedToLeftGreenZone || bestRightInCueFrameInfo.result == FindBestCueResult.SnappedToRightGreenZone))
{ {
@ -273,7 +299,24 @@ namespace Nikse.SubtitleEdit.Core.Forms
} }
else if (bestLeftOutCueFrameInfo.result == FindBestCueResult.SnappedToLeftGreenZone) else if (bestLeftOutCueFrameInfo.result == FindBestCueResult.SnappedToLeftGreenZone)
{ {
throw new InvalidOperationException("The left out cue cannot be snapped to the left side of a green zone while the right in cue is unaffected at the same time."); // The left out cue wants to go backward, while the right in cue requires no action... The "Treat as connected" setting is probably really high.
// We'll use this function in case there are any shot changes closer to the right in cue.
var lastShotChangeInBetween = GetLastShotChangeFrameInBetween(bestLeftOutCueFrameInfo.cueFrame, bestRightInCueFrameInfo.cueFrame);
if (lastShotChangeInBetween != null)
{
// Derive left out cue shot change
var leftOutCueShotChange = bestLeftOutCueFrameInfo.cueFrame + Configuration.Settings.BeautifyTimeCodes.Profile.ConnectedSubtitlesLeftGreenZone;
// We will use the closest shot change to align the cues around
AlignCuesAroundClosestShotChange(leftOutCueShotChange, lastShotChangeInBetween.Value);
}
else
{
// There are no shot changes at all between the two cues! That likely means they are overlapping.
// Since the left out cue requires a change, we'll accommodate that: put the right in cue on the edge of the green zone, and push the previous subtitle backward.
newRightInCueFrame = bestLeftOutCueFrameInfo.cueFrame;
newLeftOutCueFrame = newRightInCueFrame - Configuration.Settings.BeautifyTimeCodes.Profile.Gap;
}
} }
else if (bestLeftOutCueFrameInfo.result == FindBestCueResult.SnappedToRightGreenZone) else if (bestLeftOutCueFrameInfo.result == FindBestCueResult.SnappedToRightGreenZone)
{ {
@ -289,7 +332,24 @@ namespace Nikse.SubtitleEdit.Core.Forms
} }
else if (bestRightInCueFrameInfo.result == FindBestCueResult.SnappedToRightGreenZone) else if (bestRightInCueFrameInfo.result == FindBestCueResult.SnappedToRightGreenZone)
{ {
throw new InvalidOperationException("The right in cue cannot be snapped to the right side of a green zone while the left out cue is unaffected at the same time."); // The right in cue wants to go forward, while the left out cue requires no action... The "Treat as connected" setting is probably really high.
// We'll use this function in case there are any shot changes closer to the left out cue.
var firstShotChangeInBetween = GetFirstShotChangeFrameInBetween(bestLeftOutCueFrameInfo.cueFrame, bestRightInCueFrameInfo.cueFrame);
if (firstShotChangeInBetween != null)
{
// Derive right in cue shot change
var rightInCueShotChange = bestRightInCueFrameInfo.cueFrame - Configuration.Settings.BeautifyTimeCodes.Profile.ConnectedSubtitlesRightGreenZone;
// We will use the closest shot change to align the cues around
AlignCuesAroundClosestShotChange(firstShotChangeInBetween.Value, rightInCueShotChange);
}
else
{
// There are no shot changes at all between the two cues! That likely means they are overlapping.
// Since the right in cue requires a change, we'll accommodate that: put the left out cue on the edge of the green zone, and push the next subtitle forward.
newLeftOutCueFrame = bestRightInCueFrameInfo.cueFrame;
newRightInCueFrame = newLeftOutCueFrame + Configuration.Settings.BeautifyTimeCodes.Profile.Gap;
}
} }
else else
{ {
@ -422,6 +482,23 @@ namespace Nikse.SubtitleEdit.Core.Forms
return false; return false;
} }
var distance = rightParagraph.StartTime.TotalMilliseconds - leftParagraph.EndTime.TotalMilliseconds;
// Check if there is an overlap
if (distance < 0)
{
// If an overlap threshold is set, don't chain if threshold exceeded
if (Configuration.Settings.BeautifyTimeCodes.OverlapThreshold > 0 && Math.Abs(distance) >= Configuration.Settings.BeautifyTimeCodes.OverlapThreshold)
{
return false;
}
else
{
// We are continuing, but there is still an overlap, so fix that first
leftParagraph.EndTime.TotalMilliseconds = rightParagraph.StartTime.TotalMilliseconds - 1;
}
}
var newLeftOutCueFrame = MillisecondsToFrames(leftParagraph.EndTime.TotalMilliseconds); var newLeftOutCueFrame = MillisecondsToFrames(leftParagraph.EndTime.TotalMilliseconds);
var newRightInCueFrame = MillisecondsToFrames(rightParagraph.StartTime.TotalMilliseconds); var newRightInCueFrame = MillisecondsToFrames(rightParagraph.StartTime.TotalMilliseconds);
@ -716,8 +793,19 @@ namespace Nikse.SubtitleEdit.Core.Forms
var previousParagraph = _subtitle.Paragraphs.ElementAtOrDefault(index - 1); var previousParagraph = _subtitle.Paragraphs.ElementAtOrDefault(index - 1);
if (previousParagraph != null) if (previousParagraph != null)
{ {
var previousOutCueFrame = MillisecondsToFrames(previousParagraph.EndTime.TotalMilliseconds); var distance = previousParagraph.StartTime.TotalMilliseconds - paragraph.EndTime.TotalMilliseconds;
newCueFrame = Math.Max(bestCueFrame, previousOutCueFrame + Configuration.Settings.BeautifyTimeCodes.Profile.Gap);
// If an overlap threshold is set, don't fix if threshold exceeded
if (distance < 0 && Configuration.Settings.BeautifyTimeCodes.OverlapThreshold > 0 && Math.Abs(distance) >= Configuration.Settings.BeautifyTimeCodes.OverlapThreshold)
{
newCueFrame = bestCueFrame;
}
else
{
// Else, limit to adjacent subtitle
var previousOutCueFrame = MillisecondsToFrames(previousParagraph.EndTime.TotalMilliseconds);
newCueFrame = Math.Max(bestCueFrame, previousOutCueFrame + Configuration.Settings.BeautifyTimeCodes.Profile.Gap);
}
} }
else else
{ {
@ -729,8 +817,19 @@ namespace Nikse.SubtitleEdit.Core.Forms
var nextParagraph = _subtitle.Paragraphs.ElementAtOrDefault(index + 1); var nextParagraph = _subtitle.Paragraphs.ElementAtOrDefault(index + 1);
if (nextParagraph != null) if (nextParagraph != null)
{ {
var nextInCueFrame = MillisecondsToFrames(nextParagraph.StartTime.TotalMilliseconds); var distance = paragraph.StartTime.TotalMilliseconds - nextParagraph.EndTime.TotalMilliseconds;
newCueFrame = Math.Min(bestCueFrame, nextInCueFrame - Configuration.Settings.BeautifyTimeCodes.Profile.Gap);
// If an overlap threshold is set, don't fix if threshold exceeded
if (distance < 0 && Configuration.Settings.BeautifyTimeCodes.OverlapThreshold > 0 && Math.Abs(distance) >= Configuration.Settings.BeautifyTimeCodes.OverlapThreshold)
{
newCueFrame = bestCueFrame;
}
else
{
// Else, limit to adjacent subtitle
var nextInCueFrame = MillisecondsToFrames(nextParagraph.StartTime.TotalMilliseconds);
newCueFrame = Math.Min(bestCueFrame, nextInCueFrame - Configuration.Settings.BeautifyTimeCodes.Profile.Gap);
}
} }
else else
{ {
@ -902,6 +1001,16 @@ namespace Nikse.SubtitleEdit.Core.Forms
return _shotChangesFrames.FirstWithin(leftCueFrame, rightCueFrame); return _shotChangesFrames.FirstWithin(leftCueFrame, rightCueFrame);
} }
private int? GetLastShotChangeFrameInBetween(int leftCueFrame, int rightCueFrame)
{
if (_shotChangesFrames == null || _shotChangesFrames.Count == 0)
{
return null;
}
return _shotChangesFrames.LastWithin(leftCueFrame, rightCueFrame);
}
private int? GetClosestShotChangeFrame(int cueFrame) private int? GetClosestShotChangeFrame(int cueFrame)
{ {
if (_shotChangesFrames == null || _shotChangesFrames.Count == 0) if (_shotChangesFrames == null || _shotChangesFrames.Count == 0)

View File

@ -1068,6 +1068,7 @@ Command line: {1} {2}
<XRequiresALocalWebServer>"{0}" 需要在本地运行的网页服务器!</XRequiresALocalWebServer> <XRequiresALocalWebServer>"{0}" 需要在本地运行的网页服务器!</XRequiresALocalWebServer>
<XRequiresAnApiKey>"{0}"需要一个API密钥。</XRequiresAnApiKey> <XRequiresAnApiKey>"{0}"需要一个API密钥。</XRequiresAnApiKey>
<ReadMore>了解更多?</ReadMore> <ReadMore>了解更多?</ReadMore>
<Formality>正式程度</Formality>
</GoogleTranslate> </GoogleTranslate>
<GoogleOrMicrosoftTranslate> <GoogleOrMicrosoftTranslate>
<Title>谷歌 与 微软 翻译</Title> <Title>谷歌 与 微软 翻译</Title>