JD2022-TU1/main/tools/framework/JD.Timeline/Karaoke/LyricsChecker.cs

223 lines
No EOL
9.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using JD.Collections.Extensions;
using JD.Timeline.View.Pads;
using TeaBox;
using TeaBox.ObjectModel;
using JD.Core;
using JD.Core.Collections;
using JD.Kernel.Structs;
using JD.LogService;
using JD.Serialization;
namespace JD.Timeline.Model
{
public class LyricsChecker : IDisposable
{
private const char BreakLineChar = '↵';
private const char SpaceChar = '·';
private struct TapeCharacter
{
public char Character;
public KaraokeClip KaraokeClip;
};
private struct ReferenceCharacter
{
public char Character;
public LyricWord LyricWord;
};
private readonly OperationTaskCollector _operationTaskCollector = new OperationTaskCollector();
private readonly ILogger _log;
private TapeCharacter _mismatchTapeCharacter;
private ReferenceCharacter _mismatchReferenceCharacter;
private readonly LogCategory _logCategory;
/// <summary>
/// Constructs the helper to check the lyrics state.
/// </summary>
/// <param name="tape"></param>
/// <param name="lyrics"></param>
/// <param name="log">When specified, the checker logs inside the logger instead of inside the Application error list.</param>
/// <param name="logCategory">By default, logs problems as errors, but can log as warning as well.</param>
public LyricsChecker(Tape tape, Lyrics lyrics, ILogger log = null, LogCategory logCategory = LogCategory.Error)
{
_log = log;
_logCategory = logCategory;
Tape = tape;
Lyrics = lyrics;
tape.Clips.ItemChanged += Clips_ItemChanged;
tape.Clips.CollectionChanged += Clips_CollectionChanged;
UpdateSyncState();
UpdateErrors();
}
public void Dispose()
{
Tape.Clips.ItemChanged -= Clips_ItemChanged;
Tape.Clips.CollectionChanged -= Clips_CollectionChanged;
}
void Clips_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
UpdateSyncState();
UpdateErrors();
}
void Clips_ItemChanged(object sender, ItemChangedEventArgs<Clip> args)
{
UpdateSyncState();
UpdateErrors();
}
private void UpdateSyncState()
{
var charactersInTape = (from clip in Tape.Clips.OfType<KaraokeClip>()
where !string.IsNullOrEmpty(clip.Lyrics)
orderby clip.StartFrame
let lyrics = clip.Lyrics + (clip.IsEndOfLine ? BreakLineChar.ToString() : "")
from character in lyrics.ToCharArray()
select new TapeCharacter { Character = character == ' ' ? SpaceChar : character, KaraokeClip = clip }).ToArray();
var charactersInReference = (from word in Lyrics.AllWords()
let lyrics = word.Value + (word.IsEndOfLine() ? BreakLineChar.ToString() : "")
from character in lyrics.ToCharArray()
select new ReferenceCharacter { Character = character == ' ' ? SpaceChar : character, LyricWord = word }).ToArray();
//We compare tape and reference, character by character
LyricWord wordHavingAnError = null;
for (int i = 0; i < charactersInReference.Length; i++)
{
if (wordHavingAnError == null && i < charactersInTape.Length)
{
if (charactersInTape[i].Character == charactersInReference[i].Character)
{
charactersInReference[i].LyricWord.State = LyricWord.LyricWordState.Valid;
}
else if (charactersInTape[i].Character != charactersInReference[i].Character)
{
charactersInReference[i].LyricWord.State = LyricWord.LyricWordState.Error;
wordHavingAnError = charactersInReference[i].LyricWord;
_mismatchTapeCharacter = charactersInTape[i];
_mismatchReferenceCharacter = charactersInReference[i];
var reference = charactersInReference.Select(x => x.Character);
var actual = charactersInTape.Select(x => x.Character);
Lyrics.UnmetExpectation = CreateExpectationString(reference, actual, i);
}
charactersInReference[i].LyricWord.AssociatedClip = charactersInTape[i].KaraokeClip;
}
else if (charactersInReference[i].LyricWord != wordHavingAnError)
{
charactersInReference[i].LyricWord.State = LyricWord.LyricWordState.Undefined;
charactersInReference[i].LyricWord.AssociatedClip = null;
}
}
//We set the completion state
var wordUndefinedState = Lyrics.AllWords().FirstOrDefault(x => x.State == LyricWord.LyricWordState.Undefined);
if (wordHavingAnError != null)
{
Lyrics.CompletionState = LyricsCompletionState.Error;
}
else if (wordUndefinedState != null)
{
Lyrics.CompletionState = LyricsCompletionState.InProgress;
}
else
{
Lyrics.CompletionState = LyricsCompletionState.Completed;
}
}
/// <summary>
/// Creates an error message looking like that:
///
/// expected: my text
/// actual: my Text
/// ^
/// </summary>
private string CreateExpectationString(IEnumerable<char> referenceChars, IEnumerable<char> actualChars, int indexOfError)
{
int startIndex = Math.Max(indexOfError - 5, 0);
int labelLength = "reference: ".Length;
string reference = "reference: " + new string(referenceChars.Skip(startIndex).Take(10).ToArray());
string actual = "clips: ".PadRight(labelLength) + new string(actualChars.Skip(startIndex).Take(10).ToArray());
string errorLocation = "".PadLeft(labelLength + indexOfError - startIndex) + "^";
return string.Concat(reference, "\n", actual, "\n", errorLocation);
}
private IDisposable AcquireLog(out LoggingContext log)
{
if (_log != null)
{
log = LoggingContext.FromLogger(_log);
return null;
}
return _operationTaskCollector.OperationScope(out log);
}
private void UpdateErrors()
{
LoggingContext loggingContext;
using (AcquireLog(out loggingContext))
{
if (Lyrics.Paragraphs.Count <= 1)
{
loggingContext.Log(
new Error(_logCategory)
{
Origin = new FileLocation(DataRoot.ToAbsolutePath(Tape.Id)),
Message = "The current lyrics don't have any paragraph. Please contact the person in charge of lyrics or change it manually."
});
}
var wordWithError = Lyrics.AllWords().FirstOrDefault(x => x.State == LyricWord.LyricWordState.Error);
if (wordWithError != null && wordWithError.AssociatedClip != null)
{
var message = $"There is an error with the clip '{wordWithError.AssociatedClip.Lyrics}' not matching reference '{wordWithError.Value}'.";
if (_mismatchReferenceCharacter.Character == BreakLineChar && _mismatchTapeCharacter.Character != BreakLineChar)
{
message = $"You forgot the end of line on clip '{_mismatchReferenceCharacter.LyricWord.Value}'.";
}
else if (_mismatchReferenceCharacter.Character != BreakLineChar && _mismatchTapeCharacter.Character == BreakLineChar)
{
message = $"You have an end of line in clip '{_mismatchTapeCharacter.KaraokeClip.Lyrics}' that doesn't exist in reference.";
}
else if (_mismatchReferenceCharacter.Character == SpaceChar && _mismatchTapeCharacter.Character != SpaceChar)
{
message = $"You forgot a space in clip '{_mismatchReferenceCharacter.LyricWord.Value}'.";
}
else if (_mismatchReferenceCharacter.Character != SpaceChar && _mismatchTapeCharacter.Character == SpaceChar)
{
message = $"You have a space in clip '{_mismatchTapeCharacter.KaraokeClip.Lyrics}' that doesn't exist in reference.";
}
loggingContext.Log(
new Error(_logCategory)
{
Origin = new FileLocation(DataRoot.ToAbsolutePath(Tape.Id), new List<uint> { wordWithError.AssociatedClip.Id }),
Message = message
});
var lyricsPad = TeaBox.ObjectModel.Application.Main.Pads.Select(x => x.Content).FirstOf<LyricsPad>();
if (lyricsPad != null)
{
lyricsPad.LyricsControl.ScrollToWord(wordWithError);
}
}
}
}
public Tape Tape { get; set; }
public Lyrics Lyrics { get; set; }
}
}