Update iOS to use onnxruntime 1.26.0 - #3678
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughUpgrades the iOS ONNX Runtime XCFramework dependency from 1.17.1 to 1.26.0 across build scripts and Xcode projects, switching from tarball to zip provisioning and updating include-dir paths to the new per-slice layout. Adds framework-style library fallback in CMake. Extends the Swift TTS wrapper with ChangesONNX Runtime 1.26.0 iOS Upgrade
iOS Supertonic TTS Support
Sequence Diagram(s)sequenceDiagram
participant User
participant ContentView
participant TtsProgressHandler
participant SherpaOnnxOfflineTtsWrapper
participant AVAudioEngine
User->>ContentView: tap Generate
ContentView->>TtsProgressHandler: startPlayback()
TtsProgressHandler->>AVAudioEngine: start engine and player node
alt isSupertonic
ContentView->>SherpaOnnxOfflineTtsWrapper: generateWithConfig(text, lang, steps)
else standard
ContentView->>SherpaOnnxOfflineTtsWrapper: generateWithCallbackWithArg(text, sid)
end
SherpaOnnxOfflineTtsWrapper-->>TtsProgressHandler: appendSamples(samples, progress)
TtsProgressHandler->>AVAudioEngine: schedule audio buffer
TtsProgressHandler-->>ContentView: publish progress (0.0–1.0)
User->>ContentView: tap Stop (optional)
ContentView-->>SherpaOnnxOfflineTtsWrapper: return stopFlag = 1
ContentView->>TtsProgressHandler: finishGeneration()
TtsProgressHandler->>AVAudioEngine: stop and reset
ContentView-->>User: enable Play / Save / Share
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request updates the iOS build scripts, CMake configurations, and Xcode projects to support ONNX Runtime 1.26.0, and adds support for Supertonic TTS in the iOS SwiftUI TTS application, including real-time audio playback, progress tracking, and WAV export. The review feedback highlights critical issues that need to be addressed: a data race in TtsProgressHandler requiring synchronization, unsafe background thread access to @State properties in generate(), a potential runtime crash from force-unwrapping sid, and an inefficient C-string emptiness check in the Swift API wrapper.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| class TtsProgressHandler: ObservableObject { | ||
| @Published var progress: Float = 0.0 | ||
| @Published var isGenerating = false | ||
| private var audioEngine: AVAudioEngine? | ||
| private var playerNode: AVAudioPlayerNode? | ||
| private var audioFormat: AVAudioFormat? | ||
| private var sampleRate: Float = 22050 | ||
| private var pendingBuffers = 0 | ||
| private var shouldStop = false | ||
|
|
||
| func startPlayback(sampleRate: Float) { | ||
| self.sampleRate = sampleRate | ||
| self.pendingBuffers = 0 | ||
| self.shouldStop = false | ||
|
|
||
| do { | ||
| let session = AVAudioSession.sharedInstance() | ||
| try session.setCategory(.playback, mode: .default) | ||
| try session.setActive(true) | ||
| } catch { | ||
| print("AVAudioSession error: \(error)") | ||
| } | ||
|
|
||
| let engine = AVAudioEngine() | ||
| let player = AVAudioPlayerNode() | ||
| let format = AVAudioFormat( | ||
| commonFormat: .pcmFormatFloat32, | ||
| sampleRate: Double(sampleRate), | ||
| channels: 1, | ||
| interleaved: false) | ||
|
|
||
| engine.attach(player) | ||
| engine.connect(player, to: engine.mainMixerNode, format: format) | ||
|
|
||
| do { | ||
| try engine.start() | ||
| } catch { | ||
| print("AVAudioEngine start error: \(error)") | ||
| return | ||
| } | ||
|
|
||
| self.audioEngine = engine | ||
| self.playerNode = player | ||
| self.audioFormat = format | ||
|
|
||
| player.play() | ||
|
|
||
| DispatchQueue.main.async { | ||
| self.isGenerating = true | ||
| self.progress = 0.0 | ||
| } | ||
| } | ||
|
|
||
| func appendSamples(_ samples: UnsafePointer<Float>?, count: Int32, progress: Float) { | ||
| guard !shouldStop, | ||
| let playerNode = playerNode, | ||
| let audioFormat = audioFormat, | ||
| let samples = samples, | ||
| count > 0 | ||
| else { return } | ||
|
|
||
| let frameCount = AVAudioFrameCount(count) | ||
| guard let buffer = AVAudioPCMBuffer( | ||
| pcmFormat: audioFormat, frameCapacity: frameCount) | ||
| else { return } | ||
|
|
||
| buffer.frameLength = frameCount | ||
| let channelData = buffer.floatChannelData![0] | ||
| memcpy(channelData, samples, Int(count) * MemoryLayout<Float>.size) | ||
|
|
||
| pendingBuffers += 1 | ||
| playerNode.scheduleBuffer(buffer) { [weak self] in | ||
| DispatchQueue.main.async { | ||
| self?.pendingBuffers -= 1 | ||
| } | ||
| } | ||
|
|
||
| DispatchQueue.main.async { | ||
| self.progress = progress | ||
| } | ||
| } | ||
|
|
||
| /// Returns 0 to stop generation, 1 to continue | ||
| var stopFlag: Int32 { | ||
| return shouldStop ? 0 : 1 | ||
| } | ||
|
|
||
| func requestStop() { | ||
| shouldStop = true | ||
| } | ||
|
|
||
| func finishGeneration() { | ||
| // Wait for all scheduled buffers to finish playing, then clean up | ||
| DispatchQueue.global(qos: .background).async { [weak self] in | ||
| while let self = self, self.pendingBuffers > 0 && !self.shouldStop { | ||
| Thread.sleep(forTimeInterval: 0.05) | ||
| } | ||
|
|
||
| if self?.shouldStop == true { | ||
| // Immediate stop requested | ||
| self?.playerNode?.stop() | ||
| } else { | ||
| // Let the last buffers play out | ||
| Thread.sleep(forTimeInterval: 0.3) | ||
| } | ||
|
|
||
| DispatchQueue.main.async { | ||
| self?.playerNode?.stop() | ||
| self?.audioEngine?.stop() | ||
| self?.audioEngine = nil | ||
| self?.playerNode = nil | ||
| self?.audioFormat = nil | ||
| self?.isGenerating = false | ||
| if self?.shouldStop != true { | ||
| self?.progress = 1.0 | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The TtsProgressHandler class accesses and modifies pendingBuffers and shouldStop concurrently from multiple threads (e.g., the background thread running generate(), the main thread via scheduleBuffer's completion handler, and another background thread in finishGeneration()). This causes a data race, which can lead to undefined behavior or crashes. Use an NSLock to synchronize access to these properties.
class TtsProgressHandler: ObservableObject {
@Published var progress: Float = 0.0
@Published var isGenerating = false
private var audioEngine: AVAudioEngine?
private var playerNode: AVAudioPlayerNode?
private var audioFormat: AVAudioFormat?
private var sampleRate: Float = 22050
private let lock = NSLock()
private var _pendingBuffers = 0
private var _shouldStop = false
private var pendingBuffers: Int {
lock.lock()
defer { lock.unlock() }
return _pendingBuffers
}
private var shouldStop: Bool {
lock.lock()
defer { lock.unlock() }
return _shouldStop
}
func startPlayback(sampleRate: Float) {
self.sampleRate = sampleRate
lock.lock()
self._pendingBuffers = 0
self._shouldStop = false
lock.unlock()
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playback, mode: .default)
try session.setActive(true)
} catch {
print("AVAudioSession error: \(error)")
}
let engine = AVAudioEngine()
let player = AVAudioPlayerNode()
let format = AVAudioFormat(
commonFormat: .pcmFormatFloat32,
sampleRate: Double(sampleRate),
channels: 1,
interleaved: false)
engine.attach(player)
engine.connect(player, to: engine.mainMixerNode, format: format)
do {
try engine.start()
} catch {
print("AVAudioEngine start error: \(error)")
return
}
self.audioEngine = engine
self.playerNode = player
self.audioFormat = format
player.play()
DispatchQueue.main.async {
self.isGenerating = true
self.progress = 0.0
}
}
func appendSamples(_ samples: UnsafePointer<Float>?, count: Int32, progress: Float) {
guard !shouldStop,
let playerNode = playerNode,
let audioFormat = audioFormat,
let samples = samples,
count > 0
else { return }
let frameCount = AVAudioFrameCount(count)
guard let buffer = AVAudioPCMBuffer(
pcmFormat: audioFormat, frameCapacity: frameCount)
else { return }
buffer.frameLength = frameCount
let channelData = buffer.floatChannelData![0]
memcpy(channelData, samples, Int(count) * MemoryLayout<Float>.size)
lock.lock()
_pendingBuffers += 1
lock.unlock()
playerNode.scheduleBuffer(buffer) { [weak self] in
guard let self = self else { return }
self.lock.lock()
self._pendingBuffers -= 1
self.lock.unlock()
}
DispatchQueue.main.async {
self.progress = progress
}
}
/// Returns 0 to stop generation, 1 to continue
var stopFlag: Int32 {
return shouldStop ? 0 : 1
}
func requestStop() {
lock.lock()
_shouldStop = true
lock.unlock()
}
func finishGeneration() {
// Wait for all scheduled buffers to finish playing, then clean up
DispatchQueue.global(qos: .background).async { [weak self] in
while let self = self, self.pendingBuffers > 0 && !self.shouldStop {
Thread.sleep(forTimeInterval: 0.05)
}
guard let self = self else { return }
if self.shouldStop {
// Immediate stop requested
self.playerNode?.stop()
} else {
// Let the last buffers play out
Thread.sleep(forTimeInterval: 0.3)
}
DispatchQueue.main.async {
self.playerNode?.stop()
self.audioEngine?.stop()
self.audioEngine = nil
self.playerNode = nil
self.audioFormat = nil
self.isGenerating = false
if !self.shouldStop {
self.progress = 1.0
}
}
}
}
}| private func generate() { | ||
| let speakerId = Int(self.sid) ?? 0 | ||
| let t = self.text.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| if t.isEmpty { | ||
| self.showAlert = true | ||
| return | ||
| } | ||
|
|
||
| if self.filename.absoluteString.isEmpty { | ||
| let tempDirectoryURL = NSURL.fileURL( | ||
| withPath: NSTemporaryDirectory(), isDirectory: true) | ||
| self.filename = tempDirectoryURL.appendingPathComponent("test.wav") | ||
| } | ||
|
|
||
| let handler = progressHandler | ||
| let sampleRate = Float(tts.sampleRate) | ||
|
|
||
| DispatchQueue.global(qos: .userInitiated).async { | ||
| handler.startPlayback(sampleRate: sampleRate) | ||
|
|
||
| let arg = Unmanaged.passUnretained(handler).toOpaque() | ||
|
|
||
| let audio: SherpaOnnxGeneratedAudioWrapper | ||
|
|
||
| if tts.isSupertonic { | ||
| let progressCallback: TtsProgressCallbackWithArg = { | ||
| samples, n, progress, arg in | ||
| let h = Unmanaged<TtsProgressHandler>.fromOpaque(arg!) | ||
| .takeUnretainedValue() | ||
| h.appendSamples(samples, count: n, progress: progress) | ||
| return h.stopFlag | ||
| } | ||
|
|
||
| var genConfig = SherpaOnnxGenerationConfigSwift() | ||
| genConfig.sid = speakerId | ||
| genConfig.speed = Float(self.speed) | ||
| genConfig.numSteps = self.numSteps | ||
| genConfig.extra = ["lang": self.lang] | ||
| audio = tts.generateWithConfig( | ||
| text: t, config: genConfig, | ||
| callback: progressCallback, arg: arg) | ||
| } else { | ||
| let simpleCallback: TtsCallbackWithArg = { samples, n, arg in | ||
| let h = Unmanaged<TtsProgressHandler>.fromOpaque(arg!) | ||
| .takeUnretainedValue() | ||
| h.appendSamples(samples, count: n, progress: 1.0) | ||
| return h.stopFlag | ||
| } | ||
|
|
||
| audio = tts.generateWithCallbackWithArg( | ||
| text: t, callback: simpleCallback, arg: arg, | ||
| sid: speakerId, speed: Float(self.speed)) | ||
| } | ||
|
|
||
| let _ = audio.save(filename: self.filename.path) | ||
|
|
||
| handler.finishGeneration() | ||
|
|
||
| DispatchQueue.main.async { | ||
| self.audioPlayer = try? AVAudioPlayer(contentsOf: self.filename) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The generate() method dispatches a block to a background thread (DispatchQueue.global(qos: .userInitiated).async) which directly accesses @State properties such as self.speed, self.numSteps, self.lang, and self.filename. Accessing @State properties from background threads is unsafe in SwiftUI and can lead to crashes or undefined behavior. Capture these values as local constants on the main thread before dispatching to the background thread.
private func generate() {
let speakerId = Int(self.sid) ?? 0
let t = self.text.trimmingCharacters(in: .whitespacesAndNewlines)
if t.isEmpty {
self.showAlert = true
return
}
if self.filename.absoluteString.isEmpty {
let tempDirectoryURL = NSURL.fileURL(
withPath: NSTemporaryDirectory(), isDirectory: true)
self.filename = tempDirectoryURL.appendingPathComponent("test.wav")
}
let handler = progressHandler
let sampleRate = Float(tts.sampleRate)
let speed = Float(self.speed)
let numSteps = self.numSteps
let lang = self.lang
let targetFilename = self.filename
DispatchQueue.global(qos: .userInitiated).async {
handler.startPlayback(sampleRate: sampleRate)
let arg = Unmanaged.passUnretained(handler).toOpaque()
let audio: SherpaOnnxGeneratedAudioWrapper
if tts.isSupertonic {
let progressCallback: TtsProgressCallbackWithArg = {
samples, n, progress, arg in
let h = Unmanaged<TtsProgressHandler>.fromOpaque(arg!)
.takeUnretainedValue()
h.appendSamples(samples, count: n, progress: progress)
return h.stopFlag
}
var genConfig = SherpaOnnxGenerationConfigSwift()
genConfig.sid = speakerId
genConfig.speed = speed
genConfig.numSteps = numSteps
genConfig.extra = ["lang": lang]
audio = tts.generateWithConfig(
text: t, config: genConfig,
callback: progressCallback, arg: arg)
} else {
let simpleCallback: TtsCallbackWithArg = { samples, n, arg in
let h = Unmanaged<TtsProgressHandler>.fromOpaque(arg!)
.takeUnretainedValue()
h.appendSamples(samples, count: n, progress: 1.0)
return h.stopFlag
}
audio = tts.generateWithCallbackWithArg(
text: t, callback: simpleCallback, arg: arg,
sid: speakerId, speed: speed)
}
let _ = audio.save(filename: targetFilename.path)
handler.finishGeneration()
DispatchQueue.main.async {
self.audioPlayer = try? AVAudioPlayer(contentsOf: targetFilename)
}
}
}| Stepper("\(Int(sid)! + 1)", value: Binding( | ||
| get: { (Int(sid) ?? 0) + 1 }, | ||
| set: { sid = "\($0 - 1)" } | ||
| ), in: 1...Int(tts.numSpeakers)) |
There was a problem hiding this comment.
Force-unwrapping Int(sid)! is unsafe and can cause a runtime crash if sid is ever set to an invalid integer string. Use nil-coalescing to safely default to 0 instead.
| Stepper("\(Int(sid)! + 1)", value: Binding( | |
| get: { (Int(sid) ?? 0) + 1 }, | |
| set: { sid = "\($0 - 1)" } | |
| ), in: 1...Int(tts.numSpeakers)) | |
| Stepper("\((Int(sid) ?? 0) + 1)", value: Binding( | |
| get: { (Int(sid) ?? 0) + 1 }, | |
| set: { sid = "\($0 - 1)" } | |
| ), in: 1...Int(tts.numSpeakers)) |
| isSupertonic = config.pointee.model.supertonic.duration_predictor != nil | ||
| && String(cString: config.pointee.model.supertonic.duration_predictor) != "" |
There was a problem hiding this comment.
Initializing a Swift String from a C string pointer via String(cString:) just to check if it is empty is inefficient because it allocates memory and copies the string. Since you only need to check if the C string is empty, you can directly check if the first character pointed to is the null terminator (0).
| isSupertonic = config.pointee.model.supertonic.duration_predictor != nil | |
| && String(cString: config.pointee.model.supertonic.duration_predictor) != "" | |
| isSupertonic = config.pointee.model.supertonic.duration_predictor != nil | |
| && config.pointee.model.supertonic.duration_predictor.pointee != 0 |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@build-ios-no-tts.sh`:
- Line 8: The ONNX Runtime version is updated to 1.26.0 which requires iOS
15.1+, but the iOS deployment target is still set to 13.0 in the
DEPLOYMENT_TARGET variable, creating an incompatibility. Update all occurrences
of `-DDEPLOYMENT_TARGET=13.0` to `-DDEPLOYMENT_TARGET=15.1` across all three
build scripts: build-ios-no-tts.sh, build-ios-shared.sh, and build-ios.sh to
align with the ORT 1.26.0 requirement.
In `@ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift`:
- Around line 242-247: The audioPlayer property can be nil if its initialization
with try? fails silently, but the Button action at line 243 force-calls play()
without nil checking, which causes a crash. Replace the force call with optional
chaining so the play() method is only called if audioPlayer is not nil.
Additionally, update the disabled state logic to also check if audioPlayer is
nil, so the Play button is disabled not only when filename is empty or
generation is in progress, but also when audioPlayer initialization failed.
- Around line 20-22: The properties `pendingBuffers` and `shouldStop` are
accessed concurrently from multiple threads (native callback thread via
`appendSamples()`, main thread, and background thread) without synchronization,
creating data races. Protect these shared mutable variables by wrapping them in
an NSLock, using a serial DispatchQueue for coordinated access, or converting
the class to an actor with isolated access to these properties. Apply the chosen
synchronization mechanism consistently across all access points including the
native callback invocations and the main/background thread modifications to
ensure thread-safe reads and writes.
In `@swift-api-examples/SherpaOnnx.swift`:
- Around line 1391-1397: The init method assigns the result of
SherpaOnnxCreateOfflineTts(config) directly to the tts property without
validating whether it returns nil. Since SherpaOnnxCreateOfflineTts can return
NULL on error and tts is later accessed without nil checks in sampleRate and
numSpeakers properties, add an explicit guard statement after the
SherpaOnnxCreateOfflineTts call to check that the result is not nil, and fail
the initialization (e.g., with fatalError or by returning early) if creation
fails.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9e106382-46de-45fe-b205-ea41c5e1ec83
📒 Files selected for processing (12)
.gitignorebuild-ios-no-tts.shbuild-ios-shared.shbuild-ios.shcmake/onnxruntime.cmakeios-swiftui/SherpaOnnx2Pass/SherpaOnnx2Pass.xcodeproj/project.pbxprojios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.pbxprojios-swiftui/SherpaOnnxSubtitle/SherpaOnnxSubtitle.xcodeproj/project.pbxprojios-swiftui/SherpaOnnxTts/SherpaOnnxTts.xcodeproj/project.pbxprojios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swiftios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ViewModel.swiftswift-api-examples/SherpaOnnx.swift
| mkdir -p $dir | ||
| cd $dir | ||
| onnxruntime_version=1.17.1 | ||
| onnxruntime_version=${SHERPA_ONNX_ONNXRUNTIME_VERSION:-1.26.0} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Deployment target values in iOS build scripts:"
rg -n --no-heading 'DDEPLOYMENT_TARGET=' build-ios-no-tts.sh build-ios-shared.sh build-ios.sh
echo
echo "MinimumOSVersion declared by downloaded onnxruntime XCFramework slices (if present):"
python - <<'PY'
import glob, plistlib
paths = glob.glob('build-ios*/ios-onnxruntime/*/onnxruntime.xcframework/**/Info.plist', recursive=True)
if not paths:
print("No onnxruntime Info.plist found under build-ios* yet.")
for p in sorted(paths):
try:
with open(p, 'rb') as f:
data = plistlib.load(f)
print(f"{p}: MinimumOSVersion={data.get('MinimumOSVersion')}")
except Exception as e:
print(f"{p}: parse_error={e}")
PYRepository: k2-fsa/sherpa-onnx
Length of output: 706
🌐 Web query:
ONNX Runtime 1.26.0 iOS minimum deployment target requirements
💡 Result:
For ONNX Runtime 1.26.0, the minimum supported iOS deployment target is iOS 15.1 [1][2][3]. This requirement was established in previous versions to align with React Native 0.76 and remains in effect for version 1.26.0 [1][2]. If you encounter build or distribution errors related to the MinimumOSVersion in your Xcode project, you may need to ensure your project's deployment target is set to at least 15.1 [3]. In some cases where an integration or pod specifies a different target, you may need to manually update the Info.plist file within the ONNX Runtime framework packages to match your project's requirements if you are encountering validation conflicts during App Store submission [3]. When building ONNX Runtime for iOS from source, you can explicitly set the deployment target using the --apple_deploy_target flag in the build script [4][5].
Citations:
- 1: https://npmx.dev/package-changelog/onnxruntime-node/v/1.26.0
- 2: Update min iOS version to 15.1 to align with React Native 0.76 microsoft/onnxruntime#23292
- 3: https://heyletslearnsomething.com/blog/invalid-minimumosversion-onnxruntime
- 4: https://onnxruntime.ai/docs/build/ios.html
- 5: https://github.com/microsoft/onnxruntime/blob/gh-pages/docs/build/ios.md
🏁 Script executed:
#!/bin/bash
# Search for any iOS deployment target context in the repo
rg -i "deployment.target|ios.*15|ort.*1\.26" --type sh --type md -A 2 -B 2Repository: k2-fsa/sherpa-onnx
Length of output: 278
🏁 Script executed:
#!/bin/bash
# Check git history and PR context for iOS version mentions around this change
git log --oneline --all -20 | head -20
git log --all --grep="deploy\|ios\|ort\|1\.26" -i --oneline | head -20Repository: k2-fsa/sherpa-onnx
Length of output: 2593
🏁 Script executed:
#!/bin/bash
# Check if deployment targets were updated recently after the ORT 1.26.0 change
git log -p dddaea6e..HEAD -- build-ios.sh build-ios-shared.sh build-ios-no-tts.sh | grep -A 3 -B 3 "DEPLOYMENT_TARGET" | head -50Repository: k2-fsa/sherpa-onnx
Length of output: 44
🏁 Script executed:
#!/bin/bash
# Check current HEAD state of deployment targets
echo "=== Current deployment targets in scripts ==="
rg -n 'DEPLOYMENT_TARGET=' build-ios.sh build-ios-shared.sh build-ios-no-tts.shRepository: k2-fsa/sherpa-onnx
Length of output: 565
Update iOS deployment target to 15.1 alongside ONNX Runtime 1.26.0.
onnxruntime_version changed on Line 8 to 1.26.0, but this script still uses -DDEPLOYMENT_TARGET=13.0 (Lines 59, 88, 121). ORT 1.26.0 requires iOS 15.1+, creating a linker/runtime incompatibility. Update the deployment target in build-ios-no-tts.sh, build-ios-shared.sh, and build-ios.sh.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@build-ios-no-tts.sh` at line 8, The ONNX Runtime version is updated to 1.26.0
which requires iOS 15.1+, but the iOS deployment target is still set to 13.0 in
the DEPLOYMENT_TARGET variable, creating an incompatibility. Update all
occurrences of `-DDEPLOYMENT_TARGET=13.0` to `-DDEPLOYMENT_TARGET=15.1` across
all three build scripts: build-ios-no-tts.sh, build-ios-shared.sh, and
build-ios.sh to align with the ORT 1.26.0 requirement.
| Button(action: { | ||
| self.audioPlayer.play() | ||
| }) { | ||
| Text("Play") | ||
| }.disabled(filename.absoluteString.isEmpty) | ||
| }.disabled(filename.absoluteString.isEmpty || progressHandler.isGenerating) | ||
| Spacer() |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "ContentView.swift" -path "*/SherpaOnnxTts/*" | head -20Repository: k2-fsa/sherpa-onnx
Length of output: 122
🏁 Script executed:
wc -l ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swiftRepository: k2-fsa/sherpa-onnx
Length of output: 124
🏁 Script executed:
cat -n ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift | sed -n '230,260p'Repository: k2-fsa/sherpa-onnx
Length of output: 1554
🏁 Script executed:
cat -n ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift | sed -n '320,350p'Repository: k2-fsa/sherpa-onnx
Length of output: 1343
🏁 Script executed:
rg -n "audioPlayer" ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift -A 2 -B 2Repository: k2-fsa/sherpa-onnx
Length of output: 595
🏁 Script executed:
rg -n "audioPlayer" ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swiftRepository: k2-fsa/sherpa-onnx
Length of output: 242
Add optional chaining to prevent crash when audio player initialization fails.
Line 334 uses try? to initialize audioPlayer, which can silently fail and leave the property as nil. Line 243 force-calls self.audioPlayer.play() without checking for nil, causing a crash if initialization failed. The disabled state also needs to prevent tapping when audioPlayer is nil.
Suggested fix
Button(action: {
- self.audioPlayer.play()
+ self.audioPlayer?.play()
}) {
Text("Play")
- }.disabled(filename.absoluteString.isEmpty || progressHandler.isGenerating)
+ }.disabled(self.audioPlayer == nil || filename.absoluteString.isEmpty || progressHandler.isGenerating)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift` around lines 242 -
247, The audioPlayer property can be nil if its initialization with try? fails
silently, but the Button action at line 243 force-calls play() without nil
checking, which causes a crash. Replace the force call with optional chaining so
the play() method is only called if audioPlayer is not nil. Additionally, update
the disabled state logic to also check if audioPlayer is nil, so the Play button
is disabled not only when filename is empty or generation is in progress, but
also when audioPlayer initialization failed.
| init( | ||
| config: UnsafePointer<SherpaOnnxOfflineTtsConfig>! | ||
| ) { | ||
| isSupertonic = config.pointee.model.supertonic.duration_predictor != nil | ||
| && String(cString: config.pointee.model.supertonic.duration_predictor) != "" | ||
| tts = SherpaOnnxCreateOfflineTts(config) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find the file
fd -t f "SherpaOnnx.swift" | head -20Repository: k2-fsa/sherpa-onnx
Length of output: 98
🏁 Script executed:
# Once we have the file, let's inspect the lines in question
cat -n swift-api-examples/SherpaOnnx.swift | sed -n '1380,1400p'Repository: k2-fsa/sherpa-onnx
Length of output: 811
🏁 Script executed:
# Let's also find the tts property definition
rg -n "var tts" swift-api-examples/SherpaOnnx.swiftRepository: k2-fsa/sherpa-onnx
Length of output: 44
🏁 Script executed:
# Let's search for SherpaOnnxCreateOfflineTts to understand its return type
rg -n "SherpaOnnxCreateOfflineTts" swift-api-examples/SherpaOnnx.swift | head -20Repository: k2-fsa/sherpa-onnx
Length of output: 112
🏁 Script executed:
# Let's find the class definition and tts property
cat -n swift-api-examples/SherpaOnnx.swift | sed -n '1350,1400p'Repository: k2-fsa/sherpa-onnx
Length of output: 1991
🏁 Script executed:
# Search for tts property more broadly
rg -n "tts" swift-api-examples/SherpaOnnx.swift | grep -E "(var|let|private|public)" | head -20Repository: k2-fsa/sherpa-onnx
Length of output: 115
🏁 Script executed:
# Let's see the full class definition around this init
cat -n swift-api-examples/SherpaOnnx.swift | sed -n '1320,1410p'Repository: k2-fsa/sherpa-onnx
Length of output: 3435
🏁 Script executed:
# Let's look for the C function declaration or documentation
fd -t f "\.h$" | xargs rg "SherpaOnnxCreateOfflineTts" 2>/dev/null | head -20Repository: k2-fsa/sherpa-onnx
Length of output: 1408
🏁 Script executed:
# Let's also check for any comments or documentation about the function
rg -B5 -A10 "SherpaOnnxCreateOfflineTts" swift-api-examples/SherpaOnnx.swiftRepository: k2-fsa/sherpa-onnx
Length of output: 593
🏁 Script executed:
# Let's search the entire repo for this function
rg "SherpaOnnxCreateOfflineTts" --type swift --type c --type h | head -30Repository: k2-fsa/sherpa-onnx
Length of output: 2489
🏁 Script executed:
# Let's examine the C header file for the function signature and documentation
rg -B10 -A5 "SHERPA_ONNX_API const SherpaOnnxOfflineTts \*SherpaOnnxCreateOfflineTts" sherpa-onnx/c-api/c-api.hRepository: k2-fsa/sherpa-onnx
Length of output: 1275
Guard TTS creation failure before storing tts.
The C API SherpaOnnxCreateOfflineTts() can return NULL on error (as documented for the HarmonyOS variant), but line 1396 assigns the result directly to an IUO without validation. Since tts is accessed without nil checks in sampleRate (line 1382) and numSpeakers (line 1387), a failed creation will cause a force-unwrap crash. The deinit already guards with if let tts, confirming the function can return nil. Use an explicit guard to fail fast.
Suggested fix
init(
config: UnsafePointer<SherpaOnnxOfflineTtsConfig>!
) {
isSupertonic = config.pointee.model.supertonic.duration_predictor != nil
&& String(cString: config.pointee.model.supertonic.duration_predictor) != ""
- tts = SherpaOnnxCreateOfflineTts(config)
+ guard let ptr = SherpaOnnxCreateOfflineTts(config) else {
+ fatalError("Failed to create SherpaOnnxOfflineTts")
+ }
+ tts = ptr
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@swift-api-examples/SherpaOnnx.swift` around lines 1391 - 1397, The init
method assigns the result of SherpaOnnxCreateOfflineTts(config) directly to the
tts property without validating whether it returns nil. Since
SherpaOnnxCreateOfflineTts can return NULL on error and tts is later accessed
without nil checks in sampleRate and numSpeakers properties, add an explicit
guard statement after the SherpaOnnxCreateOfflineTts call to check that the
result is not nil, and fail the initialization (e.g., with fatalError or by
returning early) if creation fails.
Fixes #3639
cc @lshaoshuai @christianbrugger
Summary by CodeRabbit
espeak-ng-data