From ace9a51b64f71d5236205cd22d1abe27b67bbec2 Mon Sep 17 00:00:00 2001 From: Alex-Wengg Date: Wed, 7 Oct 2026 21:59:43 -0400 Subject: [PATCH 1/3] feat(package): TTS and Diarizer opt-out traits (#990) ASR-only consumers compiled the TTS and diarization subsystems and shipped the ~1 MB LuxTTS G2P resource bundle even though nothing could reach them. Package@swift-6.2.swift now declares `TTS` and `Diarizer` traits, default-on alongside `NemoTextProcessing`, so `traits: []` builds ASR + VAD only. - Every file under Sources/FluidAudio/TTS and Sources/FluidAudio/Diarizer is wrapped in `#if TTS` / `#if Diarizer` (the formatter does not indent conditional blocks, so each file gains two lines). - The G2P lexicon moves into its own `LuxTtsG2pResources` target. SwiftPM cannot condition resources inside a target, so the trait conditions the dependency edge instead and the bundle drops out. - `DiarizerError` gets its own unguarded file because Shared/ANEMemoryOptimizer throws it. ModelNames guards its members that use Sortformer, PocketTTS, or Chatterbox types. Inflect frame buckets now live in ModelNames, and InflectConstants reads them from there. - Package.swift (tools < 6.2) and the podspec define both flags unconditionally, so those builds compile everything as before. Behavior change: `traits: []` used to drop only the NeMo engine. Apps that also use diarization now need `traits: ["Diarizer"]`. Leaving it out gives a compile error, never a silent behavior change. Measured on a minimal macOS consumer (release build, strip -x): the default traits give 14.54 MB plus a 1.0 MB bundle, `["NemoTextProcessing"]` gives 3.80 MB, and `[]` gives 3.13 MB with no bundle. Every trait subset builds the library target; the CLI and tests still need TTS and Diarizer, so CI's no-Nemo job now passes `--traits TTS,Diarizer`. --- .github/workflows/tests.yml | 31 ++++++++++++++-- .gitignore | 4 +-- Documentation/ASR/PostProcessing.md | 4 ++- FluidAudio.podspec | 2 ++ Package.swift | 12 ++++++- Package@swift-6.2.swift | 29 +++++++++++---- README.md | 15 ++++++++ .../Diarizer/Clustering/SpeakerManager.swift | 2 ++ .../Clustering/SpeakerOperations.swift | 2 ++ .../Diarizer/Clustering/SpeakerTypes.swift | 2 ++ .../Diarizer/Core/DiarizerError.swift | 34 ++++++++++++++++++ .../Diarizer/Core/DiarizerManager.swift | 2 ++ .../Diarizer/Core/DiarizerModels.swift | 2 ++ .../Diarizer/Core/DiarizerTypes.swift | 34 ++---------------- .../FluidAudio/Diarizer/DiarizationDER.swift | 2 ++ .../Diarizer/DiarizerProtocol.swift | 2 ++ .../Diarizer/DiarizerTimeline.swift | 2 ++ .../Extraction/EmbeddingExtractor.swift | 2 ++ .../Diarizer/HungarianAssignment.swift | 2 ++ .../Diarizer/LS-EEND/LSEENDDiarizer.swift | 2 ++ .../Diarizer/LS-EEND/LSEENDInference.swift | 2 ++ .../Diarizer/LS-EEND/LSEENDPreprocessor.swift | 2 ++ .../Diarizer/LS-EEND/LSEENDTypes.swift | 2 ++ .../Nemotron3/Nemotron3Diarizer.swift | 2 ++ .../Diarizer/Nemotron3/Nemotron3Models.swift | 2 ++ .../Nemotron3/Nemotron3StateUpdater.swift | 2 ++ .../Diarizer/Nemotron3/Nemotron3Types.swift | 2 ++ .../Offline/Clustering/AHCClustering.swift | 2 ++ .../ConstrainedClusterAssignment.swift | 2 ++ .../Offline/Clustering/KMeansClustering.swift | 2 ++ .../Clustering/SpeakerCountConstraints.swift | 2 ++ .../Offline/Clustering/VBxClustering.swift | 2 ++ .../Offline/Core/OfflineDiarizerManager.swift | 2 ++ .../Offline/Core/OfflineDiarizerModels.swift | 2 ++ .../Offline/Core/OfflineDiarizerTypes.swift | 2 ++ .../Offline/Core/PreparedDiarization.swift | 2 ++ .../OfflineEmbeddingExtractor.swift | 2 ++ .../Offline/Extraction/PLDATransform.swift | 2 ++ .../Extraction/SilenceAwareFbank.swift | 2 ++ .../Extraction/WeightInterpolation.swift | 2 ++ .../OfflineSegmentationProcessor.swift | 2 ++ .../Offline/Utils/OfflineReconstruction.swift | 2 ++ .../Offline/Utils/VDSPOperations.swift | 2 ++ .../Offline/Utils/ZeroVoteReembedder.swift | 2 ++ .../Segmentation/AudioValidation.swift | 2 ++ .../Segmentation/SegmentationProcessor.swift | 2 ++ .../Diarizer/Segmentation/SlidingWindow.swift | 2 ++ .../Offline/OfflineSortformerDiarizer.swift | 2 ++ .../Offline/SortformerSpeakerStitcher.swift | 2 ++ .../Sortformer/SortformerDiarizer.swift | 2 ++ .../Sortformer/SortformerModelInference.swift | 2 ++ .../Sortformer/SortformerStateUpdater.swift | 2 ++ .../Diarizer/Sortformer/SortformerTypes.swift | 2 ++ Sources/FluidAudio/FluidAudioSwift.swift | 8 +++-- Sources/FluidAudio/ModelNames.swift | 21 +++++++++-- .../Chatterbox/Assets/ChatterboxTables.swift | 2 ++ .../TTS/Chatterbox/ChatterboxConstants.swift | 2 ++ .../TTS/Chatterbox/ChatterboxError.swift | 2 ++ .../TTS/Chatterbox/ChatterboxManager.swift | 2 ++ .../TTS/Chatterbox/ChatterboxModels.swift | 2 ++ .../Nano/ChatterboxNanoConstants.swift | 2 ++ .../Nano/ChatterboxNanoManager.swift | 2 ++ .../Nano/ChatterboxNanoModels.swift | 2 ++ .../Nano/ChatterboxNanoSynthesizer.swift | 2 ++ .../Nano/ChatterboxNanoTokenizer.swift | 2 ++ .../ChatterboxAlignmentAnalyzer.swift | 2 ++ .../Pipeline/ChatterboxMLSupport.swift | 2 ++ .../Pipeline/ChatterboxSynthesizer.swift | 2 ++ .../Tokenizer/ChatterboxTokenizer.swift | 2 ++ Sources/FluidAudio/TTS/G2P/G2PModel.swift | 2 ++ .../TTS/G2P/MultilingualG2PError.swift | 2 ++ .../TTS/G2P/MultilingualG2PLanguage.swift | 2 ++ .../TTS/G2P/MultilingualG2PModel.swift | 2 ++ .../Inflect/Assets/InflectModelStore.swift | 2 ++ .../Assets/InflectResourceDownloader.swift | 2 ++ .../TTS/Inflect/InflectConstants.swift | 4 ++- .../FluidAudio/TTS/Inflect/InflectError.swift | 2 ++ .../TTS/Inflect/InflectManager.swift | 2 ++ .../TTS/Inflect/InflectSymbols.swift | 2 ++ .../TTS/Inflect/InflectVariant.swift | 2 ++ .../TTS/Inflect/Pipeline/InflectNoise.swift | 2 ++ .../Inflect/Pipeline/InflectSynthesizer.swift | 2 ++ .../Assets/KokoroAneModelCacheMigration.swift | 2 ++ .../Assets/KokoroAneResourceDownloader.swift | 2 ++ .../TTS/KokoroAne/Assets/KokoroAneVocab.swift | 2 ++ .../English/KokoroAneEnglishPhonemizer.swift | 2 ++ .../TTS/KokoroAne/G2P/French/FrenchG2P.swift | 2 ++ .../G2P/Japanese/JapaneseCutlet.swift | 2 ++ .../KokoroAne/G2P/Japanese/JapaneseG2P.swift | 2 ++ .../Japanese/JapaneseMecabDictionary.swift | 2 ++ .../G2P/Japanese/JapaneseTokenizer.swift | 2 ++ .../TTS/KokoroAne/G2P/KokoroAneLexicon.swift | 2 ++ .../G2P/Mandarin/MandarinBertTokenizer.swift | 2 ++ .../G2P/Mandarin/MandarinBopomofoMap.swift | 2 ++ .../G2P/Mandarin/MandarinCustomLexicon.swift | 2 ++ .../G2P/Mandarin/MandarinErhua.swift | 2 ++ .../KokoroAne/G2P/Mandarin/MandarinG2P.swift | 2 ++ .../G2P/Mandarin/MandarinG2pwModel.swift | 2 ++ .../G2P/Mandarin/MandarinJiebaHmm.swift | 2 ++ .../G2P/Mandarin/MandarinJiebaTables.swift | 2 ++ .../Mandarin/MandarinNumberNormalizer.swift | 2 ++ .../G2P/Mandarin/MandarinPinyinDict.swift | 2 ++ .../Mandarin/MandarinPinyinNormalizer.swift | 2 ++ .../Mandarin/MandarinPolyphoneCatalog.swift | 2 ++ .../G2P/Mandarin/MandarinToneSandhi.swift | 2 ++ .../G2P/Mandarin/MandarinToneSandhiPOS.swift | 2 ++ .../G2P/RomanceNumberNormalizer.swift | 2 ++ .../KokoroAne/G2P/Spanish/SpanishG2P.swift | 2 ++ .../TTS/KokoroAne/KokoroAneConstants.swift | 2 ++ .../TTS/KokoroAne/KokoroAneError.swift | 2 ++ .../TTS/KokoroAne/KokoroAneManager.swift | 2 ++ .../Pipeline/KokoroAneModelStore.swift | 2 ++ .../KokoroAneSynthesizer+Conversion.swift | 2 ++ .../Pipeline/KokoroAneSynthesizer+Types.swift | 2 ++ .../Pipeline/KokoroAneSynthesizer.swift | 2 ++ .../Pipeline/KokoroAneVoicePack.swift | 2 ++ .../TTS/KokoroAne/V3/KokoroAneV3Arrays.swift | 2 ++ .../TTS/KokoroAne/V3/KokoroAneV3Assets.swift | 2 ++ .../V3/KokoroAneV3NativeSource.swift | 2 ++ .../KokoroAne/V3/KokoroAneV3Pipeline.swift | 2 ++ .../TTS/KokoroAne/V3/KokoroAneVersion.swift | 2 ++ .../LuxTts/G2p/LuxTtsEnglishNormalizer.swift | 2 ++ .../FluidAudio/TTS/LuxTts/G2p/LuxTtsG2p.swift | 7 ++-- .../TTS/LuxTts/LuxTtsConstants.swift | 2 ++ .../TTS/LuxTts/LuxTtsContinuation.swift | 2 ++ .../FluidAudio/TTS/LuxTts/LuxTtsError.swift | 2 ++ .../FluidAudio/TTS/LuxTts/LuxTtsManager.swift | 2 ++ .../TTS/LuxTts/LuxTtsMelExtractor.swift | 2 ++ .../TTS/LuxTts/LuxTtsModelStore.swift | 2 ++ .../FluidAudio/TTS/LuxTts/LuxTtsSolver.swift | 2 ++ .../TTS/LuxTts/LuxTtsSynthesizer.swift | 2 ++ .../TTS/LuxTts/LuxTtsTokenizer.swift | 2 ++ .../TTS/NeuTts/NeuTtsConstants.swift | 2 ++ .../FluidAudio/TTS/NeuTts/NeuTtsManager.swift | 2 ++ .../FluidAudio/TTS/NeuTts/NeuTtsModels.swift | 2 ++ .../FluidAudio/TTS/NeuTts/NeuTtsPrompt.swift | 2 ++ .../TTS/NeuTts/NeuTtsSynthesizer.swift | 2 ++ .../NeuTts/Tokenizer/NeuTtsBpeTokenizer.swift | 2 ++ .../Assets/PocketTtsConstantsLoader.swift | 2 ++ .../Assets/PocketTtsResourceDownloader.swift | 2 ++ .../Pipeline/PocketTtsLayerKeys.swift | 2 ++ .../Pipeline/PocketTtsMimiKeys.swift | 2 ++ .../Pipeline/PocketTtsModelStore.swift | 2 ++ .../PocketTTS/Pipeline/PocketTtsSession.swift | 2 ++ .../Pipeline/PocketTtsStateEngine.swift | 2 ++ .../Pipeline/PocketTtsSynthesizer+Flow.swift | 2 ++ .../PocketTtsSynthesizer+KVCache.swift | 2 ++ .../Pipeline/PocketTtsSynthesizer+Mimi.swift | 2 ++ .../Pipeline/PocketTtsSynthesizer+State.swift | 2 ++ .../Pipeline/PocketTtsSynthesizer+Types.swift | 2 ++ .../Pipeline/PocketTtsSynthesizer.swift | 2 ++ .../Pipeline/PocketTtsVoiceCloner.swift | 2 ++ .../TTS/PocketTTS/PocketTTSError.swift | 2 ++ .../TTS/PocketTTS/PocketTtsComputeUnits.swift | 2 ++ .../TTS/PocketTTS/PocketTtsConstants.swift | 2 ++ .../TTS/PocketTTS/PocketTtsManager.swift | 2 ++ .../Tokenizer/SentencePieceProto.swift | 2 ++ .../Tokenizer/SentencePieceTokenizer.swift | 2 ++ .../FluidAudio/TTS/SSML/SSMLProcessor.swift | 2 ++ .../FluidAudio/TTS/SSML/SSMLTagParser.swift | 2 ++ Sources/FluidAudio/TTS/SSML/SSMLTypes.swift | 2 ++ .../TTS/SSML/SayAsInterpreter.swift | 2 ++ .../TTS/Shared/AudioPostProcessor.swift | 2 ++ .../TTS/Shared/EnglishInitialisms.swift | 2 ++ .../TTS/Shared/EnglishTextNormalizer.swift | 2 ++ .../TTS/Shared/LexiconAssetCache.swift | 2 ++ .../TTS/Shared/NemoTextNormalizer.swift | 2 ++ .../TTS/Shared/PhonemeChunker.swift | 2 ++ Sources/FluidAudio/TTS/Shared/TTSError.swift | 2 ++ .../TTS/Shared/TtsCacheDirectory.swift | 2 ++ .../TTS/Shared/TtsComputeUnitPreset.swift | 2 ++ .../Assets/StyleTTS2ModelStore.swift | 2 ++ .../Assets/StyleTTS2ResourceDownloader.swift | 2 ++ .../Preprocess/StyleTTS2MelExtractor.swift | 2 ++ .../Sampler/StyleTTS2DiffusionSchedule.swift | 2 ++ .../Synthesize/StyleTTS2GlueOps.swift | 2 ++ .../Synthesize/StyleTTS2MultiArray.swift | 2 ++ .../Synthesize/StyleTTS2Synthesizer.swift | 2 ++ .../Tokenizer/StyleTTS2Phonemizer.swift | 2 ++ .../Tokenizer/StyleTTS2TextCleaner.swift | 2 ++ .../TTS/StyleTTS2/StyleTTS2Constants.swift | 2 ++ .../TTS/StyleTTS2/StyleTTS2Error.swift | 2 ++ .../TTS/StyleTTS2/StyleTTS2Manager.swift | 2 ++ .../Assets/Supertonic3ModelStore.swift | 2 ++ .../Supertonic3ResourceDownloader.swift | 2 ++ .../Preprocess/Supertonic3TextChunker.swift | 2 ++ .../Supertonic3UnicodeProcessor.swift | 2 ++ .../Synthesize/Supertonic3LatentSampler.swift | 2 ++ .../Synthesize/Supertonic3MultiArray.swift | 2 ++ .../Synthesize/Supertonic3Synthesizer.swift | 2 ++ .../Supertonic3/Supertonic3Constants.swift | 2 ++ .../TTS/Supertonic3/Supertonic3Error.swift | 2 ++ .../TTS/Supertonic3/Supertonic3Manager.swift | 2 ++ .../TTS/Supertonic3/Supertonic3Types.swift | 2 ++ Sources/FluidAudio/TTS/TtsBackend.swift | 2 ++ Sources/FluidAudio/TTS/TtsConstants.swift | 2 ++ .../LuxTtsG2pResources.swift | 7 ++++ .../Resources/luxtts_en_us_g2p_aux.json | 0 .../Resources/luxtts_en_us_lexicon.tsv.zz | Bin 199 files changed, 526 insertions(+), 52 deletions(-) create mode 100644 Sources/FluidAudio/Diarizer/Core/DiarizerError.swift create mode 100644 Sources/LuxTtsG2pResources/LuxTtsG2pResources.swift rename Sources/{FluidAudio/TTS/LuxTts/G2p => LuxTtsG2pResources}/Resources/luxtts_en_us_g2p_aux.json (100%) rename Sources/{FluidAudio/TTS/LuxTts/G2p => LuxTtsG2pResources}/Resources/luxtts_en_us_lexicon.tsv.zz (100%) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 97e87892a..04fe812bf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,8 +56,9 @@ jobs: sudo xcode-select -s /Applications/Xcode_26.3.app swift --version + # The CLI and tests need the TTS and Diarizer traits (#990); keep those on. - name: Build with the trait disabled - run: swift build --disable-default-traits --product fluidaudiocli + run: swift build --traits TTS,Diarizer --product fluidaudiocli - name: Assert no engine symbols linked run: | @@ -66,9 +67,35 @@ jobs: test "$count" -eq 0 - name: Run tests with the trait disabled - run: swift test --disable-default-traits --filter 'TextNormalizerUnavailableTests|NemoTextNormalizerUnavailableTests|TextNormalizerTests|NemoTextNormalizerTests' + run: swift test --traits TTS,Diarizer --filter 'TextNormalizerUnavailableTests|NemoTextNormalizerUnavailableTests|TextNormalizerTests|NemoTextNormalizerTests' timeout-minutes: 20 + build-library-trait-subsets: + name: Build library with TTS / Diarizer traits disabled (macOS) + runs-on: macos-15 + steps: + - name: Checkout code + uses: actions/checkout@v5 + + # #990: ASR-only consumers resolve with `traits: []` (or `["Diarizer"]`) to drop + # the TTS and diarization subsystems. Only the library target has to build that way. + - name: Select Xcode 26 + run: | + sudo xcode-select -s /Applications/Xcode_26.3.app + swift --version + + - name: Build with no traits + run: swift build --disable-default-traits --target FluidAudio + + - name: Assert no TTS resource bundle + run: test ! -e .build/debug/FluidAudio_LuxTtsG2pResources.bundle + + - name: Build with TTS only + run: swift build --traits TTS --target FluidAudio + + - name: Build with Diarizer only + run: swift build --traits Diarizer --target FluidAudio + build-macos-x86_64: name: Build Swift Package (macOS x86_64 cross-compile) runs-on: macos-15 diff --git a/.gitignore b/.gitignore index 987ba0408..6879d3d61 100644 --- a/.gitignore +++ b/.gitignore @@ -108,8 +108,8 @@ Resources/ !Tests/FluidAudioTests/TTS/LuxTts/Resources/** Tests/FluidAudioTests/TTS/LuxTts/Resources/*.wav # LuxTts bundled G2P lexicon (probe_lexicon.py in mobius zipvoice) -!Sources/FluidAudio/TTS/LuxTts/G2p/Resources/ -!Sources/FluidAudio/TTS/LuxTts/G2p/Resources/** +!Sources/LuxTtsG2pResources/Resources/ +!Sources/LuxTtsG2pResources/Resources/** scripts/ !Scripts/parakeet_subset_benchmark.sh !Scripts/diarizer_subset_benchmark.sh diff --git a/Documentation/ASR/PostProcessing.md b/Documentation/ASR/PostProcessing.md index 4de0b3ff1..393f4f97f 100644 --- a/Documentation/ASR/PostProcessing.md +++ b/Documentation/ASR/PostProcessing.md @@ -81,6 +81,8 @@ The engine is a prebuilt Rust static library (about 8 MB per architecture slice .package(url: "https://github.com/FluidInference/FluidAudio.git", from: "0.15.7", traits: []) ``` +Since #990, `traits: []` also turns off the `TTS` and `Diarizer` traits. To drop only the engine, list the traits you still need, e.g. `traits: ["TTS", "Diarizer"]` (see [Package traits](../../README.md#installation)). + With the trait disabled: - `TextNormalizer` and `NemoTextNormalizer` remain in the API. `isNativeAvailable`, `isTnAvailable`, and `NemoTextNormalizer.isAvailable` report `false`. @@ -116,4 +118,4 @@ Existing `import FluidAudio` lines keep compiling. Measured on a universal macOS **The xcframework still downloads.** The binary target is declared unconditionally and only the dependency edge is trait-conditioned, so a clean resolve still fetches the 49 MB `NemoTextProcessing.xcframework.zip` even with the trait off. Ship size is unaffected; CI and cold checkouts pay the download. That is a SwiftPM limitation, not something the package can change. -To build the package itself without the engine: `swift build --disable-default-traits`. +To build the package itself without the engine: `swift build --traits TTS,Diarizer` (the CLI and tests need both). diff --git a/FluidAudio.podspec b/FluidAudio.podspec index 494e7cd8c..c4f33a2b8 100644 --- a/FluidAudio.podspec +++ b/FluidAudio.podspec @@ -22,6 +22,8 @@ Pod::Spec.new do |spec| spec.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', + # SwiftPM trait flags (#990); CocoaPods always builds every subsystem. + 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => '$(inherited) TTS Diarizer', 'ARCHS[sdk=macosx*]' => 'arm64', 'EXCLUDED_ARCHS[sdk=macosx*]' => 'x86_64', 'ARCHS[sdk=iphonesimulator*]' => 'arm64', diff --git a/Package.swift b/Package.swift index 5bae7810b..0b0a1eeba 100644 --- a/Package.swift +++ b/Package.swift @@ -28,12 +28,22 @@ let package = Package( "MachTaskSelfWrapper", // The prebuilt xcframework has no visionOS slice. .target(name: "NemoTextProcessing", condition: .when(platforms: [.macOS, .iOS, .macCatalyst])), + "LuxTtsG2pResources", ], path: "Sources/FluidAudio", exclude: ["ASR/Parakeet/Unified/benchmark.md"], + swiftSettings: [ + // Package@swift-6.2.swift exposes these as opt-out traits (#990). + .define("TTS"), + .define("Diarizer"), + ] + ), + .target( + name: "LuxTtsG2pResources", + path: "Sources/LuxTtsG2pResources", resources: [ // Keep .process: .copy of a Resources-named directory breaks Apple code signing on iOS. - .process("TTS/LuxTts/G2p/Resources") + .process("Resources") ] ), // Byte-exact NeMo text normalization (FST engine, all 7 languages). diff --git a/Package@swift-6.2.swift b/Package@swift-6.2.swift index 5d04a1f8a..36002ec48 100644 --- a/Package@swift-6.2.swift +++ b/Package@swift-6.2.swift @@ -2,9 +2,9 @@ import PackageDescription import Foundation -// Tools 6.2+ manifest: identical to Package.swift plus the `NemoTextProcessing` -// trait. Keep the two in sync; Package.swift serves toolchains < 6.2, which -// always link the engine. (SwiftPM 6.1 accepts the trait syntax but still +// Tools 6.2+ manifest: identical to Package.swift plus the `NemoTextProcessing`, +// `TTS` and `Diarizer` traits. Keep the two in sync; Package.swift serves +// toolchains < 6.2, which always link the engine and compile every subsystem. (SwiftPM 6.1 accepts the trait syntax but still // links a trait-conditioned binary target — verified on Xcode 16.4 — so the // opt-out is gated at 6.2.) @@ -36,7 +36,18 @@ let package = Package( name: "NemoTextProcessing", description: "Link the bundled NeMo text-normalization engine (TTS frontends, ITN)." ), - .default(enabledTraits: ["NemoTextProcessing"]), + // Opt out of whole subsystems for apps that never reach them (#990): + // .package(url: ..., traits: []) // ASR/VAD only + // .package(url: ..., traits: ["Diarizer"]) // ASR/VAD + diarization + .trait( + name: "TTS", + description: "Text-to-speech backends (Kokoro, PocketTTS, LuxTTS, ...) and their bundled G2P resources." + ), + .trait( + name: "Diarizer", + description: "Speaker diarization (pyannote, Sortformer, LS-EEND, Nemotron 3)." + ), + .default(enabledTraits: ["NemoTextProcessing", "TTS", "Diarizer"]), ], dependencies: [], targets: [ @@ -50,12 +61,18 @@ let package = Package( name: "NemoTextProcessing", condition: .when(platforms: [.macOS, .iOS, .macCatalyst], traits: ["NemoTextProcessing"]) ), + .target(name: "LuxTtsG2pResources", condition: .when(traits: ["TTS"])), ], path: "Sources/FluidAudio", - exclude: ["ASR/Parakeet/Unified/benchmark.md"], + exclude: ["ASR/Parakeet/Unified/benchmark.md"] + ), + // Separate target so the ~1 MB G2P bundle drops out with the `TTS` trait. + .target( + name: "LuxTtsG2pResources", + path: "Sources/LuxTtsG2pResources", resources: [ // Keep .process: .copy of a Resources-named directory breaks Apple code signing on iOS. - .process("TTS/LuxTts/G2p/Resources") + .process("Resources") ] ), // Byte-exact NeMo text normalization (FST engine, all 7 languages). diff --git a/README.md b/README.md index 4e4d80b49..fe78a3577 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,21 @@ dependencies: [ .product(name: "FluidAudio", package: "FluidAudio") ``` +**Package traits (Swift 6.2+ / Xcode 26+):** all three are on by default. Drop the subsystems an app never reaches to shrink the build: + +| Trait | Covers | +|-------|--------| +| `TTS` | All TTS backends plus the ~1 MB LuxTTS G2P resource bundle | +| `Diarizer` | Speaker diarization (pyannote, Sortformer, LS-EEND, Nemotron 3) | +| `NemoTextProcessing` | NeMo text normalization engine, see [PostProcessing.md](Documentation/ASR/PostProcessing.md#opting-out-of-the-engine) | + +```swift +.package(url: "https://github.com/FluidInference/FluidAudio.git", from: "0.17.7", traits: []) // ASR + VAD only +.package(url: "https://github.com/FluidInference/FluidAudio.git", from: "0.17.7", traits: ["Diarizer"]) // + diarization +``` + +`traits: []` turns off all three. Before #990 it only dropped the NeMo engine, so apps that use diarization and pass `traits: []` should switch to `traits: ["Diarizer"]`. Older toolchains always build everything. + **CocoaPods:** We recommend using [cocoapods-spm](https://github.com/trinhngocthuyen/cocoapods-spm) for better SPM integration, but if needed, you can also use our podspec: `pod 'FluidAudio', '~> 0.12.4'` ### Other Frameworks diff --git a/Sources/FluidAudio/Diarizer/Clustering/SpeakerManager.swift b/Sources/FluidAudio/Diarizer/Clustering/SpeakerManager.swift index fad491aa0..e176df54b 100644 --- a/Sources/FluidAudio/Diarizer/Clustering/SpeakerManager.swift +++ b/Sources/FluidAudio/Diarizer/Clustering/SpeakerManager.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation import OSLog @@ -634,3 +635,4 @@ public struct SpeakerManager: Sendable { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Clustering/SpeakerOperations.swift b/Sources/FluidAudio/Diarizer/Clustering/SpeakerOperations.swift index 0100361ae..090975fc5 100644 --- a/Sources/FluidAudio/Diarizer/Clustering/SpeakerOperations.swift +++ b/Sources/FluidAudio/Diarizer/Clustering/SpeakerOperations.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation import OSLog @@ -592,3 +593,4 @@ extension SpeakerManager { return (speakers.count, totalDuration, min(1.0, averageConfidence), speakersWithHistory) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Clustering/SpeakerTypes.swift b/Sources/FluidAudio/Diarizer/Clustering/SpeakerTypes.swift index 781469f42..109a72f33 100644 --- a/Sources/FluidAudio/Diarizer/Clustering/SpeakerTypes.swift +++ b/Sources/FluidAudio/Diarizer/Clustering/SpeakerTypes.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation @@ -280,3 +281,4 @@ public enum SpeakerInitializationMode: Sendable { /// Skip speakers whose IDs match existing ones case skip } +#endif diff --git a/Sources/FluidAudio/Diarizer/Core/DiarizerError.swift b/Sources/FluidAudio/Diarizer/Core/DiarizerError.swift new file mode 100644 index 000000000..c47b9394f --- /dev/null +++ b/Sources/FluidAudio/Diarizer/Core/DiarizerError.swift @@ -0,0 +1,34 @@ +import Foundation + +// Outside the `Diarizer` trait guard: Shared/ANEMemoryOptimizer throws it. +public enum DiarizerError: Error, LocalizedError { + case notInitialized + case modelDownloadFailed + case modelCompilationFailed + case embeddingExtractionFailed + case invalidAudioData + case processingFailed(String) + case memoryAllocationFailed + case invalidArrayBounds + + public var errorDescription: String? { + switch self { + case .notInitialized: + return "Diarization system not initialized. Call initialize() first." + case .modelDownloadFailed: + return "Failed to download required models." + case .modelCompilationFailed: + return "Failed to compile CoreML models." + case .embeddingExtractionFailed: + return "Failed to extract speaker embedding from audio." + case .invalidAudioData: + return "Invalid audio data provided." + case .processingFailed(let message): + return "Processing failed: \(message)" + case .memoryAllocationFailed: + return "Failed to allocate ANE-aligned memory." + case .invalidArrayBounds: + return "Array bounds exceeded for zero-copy view." + } + } +} diff --git a/Sources/FluidAudio/Diarizer/Core/DiarizerManager.swift b/Sources/FluidAudio/Diarizer/Core/DiarizerManager.swift index 6bf56682b..b3bffcf10 100644 --- a/Sources/FluidAudio/Diarizer/Core/DiarizerManager.swift +++ b/Sources/FluidAudio/Diarizer/Core/DiarizerManager.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import CoreML import Foundation @@ -531,3 +532,4 @@ public final class DiarizerManager { } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Core/DiarizerModels.swift b/Sources/FluidAudio/Diarizer/Core/DiarizerModels.swift index 1ac971b30..08f0be2a9 100644 --- a/Sources/FluidAudio/Diarizer/Core/DiarizerModels.swift +++ b/Sources/FluidAudio/Diarizer/Core/DiarizerModels.swift @@ -1,3 +1,4 @@ +#if Diarizer @preconcurrency import CoreML import Foundation import OSLog @@ -139,3 +140,4 @@ extension DiarizerModels { compilationDuration: loadDuration) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Core/DiarizerTypes.swift b/Sources/FluidAudio/Diarizer/Core/DiarizerTypes.swift index 92eab60db..69580a3f3 100644 --- a/Sources/FluidAudio/Diarizer/Core/DiarizerTypes.swift +++ b/Sources/FluidAudio/Diarizer/Core/DiarizerTypes.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation // MARK: - Diarization Pipeline Types @@ -233,35 +234,4 @@ public struct AudioValidationResult: Sendable { self.issues = issues } } - -public enum DiarizerError: Error, LocalizedError { - case notInitialized - case modelDownloadFailed - case modelCompilationFailed - case embeddingExtractionFailed - case invalidAudioData - case processingFailed(String) - case memoryAllocationFailed - case invalidArrayBounds - - public var errorDescription: String? { - switch self { - case .notInitialized: - return "Diarization system not initialized. Call initialize() first." - case .modelDownloadFailed: - return "Failed to download required models." - case .modelCompilationFailed: - return "Failed to compile CoreML models." - case .embeddingExtractionFailed: - return "Failed to extract speaker embedding from audio." - case .invalidAudioData: - return "Invalid audio data provided." - case .processingFailed(let message): - return "Processing failed: \(message)" - case .memoryAllocationFailed: - return "Failed to allocate ANE-aligned memory." - case .invalidArrayBounds: - return "Array bounds exceeded for zero-copy view." - } - } -} +#endif diff --git a/Sources/FluidAudio/Diarizer/DiarizationDER.swift b/Sources/FluidAudio/Diarizer/DiarizationDER.swift index f5879c485..266601996 100644 --- a/Sources/FluidAudio/Diarizer/DiarizationDER.swift +++ b/Sources/FluidAudio/Diarizer/DiarizationDER.swift @@ -1,3 +1,4 @@ +#if Diarizer // // DiarizationDER.swift // LS-EEND-Test @@ -229,3 +230,4 @@ public enum DiarizationDER { return mask } } +#endif diff --git a/Sources/FluidAudio/Diarizer/DiarizerProtocol.swift b/Sources/FluidAudio/Diarizer/DiarizerProtocol.swift index d71b946fb..32bd0af59 100644 --- a/Sources/FluidAudio/Diarizer/DiarizerProtocol.swift +++ b/Sources/FluidAudio/Diarizer/DiarizerProtocol.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation // MARK: - Diarizer Protocol @@ -115,3 +116,4 @@ public protocol Diarizer: AnyObject { @discardableResult func finalizeSession() throws -> DiarizerTimelineUpdate? } +#endif diff --git a/Sources/FluidAudio/Diarizer/DiarizerTimeline.swift b/Sources/FluidAudio/Diarizer/DiarizerTimeline.swift index 35ec72372..4974fec1d 100644 --- a/Sources/FluidAudio/Diarizer/DiarizerTimeline.swift +++ b/Sources/FluidAudio/Diarizer/DiarizerTimeline.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation // MARK: - Timeline Configuration @@ -1389,3 +1390,4 @@ public enum DiarizerTimelineError: Error, LocalizedError { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Extraction/EmbeddingExtractor.swift b/Sources/FluidAudio/Diarizer/Extraction/EmbeddingExtractor.swift index 106664c64..1dda985f5 100644 --- a/Sources/FluidAudio/Diarizer/Extraction/EmbeddingExtractor.swift +++ b/Sources/FluidAudio/Diarizer/Extraction/EmbeddingExtractor.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import CoreML import OSLog @@ -239,3 +240,4 @@ public final class EmbeddingExtractor { return embedding } } +#endif diff --git a/Sources/FluidAudio/Diarizer/HungarianAssignment.swift b/Sources/FluidAudio/Diarizer/HungarianAssignment.swift index 2f89128c3..6efa2f2aa 100644 --- a/Sources/FluidAudio/Diarizer/HungarianAssignment.swift +++ b/Sources/FluidAudio/Diarizer/HungarianAssignment.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation // MARK: - Hungarian (O(n^3) min-cost assignment on a square cost matrix) @@ -96,3 +97,4 @@ enum HungarianAssignment { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDDiarizer.swift b/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDDiarizer.swift index d0acd9a53..c334a2b9c 100644 --- a/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDDiarizer.swift +++ b/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDDiarizer.swift @@ -1,3 +1,4 @@ +#if Diarizer // // LSEENDDiarizer.swift // LS-EEND-Test @@ -383,3 +384,4 @@ public final class LSEENDDiarizer: Diarizer { finalized = false } } +#endif diff --git a/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDInference.swift b/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDInference.swift index 184297ef1..f437d34f5 100644 --- a/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDInference.swift +++ b/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDInference.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation import CoreML import Accelerate @@ -235,3 +236,4 @@ public class LSEENDInput: MLFeatureProvider { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDPreprocessor.swift b/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDPreprocessor.swift index 04f16b5e2..bfa465e60 100644 --- a/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDPreprocessor.swift +++ b/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDPreprocessor.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation import CoreML import Accelerate @@ -382,3 +383,4 @@ public struct StreamingChunkQueue { self.buffer.append(contentsOf: repeatElement(0, count: leftContextFloats)) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDTypes.swift b/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDTypes.swift index e15b7c2bb..0d54f2043 100644 --- a/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDTypes.swift +++ b/Sources/FluidAudio/Diarizer/LS-EEND/LSEENDTypes.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation import CoreML import Accelerate @@ -161,3 +162,4 @@ public enum LSEENDError: Error, LocalizedError { case invalidInputSize(String) case notInitialized } +#endif diff --git a/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Diarizer.swift b/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Diarizer.swift index d3c8ce3bc..ab70d65c3 100644 --- a/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Diarizer.swift +++ b/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Diarizer.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation @@ -458,3 +459,4 @@ struct Nemotron3StreamingFrontend { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Models.swift b/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Models.swift index a3e90d571..510f70054 100644 --- a/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Models.swift +++ b/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Models.swift @@ -1,3 +1,4 @@ +#if Diarizer @preconcurrency import CoreML import Accelerate import Foundation @@ -517,3 +518,4 @@ public struct Nemotron3Models { return result } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3StateUpdater.swift b/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3StateUpdater.swift index 92765ae2f..901136d30 100644 --- a/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3StateUpdater.swift +++ b/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3StateUpdater.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation @@ -369,3 +370,4 @@ public struct Nemotron3StateUpdater { return (topK, isDisabled) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Types.swift b/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Types.swift index 0278e446e..16bca7d89 100644 --- a/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Types.swift +++ b/Sources/FluidAudio/Diarizer/Nemotron3/Nemotron3Types.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation // MARK: - Configuration @@ -277,3 +278,4 @@ public enum Nemotron3Error: Error, LocalizedError { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Clustering/AHCClustering.swift b/Sources/FluidAudio/Diarizer/Offline/Clustering/AHCClustering.swift index 05129b91c..cebb427d2 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Clustering/AHCClustering.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Clustering/AHCClustering.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation import OSLog @@ -209,3 +210,4 @@ struct AHCClustering { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Clustering/ConstrainedClusterAssignment.swift b/Sources/FluidAudio/Diarizer/Offline/Clustering/ConstrainedClusterAssignment.swift index 021ccea71..586537209 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Clustering/ConstrainedClusterAssignment.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Clustering/ConstrainedClusterAssignment.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation /// pyannote-parity constrained assignment (`constrained_argmax`): within a @@ -41,3 +42,4 @@ enum ConstrainedClusterAssignment { return assignments } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Clustering/KMeansClustering.swift b/Sources/FluidAudio/Diarizer/Offline/Clustering/KMeansClustering.swift index bbda2029a..58c9bb534 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Clustering/KMeansClustering.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Clustering/KMeansClustering.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation import OSLog @@ -222,3 +223,4 @@ struct KMeansClustering { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Clustering/SpeakerCountConstraints.swift b/Sources/FluidAudio/Diarizer/Offline/Clustering/SpeakerCountConstraints.swift index e037c8251..941afdad8 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Clustering/SpeakerCountConstraints.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Clustering/SpeakerCountConstraints.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation import OSLog @@ -75,3 +76,4 @@ struct SpeakerCountConstraints: Sendable { return detectedCount } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Clustering/VBxClustering.swift b/Sources/FluidAudio/Diarizer/Offline/Clustering/VBxClustering.swift index 2c03d7849..77b6bf536 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Clustering/VBxClustering.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Clustering/VBxClustering.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation import OSLog @@ -774,3 +775,4 @@ struct VBxClustering { ) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerManager.swift b/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerManager.swift index dee103033..b3c0a64ee 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerManager.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerManager.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate @preconcurrency import CoreML import Foundation @@ -1043,3 +1044,4 @@ public final class OfflineDiarizerManager { logger.info("Exported \(payload.count) embeddings to \(path)") } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerModels.swift b/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerModels.swift index 2e3edd893..e05ad49d8 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerModels.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerModels.swift @@ -1,3 +1,4 @@ +#if Diarizer @preconcurrency import CoreML import Foundation import OSLog @@ -162,3 +163,4 @@ extension MLComputeUnits { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerTypes.swift b/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerTypes.swift index e08e1fb60..0ba904c60 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerTypes.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Core/OfflineDiarizerTypes.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation /// Errors surfaced by the offline diarization pipeline. @@ -752,3 +753,4 @@ extension OfflineDiarizerConfig { return copy } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Core/PreparedDiarization.swift b/Sources/FluidAudio/Diarizer/Offline/Core/PreparedDiarization.swift index 548854ab5..28f7c6468 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Core/PreparedDiarization.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Core/PreparedDiarization.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation /// Cacheable result of deterministic segmentation and embedding extraction. @@ -24,3 +25,4 @@ public struct PreparedDiarization: Sendable { public var segmentationChunkCount: Int { segmentation.numChunks } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Extraction/OfflineEmbeddingExtractor.swift b/Sources/FluidAudio/Diarizer/Offline/Extraction/OfflineEmbeddingExtractor.swift index 834027418..e37dee9a5 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Extraction/OfflineEmbeddingExtractor.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Extraction/OfflineEmbeddingExtractor.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import CoreML import Foundation @@ -1061,3 +1062,4 @@ struct OfflineEmbeddingExtractor { return 0 } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Extraction/PLDATransform.swift b/Sources/FluidAudio/Diarizer/Offline/Extraction/PLDATransform.swift index 1ba6485f8..1a7d9a0ad 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Extraction/PLDATransform.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Extraction/PLDATransform.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import CoreML import Foundation @@ -196,3 +197,4 @@ public struct PLDATransform { _ = try await pldaRhoModel.prediction(from: provider, options: options) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Extraction/SilenceAwareFbank.swift b/Sources/FluidAudio/Diarizer/Offline/Extraction/SilenceAwareFbank.swift index 380e39ebf..3fb9ee43b 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Extraction/SilenceAwareFbank.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Extraction/SilenceAwareFbank.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation /// Re-centers FBANK features on the frames that hold signal (#981). @@ -54,3 +55,4 @@ enum SilenceAwareFbank { return true } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Extraction/WeightInterpolation.swift b/Sources/FluidAudio/Diarizer/Offline/Extraction/WeightInterpolation.swift index a224f335b..0d23e1c7c 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Extraction/WeightInterpolation.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Extraction/WeightInterpolation.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation /// Utility helpers to resample soft VAD weights with the same half-pixel offset @@ -145,3 +146,4 @@ enum WeightInterpolation { return resample(input, to: outputLength) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Segmentation/OfflineSegmentationProcessor.swift b/Sources/FluidAudio/Diarizer/Offline/Segmentation/OfflineSegmentationProcessor.swift index e11511561..25f444d37 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Segmentation/OfflineSegmentationProcessor.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Segmentation/OfflineSegmentationProcessor.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate @preconcurrency import CoreML import Foundation @@ -558,3 +559,4 @@ extension OfflineSegmentationProcessor { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Utils/OfflineReconstruction.swift b/Sources/FluidAudio/Diarizer/Offline/Utils/OfflineReconstruction.swift index d39d4c2a5..d7235d25e 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Utils/OfflineReconstruction.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Utils/OfflineReconstruction.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation @@ -506,3 +507,4 @@ struct OfflineReconstruction { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Utils/VDSPOperations.swift b/Sources/FluidAudio/Diarizer/Offline/Utils/VDSPOperations.swift index 9b9fafe3a..0c861caa3 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Utils/VDSPOperations.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Utils/VDSPOperations.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation @@ -342,3 +343,4 @@ enum VDSPOperations { return result } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Offline/Utils/ZeroVoteReembedder.swift b/Sources/FluidAudio/Diarizer/Offline/Utils/ZeroVoteReembedder.swift index d9fee30cb..152918be5 100644 --- a/Sources/FluidAudio/Diarizer/Offline/Utils/ZeroVoteReembedder.swift +++ b/Sources/FluidAudio/Diarizer/Offline/Utils/ZeroVoteReembedder.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation /// Pure decision logic for the zero-vote re-embed post-pass. @@ -129,3 +130,4 @@ enum ZeroVoteReembedder { return dot / ((normA * normB).squareRoot()) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Segmentation/AudioValidation.swift b/Sources/FluidAudio/Diarizer/Segmentation/AudioValidation.swift index ff50da129..c5d45b0cb 100644 --- a/Sources/FluidAudio/Diarizer/Segmentation/AudioValidation.swift +++ b/Sources/FluidAudio/Diarizer/Segmentation/AudioValidation.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation internal struct AudioValidation { @@ -46,3 +47,4 @@ internal struct AudioValidation { return sqrt(squaredSum / Float(samples.count)) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Segmentation/SegmentationProcessor.swift b/Sources/FluidAudio/Diarizer/Segmentation/SegmentationProcessor.swift index 4a6909a94..5144b62e9 100644 --- a/Sources/FluidAudio/Diarizer/Segmentation/SegmentationProcessor.swift +++ b/Sources/FluidAudio/Diarizer/Segmentation/SegmentationProcessor.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import CoreML import Foundation @@ -238,3 +239,4 @@ public struct SegmentationProcessor { ) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Segmentation/SlidingWindow.swift b/Sources/FluidAudio/Diarizer/Segmentation/SlidingWindow.swift index 3111ab495..36de83f9a 100644 --- a/Sources/FluidAudio/Diarizer/Segmentation/SlidingWindow.swift +++ b/Sources/FluidAudio/Diarizer/Segmentation/SlidingWindow.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation internal struct Segment: Hashable { @@ -24,3 +25,4 @@ internal struct SlidingWindowFeature { var data: [[[Float]]] var slidingWindow: SlidingWindow } +#endif diff --git a/Sources/FluidAudio/Diarizer/Sortformer/Offline/OfflineSortformerDiarizer.swift b/Sources/FluidAudio/Diarizer/Sortformer/Offline/OfflineSortformerDiarizer.swift index 7ad58e739..47d654efe 100644 --- a/Sources/FluidAudio/Diarizer/Sortformer/Offline/OfflineSortformerDiarizer.swift +++ b/Sources/FluidAudio/Diarizer/Sortformer/Offline/OfflineSortformerDiarizer.swift @@ -1,3 +1,4 @@ +#if Diarizer @preconcurrency import CoreML import Foundation import OSLog @@ -389,3 +390,4 @@ public final class OfflineSortformerDiarizer { .resample(samples, from: sourceSampleRate) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Sortformer/Offline/SortformerSpeakerStitcher.swift b/Sources/FluidAudio/Diarizer/Sortformer/Offline/SortformerSpeakerStitcher.swift index 43a91c426..999377842 100644 --- a/Sources/FluidAudio/Diarizer/Sortformer/Offline/SortformerSpeakerStitcher.swift +++ b/Sources/FluidAudio/Diarizer/Sortformer/Offline/SortformerSpeakerStitcher.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation /// Cross-window speaker-permutation alignment for offline Sortformer. @@ -89,3 +90,4 @@ enum SortformerSpeakerStitcher { } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Sortformer/SortformerDiarizer.swift b/Sources/FluidAudio/Diarizer/Sortformer/SortformerDiarizer.swift index 9641ca7f6..3a0d2561c 100644 --- a/Sources/FluidAudio/Diarizer/Sortformer/SortformerDiarizer.swift +++ b/Sources/FluidAudio/Diarizer/Sortformer/SortformerDiarizer.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import CoreML import Foundation @@ -978,3 +979,4 @@ public final class SortformerDiarizer: Diarizer { return (mel, chunkLength) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Sortformer/SortformerModelInference.swift b/Sources/FluidAudio/Diarizer/Sortformer/SortformerModelInference.swift index edf09399e..01e85b5a9 100644 --- a/Sources/FluidAudio/Diarizer/Sortformer/SortformerModelInference.swift +++ b/Sources/FluidAudio/Diarizer/Sortformer/SortformerModelInference.swift @@ -1,3 +1,4 @@ +#if Diarizer @preconcurrency import CoreML import Foundation import OSLog @@ -376,3 +377,4 @@ extension SortformerModels { ) } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Sortformer/SortformerStateUpdater.swift b/Sources/FluidAudio/Diarizer/Sortformer/SortformerStateUpdater.swift index 64ce64764..3588ae6db 100644 --- a/Sources/FluidAudio/Diarizer/Sortformer/SortformerStateUpdater.swift +++ b/Sources/FluidAudio/Diarizer/Sortformer/SortformerStateUpdater.swift @@ -1,3 +1,4 @@ +#if Diarizer import Accelerate import Foundation @@ -584,3 +585,4 @@ public struct SortformerStateUpdater { return logits.map { 1.0 / (1.0 + exp(-$0)) } } } +#endif diff --git a/Sources/FluidAudio/Diarizer/Sortformer/SortformerTypes.swift b/Sources/FluidAudio/Diarizer/Sortformer/SortformerTypes.swift index 405e6c365..8bddfe6d3 100644 --- a/Sources/FluidAudio/Diarizer/Sortformer/SortformerTypes.swift +++ b/Sources/FluidAudio/Diarizer/Sortformer/SortformerTypes.swift @@ -1,3 +1,4 @@ +#if Diarizer import Foundation // MARK: - Configuration @@ -455,3 +456,4 @@ public enum SortformerError: Error, LocalizedError { } } } +#endif diff --git a/Sources/FluidAudio/FluidAudioSwift.swift b/Sources/FluidAudio/FluidAudioSwift.swift index 86d23ef3b..abe146029 100644 --- a/Sources/FluidAudio/FluidAudioSwift.swift +++ b/Sources/FluidAudio/FluidAudioSwift.swift @@ -8,12 +8,14 @@ import OSLog // MARK: - Backward Compatibility -/// Backward compatibility alias for the old config name -public typealias SpeakerDiarizationConfig = DiarizerConfig - /// Backward compatibility alias for the old error type public typealias SpeakerDiarizationError = DiarizerError +#if Diarizer +/// Backward compatibility alias for the old config name +public typealias SpeakerDiarizationConfig = DiarizerConfig +#endif + // The Swift Programming Language // https://docs.swift.org/swift-book diff --git a/Sources/FluidAudio/ModelNames.swift b/Sources/FluidAudio/ModelNames.swift index 84d1dd58d..522d8ec03 100644 --- a/Sources/FluidAudio/ModelNames.swift +++ b/Sources/FluidAudio/ModelNames.swift @@ -953,6 +953,7 @@ public enum ModelNames { } } + #if Diarizer public var defaultConfiguration: SortformerConfig { switch self { case .fastV2: @@ -971,6 +972,7 @@ public enum ModelNames { return .efficientV2_1 } } + #endif /// Compiled-model path for this variant at the default (`.fp16`) precision. public var fileName: String { @@ -982,9 +984,11 @@ public enum ModelNames { return "\(precision.subdirectory)/\(name).mlmodelc" } + #if Diarizer public func isCompatible(with config: SortformerConfig) -> Bool { defaultConfiguration.isCompatible(with: config) } + #endif } /// Repo subdirectory holding the default (`.fp16`) model set. Both v3 sets are the @@ -1007,6 +1011,7 @@ public enum ModelNames { return variant.fileName(precision: precision) } + #if Diarizer /// Bundle name for a given configuration (honors `config.precision`) public static func bundle(for config: SortformerConfig) -> String? { guard let variant = config.modelVariant else { @@ -1015,6 +1020,7 @@ public enum ModelNames { assert(variant.isCompatible(with: config), "ERROR: Model variant and configuration are not compatible.") return variant.fileName(precision: config.precision) } + #endif /// Default bundle name public static var defaultBundle: String { @@ -1191,6 +1197,7 @@ public enum ModelNames { /// per language (#793). public static let encoderRecoverPinvFile = "encoder_recover_pinv.bin" + #if TTS /// FlowLM filename for a given precision. Both variants ship in the /// same `v2//` directory upstream; only the FlowLM transformer /// has an int8 variant — `cond_step`, `flow_decoder`, and @@ -1242,6 +1249,7 @@ public enum ModelNames { ] } } + #endif /// Required files for the default precision. Kept for callers that /// haven't been updated to pass a precision argument. @@ -1431,10 +1439,13 @@ public enum ModelNames { "synthesizer_f\(frames).mlmodelc" } + /// Synthesizer frame buckets; `InflectConstants.frameBuckets` reads these. + public static let frameBuckets = [256, 384, 512, 640, 768, 896, 1024, 2048] + /// Encoder + all 8 synthesizer buckets (downloaded up front; buckets - /// load lazily). Buckets mirror `InflectConstants.frameBuckets`. + /// load lazily). public static let requiredModels: Set = Set( - [encoderFile] + InflectConstants.frameBuckets.map { synthesizerFile(frames: $0) }) + [encoderFile] + frameBuckets.map { synthesizerFile(frames: $0) }) } /// LuxTTS (ZipVoice-Distill) model names. The HF repo publishes the same @@ -1713,6 +1724,7 @@ public enum ModelNames { flowFile, vocoderFile, ] + #if TTS /// Required model set for an output capacity ("extended" swaps in /// the N1000/T2000 S3Gen pair). public static func requiredModels(capacity: ChatterboxNanoOutputCapacity) -> Set { @@ -1723,6 +1735,7 @@ public enum ModelNames { return [prefillFile, decodeFile, flowFileExtended, vocoderFileExtended] } } + #endif /// Non-model assets fetched individually (nested under `tables/` and /// `tokenizer/`, which the repo-root model walk does not descend into). public static let auxFiles: [String] = [ @@ -1852,8 +1865,12 @@ public enum ModelNames { return ModelNames.Chatterbox.requiredModels case .chatterboxNano: // Variant: "extended" → N1000/T2000 S3Gen bucket pair (~30 s). + #if TTS let capacity = ChatterboxNanoOutputCapacity(rawValue: variant ?? "") ?? .standard return ModelNames.ChatterboxNano.requiredModels(capacity: capacity) + #else + return ModelNames.ChatterboxNano.requiredModels + #endif case .luxtts: // Variants: "gpu" (macOS) / "ane" (iOS); nil → platform default. return ModelNames.LuxTts.requiredFiles(variant: variant) diff --git a/Sources/FluidAudio/TTS/Chatterbox/Assets/ChatterboxTables.swift b/Sources/FluidAudio/TTS/Chatterbox/Assets/ChatterboxTables.swift index 2f656fa6e..7ae805531 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Assets/ChatterboxTables.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Assets/ChatterboxTables.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Host-side runtime tables for Chatterbox: T3 embedding / positional tables @@ -261,3 +262,4 @@ private struct SafetensorsFile { return out } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/ChatterboxConstants.swift b/Sources/FluidAudio/TTS/Chatterbox/ChatterboxConstants.swift index 2b8f7e2d9..4511604ec 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/ChatterboxConstants.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/ChatterboxConstants.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Compile-time constants for the Chatterbox Multilingual backend. @@ -68,3 +69,4 @@ public enum ChatterboxConstants { public static let defaultLanguage = "en" public static let defaultVoice = "default" } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/ChatterboxError.swift b/Sources/FluidAudio/TTS/Chatterbox/ChatterboxError.swift index 3606a3553..87c8ddcdf 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/ChatterboxError.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/ChatterboxError.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Errors thrown by the Chatterbox Multilingual backend. @@ -38,3 +39,4 @@ public enum ChatterboxError: Error, LocalizedError { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/ChatterboxManager.swift b/Sources/FluidAudio/TTS/Chatterbox/ChatterboxManager.swift index 97bb8e084..524ad84d7 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/ChatterboxManager.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/ChatterboxManager.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Public API for Chatterbox Multilingual synthesis (ResembleAI, 24 kHz). @@ -74,3 +75,4 @@ public actor ChatterboxManager { return Audio(samples: result.samples, sampleRate: ChatterboxConstants.sampleRate) } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/ChatterboxModels.swift b/Sources/FluidAudio/TTS/Chatterbox/ChatterboxModels.swift index daa93f86a..492c2f348 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/ChatterboxModels.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/ChatterboxModels.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -125,3 +126,4 @@ struct ChatterboxModels: Sendable { return root } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoConstants.swift b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoConstants.swift index 5b7f363ac..4b3197b07 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoConstants.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoConstants.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Compile-time constants for the Chatterbox Nano backend (beta model @@ -103,3 +104,4 @@ public enum ChatterboxNanoOutputCapacity: String, CaseIterable, Sendable { max(0, flowTokenBucket - promptTokens - ChatterboxNanoConstants.silenceTokenCount) } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoManager.swift b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoManager.swift index 6e9ee4a03..2187f54ba 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoManager.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoManager.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Public API for Chatterbox Nano synthesis (ResembleAI, 110M, English, @@ -84,3 +85,4 @@ public actor ChatterboxNanoManager { return Audio(samples: result.samples, sampleRate: ChatterboxNanoConstants.sampleRate) } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoModels.swift b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoModels.swift index 4b1d52f64..fd8d2efdc 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoModels.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoModels.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -137,3 +138,4 @@ struct ChatterboxNanoModels: Sendable { return root } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoSynthesizer.swift b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoSynthesizer.swift index 7a24065b3..f48df708e 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoSynthesizer.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoSynthesizer.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -413,3 +414,4 @@ struct ChatterboxNanoSynthesizer { return samples } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoTokenizer.swift b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoTokenizer.swift index 693034641..0ad914843 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoTokenizer.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoTokenizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// GPT-2 byte-level BPE tokenizer for Chatterbox Nano/Turbo, loaded from the @@ -221,3 +222,4 @@ final class ChatterboxNanoTokenizer: Sendable { return byteToScalar.map { String(UnicodeScalar($0)!) } } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxAlignmentAnalyzer.swift b/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxAlignmentAnalyzer.swift index a2b32f0b7..0695ced51 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxAlignmentAnalyzer.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxAlignmentAnalyzer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Port of upstream `AlignmentStreamAnalyzer` (multilingual T3 sampling), @@ -128,3 +129,4 @@ struct ChatterboxAlignmentAnalyzer { currFramePos += 1 } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxMLSupport.swift b/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxMLSupport.swift index 3727e5c6d..54f06529f 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxMLSupport.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxMLSupport.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -120,3 +121,4 @@ enum ChatterboxMLSupport { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxSynthesizer.swift b/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxSynthesizer.swift index 98a4b28d0..fcbc29268 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxSynthesizer.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Pipeline/ChatterboxSynthesizer.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -502,3 +503,4 @@ extension SplitMix64 { return Float((-2.0 * Foundation.log(u1)).squareRoot() * Foundation.cos(2.0 * .pi * u2)) } } +#endif diff --git a/Sources/FluidAudio/TTS/Chatterbox/Tokenizer/ChatterboxTokenizer.swift b/Sources/FluidAudio/TTS/Chatterbox/Tokenizer/ChatterboxTokenizer.swift index 3085da7f4..85104dc2e 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Tokenizer/ChatterboxTokenizer.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Tokenizer/ChatterboxTokenizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Grapheme BPE tokenizer for Chatterbox Multilingual, mirroring upstream @@ -207,3 +208,4 @@ final class ChatterboxTokenizer: Sendable { return parts.map { vocab[$0] ?? unkId } } } +#endif diff --git a/Sources/FluidAudio/TTS/G2P/G2PModel.swift b/Sources/FluidAudio/TTS/G2P/G2PModel.swift index 1c5b45240..16040e41e 100644 --- a/Sources/FluidAudio/TTS/G2P/G2PModel.swift +++ b/Sources/FluidAudio/TTS/G2P/G2PModel.swift @@ -1,3 +1,4 @@ +#if TTS import CoreML import Foundation @@ -224,3 +225,4 @@ actor G2PModel { logger.info("Loaded G2P CoreML models") } } +#endif diff --git a/Sources/FluidAudio/TTS/G2P/MultilingualG2PError.swift b/Sources/FluidAudio/TTS/G2P/MultilingualG2PError.swift index 191fe39a7..57d187063 100644 --- a/Sources/FluidAudio/TTS/G2P/MultilingualG2PError.swift +++ b/Sources/FluidAudio/TTS/G2P/MultilingualG2PError.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Errors raised by ``MultilingualG2PModel``. @@ -17,3 +18,4 @@ public enum MultilingualG2PError: Error, LocalizedError { } } } +#endif diff --git a/Sources/FluidAudio/TTS/G2P/MultilingualG2PLanguage.swift b/Sources/FluidAudio/TTS/G2P/MultilingualG2PLanguage.swift index c95b5bf14..f4252c755 100644 --- a/Sources/FluidAudio/TTS/G2P/MultilingualG2PLanguage.swift +++ b/Sources/FluidAudio/TTS/G2P/MultilingualG2PLanguage.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Languages supported by the CharsiuG2P ByT5 multilingual model, @@ -51,3 +52,4 @@ public enum MultilingualG2PLanguage: String, CaseIterable, Sendable { } } } +#endif diff --git a/Sources/FluidAudio/TTS/G2P/MultilingualG2PModel.swift b/Sources/FluidAudio/TTS/G2P/MultilingualG2PModel.swift index fc1bb0edb..3fc04e83d 100644 --- a/Sources/FluidAudio/TTS/G2P/MultilingualG2PModel.swift +++ b/Sources/FluidAudio/TTS/G2P/MultilingualG2PModel.swift @@ -1,3 +1,4 @@ +#if TTS import CoreML import Foundation @@ -172,3 +173,4 @@ public actor MultilingualG2PModel { logger.info("Loaded multilingual G2P CoreML models from \(modelsDir.path)") } } +#endif diff --git a/Sources/FluidAudio/TTS/Inflect/Assets/InflectModelStore.swift b/Sources/FluidAudio/TTS/Inflect/Assets/InflectModelStore.swift index b6e166d3e..8a2ed50ae 100644 --- a/Sources/FluidAudio/TTS/Inflect/Assets/InflectModelStore.swift +++ b/Sources/FluidAudio/TTS/Inflect/Assets/InflectModelStore.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -88,3 +89,4 @@ public actor InflectModelStore { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Inflect/Assets/InflectResourceDownloader.swift b/Sources/FluidAudio/TTS/Inflect/Assets/InflectResourceDownloader.swift index 0aaec3f06..c6cd05399 100644 --- a/Sources/FluidAudio/TTS/Inflect/Assets/InflectResourceDownloader.swift +++ b/Sources/FluidAudio/TTS/Inflect/Assets/InflectResourceDownloader.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Downloads Inflect v2 CoreML bundles from @@ -56,3 +57,4 @@ extension InflectVariant { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Inflect/InflectConstants.swift b/Sources/FluidAudio/TTS/Inflect/InflectConstants.swift index 0b096227a..9ecef6151 100644 --- a/Sources/FluidAudio/TTS/Inflect/InflectConstants.swift +++ b/Sources/FluidAudio/TTS/Inflect/InflectConstants.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Fixed pipeline parameters for the Inflect v2 CoreML backend. Values mirror @@ -25,7 +26,7 @@ public enum InflectConstants { /// Synthesizer frame buckets (each `synthesizer_f.mlmodelc`). The /// smallest bucket ≥ the predicted frame length is used; audio is trimmed /// to the exact length afterwards. - public static let frameBuckets = [256, 384, 512, 640, 768, 896, 1024, 2048] + public static let frameBuckets = ModelNames.Inflect.frameBuckets /// Largest bucket — a chunk whose predicted duration exceeds this throws. public static var maxFrames: Int { frameBuckets.last! } @@ -44,3 +45,4 @@ public enum InflectConstants { /// Edge fade applied to each chunk (ms), matching upstream `edge_fade`. public static let edgeFadeMs: Double = 5.0 } +#endif diff --git a/Sources/FluidAudio/TTS/Inflect/InflectError.swift b/Sources/FluidAudio/TTS/Inflect/InflectError.swift index 261c20607..01e83cd95 100644 --- a/Sources/FluidAudio/TTS/Inflect/InflectError.swift +++ b/Sources/FluidAudio/TTS/Inflect/InflectError.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Errors thrown by the Inflect v2 TTS backend. @@ -35,3 +36,4 @@ public enum InflectError: Error, LocalizedError { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Inflect/InflectManager.swift b/Sources/FluidAudio/TTS/Inflect/InflectManager.swift index 68b4b1ce1..f3fe0b69f 100644 --- a/Sources/FluidAudio/TTS/Inflect/InflectManager.swift +++ b/Sources/FluidAudio/TTS/Inflect/InflectManager.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -193,3 +194,4 @@ extension MLComputeUnits { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Inflect/InflectSymbols.swift b/Sources/FluidAudio/TTS/Inflect/InflectSymbols.swift index d90078a37..9b6f760fb 100644 --- a/Sources/FluidAudio/TTS/Inflect/InflectSymbols.swift +++ b/Sources/FluidAudio/TTS/Inflect/InflectSymbols.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Inflect v2 symbol table + token encoder. @@ -77,3 +78,4 @@ enum InflectSymbols { intersperse(sequence(for: phonemes)) } } +#endif diff --git a/Sources/FluidAudio/TTS/Inflect/InflectVariant.swift b/Sources/FluidAudio/TTS/Inflect/InflectVariant.swift index 4fd0ef229..18af854ba 100644 --- a/Sources/FluidAudio/TTS/Inflect/InflectVariant.swift +++ b/Sources/FluidAudio/TTS/Inflect/InflectVariant.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Inflect v2 model size. Both share the same pipeline, symbol table, and @@ -13,3 +14,4 @@ public enum InflectVariant: String, Sendable, CaseIterable { /// Subdirectory under `FluidInference/inflect-v2-coreml/`. public var subdirectory: String { rawValue } } +#endif diff --git a/Sources/FluidAudio/TTS/Inflect/Pipeline/InflectNoise.swift b/Sources/FluidAudio/TTS/Inflect/Pipeline/InflectNoise.swift index 211bee014..7fcab4f4c 100644 --- a/Sources/FluidAudio/TTS/Inflect/Pipeline/InflectNoise.swift +++ b/Sources/FluidAudio/TTS/Inflect/Pipeline/InflectNoise.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Deterministic standard-normal generator for the VITS prior sample @@ -51,3 +52,4 @@ struct InflectNoise { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Inflect/Pipeline/InflectSynthesizer.swift b/Sources/FluidAudio/TTS/Inflect/Pipeline/InflectSynthesizer.swift index 0d9a35a4c..5874e6b74 100644 --- a/Sources/FluidAudio/TTS/Inflect/Pipeline/InflectSynthesizer.swift +++ b/Sources/FluidAudio/TTS/Inflect/Pipeline/InflectSynthesizer.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -185,3 +186,4 @@ extension Float { self = (sign == 1 ? -1 : 1) * value } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneModelCacheMigration.swift b/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneModelCacheMigration.swift index 8fbf22bb3..81a22c95b 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneModelCacheMigration.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneModelCacheMigration.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Validates and transactionally repairs legacy Kokoro ANE model caches. @@ -167,3 +168,4 @@ actor KokoroAneModelCacheMigrationCoordinator { } } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneResourceDownloader.swift b/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneResourceDownloader.swift index 066539803..88bb137a0 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneResourceDownloader.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneResourceDownloader.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Downloads the laishere/kokoro 7-stage CoreML chain + auxiliary files @@ -527,3 +528,4 @@ public enum KokoroAneResourceDownloader { return try TtsCacheDirectory.ensure().appendingPathComponent(modelsSubdirectory) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneVocab.swift b/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneVocab.swift index 6d0f83532..dbf8c7e11 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneVocab.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/Assets/KokoroAneVocab.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// IPA phoneme → token id mapping shipped as `vocab.json` alongside the @@ -61,3 +62,4 @@ public struct KokoroAneVocab: Sendable { return ids } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/English/KokoroAneEnglishPhonemizer.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/English/KokoroAneEnglishPhonemizer.swift index 7cf58a948..3a678c8fc 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/English/KokoroAneEnglishPhonemizer.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/English/KokoroAneEnglishPhonemizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// English text frontend for the KokoroAne 7-stage chain. @@ -440,3 +441,4 @@ struct KokoroAneEnglishPhonemizer: Sendable { return knownLeadingApostropheWords.contains(candidate) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/French/FrenchG2P.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/French/FrenchG2P.swift index 142794134..f364208f8 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/French/FrenchG2P.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/French/FrenchG2P.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Text frontend for the Kokoro ANE French variant. @@ -584,3 +585,4 @@ enum FrenchPhonology { static let pauseMarks: Set = [",", ".", "!", "?", ";", ":", "—", "…", "(", ")", "«", "»", "\"", "“", "”"] } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseCutlet.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseCutlet.swift index 674e589fd..b4b2e14e2 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseCutlet.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseCutlet.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Port of Misaki's Cutlet frontend (`misaki/cutlet.py`, MIT, adapted from @@ -387,3 +388,4 @@ enum JapaneseNumberReader { } } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseG2P.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseG2P.swift index 77457988a..46817ffc9 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseG2P.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseG2P.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Text frontend for the Kokoro ANE Japanese variant: MeCab tokenization over @@ -34,3 +35,4 @@ actor JapaneseG2P { tokenizer.tokenize(JapaneseCutlet.normalize(text)) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseMecabDictionary.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseMecabDictionary.swift index 7fd092d2d..47cc0aa79 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseMecabDictionary.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseMecabDictionary.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Read-only view of a MeCab binary dictionary set (`sys.dic`, `unk.dic`, @@ -283,3 +284,4 @@ final class JapaneseMecabDictionary: Sendable { return Int(matrix.withUnsafeBytes { $0.loadUnaligned(fromByteOffset: index, as: Int16.self) }) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseTokenizer.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseTokenizer.swift index 4328e6456..0caf6510b 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseTokenizer.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Japanese/JapaneseTokenizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// A MeCab-compatible Viterbi tokenizer over ``JapaneseMecabDictionary``. @@ -170,3 +171,4 @@ struct JapaneseTokenizer { } } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/KokoroAneLexicon.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/KokoroAneLexicon.swift index 8cde93928..6fcfb4c3e 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/KokoroAneLexicon.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/KokoroAneLexicon.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Word → phoneme lexicon read from a Kokoro lexicon cache @@ -54,3 +55,4 @@ struct KokoroAneLexicon: Sendable { hAspireWords.contains(word.lowercased()) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinBertTokenizer.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinBertTokenizer.swift index 3074e2705..61ea33f0b 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinBertTokenizer.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinBertTokenizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Minimal BERT WordPiece tokenizer for the g2pW polyphone disambiguator. @@ -142,3 +143,4 @@ public struct MandarinBertTokenizer: Sendable { ) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinBopomofoMap.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinBopomofoMap.swift index 2363145b5..e8e080e4b 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinBopomofoMap.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinBopomofoMap.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Pinyin syllable → Bopomofo + tone digit string, matching @@ -202,3 +203,4 @@ public enum MandarinBopomofoMap { return ("", syllable) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinCustomLexicon.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinCustomLexicon.swift index 184a7eef3..0922d4b13 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinCustomLexicon.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinCustomLexicon.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// User-supplied pronunciation overrides for the Mandarin @@ -240,3 +241,4 @@ public struct MandarinCustomLexicon: Sendable, Equatable { return set }() } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinErhua.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinErhua.swift index ac96b97f1..63e2dafbf 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinErhua.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinErhua.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Merge `儿` (er) suffixes into the preceding syllable so that @@ -111,3 +112,4 @@ public enum MandarinErhua { "狗儿", "少儿", ] } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinG2P.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinG2P.swift index 0bcbbd918..5b4fb6323 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinG2P.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinG2P.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Hanzi → Bopomofo + tone-digit string for the Kokoro v1.1-zh @@ -464,3 +465,4 @@ public struct MandarinG2P: Sendable { return out.trimmingCharacters(in: .whitespacesAndNewlines) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinG2pwModel.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinG2pwModel.swift index bb59c0232..58dc210c8 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinG2pwModel.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinG2pwModel.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -180,3 +181,4 @@ public actor MandarinG2pwModel { return out } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinJiebaHmm.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinJiebaHmm.swift index 7a020bb9f..676815233 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinJiebaHmm.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinJiebaHmm.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Jieba's character-position HMM, ported as a standalone Viterbi @@ -165,3 +166,4 @@ public struct MandarinJiebaHmm: Sendable { count: JiebaHmmState.allCases.count) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinJiebaTables.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinJiebaTables.swift index 74ddd0191..5b59b1734 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinJiebaTables.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinJiebaTables.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Binary loader for the jieba HMM tables shipped at @@ -224,3 +225,4 @@ public enum JiebaHmmState: Int, CaseIterable, Sendable { case end = 2 case single = 3 } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinNumberNormalizer.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinNumberNormalizer.swift index 28a91143f..94dc5c9dc 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinNumberNormalizer.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinNumberNormalizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Mandarin number / date / time / currency verbalization pre-pass. @@ -264,3 +265,4 @@ public enum MandarinNumberNormalizer { ] } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPinyinDict.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPinyinDict.swift index 859e5837c..001f966e3 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPinyinDict.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPinyinDict.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Hanzi → pinyin lookup, loaded from the binary `.bin` files shipped at @@ -143,3 +144,4 @@ public struct MandarinPinyinDict: Sendable { return MandarinPinyinDict(phrases: phrases, singles: singles) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPinyinNormalizer.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPinyinNormalizer.swift index 9e5ac2ca3..9364efe83 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPinyinNormalizer.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPinyinNormalizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Convert pypinyin's diacritic-form pinyin (`níhǎo`, `qiū`, `lǜ`) into @@ -110,3 +111,4 @@ public enum MandarinPinyinNormalizer { return Syllable(base: base, tone: tone) } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPolyphoneCatalog.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPolyphoneCatalog.swift index 129f58746..c051a3c61 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPolyphoneCatalog.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinPolyphoneCatalog.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Loader / lookup for `POLYPHONIC_CHARS.txt`, the pronunciation @@ -177,3 +178,4 @@ public struct MandarinPolyphoneCatalog: Sendable { } } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinToneSandhi.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinToneSandhi.swift index a55b33ae8..886f35535 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinToneSandhi.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinToneSandhi.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Minimal Mandarin tone sandhi pass operating on a flat list of @@ -73,3 +74,4 @@ public enum MandarinToneSandhi { } } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinToneSandhiPOS.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinToneSandhiPOS.swift index e6d861fbb..57b05f9dc 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinToneSandhiPOS.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Mandarin/MandarinToneSandhiPOS.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// POS-aware Mandarin tone sandhi. @@ -187,3 +188,4 @@ public enum MandarinToneSandhiPOS { } } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/RomanceNumberNormalizer.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/RomanceNumberNormalizer.swift index 15736b603..bd55eea1e 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/RomanceNumberNormalizer.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/RomanceNumberNormalizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Spanish / French number reading for builds without the NeMo engine @@ -186,3 +187,4 @@ enum RomanceNumberNormalizer { } } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/G2P/Spanish/SpanishG2P.swift b/Sources/FluidAudio/TTS/KokoroAne/G2P/Spanish/SpanishG2P.swift index 2068e50cf..4f66d7657 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/G2P/Spanish/SpanishG2P.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/G2P/Spanish/SpanishG2P.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Rule-based Spanish grapheme-to-phoneme frontend for the Kokoro ANE Spanish @@ -531,3 +532,4 @@ enum SpanishG2P { private static let nasals = Set("mnɲŋ".unicodeScalars) private static let vocalicPhones = Set("aeiouɛIAWOɪʊ".unicodeScalars) } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneConstants.swift b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneConstants.swift index 27bcd0ae9..eba36e92d 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneConstants.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneConstants.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Compile-time constants for the laishere/kokoro 7-stage CoreML chain. @@ -277,3 +278,4 @@ public enum KokoroAneVariant: String, CaseIterable, Sendable { } } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneError.swift b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneError.swift index 684b31304..279decc00 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneError.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneError.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Errors emitted by the KokoroAne TTS chain. @@ -55,3 +56,4 @@ public enum KokoroAneError: Error, LocalizedError { } } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift index aff1ae7e5..a1e3a8b5f 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/KokoroAneManager.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// High-level facade for the legacy Kokoro chain or opt-in ANE-v3 runtime @@ -541,3 +542,4 @@ public actor KokoroAneManager { } } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneModelStore.swift b/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneModelStore.swift index ffd04451c..b5b94d4b9 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneModelStore.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneModelStore.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -525,3 +526,4 @@ public actor KokoroAneModelStore { spanishLexiconRetryAfter = nil } } +#endif diff --git a/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneSynthesizer+Conversion.swift b/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneSynthesizer+Conversion.swift index 908293a9d..1a79bc629 100644 --- a/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneSynthesizer+Conversion.swift +++ b/Sources/FluidAudio/TTS/KokoroAne/Pipeline/KokoroAneSynthesizer+Conversion.swift @@ -1,3 +1,4 @@ +#if TTS import Accelerate @preconcurrency import CoreML import Foundation @@ -217,3 +218,4 @@ enum KokoroAneArrays { return (0..> 11) * (1.0 / 9_007_199_254_740_992.0) } } +#endif diff --git a/Sources/FluidAudio/TTS/NeuTts/Tokenizer/NeuTtsBpeTokenizer.swift b/Sources/FluidAudio/TTS/NeuTts/Tokenizer/NeuTtsBpeTokenizer.swift index 7632f6dbb..9e0105664 100644 --- a/Sources/FluidAudio/TTS/NeuTts/Tokenizer/NeuTtsBpeTokenizer.swift +++ b/Sources/FluidAudio/TTS/NeuTts/Tokenizer/NeuTtsBpeTokenizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Minimal byte-level BPE encoder for the NeuTTS-2E (Qwen2-style) tokenizer. @@ -175,3 +176,4 @@ final class NeuTtsBpeTokenizer: Sendable { return byteToScalar.map { String(UnicodeScalar($0)!) } } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Assets/PocketTtsConstantsLoader.swift b/Sources/FluidAudio/TTS/PocketTTS/Assets/PocketTtsConstantsLoader.swift index c06a13677..5e61f948e 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Assets/PocketTtsConstantsLoader.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Assets/PocketTtsConstantsLoader.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Pre-loaded binary constants for PocketTTS inference. @@ -460,3 +461,4 @@ public enum PocketTtsConstantsLoader { } } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Assets/PocketTtsResourceDownloader.swift b/Sources/FluidAudio/TTS/PocketTTS/Assets/PocketTtsResourceDownloader.swift index 24cf2aa5d..524b1b644 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Assets/PocketTtsResourceDownloader.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Assets/PocketTtsResourceDownloader.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Downloads PocketTTS models and constants from HuggingFace. @@ -481,3 +482,4 @@ public enum PocketTtsResourceDownloader { return try TtsCacheDirectory.ensure() } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsLayerKeys.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsLayerKeys.swift index d02a0e9eb..ae96dd4a9 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsLayerKeys.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsLayerKeys.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -206,3 +207,4 @@ struct PocketTtsLayerKeys: Sendable { return Int(String(suffix.reversed())) } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsMimiKeys.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsMimiKeys.swift index 8ee236a99..de5bedc6e 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsMimiKeys.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsMimiKeys.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -132,3 +133,4 @@ struct PocketTtsMimiKeys: Sendable { } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsModelStore.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsModelStore.swift index fea761a55..687170672 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsModelStore.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsModelStore.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -548,3 +549,4 @@ public actor PocketTtsModelStore { return data.withUnsafeBytes { Array($0.bindMemory(to: Float.self)) } } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSession.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSession.swift index 58259ed53..dd8fbe659 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSession.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSession.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -556,3 +557,4 @@ public actor PocketTtsSession { } } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsStateEngine.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsStateEngine.swift index 94c08665f..8609587d4 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsStateEngine.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsStateEngine.swift @@ -1,3 +1,4 @@ +#if TTS import Accelerate @preconcurrency import CoreML import Foundation @@ -397,3 +398,4 @@ actor PocketTtsStateEngine { return Array(UnsafeBufferPointer(start: ptr, count: count)) } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Flow.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Flow.swift index ba5c4854e..b9e8c54db 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Flow.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Flow.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -125,3 +126,4 @@ extension Float { return sqrtf(-2.0 * logf(u1)) * cosf(2.0 * .pi * u2) } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+KVCache.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+KVCache.swift index 3df703d2a..803ad435f 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+KVCache.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+KVCache.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -616,3 +617,4 @@ extension PocketTtsSynthesizer { return (transformerOut: transformerOut, eosLogit: eosLogit) } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Mimi.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Mimi.swift index e2617fc1d..3ccaf83f0 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Mimi.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Mimi.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -149,3 +150,4 @@ extension PocketTtsSynthesizer { return Array(UnsafeBufferPointer(start: ptr, count: count)) } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+State.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+State.swift index 495698f65..dce61dd5a 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+State.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+State.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -89,3 +90,4 @@ extension PocketTtsSynthesizer { return session } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Types.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Types.swift index a4a94d043..1c5f7ded3 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Types.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer+Types.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation extension PocketTtsSynthesizer { @@ -24,3 +25,4 @@ extension PocketTtsSynthesizer { /// Both use discovery because CoreML auto-generates `var_NNN` names that /// differ across language packs and 6L/24L variants. } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer.swift index f7ee353ef..7e8c9be7f 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsSynthesizer.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -1479,3 +1480,4 @@ public struct PocketTtsSynthesizer { } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsVoiceCloner.swift b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsVoiceCloner.swift index b7c65dd8f..b9a69f268 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsVoiceCloner.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Pipeline/PocketTtsVoiceCloner.swift @@ -1,3 +1,4 @@ +#if TTS import Accelerate @preconcurrency import AVFoundation @preconcurrency import CoreML @@ -406,3 +407,4 @@ public enum PocketTtsVoiceCloner { } } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/PocketTTSError.swift b/Sources/FluidAudio/TTS/PocketTTS/PocketTTSError.swift index 64254beb1..7bd03bce0 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/PocketTTSError.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/PocketTTSError.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Errors that can occur during PocketTTS synthesis. @@ -20,3 +21,4 @@ public enum PocketTTSError: LocalizedError { } } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/PocketTtsComputeUnits.swift b/Sources/FluidAudio/TTS/PocketTTS/PocketTtsComputeUnits.swift index 44ddf0045..13a0bc2ca 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/PocketTtsComputeUnits.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/PocketTtsComputeUnits.swift @@ -1,3 +1,4 @@ +#if TTS import CoreML import Foundation @@ -93,3 +94,4 @@ extension MLComputeUnits { } } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/PocketTtsConstants.swift b/Sources/FluidAudio/TTS/PocketTTS/PocketTtsConstants.swift index a5b58e372..eb24046d6 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/PocketTtsConstants.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/PocketTtsConstants.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Constants for the PocketTTS flow-matching language model TTS backend. @@ -192,3 +193,4 @@ public enum PocketTtsModelPlacement: String, Sendable, Hashable { /// Trial 23); mimi decode is unchanged. case aneState = "ane-state" } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/PocketTtsManager.swift b/Sources/FluidAudio/TTS/PocketTTS/PocketTtsManager.swift index cebf8dee5..5823bde29 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/PocketTtsManager.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/PocketTtsManager.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation import OSLog @@ -443,3 +444,4 @@ public actor PocketTtsManager { try PocketTtsVoiceCloner.loadVoice(from: url) } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Tokenizer/SentencePieceProto.swift b/Sources/FluidAudio/TTS/PocketTTS/Tokenizer/SentencePieceProto.swift index 42039f4c4..9218c91b0 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Tokenizer/SentencePieceProto.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Tokenizer/SentencePieceProto.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Minimal protobuf parser for SentencePiece `.model` files. @@ -150,3 +151,4 @@ enum SentencePieceProto { return value } } +#endif diff --git a/Sources/FluidAudio/TTS/PocketTTS/Tokenizer/SentencePieceTokenizer.swift b/Sources/FluidAudio/TTS/PocketTTS/Tokenizer/SentencePieceTokenizer.swift index 95e9745c7..d6e75d1fd 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/Tokenizer/SentencePieceTokenizer.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/Tokenizer/SentencePieceTokenizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Minimal SentencePiece unigram tokenizer for PocketTTS. @@ -154,3 +155,4 @@ public struct SentencePieceTokenizer: Sendable { return ids } } +#endif diff --git a/Sources/FluidAudio/TTS/SSML/SSMLProcessor.swift b/Sources/FluidAudio/TTS/SSML/SSMLProcessor.swift index 70a969cb2..5cf9c8577 100644 --- a/Sources/FluidAudio/TTS/SSML/SSMLProcessor.swift +++ b/Sources/FluidAudio/TTS/SSML/SSMLProcessor.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Main SSML processing orchestrator @@ -102,3 +103,4 @@ public enum SSMLProcessor { return [trimmed] } } +#endif diff --git a/Sources/FluidAudio/TTS/SSML/SSMLTagParser.swift b/Sources/FluidAudio/TTS/SSML/SSMLTagParser.swift index 8f9c13ad4..6e5402dd8 100644 --- a/Sources/FluidAudio/TTS/SSML/SSMLTagParser.swift +++ b/Sources/FluidAudio/TTS/SSML/SSMLTagParser.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Regex-based parser for SSML tags @@ -118,3 +119,4 @@ enum SSMLTagParser { return nsText.substring(with: groupRange) } } +#endif diff --git a/Sources/FluidAudio/TTS/SSML/SSMLTypes.swift b/Sources/FluidAudio/TTS/SSML/SSMLTypes.swift index fcc4906eb..890954a64 100644 --- a/Sources/FluidAudio/TTS/SSML/SSMLTypes.swift +++ b/Sources/FluidAudio/TTS/SSML/SSMLTypes.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Result of SSML preprocessing - cleaned text with phonetic overrides @@ -90,3 +91,4 @@ func digitToWord(_ char: Character) -> String? { guard let digit = Int(String(char)), digit >= 0, digit <= 9 else { return nil } return digitWords[digit] } +#endif diff --git a/Sources/FluidAudio/TTS/SSML/SayAsInterpreter.swift b/Sources/FluidAudio/TTS/SSML/SayAsInterpreter.swift index cf99056ee..dba6f2814 100644 --- a/Sources/FluidAudio/TTS/SSML/SayAsInterpreter.swift +++ b/Sources/FluidAudio/TTS/SSML/SayAsInterpreter.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Interprets say-as content based on interpret-as type @@ -411,3 +412,4 @@ public enum SayAsInterpreter { return "\(numeratorWord) \(denominatorWord)" } } +#endif diff --git a/Sources/FluidAudio/TTS/Shared/AudioPostProcessor.swift b/Sources/FluidAudio/TTS/Shared/AudioPostProcessor.swift index b2c067189..ba8fdd089 100644 --- a/Sources/FluidAudio/TTS/Shared/AudioPostProcessor.swift +++ b/Sources/FluidAudio/TTS/Shared/AudioPostProcessor.swift @@ -1,3 +1,4 @@ +#if TTS import Accelerate import Foundation @@ -144,3 +145,4 @@ public enum AudioPostProcessor { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Shared/EnglishInitialisms.swift b/Sources/FluidAudio/TTS/Shared/EnglishInitialisms.swift index 77e7d38bb..89f14fd22 100644 --- a/Sources/FluidAudio/TTS/Shared/EnglishInitialisms.swift +++ b/Sources/FluidAudio/TTS/Shared/EnglishInitialisms.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Shared helpers for reading uppercase initialisms as letter names in @@ -69,3 +70,4 @@ enum EnglishInitialisms { return letters.joined(separator: separator) } } +#endif diff --git a/Sources/FluidAudio/TTS/Shared/EnglishTextNormalizer.swift b/Sources/FluidAudio/TTS/Shared/EnglishTextNormalizer.swift index f0b2dda29..d99e4a7c7 100644 --- a/Sources/FluidAudio/TTS/Shared/EnglishTextNormalizer.swift +++ b/Sources/FluidAudio/TTS/Shared/EnglishTextNormalizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Conservative pre-tokenization text normalization for English TTS @@ -350,3 +351,4 @@ enum EnglishTextNormalizer { return mutable as String } } +#endif diff --git a/Sources/FluidAudio/TTS/Shared/LexiconAssetCache.swift b/Sources/FluidAudio/TTS/Shared/LexiconAssetCache.swift index 9d769b6f2..f41dd5f2b 100644 --- a/Sources/FluidAudio/TTS/Shared/LexiconAssetCache.swift +++ b/Sources/FluidAudio/TTS/Shared/LexiconAssetCache.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Shared Misaki lexicon JSON cache loader. @@ -72,3 +73,4 @@ public actor LexiconAssetCache { (wordToPhonemes, caseSensitiveWordToPhonemes) } } +#endif diff --git a/Sources/FluidAudio/TTS/Shared/NemoTextNormalizer.swift b/Sources/FluidAudio/TTS/Shared/NemoTextNormalizer.swift index 1ed86aa47..0d0fb7bb0 100644 --- a/Sources/FluidAudio/TTS/Shared/NemoTextNormalizer.swift +++ b/Sources/FluidAudio/TTS/Shared/NemoTextNormalizer.swift @@ -1,3 +1,4 @@ +#if TTS #if canImport(CNemoTextProcessing) import CNemoTextProcessing #endif @@ -52,3 +53,4 @@ public enum NemoTextNormalizer { #endif } } +#endif diff --git a/Sources/FluidAudio/TTS/Shared/PhonemeChunker.swift b/Sources/FluidAudio/TTS/Shared/PhonemeChunker.swift index 31ab66a44..6711a4c86 100644 --- a/Sources/FluidAudio/TTS/Shared/PhonemeChunker.swift +++ b/Sources/FluidAudio/TTS/Shared/PhonemeChunker.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Splits a phoneme / IPA string into chunks that each fit a model's input @@ -100,3 +101,4 @@ enum PhonemeChunker { if !text.isEmpty { chunks.append(text) } } } +#endif diff --git a/Sources/FluidAudio/TTS/Shared/TTSError.swift b/Sources/FluidAudio/TTS/Shared/TTSError.swift index cb915db34..f9da87b4f 100644 --- a/Sources/FluidAudio/TTS/Shared/TTSError.swift +++ b/Sources/FluidAudio/TTS/Shared/TTSError.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation public enum TTSError: LocalizedError { @@ -19,3 +20,4 @@ public enum TTSError: LocalizedError { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Shared/TtsCacheDirectory.swift b/Sources/FluidAudio/TTS/Shared/TtsCacheDirectory.swift index d128ac0b2..e7affe5a6 100644 --- a/Sources/FluidAudio/TTS/Shared/TtsCacheDirectory.swift +++ b/Sources/FluidAudio/TTS/Shared/TtsCacheDirectory.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Cross-backend TTS cache root. @@ -43,3 +44,4 @@ public enum TtsCacheDirectory { return cacheDirectory } } +#endif diff --git a/Sources/FluidAudio/TTS/Shared/TtsComputeUnitPreset.swift b/Sources/FluidAudio/TTS/Shared/TtsComputeUnitPreset.swift index 2ae24e560..98782c5ec 100644 --- a/Sources/FluidAudio/TTS/Shared/TtsComputeUnitPreset.swift +++ b/Sources/FluidAudio/TTS/Shared/TtsComputeUnitPreset.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -80,3 +81,4 @@ public enum TtsComputeUnitPreset: String, Sendable, CaseIterable { } } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/Assets/StyleTTS2ModelStore.swift b/Sources/FluidAudio/TTS/StyleTTS2/Assets/StyleTTS2ModelStore.swift index a98511c29..3156afd22 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/Assets/StyleTTS2ModelStore.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/Assets/StyleTTS2ModelStore.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -267,3 +268,4 @@ public actor StyleTTS2ModelStore { } } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/Assets/StyleTTS2ResourceDownloader.swift b/Sources/FluidAudio/TTS/StyleTTS2/Assets/StyleTTS2ResourceDownloader.swift index dc25f2778..4526c8472 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/Assets/StyleTTS2ResourceDownloader.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/Assets/StyleTTS2ResourceDownloader.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Downloads StyleTTS2 LibriTTS (iteration_3) CoreML models from HuggingFace. @@ -158,3 +159,4 @@ public enum StyleTTS2ResourceDownloader { return root } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Preprocess/StyleTTS2MelExtractor.swift b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Preprocess/StyleTTS2MelExtractor.swift index 5dab160e7..fc1fca590 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Preprocess/StyleTTS2MelExtractor.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Preprocess/StyleTTS2MelExtractor.swift @@ -1,3 +1,4 @@ +#if TTS import Accelerate import Foundation @@ -249,3 +250,4 @@ public final class StyleTTS2MelExtractor { return out } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Sampler/StyleTTS2DiffusionSchedule.swift b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Sampler/StyleTTS2DiffusionSchedule.swift index eb8db0744..87d0f23a9 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Sampler/StyleTTS2DiffusionSchedule.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Sampler/StyleTTS2DiffusionSchedule.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Karras-noise + ADPM2-step helpers for the StyleTTS2 fused diffusion sampler. @@ -79,3 +80,4 @@ public struct StyleTTS2NoiseSource: Sendable { return out } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2GlueOps.swift b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2GlueOps.swift index db5d6c772..0bbf55f76 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2GlueOps.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2GlueOps.swift @@ -1,3 +1,4 @@ +#if TTS import Accelerate import CoreML import Foundation @@ -160,3 +161,4 @@ public enum StyleTTS2GlueOps { return (ref, s) } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2MultiArray.swift b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2MultiArray.swift index f1f818f0d..7a4af09a6 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2MultiArray.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2MultiArray.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -84,3 +85,4 @@ public enum StyleTTS2MultiArray { return arr.shape.map { $0.intValue } } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2Synthesizer.swift b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2Synthesizer.swift index eb199e174..29699d24f 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2Synthesizer.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Synthesize/StyleTTS2Synthesizer.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -436,3 +437,4 @@ public actor StyleTTS2Synthesizer { return descMap.keys.sorted() } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Tokenizer/StyleTTS2Phonemizer.swift b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Tokenizer/StyleTTS2Phonemizer.swift index d11d6dfcf..646d5b6d3 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Tokenizer/StyleTTS2Phonemizer.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Tokenizer/StyleTTS2Phonemizer.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// English-only StyleTTS2 phonemizer. @@ -272,3 +273,4 @@ public struct StyleTTS2Phonemizer: Sendable { return out } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Tokenizer/StyleTTS2TextCleaner.swift b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Tokenizer/StyleTTS2TextCleaner.swift index 1b9d31050..7619b0ff6 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Tokenizer/StyleTTS2TextCleaner.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/Pipeline/Tokenizer/StyleTTS2TextCleaner.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Pure-Swift port of `mobius/styletts2/text_utils.py::TextCleaner`. @@ -69,3 +70,4 @@ public enum StyleTTS2TextCleaner { /// Total symbol count (used by tests + as a sanity check on shape). public static var vocabularySize: Int { symbols.count } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Constants.swift b/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Constants.swift index e4de33b9c..74d2a75e3 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Constants.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Constants.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Compile-time constants for the StyleTTS2 LibriTTS (iteration_3) pipeline. @@ -84,3 +85,4 @@ public enum StyleTTS2Constants { /// the shift. public static let applyHifiganAsrShift: Bool = true } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Error.swift b/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Error.swift index 73c51b4ee..9a7d46e82 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Error.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Error.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Errors that can surface during StyleTTS2 initialization or synthesis. @@ -39,3 +40,4 @@ public enum StyleTTS2Error: Error, LocalizedError, Sendable { } } } +#endif diff --git a/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Manager.swift b/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Manager.swift index 87084dd21..1b67b31f4 100644 --- a/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Manager.swift +++ b/Sources/FluidAudio/TTS/StyleTTS2/StyleTTS2Manager.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -335,3 +336,4 @@ extension MLComputeUnits { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Assets/Supertonic3ModelStore.swift b/Sources/FluidAudio/TTS/Supertonic3/Assets/Supertonic3ModelStore.swift index fcf6cad54..b9338fef4 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Assets/Supertonic3ModelStore.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Assets/Supertonic3ModelStore.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -199,3 +200,4 @@ public actor Supertonic3ModelStore { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Assets/Supertonic3ResourceDownloader.swift b/Sources/FluidAudio/TTS/Supertonic3/Assets/Supertonic3ResourceDownloader.swift index d174cf61e..b7b26a5e3 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Assets/Supertonic3ResourceDownloader.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Assets/Supertonic3ResourceDownloader.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Downloads the Supertonic-3 CoreML assets from HuggingFace. @@ -105,3 +106,4 @@ public enum Supertonic3ResourceDownloader { return root } } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Preprocess/Supertonic3TextChunker.swift b/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Preprocess/Supertonic3TextChunker.swift index a60e61367..cecc1bb59 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Preprocess/Supertonic3TextChunker.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Preprocess/Supertonic3TextChunker.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Long-text chunker that mirrors `chunkText()` from the upstream Supertonic @@ -171,3 +172,4 @@ enum Supertonic3TextChunker { return sentences.isEmpty ? [text] : sentences } } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Preprocess/Supertonic3UnicodeProcessor.swift b/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Preprocess/Supertonic3UnicodeProcessor.swift index b9fb1ad2d..26af51f67 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Preprocess/Supertonic3UnicodeProcessor.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Preprocess/Supertonic3UnicodeProcessor.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Text preprocessor that mirrors the upstream `UnicodeProcessor` from @@ -194,3 +195,4 @@ struct Supertonic3UnicodeProcessor { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3LatentSampler.swift b/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3LatentSampler.swift index 124dc89b2..43e879155 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3LatentSampler.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3LatentSampler.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Helper that mirrors `sampleNoisyLatent()` / `getLatentMask()` from the @@ -82,3 +83,4 @@ enum Supertonic3LatentSampler { return out } } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3MultiArray.swift b/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3MultiArray.swift index e37f52fbe..adc29510f 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3MultiArray.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3MultiArray.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -81,3 +82,4 @@ public enum Supertonic3MultiArray { return out } } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3Synthesizer.swift b/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3Synthesizer.swift index 732649e79..08608af0e 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3Synthesizer.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Pipeline/Synthesize/Supertonic3Synthesizer.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -305,3 +306,4 @@ struct Supertonic3Synthesizer { return try Supertonic3MultiArray.makeFloat32(values, shape: shape) } } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Constants.swift b/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Constants.swift index b80a0cdb0..674590350 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Constants.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Constants.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Compile-time constants for the Supertonic-3 multilingual TTS pipeline. @@ -105,3 +106,4 @@ public enum Supertonic3Constants { /// Languages that should use the tighter `maxChunkLengthCJK` (57-char) chunker. public static let cjkLanguages: Set = ["ko", "ja"] } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Error.swift b/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Error.swift index fe5d9eed5..5f56fd93d 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Error.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Error.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Errors that can surface during Supertonic-3 initialization or synthesis. @@ -48,3 +49,4 @@ public enum Supertonic3Error: Error, LocalizedError, Sendable { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Manager.swift b/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Manager.swift index c25961df4..325e57df0 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Manager.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Manager.swift @@ -1,3 +1,4 @@ +#if TTS @preconcurrency import CoreML import Foundation @@ -144,3 +145,4 @@ extension MLComputeUnits { } } } +#endif diff --git a/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Types.swift b/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Types.swift index e77968abb..e3e30cef8 100644 --- a/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Types.swift +++ b/Sources/FluidAudio/TTS/Supertonic3/Supertonic3Types.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// On-disk schema of the upstream `tts.json` config. @@ -265,3 +266,4 @@ public struct Supertonic3VoiceStyle: Sendable { return out } } +#endif diff --git a/Sources/FluidAudio/TTS/TtsBackend.swift b/Sources/FluidAudio/TTS/TtsBackend.swift index 9a0162b5e..af0caca7b 100644 --- a/Sources/FluidAudio/TTS/TtsBackend.swift +++ b/Sources/FluidAudio/TTS/TtsBackend.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Available TTS synthesis backends. @@ -72,3 +73,4 @@ public enum TtsBackend: Sendable { /// - Note: Beta — this is a beta model conversion; API, model artifacts, and accuracy may change. case chatterboxNano } +#endif diff --git a/Sources/FluidAudio/TTS/TtsConstants.swift b/Sources/FluidAudio/TTS/TtsConstants.swift index 96a894a1b..62d1ecf2d 100644 --- a/Sources/FluidAudio/TTS/TtsConstants.swift +++ b/Sources/FluidAudio/TTS/TtsConstants.swift @@ -1,3 +1,4 @@ +#if TTS import Foundation /// Shared TTS constants. Backend-specific tuning lives next to each backend @@ -9,3 +10,4 @@ public enum TtsConstants { /// Currently matches the KokoroAne English default (`af_heart`). public static let recommendedVoice = "af_heart" } +#endif diff --git a/Sources/LuxTtsG2pResources/LuxTtsG2pResources.swift b/Sources/LuxTtsG2pResources/LuxTtsG2pResources.swift new file mode 100644 index 000000000..c536d1a44 --- /dev/null +++ b/Sources/LuxTtsG2pResources/LuxTtsG2pResources.swift @@ -0,0 +1,7 @@ +import Foundation + +/// Bundled LuxTTS English G2P lexicon + aux tables. A separate target so the +/// resource bundle is only linked when the `TTS` trait is enabled (#990). +public enum LuxTtsG2pResources { + public static var bundle: Bundle { .module } +} diff --git a/Sources/FluidAudio/TTS/LuxTts/G2p/Resources/luxtts_en_us_g2p_aux.json b/Sources/LuxTtsG2pResources/Resources/luxtts_en_us_g2p_aux.json similarity index 100% rename from Sources/FluidAudio/TTS/LuxTts/G2p/Resources/luxtts_en_us_g2p_aux.json rename to Sources/LuxTtsG2pResources/Resources/luxtts_en_us_g2p_aux.json diff --git a/Sources/FluidAudio/TTS/LuxTts/G2p/Resources/luxtts_en_us_lexicon.tsv.zz b/Sources/LuxTtsG2pResources/Resources/luxtts_en_us_lexicon.tsv.zz similarity index 100% rename from Sources/FluidAudio/TTS/LuxTts/G2p/Resources/luxtts_en_us_lexicon.tsv.zz rename to Sources/LuxTtsG2pResources/Resources/luxtts_en_us_lexicon.tsv.zz From 213ecf1291aa4687534302132ba2668dbcbafeeb Mon Sep 17 00:00:00 2001 From: Alex-Wengg Date: Wed, 14 Oct 2026 01:54:18 -0400 Subject: [PATCH 2/3] fix(package): review follow-ups for TTS/Diarizer traits (#990) CocoaPods: the pod now defines TTS, so LuxTtsG2p.swift compiled and hit `import LuxTtsG2pResources`, a module the podspec never built. The Core subspec now compiles Sources/LuxTtsG2pResources and ships the lexicon as a `LuxTtsG2pResources` resource bundle; `LuxTtsG2pResources.bundle` uses `Bundle.module` under SwiftPM and locates that bundle otherwise, and the import is `canImport`-guarded. `pod lib lint --platforms=macos` passes. ANEMemoryOptimizer (shared by the TDT decoder and VAD) threw DiarizerError, which was the only reason DiarizerError.swift stayed outside `#if Diarizer`. It now throws ANEMemoryUtils.ANEMemoryError (gains `invalidArrayBounds` and LocalizedError descriptions), and DiarizerError plus the SpeakerDiarizationError alias are guarded like the rest of Diarizer/. PocketTtsPrecision, PocketTtsModelPlacement and ChatterboxNanoOutputCapacity move outside the TTS guard (generationBudget stays inside, it reads ChatterboxNanoConstants), so ModelNames needs no `#if TTS` and `requiredModelNames(for: .chatterboxNano, variant: "extended")` honours the variant in every build. Docs/CI: the Xcode shim recipe now passes `traits: ["TTS", "Diarizer"]` (with `traits: []` it would drop diarization, contradicting the measured sentence next to it). The trait-matrix job asserts the resource bundle is present after the TTS-only build so the absent check cannot pass vacuously. Stale podspec comment about Sources/FluidAudioTTS removed. Tests: LuxTtsG2pResourcesTests pins both files at the bundle root; Inflect wiring pins the runtime/download bucket lists; the zero-copy bounds test checks the thrown type. --- .github/workflows/tests.yml | 5 +++ Documentation/ASR/PostProcessing.md | 5 ++- FluidAudio.podspec | 12 ++++-- .../Diarizer/Core/DiarizerError.swift | 3 +- Sources/FluidAudio/FluidAudioSwift.swift | 2 +- Sources/FluidAudio/ModelNames.swift | 8 ---- .../Shared/ANEMemoryOptimizer.swift | 10 ++--- .../FluidAudio/Shared/ANEMemoryUtils.swift | 16 +++++++- .../Nano/ChatterboxNanoConstants.swift | 5 +++ .../FluidAudio/TTS/LuxTts/G2p/LuxTtsG2p.swift | 2 + .../TTS/PocketTTS/PocketTtsConstants.swift | 3 +- .../LuxTtsG2pResources.swift | 22 +++++++++- .../Shared/ANEMemoryOptimizerTests.swift | 17 +++++++- .../TTS/Inflect/InflectWiringTests.swift | 9 +++++ .../TTS/LuxTts/LuxTtsG2pResourcesTests.swift | 40 +++++++++++++++++++ 15 files changed, 133 insertions(+), 26 deletions(-) create mode 100644 Tests/FluidAudioTests/TTS/LuxTts/LuxTtsG2pResourcesTests.swift diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 04fe812bf..8fc891aca 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -93,6 +93,11 @@ jobs: - name: Build with TTS only run: swift build --traits TTS --target FluidAudio + # Positive control for the assertion above: the same path must appear once TTS is on, + # otherwise a renamed bundle or build-dir layout change would make the check pass vacuously. + - name: Assert TTS resource bundle present + run: test -e .build/debug/FluidAudio_LuxTtsG2pResources.bundle + - name: Build with Diarizer only run: swift build --traits Diarizer --target FluidAudio diff --git a/Documentation/ASR/PostProcessing.md b/Documentation/ASR/PostProcessing.md index 393f4f97f..2366da819 100644 --- a/Documentation/ASR/PostProcessing.md +++ b/Documentation/ASR/PostProcessing.md @@ -101,7 +101,8 @@ let package = Package( platforms: [.macOS(.v14), .iOS(.v17)], products: [.library(name: "FluidAudioShim", targets: ["FluidAudioShim"])], dependencies: [ - .package(url: "https://github.com/FluidInference/FluidAudio.git", from: "0.15.7", traits: []) + // `traits: []` would also drop TTS and diarization (#990); list the subsystems you keep. + .package(url: "https://github.com/FluidInference/FluidAudio.git", from: "0.15.7", traits: ["TTS", "Diarizer"]) ], targets: [ .target(name: "FluidAudioShim", dependencies: [.product(name: "FluidAudio", package: "FluidAudio")]) @@ -114,7 +115,7 @@ let package = Package( @_exported import FluidAudio ``` -Existing `import FluidAudio` lines keep compiling. Measured on a universal macOS app this way (Xcode 26.3): 16.85 MB off the executable, 12.7%, zero engine symbols, ASR/diarization symbols unchanged. +Existing `import FluidAudio` lines keep compiling. Measured on a universal macOS app this way (Xcode 26.3, engine trait off, TTS and Diarizer on): 16.85 MB off the executable, 12.7%, zero engine symbols, ASR/diarization/TTS symbols unchanged. An ASR-only app can use `traits: []` in the shim instead and drop those subsystems too. **The xcframework still downloads.** The binary target is declared unconditionally and only the dependency edge is trait-conditioned, so a clean resolve still fetches the 49 MB `NemoTextProcessing.xcframework.zip` even with the trait off. Ship size is unaffected; CI and cold checkouts pay the download. That is a SwiftPM limitation, not something the package can change. diff --git a/FluidAudio.podspec b/FluidAudio.podspec index c4f33a2b8..c4c89a887 100644 --- a/FluidAudio.podspec +++ b/FluidAudio.podspec @@ -22,7 +22,8 @@ Pod::Spec.new do |spec| spec.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', - # SwiftPM trait flags (#990); CocoaPods always builds every subsystem. + # SwiftPM trait flags (#990). CocoaPods has no trait support, so the pod always + # builds every subsystem; the LuxTTS G2P resources ship as a resource bundle below. 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => '$(inherited) TTS Diarizer', 'ARCHS[sdk=macosx*]' => 'arm64', 'EXCLUDED_ARCHS[sdk=macosx*]' => 'x86_64', @@ -59,11 +60,14 @@ Pod::Spec.new do |spec| spec.subspec "Core" do |core| core.dependency "#{spec.name}/FastClusterWrapper" core.dependency "#{spec.name}/MachTaskSelfWrapper" - core.source_files = "Sources/FluidAudio/**/*.swift" + core.source_files = "Sources/FluidAudio/**/*.swift", "Sources/LuxTtsG2pResources/**/*.swift" + # SwiftPM serves these via a separate `LuxTtsG2pResources` target (trait-pruned); + # `LuxTtsG2pResources.bundle` falls back to this bundle when `Bundle.module` is absent. + core.resource_bundles = { + "LuxTtsG2pResources" => ["Sources/LuxTtsG2pResources/Resources/*"] + } # iOS Configuration - # TTS sources are moved under `Sources/FluidAudioTTS` and are not part of the Core subspec. - # iOS builds include ASR, Diarization, and VAD. core.ios.frameworks = "CoreML", "AVFoundation", "Accelerate", "UIKit" # macOS Configuration diff --git a/Sources/FluidAudio/Diarizer/Core/DiarizerError.swift b/Sources/FluidAudio/Diarizer/Core/DiarizerError.swift index c47b9394f..c50bddf55 100644 --- a/Sources/FluidAudio/Diarizer/Core/DiarizerError.swift +++ b/Sources/FluidAudio/Diarizer/Core/DiarizerError.swift @@ -1,6 +1,6 @@ +#if Diarizer import Foundation -// Outside the `Diarizer` trait guard: Shared/ANEMemoryOptimizer throws it. public enum DiarizerError: Error, LocalizedError { case notInitialized case modelDownloadFailed @@ -32,3 +32,4 @@ public enum DiarizerError: Error, LocalizedError { } } } +#endif diff --git a/Sources/FluidAudio/FluidAudioSwift.swift b/Sources/FluidAudio/FluidAudioSwift.swift index abe146029..57e0baaf0 100644 --- a/Sources/FluidAudio/FluidAudioSwift.swift +++ b/Sources/FluidAudio/FluidAudioSwift.swift @@ -8,10 +8,10 @@ import OSLog // MARK: - Backward Compatibility +#if Diarizer /// Backward compatibility alias for the old error type public typealias SpeakerDiarizationError = DiarizerError -#if Diarizer /// Backward compatibility alias for the old config name public typealias SpeakerDiarizationConfig = DiarizerConfig #endif diff --git a/Sources/FluidAudio/ModelNames.swift b/Sources/FluidAudio/ModelNames.swift index 522d8ec03..2b071bc39 100644 --- a/Sources/FluidAudio/ModelNames.swift +++ b/Sources/FluidAudio/ModelNames.swift @@ -1197,7 +1197,6 @@ public enum ModelNames { /// per language (#793). public static let encoderRecoverPinvFile = "encoder_recover_pinv.bin" - #if TTS /// FlowLM filename for a given precision. Both variants ship in the /// same `v2//` directory upstream; only the FlowLM transformer /// has an int8 variant — `cond_step`, `flow_decoder`, and @@ -1249,7 +1248,6 @@ public enum ModelNames { ] } } - #endif /// Required files for the default precision. Kept for callers that /// haven't been updated to pass a precision argument. @@ -1724,7 +1722,6 @@ public enum ModelNames { flowFile, vocoderFile, ] - #if TTS /// Required model set for an output capacity ("extended" swaps in /// the N1000/T2000 S3Gen pair). public static func requiredModels(capacity: ChatterboxNanoOutputCapacity) -> Set { @@ -1735,7 +1732,6 @@ public enum ModelNames { return [prefillFile, decodeFile, flowFileExtended, vocoderFileExtended] } } - #endif /// Non-model assets fetched individually (nested under `tables/` and /// `tokenizer/`, which the repo-root model walk does not descend into). public static let auxFiles: [String] = [ @@ -1865,12 +1861,8 @@ public enum ModelNames { return ModelNames.Chatterbox.requiredModels case .chatterboxNano: // Variant: "extended" → N1000/T2000 S3Gen bucket pair (~30 s). - #if TTS let capacity = ChatterboxNanoOutputCapacity(rawValue: variant ?? "") ?? .standard return ModelNames.ChatterboxNano.requiredModels(capacity: capacity) - #else - return ModelNames.ChatterboxNano.requiredModels - #endif case .luxtts: // Variants: "gpu" (macOS) / "ane" (iOS); nil → platform default. return ModelNames.LuxTts.requiredFiles(variant: variant) diff --git a/Sources/FluidAudio/Shared/ANEMemoryOptimizer.swift b/Sources/FluidAudio/Shared/ANEMemoryOptimizer.swift index 4541e473e..b81ccafc2 100644 --- a/Sources/FluidAudio/Shared/ANEMemoryOptimizer.swift +++ b/Sources/FluidAudio/Shared/ANEMemoryOptimizer.swift @@ -3,7 +3,7 @@ import CoreML import Foundation import Metal -/// ANE-optimized memory management for speaker diarization pipeline +/// ANE-optimized memory management shared by the diarization, ASR and VAD pipelines. public final class ANEMemoryOptimizer { // Use shared ANE constants public static let aneAlignment = ANEMemoryUtils.aneAlignment @@ -25,10 +25,10 @@ public final class ANEMemoryOptimizer { dataType: dataType, zeroClear: true ) - } catch ANEMemoryUtils.ANEMemoryError.allocationFailed { - throw DiarizerError.memoryAllocationFailed + } catch let error as ANEMemoryUtils.ANEMemoryError { + throw error } catch { - throw DiarizerError.memoryAllocationFailed + throw ANEMemoryUtils.ANEMemoryError.allocationFailed } } @@ -96,7 +96,7 @@ public final class ANEMemoryOptimizer { sourceArray.shape.isEmpty ? 0 : sourceArray.strides[0].intValue * sourceArray.shape[0].intValue guard offset + viewSpan <= sourceSpan else { - throw DiarizerError.invalidArrayBounds + throw ANEMemoryUtils.ANEMemoryError.invalidArrayBounds } // Calculate byte offset diff --git a/Sources/FluidAudio/Shared/ANEMemoryUtils.swift b/Sources/FluidAudio/Shared/ANEMemoryUtils.swift index 6e6ff8aa2..c7c4cf75f 100644 --- a/Sources/FluidAudio/Shared/ANEMemoryUtils.swift +++ b/Sources/FluidAudio/Shared/ANEMemoryUtils.swift @@ -13,10 +13,24 @@ public enum ANEMemoryUtils { public static let aneTileSize = 16 /// Errors that can occur during ANE memory operations - public enum ANEMemoryError: Error { + public enum ANEMemoryError: Error, LocalizedError { case allocationFailed case invalidShape case unsupportedDataType + case invalidArrayBounds + + public var errorDescription: String? { + switch self { + case .allocationFailed: + return "Failed to allocate ANE-aligned memory." + case .invalidShape: + return "Invalid shape for ANE-aligned array." + case .unsupportedDataType: + return "Unsupported MLMultiArray data type for ANE memory operation." + case .invalidArrayBounds: + return "Array bounds exceeded for zero-copy view." + } + } } /// Create ANE-aligned MLMultiArray with optimized memory layout diff --git a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoConstants.swift b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoConstants.swift index 4b3197b07..331612f96 100644 --- a/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoConstants.swift +++ b/Sources/FluidAudio/TTS/Chatterbox/Nano/ChatterboxNanoConstants.swift @@ -68,7 +68,9 @@ public enum ChatterboxNanoConstants { public static let defaultVoice = "default" } +#endif +// Outside the `TTS` guard: `ModelNames.ChatterboxNano` keys required-file sets on this. /// Which S3Gen flow/vocoder bucket pair to download and load. The flow /// bucket holds `voice prompt tokens + generated speech tokens + 3 silence /// tokens`, so the audio each capacity can generate depends on the voice: @@ -97,7 +99,10 @@ public enum ChatterboxNanoOutputCapacity: String, CaseIterable, Sendable { /// Mel frames produced by the flow bucket (2 per token) = HiFT bucket. public var melFrameBucket: Int { 2 * flowTokenBucket } +} +#if TTS +extension ChatterboxNanoOutputCapacity { /// Speech tokens available for generation once `promptTokens` (the /// loaded voice's prompt) and the appended silence tokens are counted. public func generationBudget(promptTokens: Int) -> Int { diff --git a/Sources/FluidAudio/TTS/LuxTts/G2p/LuxTtsG2p.swift b/Sources/FluidAudio/TTS/LuxTts/G2p/LuxTtsG2p.swift index b711dd240..0c8fb605b 100644 --- a/Sources/FluidAudio/TTS/LuxTts/G2p/LuxTtsG2p.swift +++ b/Sources/FluidAudio/TTS/LuxTts/G2p/LuxTtsG2p.swift @@ -1,6 +1,8 @@ #if TTS import Foundation +#if canImport(LuxTtsG2pResources) import LuxTtsG2pResources +#endif /// English text → espeak-IPA phonemes for LuxTTS (phase 2). /// diff --git a/Sources/FluidAudio/TTS/PocketTTS/PocketTtsConstants.swift b/Sources/FluidAudio/TTS/PocketTTS/PocketTtsConstants.swift index eb24046d6..a67436f5d 100644 --- a/Sources/FluidAudio/TTS/PocketTTS/PocketTtsConstants.swift +++ b/Sources/FluidAudio/TTS/PocketTTS/PocketTtsConstants.swift @@ -105,7 +105,9 @@ public enum PocketTtsLanguage: String, Sendable, CaseIterable { "v2.1/\(rawValue)" } } +#endif +// Outside the `TTS` guard: `ModelNames.PocketTts` keys required-file sets on these. /// Precision variant of the PocketTTS CoreML models. /// /// Both variants live in the upstream `FluidInference/pocket-tts-coreml` repo @@ -193,4 +195,3 @@ public enum PocketTtsModelPlacement: String, Sendable, Hashable { /// Trial 23); mimi decode is unchanged. case aneState = "ane-state" } -#endif diff --git a/Sources/LuxTtsG2pResources/LuxTtsG2pResources.swift b/Sources/LuxTtsG2pResources/LuxTtsG2pResources.swift index c536d1a44..bcaf9a1a4 100644 --- a/Sources/LuxTtsG2pResources/LuxTtsG2pResources.swift +++ b/Sources/LuxTtsG2pResources/LuxTtsG2pResources.swift @@ -1,7 +1,25 @@ import Foundation -/// Bundled LuxTTS English G2P lexicon + aux tables. A separate target so the -/// resource bundle is only linked when the `TTS` trait is enabled (#990). +/// Bundled LuxTTS English G2P lexicon + aux tables. A separate SwiftPM target so +/// the resource bundle is only linked when the `TTS` trait is enabled (#990). +/// CocoaPods compiles this file into the FluidAudio pod and ships the same files +/// as the `LuxTtsG2pResources` resource bundle. public enum LuxTtsG2pResources { + #if SWIFT_PACKAGE public static var bundle: Bundle { .module } + #else + private final class BundleToken {} + + public static var bundle: Bundle { + let host = Bundle(for: BundleToken.self) + for candidate in [host, Bundle.main] { + if let url = candidate.url(forResource: "LuxTtsG2pResources", withExtension: "bundle"), + let bundle = Bundle(url: url) + { + return bundle + } + } + return host + } + #endif } diff --git a/Tests/FluidAudioTests/Shared/ANEMemoryOptimizerTests.swift b/Tests/FluidAudioTests/Shared/ANEMemoryOptimizerTests.swift index af6d5f2da..e23d6845d 100644 --- a/Tests/FluidAudioTests/Shared/ANEMemoryOptimizerTests.swift +++ b/Tests/FluidAudioTests/Shared/ANEMemoryOptimizerTests.swift @@ -200,7 +200,22 @@ final class ANEMemoryOptimizerTests: XCTestCase { // 100-element source cannot back it even though it holds 100 logical elements. let source = try optimizer.createAlignedArray(shape: [100], dataType: .float32) - XCTAssertThrowsError(try optimizer.createZeroCopyView(from: source, shape: [10, 10])) + XCTAssertThrowsError(try optimizer.createZeroCopyView(from: source, shape: [10, 10])) { error in + // Shared code must not surface diarizer-specific errors (#990). + guard case ANEMemoryUtils.ANEMemoryError.invalidArrayBounds = error else { + return XCTFail("expected ANEMemoryError.invalidArrayBounds, got \(error)") + } + XCTAssertNotNil(error.localizedDescription) + } + } + + func testANEMemoryErrorsHaveDescriptions() { + let errors: [ANEMemoryUtils.ANEMemoryError] = [ + .allocationFailed, .invalidShape, .unsupportedDataType, .invalidArrayBounds, + ] + for error in errors { + XCTAssertFalse(error.errorDescription?.isEmpty ?? true, "\(error) has no description") + } } func testZeroCopyViewAcceptsSpanWithinSource() throws { diff --git a/Tests/FluidAudioTests/TTS/Inflect/InflectWiringTests.swift b/Tests/FluidAudioTests/TTS/Inflect/InflectWiringTests.swift index 70502cd97..15184e76f 100644 --- a/Tests/FluidAudioTests/TTS/Inflect/InflectWiringTests.swift +++ b/Tests/FluidAudioTests/TTS/Inflect/InflectWiringTests.swift @@ -35,6 +35,15 @@ final class InflectWiringTests: XCTestCase { XCTAssertEqual(InflectConstants.maxFrames, 2048) } + /// `ModelNames.Inflect.frameBuckets` is the download-side source of truth + /// (it lives outside the `TTS` guard, #990); the runtime constants must + /// keep reading the same list so every downloaded bucket is loadable. + func testRuntimeFrameBucketsMatchDownloadedBuckets() { + XCTAssertEqual(InflectConstants.frameBuckets, ModelNames.Inflect.frameBuckets) + XCTAssertEqual(InflectConstants.frameBuckets, [256, 384, 512, 640, 768, 896, 1024, 2048]) + XCTAssertEqual(ModelNames.Inflect.requiredModels.count, ModelNames.Inflect.frameBuckets.count + 1) + } + func testInterChannelsPerVariant() { XCTAssertEqual(InflectConstants.interChannels(for: .micro), 192) XCTAssertEqual(InflectConstants.interChannels(for: .nano), 128) diff --git a/Tests/FluidAudioTests/TTS/LuxTts/LuxTtsG2pResourcesTests.swift b/Tests/FluidAudioTests/TTS/LuxTts/LuxTtsG2pResourcesTests.swift new file mode 100644 index 000000000..dddc8f062 --- /dev/null +++ b/Tests/FluidAudioTests/TTS/LuxTts/LuxTtsG2pResourcesTests.swift @@ -0,0 +1,40 @@ +import LuxTtsG2pResources +import XCTest + +@testable import FluidAudio + +/// The LuxTTS G2P tables live in their own SwiftPM target so the `TTS` trait +/// can prune the bundle (#990). Pin that the bundle still carries both files +/// at its root, which is what `LuxTtsG2p.init` looks up. +final class LuxTtsG2pResourcesTests: XCTestCase { + + func testBundleContainsLexiconAndAuxTablesAtRoot() throws { + let bundle = LuxTtsG2pResources.bundle + + let lexicon = try XCTUnwrap( + bundle.url(forResource: "luxtts_en_us_lexicon.tsv", withExtension: "zz"), + "compressed lexicon missing from LuxTtsG2pResources bundle") + let aux = try XCTUnwrap( + bundle.url(forResource: "luxtts_en_us_g2p_aux", withExtension: "json"), + "G2P aux tables missing from LuxTtsG2pResources bundle") + + // `resourceURL` is the bundle root on iOS and `Contents/Resources` on macOS. + let root = try XCTUnwrap(bundle.resourceURL).standardizedFileURL.path + XCTAssertEqual(lexicon.deletingLastPathComponent().standardizedFileURL.path, root) + XCTAssertEqual(aux.deletingLastPathComponent().standardizedFileURL.path, root) + XCTAssertFalse( + FileManager.default.fileExists( + atPath: root + "/Resources"), + "`.process` must flatten the Resources directory (a top-level Resources dir breaks iOS code signing)") + } + + func testBundledFilesAreNonEmpty() throws { + let bundle = LuxTtsG2pResources.bundle + for (name, ext) in [("luxtts_en_us_lexicon.tsv", "zz"), ("luxtts_en_us_g2p_aux", "json")] { + let url = try XCTUnwrap(bundle.url(forResource: name, withExtension: ext)) + let size = try XCTUnwrap( + FileManager.default.attributesOfItem(atPath: url.path)[.size] as? NSNumber) + XCTAssertGreaterThan(size.intValue, 0, "\(name).\(ext) is empty") + } + } +} From 2ac4f7d6b992b521bebe4589a8c9be68b680264d Mon Sep 17 00:00:00 2001 From: Alex-Wengg Date: Wed, 14 Oct 2026 01:55:11 -0400 Subject: [PATCH 3/3] test(luxtts): resolve fixture root via resourceURL Xcode 27 `swift test` builds a macOS-style test bundle whose resources sit under Contents/Resources, so the bundleURL-relative lookup failed locally while flat bundles in CI passed. resourceURL is the root in both layouts. --- Tests/FluidAudioTests/TTS/LuxTts/LuxTtsG2pTests.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Tests/FluidAudioTests/TTS/LuxTts/LuxTtsG2pTests.swift b/Tests/FluidAudioTests/TTS/LuxTts/LuxTtsG2pTests.swift index a4f0782ae..3559e07ae 100644 --- a/Tests/FluidAudioTests/TTS/LuxTts/LuxTtsG2pTests.swift +++ b/Tests/FluidAudioTests/TTS/LuxTts/LuxTtsG2pTests.swift @@ -12,17 +12,19 @@ import XCTest /// the mobius worktree; these tests pin representative behaviors. final class LuxTtsG2pTests: XCTestCase { - func testFixtureResourcesAreProcessedAtBundleRoot() { + func testFixtureResourcesAreProcessedAtBundleRoot() throws { + // `resourceURL` is the bundle root for flat bundles and `Contents/Resources` + // for macOS-style bundles (Xcode 27 `swift test` builds the latter). + let root = try XCTUnwrap(Bundle.module.resourceURL) XCTAssertFalse( FileManager.default.fileExists( - atPath: Bundle.module.bundleURL - .appendingPathComponent("Resources", isDirectory: true).path + atPath: root.appendingPathComponent("Resources", isDirectory: true).path ), "LuxTTS test resources must not create a reserved top-level Resources directory" ) XCTAssertTrue( FileManager.default.fileExists( - atPath: Bundle.module.bundleURL.appendingPathComponent("luxtts_fixtures.json").path + atPath: root.appendingPathComponent("luxtts_fixtures.json").path ) ) }