Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Fix Swift 6 concurrency errors in SlidingWindowAsrManager
Fixes actor isolation violations that appeared with stricter Swift 6
concurrency checking in newer Xcode versions.

The issue was caused by extracting actor references from properties
into local variables using if-let/guard-let, which changes isolation
context and risks data races.

Solution uses optional chaining with proper scoping:
- Avoids force unwrapping (repository rule)
- Prevents actor isolation violations (Swift 6 requirement)
- Handles actor reentrancy safely (asrManager can become nil after await)
- Uses if-let for conditional blocks to avoid skipping critical state updates

Changes:
- reset(): Optional chaining for resetDecoderState
- finish(): Guard-let on processTranscriptionResult return value
- processWindow(): Guard-let for required results, if-let for optional rescoring
- All early-return guards use guard-let at function level
- Conditional block uses if-let to avoid premature function exit

Fixes prevent partial state mutations and ensure subscriber notifications
always occur even if optional vocabulary rescoring fails.
  • Loading branch information
Alex-Wengg committed Mar 30, 2026
commit 6f3c17f4571fde4d461d85c678414c1d47402345
Original file line number Diff line number Diff line change
Expand Up @@ -220,15 +220,16 @@ public actor SlidingWindowAsrManager {
if !confirmedTranscript.isEmpty { parts.append(confirmedTranscript) }
if !volatileTranscript.isEmpty { parts.append(volatileTranscript) }
finalText = parts.joined(separator: " ")
} else if let asrManager = asrManager, !accumulatedTokens.isEmpty {
let finalResult = await asrManager.processTranscriptionResult(
} else if !accumulatedTokens.isEmpty,
let finalResult = await asrManager?.processTranscriptionResult(
tokenIds: accumulatedTokens,
timestamps: [],
confidences: [], // No per-token confidences needed for final text
encoderSequenceLength: 0,
audioSamples: [], // Not needed for final text conversion
processingTime: 0
)
{
finalText = finalResult.text
} else {
var parts: [String] = []
Expand All @@ -252,9 +253,7 @@ public actor SlidingWindowAsrManager {
nextWindowCenterStart = 0

// Reset decoder state for the current audio source
if let asrManager = asrManager {
try await asrManager.resetDecoderState(for: audioSource)
}
try await asrManager?.resetDecoderState(for: audioSource)

// Reset sliding window state
segmentIndex = 0
Expand Down Expand Up @@ -375,20 +374,21 @@ public actor SlidingWindowAsrManager {
windowStartSample: Int,
isLastChunk: Bool = false
) async {
guard let asrManager = asrManager else { return }

do {
let chunkStartTime = Date()

// Start frame offset is now handled by decoder's timeJump mechanism

// Call AsrManager directly with deduplication
let (tokens, timestamps, confidences, _) = try await asrManager.transcribeChunk(
windowSamples,
source: audioSource,
previousTokens: accumulatedTokens,
isLastChunk: isLastChunk
)
guard
let result = try await asrManager?.transcribeChunk(
windowSamples,
source: audioSource,
previousTokens: accumulatedTokens,
isLastChunk: isLastChunk
)
else { return }
let (tokens, timestamps, confidences, _) = result

let adjustedTimestamps = Self.applyGlobalFrameOffset(
to: timestamps,
Expand All @@ -405,14 +405,16 @@ public actor SlidingWindowAsrManager {

// Convert only the current chunk tokens to text for clean incremental updates
// The final result will use all accumulated tokens for proper deduplication
let interim = await asrManager.processTranscriptionResult(
tokenIds: tokens, // Only current chunk tokens for progress updates
timestamps: adjustedTimestamps,
confidences: confidences,
encoderSequenceLength: 0,
audioSamples: windowSamples,
processingTime: processingTime
)
guard
let interim = await asrManager?.processTranscriptionResult(
tokenIds: tokens, // Only current chunk tokens for progress updates
timestamps: adjustedTimestamps,
confidences: confidences,
encoderSequenceLength: 0,
audioSamples: windowSamples,
processingTime: processingTime
)
else { return }

logger.debug(
"Chunk \(self.processedChunks): '\(interim.text)', time: \(String(format: "%.3f", processingTime))s)"
Expand All @@ -425,16 +427,17 @@ public actor SlidingWindowAsrManager {

// Rescore before updating transcript state so finish() returns rescored content
var displayResult = interim
if shouldConfirm && vocabBoostingEnabled {
let chunkLocalTimings =
await asrManager.processTranscriptionResult(
tokenIds: tokens,
timestamps: timestamps, // Original chunk-local timestamps (not adjusted)
confidences: confidences,
encoderSequenceLength: 0,
audioSamples: windowSamples,
processingTime: processingTime
).tokenTimings ?? []
if shouldConfirm && vocabBoostingEnabled,
let chunkLocalResult = await asrManager?.processTranscriptionResult(
tokenIds: tokens,
timestamps: timestamps, // Original chunk-local timestamps (not adjusted)
confidences: confidences,
encoderSequenceLength: 0,
audioSamples: windowSamples,
processingTime: processingTime
)
{
let chunkLocalTimings = chunkLocalResult.tokenTimings ?? []

if let rescored = await applyVocabularyRescoring(
text: interim.text,
Expand Down Expand Up @@ -624,23 +627,23 @@ public actor SlidingWindowAsrManager {

/// Reset decoder state for error recovery
private func resetDecoderForRecovery() async {
if let asrManager = asrManager {
guard asrManager != nil else { return }

do {
try await asrManager?.resetDecoderState(for: audioSource)
logger.info("Successfully reset decoder state during error recovery")
} catch {
logger.error("Failed to reset decoder state during recovery: \(error)")

// Last resort: try to reinitialize the ASR manager
do {
try await asrManager.resetDecoderState(for: audioSource)
logger.info("Successfully reset decoder state during error recovery")
let models = try await AsrModels.downloadAndLoad()
let newAsrManager = AsrManager(config: config.asrConfig)
try await newAsrManager.loadModels(models)
self.asrManager = newAsrManager
logger.info("Successfully reinitialized ASR manager during error recovery")
} catch {
logger.error("Failed to reset decoder state during recovery: \(error)")

// Last resort: try to reinitialize the ASR manager
do {
let models = try await AsrModels.downloadAndLoad()
let newAsrManager = AsrManager(config: config.asrConfig)
try await newAsrManager.loadModels(models)
self.asrManager = newAsrManager
logger.info("Successfully reinitialized ASR manager during error recovery")
} catch {
logger.error("Failed to reinitialize ASR manager during recovery: \(error)")
}
logger.error("Failed to reinitialize ASR manager during recovery: \(error)")
}
}
}
Expand Down
Loading