223 lines
No EOL
9.7 KiB
C#
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; }
|
|
}
|
|
} |