diff --git a/.github/workflows/framework-app-store-validation.yml b/.github/workflows/framework-app-store-validation.yml deleted file mode 100644 index fe6be1391..000000000 --- a/.github/workflows/framework-app-store-validation.yml +++ /dev/null @@ -1,185 +0,0 @@ -name: Framework App Store Validation - -on: - pull_request: - branches: [main] - paths: - - 'Sources/FluidAudio/Frameworks/**' - - '.github/workflows/framework-app-store-validation.yml' - push: - branches: [main] - paths: - - 'Sources/FluidAudio/Frameworks/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - validate-app-store-compliance: - name: Validate App Store Compliance - runs-on: macos-15 - - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Build Package - run: | - echo "๐Ÿ“ฆ Building package to validate framework linkage..." - swift build -v 2>&1 | tee build.log - - - name: Check Build Log for Framework Errors - run: | - echo "๐Ÿ” Checking build log for framework-related errors..." - - # Check for dyld errors (broken framework structure) - if grep -i "dyld.*error\|library not loaded\|cannot find.*framework" build.log; then - echo "โŒ ERROR: dyld errors detected - framework structure may be broken" - exit 1 - fi - - # Check for code signing errors - if grep -i "code sign\|codesign.*error" build.log | grep -v "No identity found\|Developer ID"; then - echo "โš ๏ธ Code signing warnings (may be expected)" - fi - - echo "โœ… No dyld errors detected" - - - name: Run Framework Link Tests - run: | - echo "๐Ÿงช Running framework linkage tests..." - swift test -v --filter FrameworkLinkTests 2>&1 | tee test.log || true - - - name: Verify Framework Symlink Structure for App Store - run: | - set -e - - echo "๐Ÿ”— Verifying App Store compliance of framework structure..." - echo "" - - FRAMEWORK_PATH="Sources/FluidAudio/Frameworks/ESpeakNG.xcframework" - - # Function to check App Store compliance - check_app_store_compliance() { - local framework_dir="$1" - local variant_name="$2" - - if [ ! -d "$framework_dir" ]; then - return 0 - fi - - echo "๐Ÿ“‹ Checking $variant_name for App Store compliance..." - - # For macOS frameworks with versioning, check symlink structure - if [ -d "$framework_dir/Versions" ]; then - # Apple's App Store validation requires symlinks to point to Versions/Current - # not directly to Versions/A (which causes ITMS-90291 errors) - - local errors=0 - - # Check each top-level symlink - for symlink in ESpeakNG Headers Modules Resources; do - if [ -L "$framework_dir/$symlink" ]; then - local target=$(readlink "$framework_dir/$symlink") - - # App Store requires Versions/Current, not Versions/A - if [[ "$target" == "Versions/A"* ]]; then - echo " โŒ ITMS-90291: $symlink points to Versions/A/" - echo " This would cause App Store validation failure" - errors=$((errors + 1)) - elif [[ "$target" == "Versions/Current"* ]]; then - echo " โœ“ $symlink โ†’ Versions/Current/ (App Store compliant)" - else - echo " โŒ Unexpected symlink target: $symlink โ†’ $target" - errors=$((errors + 1)) - fi - fi - done - - local info_current="$framework_dir/Versions/Current/Resources/Info.plist" - local info_symlink="$framework_dir/Resources/Info.plist" - if [ ! -f "$info_current" ]; then - echo " โŒ Info.plist missing at Versions/Current/Resources/Info.plist" - errors=$((errors + 1)) - else - echo " โœ“ Info.plist located at Versions/Current/Resources/Info.plist" - fi - if [ ! -f "$info_symlink" ]; then - echo " โŒ Resources/Info.plist symlink missing" - errors=$((errors + 1)) - fi - - if [ $errors -gt 0 ]; then - return 1 - fi - fi - - return 0 - } - - # Check all variants - all_valid=true - for variant in "$FRAMEWORK_PATH"/*; do - if [ -d "$variant" ] && [ -d "$variant/ESpeakNG.framework" ]; then - variant_name=$(basename "$variant") - if ! check_app_store_compliance "$variant/ESpeakNG.framework" "$variant_name"; then - all_valid=false - fi - fi - done - - if [ "$all_valid" = true ]; then - echo "" - echo "โœ… Framework structure is App Store compliant" - exit 0 - else - echo "" - echo "โŒ Framework structure has App Store compliance issues" - exit 1 - fi - - - name: Validate Framework Resources - run: | - echo "๐Ÿ“ฆ Validating framework resources..." - echo "" - - FRAMEWORK_PATH="Sources/FluidAudio/Frameworks/ESpeakNG.xcframework" - - found_resources=false - while IFS= read -r -d '' framework_dir; do - found_resources=true - if [ -L "$framework_dir/Resources" ]; then - echo "โœ“ $(basename "$framework_dir") Resources symlink resolves" - resource_count=$(find "$framework_dir/Resources" -type f | wc -l | tr -d '[:space:]') - echo " Found $resource_count resource files" - elif [ -d "$framework_dir/Resources" ]; then - echo "โš ๏ธ $(basename "$framework_dir") has a real Resources directory (expected symlink for versioned frameworks)" - fi - done < <(find "$FRAMEWORK_PATH" -type d -name "ESpeakNG.framework" -print0) - - if [ "$found_resources" = false ]; then - echo "โš ๏ธ No ESpeakNG.framework directories found when validating resources" - fi - - echo "โœ… Resources validation complete" - - - name: Summary - if: always() - run: | - echo "" - echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - echo "Framework Validation Summary" - echo "โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”" - echo "" - echo "โœ“ Build log checked for dyld errors" - echo "โœ“ Framework linkage verified" - echo "โœ“ Symlink structure validated" - echo "โœ“ App Store compliance verified" - echo "" - echo "These checks catch:" - echo " โ€ข Binary naming errors (e.g., ESPeakNG vs ESpeakNG)" - echo " โ€ข Broken symlink chains" - echo " โ€ข ITMS-90291 validation errors" - echo " โ€ข Framework structure compliance issues" - echo "" diff --git a/.github/workflows/framework-validation.yml b/.github/workflows/framework-validation.yml deleted file mode 100644 index e59389903..000000000 --- a/.github/workflows/framework-validation.yml +++ /dev/null @@ -1,216 +0,0 @@ -name: Framework Structure Validation - -on: - pull_request: - branches: [main] - paths: - - 'Sources/FluidAudio/Frameworks/**' - push: - branches: [main] - paths: - - 'Sources/FluidAudio/Frameworks/**' - -jobs: - validate-framework-structure: - name: Validate Framework Structure - runs-on: macos-15 - - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Check ESpeakNG Framework Structure - run: | - set -e - - FRAMEWORK_PATH="Sources/FluidAudio/Frameworks/ESpeakNG.xcframework" - - echo "๐Ÿ” Validating framework structure..." - echo "" - - # Function to validate a single framework variant - validate_framework() { - local variant_path="$1" - local variant_name=$(basename "$(dirname "$variant_path")") - local variant_name_lower=$(echo "$variant_name" | tr '[:upper:]' '[:lower:]') - local requires_versioned=false - - # Mac Catalyst and macOS variants must be packaged as versioned bundles - if [[ "$variant_name_lower" == *"maccatalyst"* || "$variant_name_lower" == *"macos"* ]]; then - requires_versioned=true - fi - - if [ ! -d "$variant_path" ]; then - echo "โš ๏ธ Skipping $variant_name (not found)" - return 0 - fi - - echo "๐Ÿ“ฆ Validating $variant_name variant..." - - # Check if framework uses versioning (macOS / Catalyst) or flat structure (iOS) - if [ -d "$variant_path/Versions" ]; then - echo " โœ“ Versioned framework detected" - - # Check Versions/Current symlink exists - if [ ! -L "$variant_path/Versions/Current" ]; then - echo " โŒ ERROR: Versions/Current symlink not found" - return 1 - fi - echo " โœ“ Versions/Current symlink exists" - - # Check Versions/Current points to A - local current_target - current_target=$(readlink "$variant_path/Versions/Current") - if [ "$current_target" != "A" ]; then - echo " โŒ ERROR: Versions/Current points to '$current_target' instead of 'A'" - return 1 - fi - echo " โœ“ Versions/Current โ†’ A" - - # Check top-level symlinks point to Versions/Current/* (not Versions/A/*) - local symlinks=("ESpeakNG" "Headers" "Modules" "Resources") - for symlink in "${symlinks[@]}"; do - if [ ! -L "$variant_path/$symlink" ]; then - echo " โŒ ERROR: $symlink is not a symlink" - return 1 - fi - - local target - target=$(readlink "$variant_path/$symlink") - if [[ ! "$target" =~ ^Versions/Current/ ]]; then - echo " โŒ ERROR: $symlink points to '$target' instead of Versions/Current/*" - return 1 - fi - done - echo " โœ“ Top-level symlinks point to Versions/Current/*" - - # Ensure Info.plist is stored within the versioned resources - local info_plist_versioned="$variant_path/Versions/A/Resources/Info.plist" - local info_plist_current="$variant_path/Versions/Current/Resources/Info.plist" - local info_plist_resources="$variant_path/Resources/Info.plist" - if [ ! -f "$info_plist_versioned" ]; then - echo " โŒ ERROR: Info.plist missing at Versions/A/Resources/Info.plist" - return 1 - fi - if [ ! -f "$info_plist_current" ]; then - echo " โŒ ERROR: Info.plist missing at Versions/Current/Resources/Info.plist" - return 1 - fi - if [ ! -f "$info_plist_resources" ]; then - echo " โŒ ERROR: Info.plist missing via Resources/Info.plist symlink" - return 1 - fi - echo " โœ“ Info.plist correctly nested inside Resources" - - # Check that the actual binary exists and is valid - local binary_path="$variant_path/Versions/A/ESpeakNG" - if [ ! -f "$binary_path" ]; then - echo " โŒ ERROR: Binary not found at $binary_path" - return 1 - fi - echo " โœ“ Binary file exists: Versions/A/ESpeakNG" - - # Verify it's a valid Mach-O binary - local file_type - file_type=$(file "$binary_path" | grep -o "Mach-O\|executable") - if [ -z "$file_type" ]; then - echo " โŒ ERROR: Binary is not a valid Mach-O file" - return 1 - fi - echo " โœ“ Valid Mach-O binary" - - # Try to verify code signature (if signed) - if codesign -v "$binary_path" 2>/dev/null; then - echo " โœ“ Code signature valid" - else - echo " โš ๏ธ Code signature not valid (expected for local builds)" - fi - else - if [ "$requires_versioned" = true ]; then - echo " โŒ ERROR: $variant_name must use a versioned framework layout (missing Versions/ directory)" - return 1 - fi - - # Flat framework structure (iOS) - echo " โœ“ Flat framework structure detected (iOS)" - - # Check binary exists directly - local binary_path="$variant_path/ESpeakNG" - if [ ! -f "$binary_path" ]; then - echo " โŒ ERROR: Binary not found at ESpeakNG" - return 1 - fi - echo " โœ“ Binary file exists: ESpeakNG" - - # Verify it's a valid Mach-O binary - local file_type - file_type=$(file "$binary_path" | grep -o "Mach-O\|executable") - if [ -z "$file_type" ]; then - echo " โŒ ERROR: Binary is not a valid Mach-O file" - return 1 - fi - echo " โœ“ Valid Mach-O binary" - fi - - echo "" - return 0 - } - # Validate all framework variants - all_valid=true - for variant in "$FRAMEWORK_PATH"/*; do - if [ -d "$variant" ] && [ -d "$variant/ESpeakNG.framework" ]; then - validate_framework "$variant/ESpeakNG.framework" || all_valid=false - fi - done - - if [ "$all_valid" = true ]; then - echo "โœ… All framework variants are valid!" - exit 0 - else - echo "โŒ Framework validation failed" - exit 1 - fi - - - name: Check for Common Framework Issues - run: | - set -e - - FRAMEWORK_PATH="Sources/FluidAudio/Frameworks/ESpeakNG.xcframework" - - echo "๐Ÿ”Ž Checking for common framework structure issues..." - echo "" - - echo "๐Ÿ”  Verifying binary casing inside versioned frameworks..." - found_versioned=false - while IFS= read -r -d '' version_dir; do - found_versioned=true - actual_name=$(python3 -c 'import os, sys; path=sys.argv[1]; target="espeakng"; print(next((n for n in os.listdir(path) if n.lower()==target), ""))' "$version_dir") - if [ -z "$actual_name" ]; then - echo "โŒ ERROR: No ESpeakNG binary found inside $version_dir" - exit 1 - fi - if [ "$actual_name" != "ESpeakNG" ]; then - echo "โŒ ERROR: Binary '$actual_name' found in $version_dir (expected ESpeakNG)" - echo " This would cause dyld errors on case-sensitive filesystems" - exit 1 - fi - done < <(find "$FRAMEWORK_PATH" -type d -path "*/ESpeakNG.framework/Versions/A" -print0) - - if [ "$found_versioned" = false ]; then - echo " โš ๏ธ No versioned frameworks found to inspect" - else - echo " โœ“ Binary casing validated in versioned slices" - fi - - echo "" - echo "๐Ÿ”— Checking for broken symlinks..." - while IFS= read -r -d '' framework; do - while IFS= read -r -d '' link; do - if [ -L "$link" ] && ! [ -e "$link" ]; then - echo "โŒ ERROR: Broken symlink: $(basename "$link")" - exit 1 - fi - done < <(find "$framework" -type l -print0) - done < <(find "$FRAMEWORK_PATH" -type d -name "ESpeakNG.framework" -print0) - - echo "โœ… All symlinks are valid" diff --git a/.github/workflows/kokoro-tts-test.yml b/.github/workflows/kokoro-tts-test.yml new file mode 100644 index 000000000..1f534892d --- /dev/null +++ b/.github/workflows/kokoro-tts-test.yml @@ -0,0 +1,136 @@ +name: Kokoro TTS Smoke Test + +on: + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + kokoro-tts-smoke-test: + name: Kokoro TTS Smoke Test + runs-on: macos-15 + permissions: + contents: read + pull-requests: write + + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v5 + + - uses: swift-actions/setup-swift@v2 + with: + swift-version: "6.1" + + - name: Cache Dependencies + uses: actions/cache@v4 + with: + path: | + .build + ~/Library/Application Support/FluidAudio/Models/kokoro-82m-coreml + ~/Library/Caches/Homebrew + /usr/local/Cellar/ffmpeg + /opt/homebrew/Cellar/ffmpeg + key: ${{ runner.os }}-kokoro-tts-${{ hashFiles('Package.resolved', 'Sources/FluidAudio/TTS/Kokoro/**', 'Sources/FluidAudio/ModelNames.swift') }} + + - name: Install ffmpeg + run: | + brew install ffmpeg || echo "ffmpeg may already be installed" + + - name: Build + run: swift build -c release + + - name: Run Smoke Test + id: smoketest + run: | + BENCHMARK_START=$(date +%s) + + echo "=========================================" + echo "Kokoro TTS smoke test" + echo "=========================================" + echo "" + + TEXT="I can't believe we finally made it to the summit after climbing for twelve exhausting hours through wind and rain, but wow, this view of the endless mountain ranges stretching to the horizon makes every single difficult step completely worth the journey." + + if .build/release/fluidaudiocli tts "$TEXT" \ + --output kokoro_output.wav \ + --auto-download 2>&1; then + echo "Smoke test PASSED - pipeline completed without crash" + echo "SMOKE_STATUS=PASSED" >> $GITHUB_OUTPUT + else + EXIT_CODE=$? + echo "Smoke test FAILED with exit code $EXIT_CODE" + echo "SMOKE_STATUS=FAILED" >> $GITHUB_OUTPUT + fi + + if [ -f kokoro_output.wav ]; then + SIZE=$(stat -f%z kokoro_output.wav 2>/dev/null || stat -c%s kokoro_output.wav 2>/dev/null) + echo "Output file size: $SIZE bytes" + echo "FILE_SIZE=$SIZE" >> $GITHUB_OUTPUT + else + echo "FILE_SIZE=0" >> $GITHUB_OUTPUT + fi + + EXECUTION_TIME=$(( ($(date +%s) - BENCHMARK_START) / 60 ))m$(( ($(date +%s) - BENCHMARK_START) % 60 ))s + echo "EXECUTION_TIME=$EXECUTION_TIME" >> $GITHUB_OUTPUT + + - name: Comment PR + if: github.event_name == 'pull_request' + continue-on-error: true + uses: actions/github-script@v7 + with: + script: | + const status = '${{ steps.smoketest.outputs.SMOKE_STATUS }}'; + const emoji = status === 'PASSED' ? 'โœ…' : 'โŒ'; + const fileSize = '${{ steps.smoketest.outputs.FILE_SIZE }}'; + const fileSizeKB = (parseInt(fileSize) / 1024).toFixed(1); + + const body = `## Kokoro TTS Smoke Test ${emoji} + + | Check | Result | + |-------|--------| + | Build | โœ… | + | Model download | ${emoji} | + | Model load | ${emoji} | + | Synthesis pipeline | ${emoji} | + | Output WAV | ${parseInt(fileSize) > 0 ? 'โœ…' : 'โŒ'} (${fileSizeKB} KB) | + + Runtime: ${{ steps.smoketest.outputs.EXECUTION_TIME }} + + **Note:** Kokoro TTS uses CoreML flow matching + Vocos vocoder. CI VM lacks physical ANE โ€” performance may differ from Apple Silicon. + + `; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(c => + c.body.includes('') + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + - name: Upload Output + if: always() + uses: actions/upload-artifact@v4 + with: + name: kokoro-tts-output + path: kokoro_output.wav + retention-days: 7 diff --git a/.github/workflows/tts-test.yml b/.github/workflows/tts-test.yml deleted file mode 100644 index ba3f7c2d1..000000000 --- a/.github/workflows/tts-test.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Kokoro TTS Test - -on: - pull_request: - branches: [ main ] - workflow_dispatch: - -jobs: - kokoro-tts-test: - runs-on: macos-15 - env: - FLUIDAUDIO_ENABLE_TTS: "1" - - steps: - - uses: actions/checkout@v4 - - - name: Build FluidAudio (with TTS) - run: | - swift build -c release - - - name: Generate TTS Audio (Kokoro) - run: | - echo "๐ŸŽค Generating TTS audio (ground truth test)..." - TEXT="I can't believe we finally made it to the summit after climbing for twelve exhausting hours through wind and rain, but wow, this view of the endless mountain ranges stretching to the horizon makes every single difficult step completely worth the journey." - - # This will auto-download model and generate audio - swift run --configuration release fluidaudiocli tts "$TEXT" --output kokoro_output.wav --auto-download - - # Verify output - if [ -f kokoro_output.wav ]; then - SIZE=$(ls -l kokoro_output.wav | awk '{print $5}') - echo "โœ… TTS successful: kokoro_output.wav ($SIZE bytes)" - else - echo "โŒ Output file not created" - exit 1 - fi - - - name: Upload Audio Output - if: always() - uses: actions/upload-artifact@v4 - with: - name: kokoro-tts-output - path: kokoro_output.wav - retention-days: 7 -