evaluatePreAlignedWords static method
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(),
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;
}