evaluatePreAlignedWords static method

Map<int, List<ReciterError>> evaluatePreAlignedWords({
  1. required List<PhonemeGroupAlignment> alignments,
  2. required String fullPhonemes,
  3. required List<int> wordBoundaries,
  4. required String currentAsrText,
  5. required List<double> trackingTimestamps,
  6. required int bestAsrStartIdx,
  7. required int targetCharCursor,
  8. required int startWordId,
  9. required int nextWordId,
  10. required int totalAyahWords,
  11. List<WordTajweedRule> expectedWordRules = const [],
  12. TrackerConfig config = const TrackerConfig(),
})

Evaluates pre-aligned phoneme traces for a specific committed word window.

Implementation

static Map<int, List<ReciterError>> evaluatePreAlignedWords({
  required List<PhonemeGroupAlignment> alignments,
  required String fullPhonemes,
  required List<int> wordBoundaries,
  required String currentAsrText,
  required List<double> trackingTimestamps,
  required int bestAsrStartIdx,
  required int targetCharCursor,
  required int startWordId,
  required int nextWordId,
  required int totalAyahWords,
  List<WordTajweedRule> expectedWordRules = const [],
  TrackerConfig config = const TrackerConfig(),
}) {
  final Map<int, List<ReciterError>> errorsByWord = {};

  for (int w = startWordId; w < nextWordId; w++) {
    if (w < 0 || w >= wordBoundaries.length - 1) continue;

    final int wordRefStart = wordBoundaries[w];
    final int wordRefEnd = (w + 1 < wordBoundaries.length)
        ? wordBoundaries[w + 1]
        : fullPhonemes.length;
    if (wordRefStart >= wordRefEnd) continue;

    final String wordText = fullPhonemes.substring(
      wordRefStart,
      min(wordRefEnd, fullPhonemes.length),
    );

    // 1. Build cohesive phonetic spans for this word
    final List<_PhoneticSpan> spans = _buildWordSpans(
      fullPhonemes: fullPhonemes,
      wordRefStart: wordRefStart,
      wordRefEnd: wordRefEnd,
      expectedWordRules: expectedWordRules,
    );

    final List<ReciterError> wordErrors = [];

    // 2. Evaluate each span with aggregated ASR alignments and durations
    for (final span in spans) {
      // Collect all alignment items belonging to this reference span
      final spanAlignments = alignments.where((a) {
        final absRef = targetCharCursor + a.refIdx;
        return absRef >= span.refStart && absRef < span.refEnd;
      }).toList();

      if (spanAlignments.isEmpty) continue;

      // Collect matched predicted characters and sum actual acoustic duration
      final List<String> predChunks = [];
      final Set<int> usedPredIndices = {};
      double totalSpanDuration = 0.0;
      bool hasDelete = false;

      for (final a in spanAlignments) {
        if (a.opType == 'delete') {
          hasDelete = true;
        }
        final absPred = bestAsrStartIdx + a.predIdx;
        if (absPred >= 0 && absPred < currentAsrText.length) {
          predChunks.add(currentAsrText[absPred]);
          if (!usedPredIndices.contains(absPred)) {
            usedPredIndices.add(absPred);
            if (absPred < trackingTimestamps.length) {
              totalSpanDuration += trackingTimestamps[absPred];
            }
          }
        }
      }

      final String predText = predChunks.join('');

      // Evaluate the span against Madd, Shaddah, Ghunnah, Tashkeel, or Consonants
      final spanErrors = _evaluateSpan(
        span: span,
        predText: predText,
        spanDuration: totalSpanDuration,
        hasDelete: hasDelete,
        wordText: wordText,
        wordRefEnd: wordRefEnd,
        config: config,
      );

      wordErrors.addAll(spanErrors);
    }

    if (wordErrors.isNotEmpty) {
      // Sort errors by UI priority
      wordErrors.sort(
        (a, b) => _getErrorPriority(a).compareTo(_getErrorPriority(b)),
      );

      // Filter out expected ASR noise and surplus duration
      wordErrors.removeWhere((e) {
        if (e.durationStatus == TajweedDurationStatus.surplus) return true;
        if (e.errorType == ErrorCategory.normal) {
          return config.hideExpectedAsrNoise && _isExpectedAsrNoise(e, config);
        }
        return false;
      });

      // Deduplicate identical errors on the same rule/phoneme
      final List<ReciterError> deduplicated = [];
      final Set<String> seenKeys = {};
      for (final e in wordErrors) {
        final key = '${e.errorType.name}_${e.expectedRule?.runtimeType}_${e.expectedPh}';
        if (!seenKeys.contains(key)) {
          seenKeys.add(key);
          deduplicated.add(e);
        }
      }

      if (deduplicated.isNotEmpty) {
        errorsByWord[w] = deduplicated;

        for (var e in deduplicated) {
          String ruleInfo = e.expectedRule != null
              ? ' | Rule: ${e.expectedRule!.name.en} (req: ${e.expectedDuration?.toStringAsFixed(2)}s, got: ${e.actualDuration?.toStringAsFixed(2)}s)'
              : '';
          DebugLogger.log(
            'Error',
            '🚨 [ERROR LOG] Word "$wordText" ($w) | ${e.errorType.name.toUpperCase()} -> ${e.speechErrorType.name.toUpperCase()} (Exp: "${e.expectedPh}" vs Got: "${e.predictedPh}")$ruleInfo',
          );
        }
      }
    }
  }

  return errorsByWord;
}