process method
Implementation
ProcessedAudioStream process(TranscriptionResult result) {
final int maxCount = min(result.tokens.length, result.timestamps.length);
int commonLen = 0;
final int minLen = min(_lastRawTokens.length, maxCount);
for (int i = 0; i < minLen; i++) {
if (_lastRawTokens[i] == result.tokens[i]) {
commonLen++;
} else {
break;
}
}
if (commonLen < _lastRawTokens.length) {
reset();
commonLen = 0;
}
_lastRawTokens = result.tokens.sublist(0, maxCount);
if (commonLen == maxCount) {
return ProcessedAudioStream(
tokens: _filteredTokens,
durations: _tokenDurations,
);
}
double lastBlankTs = _filteredLastBlanks.isNotEmpty ? _filteredLastBlanks.last : -1.0;
for (int i = commonLen; i < maxCount; i++) {
final String tok = result.tokens[i];
final double realTs = max(0.0, result.timestamps[i] - lookaheadDelay);
if (tok.isEmpty ||
tok == '<blank>' ||
tok == '<blk>' ||
tok == '<eps>' ||
tok == 'eps') {
lastBlankTs = realTs;
continue;
}
_filteredTokens.add(tok);
_filteredSpikeTimes.add(realTs);
_filteredLastBlanks.add(lastBlankTs);
final int fIdx = _filteredTokens.length - 1;
final double curSpike = _filteredSpikeTimes[fIdx];
final double lastBlankBefore = _filteredLastBlanks[fIdx];
// ── Max(Backward, Forward) Duration Attribution ──
//
// CTC spikes mark peak posterior probability, NOT sound onset.
// The backward interval (prev_spike → cur_spike) partially
// overlaps with BOTH the previous token's tail AND the current
// token's onset delay. Neither interval alone captures a token's
// full acoustic duration:
//
// - Short Madds (2 Harakat): backward interval is larger because
// it captures the onset delay before the CTC spike fired.
// - Long Madds (4-6 Harakat): forward interval is larger because
// the vowel is held long after the spike until the next sound.
//
// Using max(backward, forward) per token provides a robust
// estimate: whichever interval captured more of the token's
// actual acoustic time wins.
// 1. Retroactively update PREVIOUS token with its forward interval.
// The previous token's duration becomes max(backward, forward).
if (fIdx > 0) {
final int prevIdx = fIdx - 1;
final double prevSpike = _filteredSpikeTimes[prevIdx];
// If a blank (silence) occurred between spikes, the previous
// token's voicing ended at the blank, not at the current spike.
double prevEnd = curSpike;
if (lastBlankBefore > prevSpike && lastBlankBefore < curSpike) {
prevEnd = lastBlankBefore;
}
final double forwardInterval =
min(maxTokenDuration, max(0.04, prevEnd - prevSpike));
// max(backward already stored, forward just computed)
_tokenDurations[prevIdx] =
max(_tokenDurations[prevIdx], forwardInterval);
}
// 2. Current token: backward interval as initial estimate.
// Will be max'd with its forward interval when the next
// token arrives (step 1 above on the next iteration).
double prevSpikeTime = (fIdx == 0)
? max(0.0, curSpike - 0.15)
: _filteredSpikeTimes[fIdx - 1];
if (lastBlankBefore > prevSpikeTime) {
prevSpikeTime = lastBlankBefore;
}
final double backwardInterval =
min(maxTokenDuration, max(0.04, curSpike - prevSpikeTime));
_tokenDurations.add(backwardInterval);
}
return ProcessedAudioStream(
tokens: _filteredTokens,
durations: _tokenDurations,
);
}