Skip to content

Update iOS to use onnxruntime 1.26.0 - #3678

Merged
csukuangfj merged 3 commits into
k2-fsa:masterfrom
csukuangfj:ios-fix
Jun 15, 2026
Merged

csukuangfj merged 3 commits into
k2-fsa:masterfrom
csukuangfj:ios-fix

Conversation

@csukuangfj

@csukuangfj csukuangfj commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3639

cc @lshaoshuai @christianbrugger

Summary by CodeRabbit

  • New Features
    • Added progress tracking with a visual progress bar during offline audio generation
    • Added language selection and conditional supertonic controls (including configurable generation steps)
    • Added stop/cancel support for generation and playback
    • Improved speaker selection UI to show only when multiple speakers are available
  • Bug Fixes
    • Improved pre-installed onnxruntime detection across framework layouts for smoother setup
  • Chores
    • Updated iOS onnxruntime dependency to version 1.26.0 and adjusted build/package wiring accordingly
    • Updated ignore rules for espeak-ng-data

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jun 15, 2026
@coderabbitai

coderabbitai Bot commented Jun 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0d8b952e-d7ae-49be-a34f-86f96c8533f2

📥 Commits

Reviewing files that changed from the base of the PR and between dddaea6 and 42207e4.

📒 Files selected for processing (3)
  • ios-swift/SherpaOnnx/SherpaOnnx.xcodeproj/project.pbxproj
  • ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift
  • swift-api-examples/SherpaOnnx.swift

📝 Walkthrough

Walkthrough

Upgrades 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 isSupertonic, sampleRate, and numSpeakers, adds a Supertonic model factory in ViewModel, and overhauls ContentView with streaming audio playback, progress tracking, and supertonic-specific UI controls.

Changes

ONNX Runtime 1.26.0 iOS Upgrade

Layer / File(s) Summary
Build scripts: version bump and zip-based XCFramework provisioning
build-ios.sh, build-ios-no-tts.sh, build-ios-shared.sh
All three iOS build scripts change the default onnxruntime version to 1.26.0, replace the .tar.bz2 download with a .zip download/unzip/move flow, update the "already present" check to the arm64 framework binary location, and update SHERPA_ONNXRUNTIME_INCLUDE_DIR for both simulator and arm64 slices to the architecture-specific onnxruntime.framework/Headers path.
CMake: framework-style library fallback
cmake/onnxruntime.cmake
Adds a fallback to onnxruntime.framework/onnxruntime when libonnxruntime.a is absent under SHERPA_ONNXRUNTIME_LIB_DIR, and emits a FATAL_ERROR if neither path exists.
Xcode project references and .gitignore
ios-swiftui/SherpaOnnx2Pass/...pbxproj, ios-swiftui/SherpaOnnxLangID/...pbxproj, ios-swiftui/SherpaOnnxSubtitle/...pbxproj, ios-swiftui/SherpaOnnxTts/...pbxproj, ios-swift/SherpaOnnx/...pbxproj, .gitignore
Five Xcode project files update their onnxruntime.xcframework PBXFileReference paths from 1.17.1 to 1.26.0. .gitignore gains an espeak-ng-data entry.

iOS Supertonic TTS Support

Layer / File(s) Summary
SherpaOnnxOfflineTtsWrapper: isSupertonic, sampleRate, numSpeakers
swift-api-examples/SherpaOnnx.swift
Adds a stored isSupertonic bool initialized from config.pointee.model.supertonic.duration_predictor and computed sampleRate/numSpeakers properties backed by C API calls.
ViewModel: Supertonic factory and default model change
ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ViewModel.swift
Introduces getTtsForSupertonic() which loads bundled Supertonic ONNX/JSON/bin assets and returns a configured SherpaOnnxOfflineTtsWrapper. createOfflineTts() now returns getTtsFor_en_US_amy_low() as the active default.
ContentView: TtsProgressHandler, supertonic UI, generation flow
ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift
Adds TtsProgressHandler which manages AVAudioEngine/AVAudioPlayerNode streaming playback, published progress, and requestStop() cancellation. ContentView gains supertonic-only controls (language picker, steps stepper), a progress bar, a conditional multi-speaker Stepper, a private generate() method routing Sherpa callbacks through progressHandler, and a "Stop" button replacing "Generate" during generation.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • k2-fsa/sherpa-onnx#3286: Adds Supertonic model config fields and Swift bridging at the C/Swift API layer, which underpins the isSupertonic detection and generateWithConfig call sites in this PR.
  • k2-fsa/sherpa-onnx#3617: Modifies the iOS build scripts (build-ios.sh and related) to update onnxruntime version/provisioning logic, directly overlapping with the build script changes in this PR.

Poem

🐇 Hop, hop — old 1.17 must go,
A fresh 1.26 zip joins the show!
Supertonic hums through AVAudioEngine's wire,
Progress bars glow as samples climb higher.
The rabbit builds iOS, ears perked with delight —
opset five sings, and the models load right! 🎵

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: updating iOS to use onnxruntime 1.26.0, which is the primary objective across all file modifications.
Linked Issues check ✅ Passed The PR successfully addresses issue #3639 by updating onnxruntime from 1.17.1 to 1.26.0, which supports ai.onnx.ml opset 5 required by the supertonic-3-tts model.
Out of Scope Changes check ✅ Passed All changes are directly related to the onnxruntime version upgrade. While there are TTS UI/UX enhancements in ContentView.swift and ViewModel.swift, these are supporting changes to properly integrate and test the new Supertonic TTS model.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +13 to +132
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
}
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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
                }
            }
        }
    }
}

Comment on lines +275 to +337
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)
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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)
            }
        }
    }

Comment on lines +168 to +171
Stepper("\(Int(sid)! + 1)", value: Binding(
get: { (Int(sid) ?? 0) + 1 },
set: { sid = "\($0 - 1)" }
), in: 1...Int(tts.numSpeakers))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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))

Comment thread swift-api-examples/SherpaOnnx.swift Outdated
Comment on lines +1394 to +1395
isSupertonic = config.pointee.model.supertonic.duration_predictor != nil
&& String(cString: config.pointee.model.supertonic.duration_predictor) != ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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).

Suggested change
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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b74c4df and dddaea6.

📒 Files selected for processing (12)
  • .gitignore
  • build-ios-no-tts.sh
  • build-ios-shared.sh
  • build-ios.sh
  • cmake/onnxruntime.cmake
  • ios-swiftui/SherpaOnnx2Pass/SherpaOnnx2Pass.xcodeproj/project.pbxproj
  • ios-swiftui/SherpaOnnxLangID/SherpaOnnxLangID.xcodeproj/project.pbxproj
  • ios-swiftui/SherpaOnnxSubtitle/SherpaOnnxSubtitle.xcodeproj/project.pbxproj
  • ios-swiftui/SherpaOnnxTts/SherpaOnnxTts.xcodeproj/project.pbxproj
  • ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift
  • ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ViewModel.swift
  • swift-api-examples/SherpaOnnx.swift

Comment thread build-ios-no-tts.sh
mkdir -p $dir
cd $dir
onnxruntime_version=1.17.1
onnxruntime_version=${SHERPA_ONNX_ONNXRUNTIME_VERSION:-1.26.0}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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}")
PY

Repository: 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:


🏁 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 2

Repository: 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 -20

Repository: 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 -50

Repository: 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.sh

Repository: 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.

Comment thread ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift
Comment on lines +242 to +247
Button(action: {
self.audioPlayer.play()
}) {
Text("Play")
}.disabled(filename.absoluteString.isEmpty)
}.disabled(filename.absoluteString.isEmpty || progressHandler.isGenerating)
Spacer()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "ContentView.swift" -path "*/SherpaOnnxTts/*" | head -20

Repository: k2-fsa/sherpa-onnx

Length of output: 122


🏁 Script executed:

wc -l ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift

Repository: 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 2

Repository: k2-fsa/sherpa-onnx

Length of output: 595


🏁 Script executed:

rg -n "audioPlayer" ios-swiftui/SherpaOnnxTts/SherpaOnnxTts/ContentView.swift

Repository: 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.

Comment on lines 1391 to 1397
init(
config: UnsafePointer<SherpaOnnxOfflineTtsConfig>!
) {
isSupertonic = config.pointee.model.supertonic.duration_predictor != nil
&& String(cString: config.pointee.model.supertonic.duration_predictor) != ""
tts = SherpaOnnxCreateOfflineTts(config)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find the file
fd -t f "SherpaOnnx.swift" | head -20

Repository: 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.swift

Repository: 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 -20

Repository: 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 -20

Repository: 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 -20

Repository: 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.swift

Repository: 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 -30

Repository: 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.h

Repository: 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

1 participant