diff --git a/.github/workflows/mobile-build.yml b/.github/workflows/mobile-build.yml index 40e461ae..564c3113 100644 --- a/.github/workflows/mobile-build.yml +++ b/.github/workflows/mobile-build.yml @@ -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: @@ -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 @@ -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) diff --git a/.github/workflows/testflight-on-comment.yml b/.github/workflows/testflight-on-comment.yml index f72da15a..784cbdbc 100644 --- a/.github/workflows/testflight-on-comment.yml +++ b/.github/workflows/testflight-on-comment.yml @@ -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 @@ -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 }} @@ -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 @@ -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: @@ -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: | diff --git a/docs/ios-tts-local-development.md b/docs/ios-tts-local-development.md new file mode 100644 index 00000000..274167a6 --- /dev/null +++ b/docs/ios-tts-local-development.md @@ -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 diff --git a/frontend/bun.lock b/frontend/bun.lock index acf319d9..847fabcd 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "maple", diff --git a/frontend/src-tauri/.gitignore b/frontend/src-tauri/.gitignore index 502406b4..332ca1f9 100644 --- a/frontend/src-tauri/.gitignore +++ b/frontend/src-tauri/.gitignore @@ -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/ diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 4bfb6eb8..7a980a37 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -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"] } diff --git a/frontend/src-tauri/gen/apple/maple.xcodeproj/project.pbxproj b/frontend/src-tauri/gen/apple/maple.xcodeproj/project.pbxproj index 435919c9..b26d4c6e 100644 --- a/frontend/src-tauri/gen/apple/maple.xcodeproj/project.pbxproj +++ b/frontend/src-tauri/gen/apple/maple.xcodeproj/project.pbxproj @@ -251,18 +251,15 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ARCHS = ( - arm64, - "arm64-sim", - ); + ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = maple_iOS/maple_iOS.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = X773Y823TN; + DEVELOPMENT_TEAM = "X773Y823TN"; ENABLE_BITCODE = NO; - "EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64"; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "\".\"", @@ -294,11 +291,11 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", ); PRODUCT_BUNDLE_IDENTIFIER = cloud.opensecret.maple; - PRODUCT_NAME = Maple; + PRODUCT_NAME = "Maple"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; - VALID_ARCHS = "arm64 arm64-sim"; + VALID_ARCHS = arm64; }; name = debug; }; @@ -425,18 +422,15 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ARCHS = ( - arm64, - "arm64-sim", - ); + ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = maple_iOS/maple_iOS.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = X773Y823TN; + DEVELOPMENT_TEAM = "X773Y823TN"; ENABLE_BITCODE = NO; - "EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64"; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "\".\"", @@ -468,11 +462,11 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", ); PRODUCT_BUNDLE_IDENTIFIER = cloud.opensecret.maple; - PRODUCT_NAME = Maple; + PRODUCT_NAME = "Maple"; PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; - VALID_ARCHS = "arm64 arm64-sim"; + VALID_ARCHS = arm64; }; name = release; }; @@ -543,20 +537,17 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - ARCHS = ( - arm64, - "arm64-sim", - ); + ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = maple_iOS/maple_iOS.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - DEVELOPMENT_TEAM = X773Y823TN; + DEVELOPMENT_TEAM = "X773Y823TN"; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = X773Y823TN; ENABLE_BITCODE = NO; - "EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64"; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; + "EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "\".\"", @@ -588,12 +579,12 @@ "$(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)", ); PRODUCT_BUNDLE_IDENTIFIER = cloud.opensecret.maple; - PRODUCT_NAME = Maple; + PRODUCT_NAME = "Maple"; PROVISIONING_PROFILE_SPECIFIER = "86059ea7-ae8e-44af-8a58-b2ab7c78d299"; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "86059ea7-ae8e-44af-8a58-b2ab7c78d299"; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; - VALID_ARCHS = "arm64 arm64-sim"; + VALID_ARCHS = arm64; }; name = local; }; diff --git a/frontend/src-tauri/scripts/build-ios-onnxruntime-all.sh b/frontend/src-tauri/scripts/build-ios-onnxruntime-all.sh new file mode 100755 index 00000000..390c2aa8 --- /dev/null +++ b/frontend/src-tauri/scripts/build-ios-onnxruntime-all.sh @@ -0,0 +1,212 @@ +#!/bin/bash +# Build ONNX Runtime from source for iOS (device + simulator) +# This creates a static library with all dependencies (including Abseil) statically linked +# +# Prerequisites: +# - macOS with Xcode installed +# - CMake 3.26+ +# - Python 3.8+ +# - Git +# +# Usage: ./build-ios-onnxruntime-all.sh [version] +# Example: ./build-ios-onnxruntime-all.sh 1.22.2 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TAURI_DIR="$(dirname "$SCRIPT_DIR")" +ORT_VERSION="${1:-1.22.2}" +BUILD_DIR="${TAURI_DIR}/onnxruntime-build" +OUTPUT_DIR="${TAURI_DIR}/onnxruntime-ios" +XCFRAMEWORK_DIR="${OUTPUT_DIR}/onnxruntime.xcframework" + +IOS_DEPLOYMENT_TARGET="13.0" + +echo "========================================" +echo "Building ONNX Runtime ${ORT_VERSION} for iOS" +echo "(Device + Simulator)" +echo "========================================" +echo "Build directory: ${BUILD_DIR}" +echo "Output directory: ${OUTPUT_DIR}" +echo "" + +# Check prerequisites +command -v cmake >/dev/null 2>&1 || { echo "Error: cmake is required"; exit 1; } +command -v python3 >/dev/null 2>&1 || { echo "Error: python3 is required"; exit 1; } +command -v git >/dev/null 2>&1 || { echo "Error: git is required"; exit 1; } +command -v xcodebuild >/dev/null 2>&1 || { echo "Error: Xcode is required"; exit 1; } + +# Check if output already exists +if [ -d "$XCFRAMEWORK_DIR" ] && [ -f "${XCFRAMEWORK_DIR}/ios-arm64/libonnxruntime.a" ] && [ -f "${XCFRAMEWORK_DIR}/ios-arm64-simulator/libonnxruntime.a" ]; then + echo "ONNX Runtime xcframework already exists with both device and simulator libraries" + echo "To rebuild, remove: rm -rf $OUTPUT_DIR" + exit 0 +fi + +mkdir -p "$BUILD_DIR" +cd "$BUILD_DIR" + +# Clone ONNX Runtime +clone_with_retry() { + local max_attempts=3 + local attempt=1 + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt of $max_attempts..." + if git clone --depth 1 --branch "v${ORT_VERSION}" --recursive https://github.com/microsoft/onnxruntime.git; then + return 0 + fi + sleep 10 + attempt=$((attempt + 1)) + done + return 1 +} + +if [ ! -d "onnxruntime" ]; then + echo "Cloning ONNX Runtime repository..." + clone_with_retry +fi + +cd onnxruntime + +# Common cmake defines +CMAKE_EXTRA_DEFINES="CMAKE_POLICY_VERSION_MINIMUM=3.5" + +# Function to build and combine libraries +build_and_combine() { + local SYSROOT=$1 + local OUTPUT_SUFFIX=$2 + local EXTRA_CMAKE_DEFINES=$3 + + echo "" + echo "========================================" + echo "Building for ${SYSROOT} (arm64)..." + echo "========================================" + + # Clean previous build for this target + rm -rf "build/iOS/Release" + + ./build.sh \ + --build_dir build/iOS \ + --config Release \ + --use_xcode \ + --ios \ + --apple_sysroot "${SYSROOT}" \ + --osx_arch arm64 \ + --apple_deploy_target "${IOS_DEPLOYMENT_TARGET}" \ + --parallel \ + --skip_tests \ + --compile_no_warning_as_error \ + --cmake_extra_defines "${CMAKE_EXTRA_DEFINES} ${EXTRA_CMAKE_DEFINES}" + + # Find and combine libraries + local BUILD_OUTPUT_DIR="build/iOS/Release/Release-${SYSROOT}" + local COMBINED_LIB="${BUILD_OUTPUT_DIR}/libonnxruntime_combined.a" + + echo "Combining static libraries..." + local LIBS=$(find build/iOS/Release -name "*.a" -path "*Release-${SYSROOT}*" -type f | grep -v "gtest\|gmock" | sort -u) + + if [ -z "$LIBS" ]; then + echo "Error: No libraries found for ${SYSROOT}" + return 1 + fi + + libtool -static -o "$COMBINED_LIB" $LIBS 2>&1 | grep -v "warning duplicate member" || true + + if [ ! -f "$COMBINED_LIB" ]; then + echo "Error: Failed to create combined library" + return 1 + fi + + echo "Created: $COMBINED_LIB ($(du -h "$COMBINED_LIB" | cut -f1))" + + # Copy to output + mkdir -p "${XCFRAMEWORK_DIR}/${OUTPUT_SUFFIX}" + cp "$COMBINED_LIB" "${XCFRAMEWORK_DIR}/${OUTPUT_SUFFIX}/libonnxruntime.a" +} + +# Create output directories +mkdir -p "${OUTPUT_DIR}" +mkdir -p "${XCFRAMEWORK_DIR}/Headers" + +# Build for device +build_and_combine "iphoneos" "ios-arm64" "" + +# Build for simulator +# CMAKE_FIND_ROOT_PATH_MODE_LIBRARY=NEVER fixes the libiconv linking bug +build_and_combine "iphonesimulator" "ios-arm64-simulator" "CMAKE_FIND_ROOT_PATH_MODE_LIBRARY=NEVER" + +# Copy headers +HEADER_DIR=$(find build -name "onnxruntime_c_api.h" -type f | head -n 1 | xargs dirname 2>/dev/null) +if [ -n "$HEADER_DIR" ]; then + cp "${HEADER_DIR}"/*.h "${XCFRAMEWORK_DIR}/Headers/" 2>/dev/null || true +fi +if [ -d "include/onnxruntime/core/session" ]; then + cp include/onnxruntime/core/session/*.h "${XCFRAMEWORK_DIR}/Headers/" 2>/dev/null || true +fi + +# Create Info.plist +cat > "${XCFRAMEWORK_DIR}/Info.plist" << 'PLIST' + + + + + AvailableLibraries + + + HeadersPath + Headers + LibraryIdentifier + ios-arm64 + LibraryPath + libonnxruntime.a + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + HeadersPath + Headers + LibraryIdentifier + ios-arm64-simulator + LibraryPath + libonnxruntime.a + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + +PLIST + +echo "" +echo "========================================" +echo "Build complete!" +echo "========================================" +echo "" +echo "xcframework: ${XCFRAMEWORK_DIR}" +echo "" +echo "Libraries:" +ls -lh "${XCFRAMEWORK_DIR}/ios-arm64/libonnxruntime.a" +ls -lh "${XCFRAMEWORK_DIR}/ios-arm64-simulator/libonnxruntime.a" +# Generate cargo config +echo "" +echo "Generating .cargo/config.toml..." +"${SCRIPT_DIR}/setup-ios-cargo-config.sh" + +echo "" +echo "Next steps:" +echo "1. Fix arm64-sim issue if needed (see docs/troubleshooting-ios-build.md)" +echo "2. Run: just ios-dev-sim 'iPhone 16 Pro'" diff --git a/frontend/src-tauri/scripts/build-ios-onnxruntime.sh b/frontend/src-tauri/scripts/build-ios-onnxruntime.sh new file mode 100755 index 00000000..8ae0971b --- /dev/null +++ b/frontend/src-tauri/scripts/build-ios-onnxruntime.sh @@ -0,0 +1,299 @@ +#!/bin/bash +# Build ONNX Runtime from source for iOS +# This creates a static library with all dependencies (including Abseil) statically linked +# +# Prerequisites: +# - macOS with Xcode installed +# - CMake 3.26+ +# - Python 3.8+ +# - Git +# +# Usage: ./build-ios-onnxruntime.sh [version] +# Example: ./build-ios-onnxruntime.sh 1.20.1 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TAURI_DIR="$(dirname "$SCRIPT_DIR")" +# Use latest 1.22.2 - older versions have Eigen hash mismatch issues with GitLab +ORT_VERSION="${1:-1.22.2}" +BUILD_DIR="${TAURI_DIR}/onnxruntime-build" +OUTPUT_DIR="${TAURI_DIR}/onnxruntime-ios" +XCFRAMEWORK_DIR="${OUTPUT_DIR}/onnxruntime.xcframework" + +# Minimum iOS version to support +IOS_DEPLOYMENT_TARGET="13.0" + +echo "========================================" +echo "Building ONNX Runtime ${ORT_VERSION} for iOS" +echo "========================================" +echo "Build directory: ${BUILD_DIR}" +echo "Output directory: ${OUTPUT_DIR}" +echo "iOS deployment target: ${IOS_DEPLOYMENT_TARGET}" +echo "" + +# Check prerequisites +command -v cmake >/dev/null 2>&1 || { echo "Error: cmake is required but not installed."; exit 1; } +command -v python3 >/dev/null 2>&1 || { echo "Error: python3 is required but not installed."; exit 1; } +command -v git >/dev/null 2>&1 || { echo "Error: git is required but not installed."; exit 1; } +command -v xcodebuild >/dev/null 2>&1 || { echo "Error: Xcode is required but not installed."; exit 1; } + +# Check if output already exists +if [ -d "$XCFRAMEWORK_DIR" ]; then + echo "ONNX Runtime xcframework already exists at $XCFRAMEWORK_DIR" + echo "To rebuild, remove the directory first: rm -rf $OUTPUT_DIR" + exit 0 +fi + +# Create build directory +mkdir -p "$BUILD_DIR" +cd "$BUILD_DIR" + +# Clone ONNX Runtime if not already cloned (with retry for transient network errors) +clone_with_retry() { + local max_attempts=3 + local attempt=1 + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt of $max_attempts..." + if git clone --depth 1 --branch "v${ORT_VERSION}" --recursive https://github.com/microsoft/onnxruntime.git; then + return 0 + fi + echo "Clone failed, waiting 10 seconds before retry..." + sleep 10 + attempt=$((attempt + 1)) + done + echo "Failed to clone after $max_attempts attempts" + return 1 +} + +submodule_update_with_retry() { + local max_attempts=3 + local attempt=1 + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt of $max_attempts..." + if git submodule update --init --recursive; then + return 0 + fi + echo "Submodule update failed, waiting 10 seconds before retry..." + sleep 10 + attempt=$((attempt + 1)) + done + echo "Failed to update submodules after $max_attempts attempts" + return 1 +} + +if [ ! -d "onnxruntime" ]; then + echo "Cloning ONNX Runtime repository..." + clone_with_retry +else + echo "ONNX Runtime repository already cloned" + cd onnxruntime + git fetch --tags + git checkout "v${ORT_VERSION}" + submodule_update_with_retry + cd .. +fi + +cd onnxruntime + +# Common cmake extra defines to work around compatibility issues +# CMAKE_POLICY_VERSION_MINIMUM=3.5 fixes nsync compatibility with newer CMake +CMAKE_EXTRA_DEFINES="CMAKE_POLICY_VERSION_MINIMUM=3.5" + +# Build for iOS device (arm64) +echo "" +echo "========================================" +echo "Building for iOS device (arm64)..." +echo "========================================" + +./build.sh \ + --config Release \ + --use_xcode \ + --ios \ + --apple_sysroot iphoneos \ + --osx_arch arm64 \ + --apple_deploy_target "${IOS_DEPLOYMENT_TARGET}" \ + --parallel \ + --skip_tests \ + --compile_no_warning_as_error \ + --cmake_extra_defines "${CMAKE_EXTRA_DEFINES}" + +# ONNX Runtime builds multiple static libraries, we need to combine them +# The libraries are in build/iOS/Release/Release-iphoneos/ +IOS_ARM64_BUILD_DIR="build/iOS/Release/Release-iphoneos" +IOS_ARM64_COMBINED_LIB="${IOS_ARM64_BUILD_DIR}/libonnxruntime_combined.a" + +echo "" +echo "Combining iOS arm64 static libraries..." + +# Find all ONNX Runtime static libraries and combine them +# We need: onnxruntime_*, onnx*, protobuf-lite, re2, cpuinfo, abseil libs, etc. +IOS_ARM64_LIBS=$(find build/iOS/Release -name "*.a" -path "*Release-iphoneos*" -type f | grep -v "gtest\|gmock" | sort -u) + +if [ -z "$IOS_ARM64_LIBS" ]; then + echo "Error: Could not find iOS arm64 static libraries" + exit 1 +fi + +echo "Found libraries to combine:" +echo "$IOS_ARM64_LIBS" | head -20 +echo "..." + +# Use libtool to combine all static libraries into one +libtool -static -o "$IOS_ARM64_COMBINED_LIB" $IOS_ARM64_LIBS + +if [ ! -f "$IOS_ARM64_COMBINED_LIB" ]; then + echo "Error: Failed to create combined library" + exit 1 +fi + +IOS_ARM64_LIB="$IOS_ARM64_COMBINED_LIB" +echo "Created combined library: $IOS_ARM64_LIB" +ls -lh "$IOS_ARM64_LIB" + +# SKIP SIMULATOR BUILD for now +# The simulator build has a bug where it tries to link against the wrong iconv library: +# "ld: building for 'iOS-simulator', but linking in dylib built for 'iOS'" +# For TestFlight/App Store deployment, we only need the device build anyway. +# Local development can use the desktop version or a physical device. +echo "" +echo "Skipping iOS simulator build (known ONNX Runtime CMake bug with libiconv)" +echo "Device build is sufficient for TestFlight deployment" +IOS_SIM_ARM64_LIB="" + +HAS_SIM_LIB=false +if [ -n "$IOS_SIM_ARM64_LIB" ] && [ -f "$IOS_SIM_ARM64_LIB" ]; then + HAS_SIM_LIB=true +fi + +# Create output directories +echo "" +echo "========================================" +echo "Creating xcframework..." +echo "========================================" + +mkdir -p "${OUTPUT_DIR}" +mkdir -p "${XCFRAMEWORK_DIR}/ios-arm64" +mkdir -p "${XCFRAMEWORK_DIR}/Headers" + +if [ "$HAS_SIM_LIB" = true ]; then + mkdir -p "${XCFRAMEWORK_DIR}/ios-arm64-simulator" +fi + +# Copy the device library +cp "$IOS_ARM64_LIB" "${XCFRAMEWORK_DIR}/ios-arm64/libonnxruntime.a" + +# Copy the simulator library (arm64 only for now) +if [ "$HAS_SIM_LIB" = true ]; then + cp "$IOS_SIM_ARM64_LIB" "${XCFRAMEWORK_DIR}/ios-arm64-simulator/libonnxruntime.a" +else + echo "Warning: No simulator library available" +fi + +# Copy headers +HEADER_DIR=$(find build -name "onnxruntime_c_api.h" -type f | head -n 1 | xargs dirname) +if [ -n "$HEADER_DIR" ]; then + cp "${HEADER_DIR}"/*.h "${XCFRAMEWORK_DIR}/Headers/" 2>/dev/null || true +fi + +# Also copy headers from include directory +if [ -d "include/onnxruntime/core/session" ]; then + cp include/onnxruntime/core/session/*.h "${XCFRAMEWORK_DIR}/Headers/" 2>/dev/null || true +fi + +# Create Info.plist for xcframework +if [ "$HAS_SIM_LIB" = true ]; then +cat > "${XCFRAMEWORK_DIR}/Info.plist" << 'PLIST' + + + + + AvailableLibraries + + + HeadersPath + Headers + LibraryIdentifier + ios-arm64 + LibraryPath + libonnxruntime.a + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + HeadersPath + Headers + LibraryIdentifier + ios-arm64-simulator + LibraryPath + libonnxruntime.a + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + SupportedPlatformVariant + simulator + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + +PLIST +else +cat > "${XCFRAMEWORK_DIR}/Info.plist" << 'PLIST' + + + + + AvailableLibraries + + + HeadersPath + Headers + LibraryIdentifier + ios-arm64 + LibraryPath + libonnxruntime.a + SupportedArchitectures + + arm64 + + SupportedPlatform + ios + + + CFBundlePackageType + XFWK + XCFrameworkFormatVersion + 1.0 + + +PLIST +fi + +echo "" +echo "========================================" +echo "Build complete!" +echo "========================================" +echo "" +echo "ONNX Runtime xcframework created at:" +echo " ${XCFRAMEWORK_DIR}" +echo "" +echo "Contents:" +ls -la "${XCFRAMEWORK_DIR}" +echo "" +echo "Static library sizes:" +ls -lh "${XCFRAMEWORK_DIR}/ios-arm64/libonnxruntime.a" +ls -lh "${XCFRAMEWORK_DIR}/ios-arm64-simulator/libonnxruntime.a" 2>/dev/null || echo "No simulator library" +echo "" +echo "Verifying library contains key symbols:" +nm "${XCFRAMEWORK_DIR}/ios-arm64/libonnxruntime.a" 2>/dev/null | grep -i "OrtCreateSession" | head -3 || echo "Symbols check skipped" diff --git a/frontend/src-tauri/scripts/setup-ios-cargo-config.sh b/frontend/src-tauri/scripts/setup-ios-cargo-config.sh new file mode 100755 index 00000000..8ec821ff --- /dev/null +++ b/frontend/src-tauri/scripts/setup-ios-cargo-config.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Generate .cargo/config.toml for iOS ONNX Runtime linking +# This script creates the config with absolute paths based on the current directory + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TAURI_DIR="$(dirname "$SCRIPT_DIR")" +CARGO_DIR="${TAURI_DIR}/.cargo" +CONFIG_FILE="${CARGO_DIR}/config.toml" +XCFRAMEWORK_DIR="${TAURI_DIR}/onnxruntime-ios/onnxruntime.xcframework" + +# Check if xcframework exists +if [ ! -d "$XCFRAMEWORK_DIR" ]; then + echo "Error: ONNX Runtime xcframework not found at: $XCFRAMEWORK_DIR" + echo "Run ./scripts/build-ios-onnxruntime-all.sh first" + exit 1 +fi + +# Check for required libraries +if [ ! -f "${XCFRAMEWORK_DIR}/ios-arm64/libonnxruntime.a" ]; then + echo "Error: Device library not found. Build ONNX Runtime first." + exit 1 +fi + +mkdir -p "$CARGO_DIR" + +echo "Generating ${CONFIG_FILE}..." + +cat > "$CONFIG_FILE" << EOF +# Auto-generated cargo config for iOS ONNX Runtime linking +# Generated by: scripts/setup-ios-cargo-config.sh +# Regenerate with: ./scripts/setup-ios-cargo-config.sh + +[target.aarch64-apple-ios.onnxruntime] +rustc-link-search = ["${XCFRAMEWORK_DIR}/ios-arm64"] +rustc-link-lib = ["static=onnxruntime"] + +[target.aarch64-apple-ios-sim.onnxruntime] +rustc-link-search = ["${XCFRAMEWORK_DIR}/ios-arm64-simulator"] +rustc-link-lib = ["static=onnxruntime"] + +[env] +ORT_LIB_LOCATION = "${XCFRAMEWORK_DIR}/ios-arm64" +EOF + +echo "Created: $CONFIG_FILE" +echo "" +echo "Paths configured:" +echo " Device: ${XCFRAMEWORK_DIR}/ios-arm64" +echo " Simulator: ${XCFRAMEWORK_DIR}/ios-arm64-simulator" + +# Verify simulator library exists (warn if not) +if [ ! -f "${XCFRAMEWORK_DIR}/ios-arm64-simulator/libonnxruntime.a" ]; then + echo "" + echo "Warning: Simulator library not found!" + echo "Device builds will work, but simulator builds will fail." + echo "Run ./scripts/build-ios-onnxruntime-all.sh to build both." +fi diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 2deadf0e..a9cee65f 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -3,7 +3,8 @@ use tauri_plugin_deep_link::DeepLinkExt; mod pdf_extractor; mod proxy; -#[cfg(desktop)] +// TTS is available on desktop and iOS (not Android) +#[cfg(any(desktop, target_os = "ios"))] mod tts; #[cfg(desktop)] @@ -240,6 +241,7 @@ pub fn run() { }) .plugin(tauri_plugin_updater::Builder::new().build()); + // Mobile (iOS and Android) configuration #[cfg(not(desktop))] let mut builder = tauri::Builder::default() .plugin( @@ -258,7 +260,14 @@ pub fn run() { builder = builder.plugin(tauri_plugin_sign_in_with_apple::init()); } - #[cfg(not(desktop))] + // Add TTS state management for iOS + #[cfg(all(not(desktop), target_os = "ios"))] + { + builder = builder.manage(tts::TTSState::new()); + } + + // Android-specific configuration (no TTS) + #[cfg(all(not(desktop), target_os = "android"))] let app = builder .invoke_handler(tauri::generate_handler![ pdf_extractor::extract_document_content, @@ -279,6 +288,34 @@ pub fn run() { }) .plugin(tauri_plugin_updater::Builder::new().build()); + // iOS-specific configuration (with TTS) + #[cfg(all(not(desktop), target_os = "ios"))] + let app = builder + .invoke_handler(tauri::generate_handler![ + pdf_extractor::extract_document_content, + tts::tts_get_status, + tts::tts_download_models, + tts::tts_load_models, + tts::tts_synthesize, + tts::tts_unload_models, + tts::tts_delete_models, + ]) + .setup(|app| { + // Set up the deep link handler for mobile + let app_handle = app.handle().clone(); + + // Register deep link handler - note that iOS does not support runtime registration + // but the handler for incoming URLs still works + app.deep_link().on_open_url(move |event| { + if let Some(url) = event.urls().first() { + handle_deep_link_event(url.as_ref(), &app_handle); + } + }); + + Ok(()) + }) + .plugin(tauri_plugin_updater::Builder::new().build()); + app.run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/frontend/src-tauri/src/tts.rs b/frontend/src-tauri/src/tts.rs index 5c5f2566..e8b78e9b 100644 --- a/frontend/src-tauri/src/tts.rs +++ b/frontend/src-tauri/src/tts.rs @@ -612,11 +612,28 @@ impl TextToSpeech { } fn get_tts_models_dir() -> Result { - let data_dir = dirs::data_local_dir() - .context("Failed to get local data directory")? - .join("cloud.opensecret.maple") - .join("tts_models"); - Ok(data_dir) + // On iOS, we need to use a different approach since dirs::data_local_dir() may not work + #[cfg(target_os = "ios")] + { + // On iOS, store models under Library/Caches so they're not user-visible (Files app) + // and won't be iCloud-backed. + let home = std::env::var("HOME").context("Failed to get HOME directory on iOS")?; + let data_dir = PathBuf::from(home) + .join("Library") + .join("Caches") + .join("cloud.opensecret.maple") + .join("tts_models"); + return Ok(data_dir); + } + + #[cfg(not(target_os = "ios"))] + { + let data_dir = dirs::data_local_dir() + .context("Failed to get local data directory")? + .join("cloud.opensecret.maple") + .join("tts_models"); + Ok(data_dir) + } } fn load_voice_style(models_dir: &Path) -> Result