|
|
@ -23,10 +23,12 @@ public final class SonioxLiveSession { |
|
|
// Transcripts tracking |
|
|
// Transcripts tracking |
|
|
private var committedFinalTokens: [String] = [] |
|
|
private var committedFinalTokens: [String] = [] |
|
|
private var currentNonFinalTokens: [String] = [] |
|
|
private var currentNonFinalTokens: [String] = [] |
|
|
|
|
|
private var streamedPrefix: String = "" |
|
|
|
|
|
|
|
|
// Callbacks |
|
|
// Callbacks |
|
|
public var onPartialText: ((String) -> Void)? |
|
|
public var onPartialText: ((String) -> Void)? |
|
|
public var onCommittedDelta: ((String) -> Void)? |
|
|
public var onCommittedDelta: ((String) -> Void)? |
|
|
|
|
|
public var onEndTriggerDetected: (() -> Void)? |
|
|
public var onFinalResult: ((Result<String, Error>) -> Void)? |
|
|
public var onFinalResult: ((Result<String, Error>) -> Void)? |
|
|
public var onConnectionStateChanged: ((Bool) -> Void)? |
|
|
public var onConnectionStateChanged: ((Bool) -> Void)? |
|
|
public var onNetworkDegraded: ((Bool) -> Void)? |
|
|
public var onNetworkDegraded: ((Bool) -> Void)? |
|
|
@ -302,11 +304,47 @@ public final class SonioxLiveSession { |
|
|
let fullCommitted = committedFinalTokens.joined() |
|
|
let fullCommitted = committedFinalTokens.joined() |
|
|
let fullNonFinal = currentNonFinalTokens.joined() |
|
|
let fullNonFinal = currentNonFinalTokens.joined() |
|
|
let combined = fullCommitted + fullNonFinal |
|
|
let combined = fullCommitted + fullNonFinal |
|
|
|
|
|
|
|
|
|
|
|
// Check for in-flight end trigger ("and", "تمام", "end") in combined text |
|
|
|
|
|
var triggerDetected = false |
|
|
|
|
|
let trimmedCombined = combined.trimmingCharacters(in: .whitespacesAndNewlines) |
|
|
|
|
|
let currentWords = trimmedCombined.components(separatedBy: .whitespacesAndNewlines).filter { !$0.isEmpty } |
|
|
|
|
|
if let lastRaw = currentWords.last { |
|
|
|
|
|
let cleanLast = lastRaw.replacingOccurrences(of: #"[.,!?:;،؛؟"'()\[\]{}«»–—\-]"#, with: "", options: .regularExpression).lowercased() |
|
|
|
|
|
if cleanLast == "and" || cleanLast == "تمام" || cleanLast == "end" { |
|
|
|
|
|
triggerDetected = true |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Ultra-responsive real-time word-by-word streaming: |
|
|
|
|
|
// A word in 'combined' is completed as soon as a whitespace character follows it. |
|
|
|
|
|
var deltaToEmit: String? = nil |
|
|
|
|
|
if !triggerDetected { |
|
|
|
|
|
if let lastSpaceRange = combined.rangeOfCharacter(from: .whitespacesAndNewlines, options: .backwards) { |
|
|
|
|
|
let stablePortion = String(combined[..<lastSpaceRange.upperBound]) |
|
|
|
|
|
if stablePortion.count > streamedPrefix.count { |
|
|
|
|
|
if stablePortion.hasPrefix(streamedPrefix) { |
|
|
|
|
|
let delta = String(stablePortion.dropFirst(streamedPrefix.count)) |
|
|
|
|
|
streamedPrefix = stablePortion |
|
|
|
|
|
deltaToEmit = delta |
|
|
|
|
|
} else { |
|
|
|
|
|
let common = stablePortion.commonPrefix(with: streamedPrefix) |
|
|
|
|
|
if stablePortion.count > common.count { |
|
|
|
|
|
let delta = String(stablePortion.dropFirst(common.count)) |
|
|
|
|
|
streamedPrefix = stablePortion |
|
|
|
|
|
deltaToEmit = delta |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
lock.unlock() |
|
|
lock.unlock() |
|
|
|
|
|
|
|
|
// Emit streaming delta for newly committed words |
|
|
|
|
|
if !newFinals.isEmpty { |
|
|
|
|
|
let delta = newFinals.joined() |
|
|
|
|
|
|
|
|
if triggerDetected { |
|
|
|
|
|
DispatchQueue.main.async { [weak self] in |
|
|
|
|
|
self?.onEndTriggerDetected?() |
|
|
|
|
|
} |
|
|
|
|
|
} else if let delta = deltaToEmit, !delta.isEmpty { |
|
|
DispatchQueue.main.async { [weak self] in |
|
|
DispatchQueue.main.async { [weak self] in |
|
|
self?.onCommittedDelta?(delta) |
|
|
self?.onCommittedDelta?(delta) |
|
|
} |
|
|
} |
|
|
|