/// <summary> /// Removes the overlappings in one list. /// /// Example: /// /// sub1 is in list1 /// sub2 and sub3 are in list2 /// /// sub1 overlaps with sub2, sub3 /// + sub2 and sub3 overlap /// /// An example of this in the real worlds are announcements while the main characters are speaking. /// We obviously only want to map the character-dialog and the announcement to its own translation. /// /// </summary> static void RemoveOverlappings(List <ExtendedLineInfo> primaryList) { foreach (ExtendedLineInfo eli in primaryList) { ExtendedLineInfo leastFittingLine; do { leastFittingLine = null; // the function will group all overlapping lines together, so we delete the worst // line from that container and recalculate the containers (because overlapping could // have been stopped) var nonOverlappingLineContainers = UtilsSubtitle.GetNonOverlappingTimeSpans(eli.matchingLines, 0.2); foreach (var lineContainer in nonOverlappingLineContainers) { if (lineContainer.TimeSpans.Count > 1) { leastFittingLine = GetLeastFittingLine(eli.lineInfo, lineContainer.TimeSpans); break; } } // rebuild matching lines without the removed line if (leastFittingLine != null) { eli.matchingLines.Remove(leastFittingLine); continue; } } while(leastFittingLine != null); } }
/// <summary> /// By calling this function, all references to LineInfo in "listToChange" passed in /// constructor can be changed. Retiming includes removing/introducing commercial breaks. /// /// By using the "noSplitting=true" option, the whole list gets shifted as one. /// </summary> public void Retime(bool noSplitting = false) { var stopwatch = new System.Diagnostics.Stopwatch(); stopwatch.Start(); var queue = new Queue <RemainingSlice>(); // initilize first slice (with all lines) RemainingSlice fullSlice = new RemainingSlice(new List <LineInfo>(m_referenceList), new List <LineInfo>(m_listToChange)); queue.Enqueue(fullSlice); // just find best offset if (noSplitting) { double bestOffset = FindBestOffset(fullSlice); UtilsSubtitle.ShiftByTime(m_listToChange, bestOffset); return; } // Only lines that are in the same slice (list of lines on both sides) can be matched. // When processing the slices, a consecutive row of lines is recognized as "good". Two new // slices (one left of the right of the row) are then created. // // This queue will process all these slices that have some elements in them. while (queue.Count > 0) { RemainingSlice slice = queue.Dequeue(); //if(slice.listToChangeLines.Count == 0 || slice.referenceListLines.Count == 0) { // // T O D O: these shouldn't just be moved out of the way but left in another slice // // this is more debug code than anything else // UtilsSubtitle.ShiftByTime(slice.listToChangeLines, 10000); // UtilsSubtitle.ShiftByTime(slice.referenceListLines, 10000); // continue; //} double bestOffset = FindBestOffset(slice); ApplyOffset(slice, bestOffset, queue); } Console.WriteLine(stopwatch.ElapsedMilliseconds); }
/// <summary> /// Align subtitle line by constant value, to audio or to other subtitle based on settings in "perSubSettings". /// </summary> public static void AlignSub(List <LineInfo> lineList, List <LineInfo> referenceList, EpisodeInfo epInfo, Settings settings, PerSubtitleSettings thisSubSettings) { switch (thisSubSettings.AlignMode) { case PerSubtitleSettings.AlignModes.ByConstantValue: UtilsSubtitle.ShiftByTime(lineList, thisSubSettings.SubDelay); break; case PerSubtitleSettings.AlignModes.ToAudio: UtilsAlignSubToAudio alignToAudio = new UtilsAlignSubToAudio(lineList, epInfo.AudioFileDesc); UtilsSubtitle.ShiftByTime(lineList, alignToAudio.GetBestShiftValue()); break; case PerSubtitleSettings.AlignModes.ToSubtitle: if (referenceList == null) { throw new Exception("Can not align subtitle to other non-existent subtitle."); } UtilsAlignSubToSub alignToSub = new UtilsAlignSubToSub(lineList, referenceList); alignToSub.Retime(); break; } }
/** Generates a .tsv file */ public void ExportTextFile(List <CardInfo> cardInfoList, Settings settings, InfoProgress progressInfo) { String tsvFilename = settings.OutputDirectoryPath + Path.DirectorySeparatorChar + settings.DeckName + ".tsv"; Console.WriteLine(tsvFilename); // value that will be imported into Anki/SRS-Programs-Field => [sound:???.ogg] and <img src="???.jpg"/> var snapshotFields = new List <String>(cardInfoList.Count); var audioFields = new List <String>(cardInfoList.Count); foreach (var cardInfo in cardInfoList) { if (cardInfo.HasImage()) { var outputSnapshotFilename = GetSnapshotFileName(settings, cardInfo); snapshotFields.Add("<img src=\"" + outputSnapshotFilename + "\"/>"); // TODO: make this flexible } else { snapshotFields.Add(""); } if (cardInfo.HasAudio()) { var outputAudioFilename = GetAudioFileName(settings, cardInfo); audioFields.Add("[sound:" + outputAudioFilename + "]"); // TODO: make this flexible } else { audioFields.Add(""); } } using (var outputStream = new StreamWriter(tsvFilename)) { for (int i = 0; i < cardInfoList.Count; i++) { CardInfo cardInfo = cardInfoList[i]; // XXX: performance analasys then --- generate a episode-filtered list for context card search (because it has O(n^2) steps) var contextCardsTuple = UtilsSubtitle.GetContextCards(cardInfo.episodeInfo.Index, cardInfo, m_cardInfos); var previousCards = contextCardsTuple.Item1; var nextCards = contextCardsTuple.Item2; var previousCardsNativeLanguage = UtilsSubtitle.CardListToMultilineString(previousCards, UtilsCommon.LanguageType.NATIVE); var previousCardsTargetLanguage = UtilsSubtitle.CardListToMultilineString(previousCards, UtilsCommon.LanguageType.TARGET); var nextCardsNativeLanguage = UtilsSubtitle.CardListToMultilineString(nextCards, UtilsCommon.LanguageType.NATIVE); var nextCardsTargetLanguage = UtilsSubtitle.CardListToMultilineString(nextCards, UtilsCommon.LanguageType.TARGET); String keyField = cardInfo.GetKey(); String audioField = audioFields[i]; String imageField = snapshotFields[i]; String tags = String.Format("SubtitleMemorize {0} ep{1:000} {2}", settings.DeckNameModified, cardInfo.episodeInfo.Number, InfoLanguages.languages[settings.TargetLanguageIndex].tag); outputStream.WriteLine(UtilsCommon.HTMLify(keyField) + "\t" + UtilsCommon.HTMLify(imageField) + "\t" + UtilsCommon.HTMLify(audioField) + "\t" + UtilsCommon.HTMLify(cardInfo.ToSingleLine(UtilsCommon.LanguageType.TARGET)) + "\t" + UtilsCommon.HTMLify(cardInfo.ToSingleLine(UtilsCommon.LanguageType.NATIVE)) + "\t" + UtilsCommon.HTMLify(previousCardsTargetLanguage) + "\t" + UtilsCommon.HTMLify(previousCardsNativeLanguage) + "\t" + UtilsCommon.HTMLify(nextCardsTargetLanguage) + "\t" + UtilsCommon.HTMLify(nextCardsNativeLanguage) + "\t" + UtilsCommon.HTMLify(tags) ); } } }
/// <summary> /// This function will... /// a) move all lines to change in slice by offset /// b) match the lines in "listToChange" and "referenceList" /// c) find a threshold value for matchings /// d) find the longest row of consecutive matchings that are above the threshold /// e) create slices for lines before and lines after this "good row" /// f) reset offset of these remaining lines and put slice into queue /// </summary> private void ApplyOffset(RemainingSlice slice, double offset, Queue <RemainingSlice> queue) { UtilsSubtitle.ShiftByTime(slice.listToChangeLines, offset); // match lines var subtitleMatcherParameters = SubtitleMatcher.GetParameterCache(slice.referenceListLines, slice.listToChangeLines); var biMatchedLinesLinkedList = SubtitleMatcher.MatchSubtitles(subtitleMatcherParameters); var biMatchedLinesList = new List <SubtitleMatcher.BiMatchedLines>(biMatchedLinesLinkedList); biMatchedLinesList.Sort(delegate(SubtitleMatcher.BiMatchedLines x, SubtitleMatcher.BiMatchedLines y) { return(GetStartTime(x) < GetStartTime(y) ? -1 : 1); }); // -------------------------------------------------- // find threshold rating double averageRating = 0; int numRatings = 0; foreach (var biMatchedLines in biMatchedLinesList) { double rating = RateBiMatchedLines(biMatchedLines); if (rating > 0.0001) // ignore "zero" ratings { averageRating += rating; numRatings++; } } averageRating /= numRatings; double thresholdValue = averageRating * 0.8; // -------------------------------------------------- // Find longest row over threshold rating. // // Zero ratings may be inbetween good ratings when some // lines couldn't get matched (for example street-sign // translations that aren't in subtitle file in native language). // These are stepped over: There can be a limited number of zero ratings // ratings in the row except at the beginning and the end (these will // get matched at a different time if possible). int numGoodMatched = 0; int currentRowStart = 0; int allowedZeroRatings = 0; int maxNumGoodMatched = -1; int bestRowStart = 0; for (int index = 0; index < biMatchedLinesList.Count; index++) { var biMatchedLines = biMatchedLinesList[index]; double rating = RateBiMatchedLines(biMatchedLines); if (rating < thresholdValue) { // step over zero ratings if (rating > 0.000001 || allowedZeroRatings-- < 0) { numGoodMatched = 0; // not a zero rating } } else { // update row start/end if (numGoodMatched == 0) { currentRowStart = index; } numGoodMatched = index - currentRowStart + 1; // save best value if (numGoodMatched > maxNumGoodMatched) { maxNumGoodMatched = numGoodMatched; bestRowStart = currentRowStart; } allowedZeroRatings = 4; } } // could good row be found? if (maxNumGoodMatched == -1) { return; } // create new slices left and right RemainingSlice newSlice; // left slice newSlice = GetSubSlice(biMatchedLinesList, 0, bestRowStart); //UtilsSubtitle.ShiftByTime(newSlice.listToChangeLines, -offset); // by disabling this code, remaining slices are now embedded in greater slices queue.Enqueue(newSlice); // right slice newSlice = GetSubSlice(biMatchedLinesList, bestRowStart + maxNumGoodMatched + 1, biMatchedLinesList.Count); //UtilsSubtitle.ShiftByTime(newSlice.listToChangeLines, -offset); queue.Enqueue(newSlice); }