Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
61 changes: 60 additions & 1 deletion .github/workflows/mobile-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,59 @@ jobs:
working-directory: ./frontend
run: bun install

- name: Cache ONNX Runtime iOS build
uses: actions/cache@v4
id: cache-onnxruntime
with:
path: |
frontend/src-tauri/onnxruntime-ios
frontend/src-tauri/onnxruntime-build
key: onnxruntime-ios-built-1.22.2-v1
restore-keys: |
onnxruntime-ios-built-1.22.2-

- name: Build ONNX Runtime for iOS from source
if: steps.cache-onnxruntime.outputs.cache-hit != 'true'
working-directory: ./frontend/src-tauri
run: |
chmod +x scripts/build-ios-onnxruntime.sh
./scripts/build-ios-onnxruntime.sh 1.22.2
timeout-minutes: 90

- name: Verify ONNX Runtime files
run: |
echo "Checking ONNX Runtime xcframework..."
ls -la ${{ github.workspace }}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/
ls -la ${{ github.workspace }}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64/
echo ""
echo "Library info:"
file ${{ github.workspace }}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64/libonnxruntime.a
echo ""
echo "Checking for Abseil symbols (should be included):"
nm ${{ github.workspace }}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64/libonnxruntime.a 2>/dev/null | grep -i "absl" | head -20 || echo "No abseil symbols found (they may be internal)"

- name: Configure Cargo for iOS ONNX Runtime
run: |
# Create cargo config with absolute paths for iOS builds
# This overrides ort-sys's build script to use our built-from-source library
WORKSPACE="${{ github.workspace }}"
mkdir -p "${WORKSPACE}/frontend/src-tauri/.cargo"
cat > "${WORKSPACE}/frontend/src-tauri/.cargo/config.toml" << EOF
# Auto-generated cargo config for iOS ONNX Runtime linking
# Uses absolute paths because xcodebuild may run cargo from different directories

[target.aarch64-apple-ios.onnxruntime]
rustc-link-search = ["${WORKSPACE}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64"]
rustc-link-lib = ["static=onnxruntime"]

[target.aarch64-apple-ios-sim.onnxruntime]
rustc-link-search = ["${WORKSPACE}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64-simulator"]
rustc-link-lib = ["static=onnxruntime"]
EOF

echo "Generated cargo config:"
cat "${WORKSPACE}/frontend/src-tauri/.cargo/config.toml"

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
Expand Down Expand Up @@ -89,6 +142,8 @@ jobs:
VITE_OPEN_SECRET_API_URL: https://enclave.trymaple.ai
VITE_MAPLE_BILLING_API_URL: https://billing.opensecret.cloud
VITE_CLIENT_ID: ba5a14b5-d915-47b1-b7b1-afda52bc5fc6
# ONNX Runtime location for ort-sys crate
ORT_LIB_LOCATION: ${{ github.workspace }}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64

- name: Upload iOS App
uses: actions/upload-artifact@v4
Expand All @@ -99,7 +154,11 @@ jobs:
retention-days: 5

- name: Submit to TestFlight
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
# TODO: Remove ios-tts-working condition after PR #385 is merged
# For this PR, we want to test TestFlight submissions on every build
if: |
(github.event_name == 'push' && github.ref == 'refs/heads/master') ||
(github.event_name == 'pull_request' && github.head_ref == 'ios-tts-working')
run: |
# Find the actual path of the IPA file
IPA_PATH=$(find frontend/src-tauri/gen/apple/build -name "*.ipa" | head -n 1)
Expand Down
70 changes: 67 additions & 3 deletions .github/workflows/testflight-on-comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ name: TestFlight on Comment
on:
issue_comment:
types: [created]
# TODO: Remove this push trigger after PR #385 is merged
# This is a temporary workaround because issue_comment workflows run from master,
# so they don't pick up workflow changes from the PR branch
push:
branches:
- ios-tts-working

permissions:
contents: read
Expand All @@ -11,7 +17,8 @@ permissions:

jobs:
check-comment:
if: github.event.issue.pull_request && contains(github.event.comment.body, 'testflight build')
# Only run on issue_comment events, not on push
if: github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, 'testflight build')
runs-on: ubuntu-latest
outputs:
should-deploy: ${{ steps.check.outputs.should-deploy }}
Expand Down Expand Up @@ -63,12 +70,14 @@ jobs:

deploy-testflight:
needs: check-comment
if: needs.check-comment.outputs.should-deploy == 'true'
# Run on push to ios-tts branch OR when triggered by comment with approval
if: github.event_name == 'push' || needs.check-comment.outputs.should-deploy == 'true'
runs-on: macos-latest-xlarge
steps:
- uses: actions/checkout@v4
with:
ref: ${{ format('refs/pull/{0}/head', needs.check-comment.outputs.pr-number) }}
# For push events, use the current ref; for comment events, use the PR ref
ref: ${{ github.event_name == 'push' && github.ref || format('refs/pull/{0}/head', needs.check-comment.outputs.pr-number) }}

- name: Setup Bun
uses: oven-sh/setup-bun@v1
Expand Down Expand Up @@ -111,6 +120,59 @@ jobs:
working-directory: ./frontend
run: bun install

- name: Cache ONNX Runtime iOS build
uses: actions/cache@v4
id: cache-onnxruntime
with:
path: |
frontend/src-tauri/onnxruntime-ios
frontend/src-tauri/onnxruntime-build
key: onnxruntime-ios-built-1.22.2-v1
restore-keys: |
onnxruntime-ios-built-1.22.2-

- name: Build ONNX Runtime for iOS from source
if: steps.cache-onnxruntime.outputs.cache-hit != 'true'
working-directory: ./frontend/src-tauri
run: |
chmod +x scripts/build-ios-onnxruntime.sh
./scripts/build-ios-onnxruntime.sh 1.22.2
timeout-minutes: 90

- name: Verify ONNX Runtime files
run: |
echo "Checking ONNX Runtime xcframework..."
ls -la ${{ github.workspace }}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/
ls -la ${{ github.workspace }}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64/
echo ""
echo "Library info:"
file ${{ github.workspace }}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64/libonnxruntime.a
echo ""
echo "Checking for Abseil symbols (should be included):"
nm ${{ github.workspace }}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64/libonnxruntime.a 2>/dev/null | grep -i "absl" | head -20 || echo "No abseil symbols found (they may be internal)"

- name: Configure Cargo for iOS ONNX Runtime
run: |
# Create cargo config with absolute paths for iOS builds
# This overrides ort-sys's build script to use our built-from-source library
WORKSPACE="${{ github.workspace }}"
mkdir -p "${WORKSPACE}/frontend/src-tauri/.cargo"
cat > "${WORKSPACE}/frontend/src-tauri/.cargo/config.toml" << EOF
# Auto-generated cargo config for iOS ONNX Runtime linking
# Uses absolute paths because xcodebuild may run cargo from different directories

[target.aarch64-apple-ios.onnxruntime]
rustc-link-search = ["${WORKSPACE}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64"]
rustc-link-lib = ["static=onnxruntime"]

[target.aarch64-apple-ios-sim.onnxruntime]
rustc-link-search = ["${WORKSPACE}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64-simulator"]
rustc-link-lib = ["static=onnxruntime"]
EOF

echo "Generated cargo config:"
cat "${WORKSPACE}/frontend/src-tauri/.cargo/config.toml"

- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
Expand Down Expand Up @@ -147,6 +209,8 @@ jobs:
VITE_OPEN_SECRET_API_URL: https://enclave.trymaple.ai
VITE_MAPLE_BILLING_API_URL: https://billing.opensecret.cloud
VITE_CLIENT_ID: ba5a14b5-d915-47b1-b7b1-afda52bc5fc6
# ONNX Runtime location for ort-sys crate
ORT_LIB_LOCATION: ${{ github.workspace }}/frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/ios-arm64

- name: Submit to TestFlight
run: |
Expand Down
166 changes: 166 additions & 0 deletions docs/ios-tts-local-development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# iOS TTS Local Development Guide

This guide explains how to build and test the iOS TTS (Text-to-Speech) feature locally using the iOS Simulator or a physical device.

## Overview

The iOS TTS feature uses ONNX Runtime to run Kokoro TTS models. ONNX Runtime must be built from source for iOS because:
1. Pre-built binaries from HuggingFace are missing Abseil symbols
2. We need both device (arm64) and simulator (arm64) builds
3. The simulator build requires a workaround for a libiconv linking bug

## Prerequisites

- macOS with Xcode installed (16.x+)
- CMake 3.26+
- Python 3.8+
- Git
- Nix (recommended) or manually install Rust toolchain

## Quick Start

### 1. Build ONNX Runtime

```bash
just ios-build-onnxruntime
```

This builds ONNX Runtime for both device and simulator (~5-10 minutes) and automatically generates the cargo config.

The output will be in `frontend/src-tauri/onnxruntime-ios/onnxruntime.xcframework/`.

### 2. Regenerate Cargo Config (if needed)

If you move the project or need to regenerate the cargo config:

```bash
just ios-setup-cargo-config
```

This creates `frontend/src-tauri/.cargo/config.toml` with the correct absolute paths for your machine.

### 3. Fix arm64-sim Xcode Issue (if needed)

If you see this error:
```
clang: error: version '-sim' in target triple 'arm64-apple-ios13.0-simulator-sim' is invalid
```

See [troubleshooting-ios-build.md](./troubleshooting-ios-build.md) for details.

Quick fix:
```bash
just ios-fix-arch
```

### 4. Run on Simulator

```bash
# Boot simulator first
xcrun simctl boot "iPhone 16 Pro"

# Run the app
just ios-dev-sim "iPhone 16 Pro"
```

### 5. Run on Physical Device

```bash
just ios-dev
```

Note: If you have a device connected (even wirelessly), `just ios-dev` may deploy to it instead of the simulator. Use `just ios-dev-sim` to explicitly target the simulator.

## Troubleshooting

### Vite Server Not Reachable

The iOS simulator needs to connect to your development server. Ensure `frontend/vite.config.ts` has:

```typescript
server: {
host: "0.0.0.0",
port: 5173,
strictPort: true
}
```

### Missing Abseil Symbols

If you see linker errors like:
```
Undefined symbols for architecture arm64:
"_AbslInternalSpinLockDelay_lts_20240722"
```

This means the ONNX Runtime library wasn't built from source. Pre-built binaries are missing these symbols. Run `./scripts/build-ios-onnxruntime-all.sh` to build from source.

### Simulator Build Fails with libiconv Error

If the simulator build fails with:
```
ld: building for 'iOS-simulator', but linking in dylib built for 'iOS'
```

This is fixed by adding `CMAKE_FIND_ROOT_PATH_MODE_LIBRARY=NEVER` to the cmake flags. The `build-ios-onnxruntime-all.sh` script already includes this fix.

### Cargo Not Finding Library

1. Ensure `.cargo/config.toml` uses absolute paths
2. Clean and rebuild: `cd frontend/src-tauri && cargo clean`
3. Verify the library exists: `ls -la onnxruntime-ios/onnxruntime.xcframework/ios-arm64-simulator/`

## Architecture Notes

### Why Build from Source?

1. **Abseil symbols**: ONNX Runtime depends on Abseil (Google's C++ library). Pre-built binaries don't include these statically linked.

2. **Simulator support**: Pre-built libraries often only include device builds.

3. **Version compatibility**: Building from source ensures compatibility with our ort-sys Rust crate version.

### Build Artifacts

After building, you'll have:
```
frontend/src-tauri/
├── onnxruntime-build/ # Build directory (can be deleted after build)
│ └── onnxruntime/ # ONNX Runtime source
└── onnxruntime-ios/ # Output directory
└── onnxruntime.xcframework/
├── Headers/
├── Info.plist
├── ios-arm64/
│ └── libonnxruntime.a # Device library (~69MB)
└── ios-arm64-simulator/
└── libonnxruntime.a # Simulator library (~69MB)
```

### .cargo/config.toml

The cargo config tells the Rust `ort-sys` crate where to find the ONNX Runtime library. The keys are:
- `[target.aarch64-apple-ios.onnxruntime]` - Device builds
- `[target.aarch64-apple-ios-sim.onnxruntime]` - Simulator builds (ARM64 Mac)
- `[target.x86_64-apple-ios.onnxruntime]` - Simulator builds (Intel Mac)

### CI/CD

The CI workflow (`mobile-build.yml`) builds ONNX Runtime from source and caches it. The cache key includes the version number, so updating `ORT_VERSION` will trigger a rebuild.

## Cleaning Up

To free disk space after testing:

```bash
# Remove build directory (keeps the built xcframework)
rm -rf frontend/src-tauri/onnxruntime-build

# Remove everything (requires rebuilding)
rm -rf frontend/src-tauri/onnxruntime-build frontend/src-tauri/onnxruntime-ios
```

## Related Documentation

- [troubleshooting-ios-build.md](./troubleshooting-ios-build.md) - arm64-sim architecture fix
- [tts-research.md](./tts-research.md) - TTS implementation details
1 change: 1 addition & 0 deletions frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions frontend/src-tauri/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@
# will have compiled files and executables
/target/
/gen/schemas

# ONNX Runtime iOS (built from source)
/onnxruntime-ios/
/onnxruntime-build/

# Generated cargo config for iOS builds
/.cargo/
17 changes: 17 additions & 0 deletions frontend/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,22 @@ futures-util = "0.3"
dirs = "5.0"
sha2 = "0.10"

[target.'cfg(target_os = "ios")'.dependencies]
# TTS dependencies (Supertonic) - iOS
# We build ONNX Runtime 1.22.2 from source for iOS (see scripts/build-ios-onnxruntime.sh)
# We disable download-binaries and copy-dylibs since we link our own xcframework
# Need "std" for Error trait impl and file operations, "ndarray" for tensor creation
ort = { version = "2.0.0-rc.10", default-features = false, features = ["std", "ndarray"] }
ndarray = { version = "0.16" }
rand = "0.8"
rand_distr = "0.4"
hound = "3.5"
unicode-normalization = "0.1"
regex = "1.10"
reqwest = { version = "0.12", features = ["stream"] }
futures-util = "0.3"
dirs = "5.0"
sha2 = "0.10"

[target.'cfg(target_os = "android")'.dependencies]
openssl = { version = "0.10", default-features = false, features = ["vendored"] }
Loading
Loading