Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/screenshots/orukeet-active.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/orukeet-download.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/orukeet-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/screenshots/orukeet-transcription.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions Fluid.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
A0E090170000000000000002 /* OrukeetModelStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0E090170000000000000001 /* OrukeetModelStoreTests.swift */; };
7C078DA52E3B386A00FB7CAC /* FluidAudio in Frameworks */ = {isa = PBXBuildFile; productRef = 7C078DA42E3B386A00FB7CAC /* FluidAudio */; };
7C3697892ED70F9C005874CE /* DynamicNotchKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7C3697882ED70F9C005874CE /* DynamicNotchKit */; };
7C5AF14B2F15041600DE21B0 /* MediaRemoteAdapter in Frameworks */ = {isa = PBXBuildFile; productRef = 7C5AF14A2F15041600DE21B0 /* MediaRemoteAdapter */; };
Expand Down Expand Up @@ -66,6 +67,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
A0E090170000000000000001 /* OrukeetModelStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrukeetModelStoreTests.swift; sourceTree = "<group>"; };
343B29013F4441D6A797D12D /* LLMClientRequestBodyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMClientRequestBodyTests.swift; sourceTree = "<group>"; };
980330F3CE464336ADCE3E23 /* TemperatureSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureSupportTests.swift; sourceTree = "<group>"; };
A11A00000000000000000001 /* AnalyticsDatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsDatabaseTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -159,6 +161,7 @@
7CDB0A262F3C4D5600FB7CAD /* Helpers */,
7CDB0A272F3C4D5600FB7CAD /* Resources */,
7CDB0A292F3C4D5600FB7CAD /* DictationE2ETests.swift */,
A0E090170000000000000001 /* OrukeetModelStoreTests.swift */,
CD1C7A0000000000000000B1 /* CustomDictionaryManualEntryTests.swift */,
7C91B0022F42AA0100C0DEF0 /* HotkeyShortcutTests.swift */,
C0DE89000000000000000001 /* SettingsNavigationStateTests.swift */,
Expand Down Expand Up @@ -336,6 +339,7 @@
files = (
7CDB0A2E2F3C4D5600FB7CAD /* AudioFixtureLoader.swift in Sources */,
7CDB0A2D2F3C4D5600FB7CAD /* DictationE2ETests.swift in Sources */,
A0E090170000000000000002 /* OrukeetModelStoreTests.swift in Sources */,
B52000000000000000000002 /* PrivateAIDictationTokenBudgetTests.swift in Sources */,
CD1C7A0000000000000000B2 /* CustomDictionaryManualEntryTests.swift in Sources */,
7C91B0012F42AA0100C0DEF0 /* HotkeyShortcutTests.swift in Sources */,
Expand Down
39 changes: 32 additions & 7 deletions Sources/Fluid/Persistence/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4661,6 +4661,7 @@ final class SettingsStore: ObservableObject {
// MARK: - FluidAudio Models (Apple Silicon Only)

case parakeetTDT = "parakeet-tdt"
case orukeet = "orukeet-coreml-preview"
case parakeetTDTv2 = "parakeet-tdt-v2"
case parakeetRealtime = "parakeet-realtime"
case qwen3Asr = "qwen3-asr"
Expand Down Expand Up @@ -4691,6 +4692,7 @@ final class SettingsStore: ObservableObject {

var displayName: String {
switch self {
case .orukeet: return "Orukeet (Core ML Preview)"
case .parakeetTDT: return "Parakeet TDT v3 (Multilingual)"
case .parakeetTDTv2: return "Parakeet TDT v2 (English Only)"
case .parakeetRealtime: return "Parakeet Flash (Beta)"
Expand All @@ -4712,7 +4714,7 @@ final class SettingsStore: ObservableObject {

var languageSupport: String {
switch self {
case .parakeetTDT:
case .parakeetTDT, .orukeet:
return "25 Languages"
case .parakeetTDTv2: return "English Only (Higher Accuracy)"
case .parakeetRealtime: return "English Only (Live Streaming)"
Expand All @@ -4728,6 +4730,7 @@ final class SettingsStore: ObservableObject {

var downloadSize: String {
switch self {
case .orukeet: return "~445 MiB"
case .parakeetTDT: return "~460.9 MiB"
case .parakeetTDTv2: return "~442.9 MiB"
case .parakeetRealtime: return "~428.4 MiB"
Expand All @@ -4749,6 +4752,7 @@ final class SettingsStore: ObservableObject {

var expectedDownloadBytes: Int64 {
switch self {
case .orukeet: return 466_579_851
case .parakeetTDT: return 483_288_717
case .parakeetTDTv2: return 464_421_712
case .parakeetRealtime: return 449_190_189
Expand All @@ -4768,14 +4772,14 @@ final class SettingsStore: ObservableObject {

var requiresAppleSilicon: Bool {
switch self {
case .parakeetTDT, .parakeetTDTv2, .parakeetRealtime, .qwen3Asr, .cohereTranscribeSixBit, .nemotronOffline, .nemotronStreaming, .nemotronStreaming320: return true
case .orukeet, .parakeetTDT, .parakeetTDTv2, .parakeetRealtime, .qwen3Asr, .cohereTranscribeSixBit, .nemotronOffline, .nemotronStreaming, .nemotronStreaming320: return true
default: return false
}
}

var isWhisperModel: Bool {
switch self {
case .parakeetTDT, .parakeetTDTv2, .parakeetRealtime, .qwen3Asr, .cohereTranscribeSixBit, .nemotronOffline, .nemotronStreaming, .nemotronStreaming320, .appleSpeech, .appleSpeechAnalyzer: return false
case .orukeet, .parakeetTDT, .parakeetTDTv2, .parakeetRealtime, .qwen3Asr, .cohereTranscribeSixBit, .nemotronOffline, .nemotronStreaming, .nemotronStreaming320, .appleSpeech, .appleSpeechAnalyzer: return false
default: return true
}
}
Expand Down Expand Up @@ -4890,6 +4894,7 @@ final class SettingsStore: ObservableObject {
/// Human-readable marketing name for the card UI
var humanReadableName: String {
switch self {
case .orukeet: return "Orukeet - Multilingual"
case .parakeetTDT: return "Blazing Fast - Multilingual"
case .parakeetTDTv2: return "Blazing Fast - English"
case .parakeetRealtime: return "Flash Dictation"
Expand All @@ -4912,6 +4917,8 @@ final class SettingsStore: ObservableObject {
/// One-line description for the card UI
var cardDescription: String {
switch self {
case .orukeet:
return "Local transcription using Oruk's adaptation of Parakeet TDT v3. Core ML preview with CC BY-SA 4.0 weights."
case .parakeetTDT:
return "Fast multilingual transcription. Supports Bulgarian, Croatian, Czech, Danish, " +
"Dutch, English, Estonian, Finnish, French, German, Greek, Hungarian, Italian, " +
Expand Down Expand Up @@ -4953,7 +4960,7 @@ final class SettingsStore: ObservableObject {
/// Minimum recommended RAM in GB for this model to run safely
var requiredMemoryGB: Double {
switch self {
case .parakeetTDT, .parakeetTDTv2, .parakeetRealtime:
case .orukeet, .parakeetTDT, .parakeetTDTv2, .parakeetRealtime:
return 4.0
case .qwen3Asr:
return 8.0
Expand Down Expand Up @@ -4994,9 +5001,13 @@ final class SettingsStore: ObservableObject {
}
}

/// Preview models stay unrated until they have app-specific measurements.
var hasPerformanceRatings: Bool { self != .orukeet }

/// Speed rating (1-5, higher is faster)
var speedRating: Int {
switch self {
case .orukeet: return 0 // Preview has no app-specific performance rating.
case .parakeetTDT: return 5
case .parakeetTDTv2: return 5
case .parakeetRealtime: return 5
Expand All @@ -5018,6 +5029,7 @@ final class SettingsStore: ObservableObject {
/// Accuracy rating (1-5, higher is more accurate)
var accuracyRating: Int {
switch self {
case .orukeet: return 0 // Preview has no app-specific performance rating.
case .parakeetTDT: return 5
case .parakeetTDTv2: return 5
case .parakeetRealtime: return 4
Expand All @@ -5039,6 +5051,7 @@ final class SettingsStore: ObservableObject {
/// Exact speed percentage (0.0 - 1.0) for the liquid bars
var speedPercent: Double {
switch self {
case .orukeet: return 0
case .parakeetTDT: return 1.0
case .parakeetTDTv2: return 1.0
case .parakeetRealtime: return 1.0
Expand All @@ -5060,6 +5073,7 @@ final class SettingsStore: ObservableObject {
/// Exact accuracy percentage (0.0 - 1.0) for the liquid bars
var accuracyPercent: Double {
switch self {
case .orukeet: return 0
case .parakeetTDT: return 0.92
case .parakeetTDTv2: return 0.96
case .parakeetRealtime: return 0.75
Expand All @@ -5081,6 +5095,7 @@ final class SettingsStore: ObservableObject {
/// Optional badge text for the card (e.g., "FluidVoice Pick")
var badgeText: String? {
switch self {
case .orukeet: return "Preview"
case .parakeetTDT: return "FluidVoice Pick"
case .parakeetTDTv2: return "FluidVoice Pick"
case .parakeetRealtime: return "Beta"
Expand All @@ -5095,7 +5110,7 @@ final class SettingsStore: ObservableObject {
/// Optimization level for Apple Silicon (for display)
var appleSiliconOptimized: Bool {
switch self {
case .parakeetTDT, .parakeetTDTv2, .parakeetRealtime, .qwen3Asr, .cohereTranscribeSixBit, .nemotronOffline, .nemotronStreaming, .nemotronStreaming320, .appleSpeechAnalyzer:
case .orukeet, .parakeetTDT, .parakeetTDTv2, .parakeetRealtime, .qwen3Asr, .cohereTranscribeSixBit, .nemotronOffline, .nemotronStreaming, .nemotronStreaming320, .appleSpeechAnalyzer:
return true
default:
return false
Expand Down Expand Up @@ -5163,11 +5178,13 @@ final class SettingsStore: ObservableObject {
case openai = "OpenAI"
case qwen = "Qwen"
case cohere = "Cohere"
case oruk = "Oruk"
}

/// Which provider this model belongs to
var provider: Provider {
switch self {
case .orukeet: return .oruk
case .parakeetTDT, .parakeetTDTv2, .parakeetRealtime, .nemotronOffline, .nemotronStreaming, .nemotronStreaming320:
return .nvidia
case .appleSpeech, .appleSpeechAnalyzer:
Expand All @@ -5189,6 +5206,12 @@ final class SettingsStore: ObservableObject {
/// Whether this model is built-in or already downloaded on disk
var isInstalled: Bool {
switch self {
case .orukeet:
#if arch(arm64)
return OrukeetModelStore.isInstalled
#else
return false
#endif
case .appleSpeech, .appleSpeechAnalyzer:
return true
case .parakeetTDT:
Expand Down Expand Up @@ -5297,6 +5320,7 @@ final class SettingsStore: ObservableObject {
/// Brand/provider name for the model (NVIDIA, Apple, OpenAI)
var brandName: String {
switch self {
case .orukeet: return "Oruk / NVIDIA"
case .parakeetTDT, .parakeetTDTv2, .parakeetRealtime, .nemotronOffline, .nemotronStreaming, .nemotronStreaming320:
return "NVIDIA"
case .qwen3Asr:
Expand All @@ -5321,6 +5345,7 @@ final class SettingsStore: ObservableObject {
/// Brand color for the provider badge
var brandColorHex: String {
switch self {
case .orukeet: return "#725643"
case .parakeetTDT, .parakeetTDTv2, .parakeetRealtime, .nemotronOffline, .nemotronStreaming, .nemotronStreaming320:
return "#76B900"
case .qwen3Asr:
Expand Down Expand Up @@ -5751,7 +5776,7 @@ extension SettingsStore {
extension SettingsStore.SpeechModel {
var supportedLanguageCodes: String? {
switch self {
case .parakeetTDT:
case .parakeetTDT, .orukeet:
return "BG, HR, CS, DA, NL, EN, ET, FI, FR, DE, EL, HU, IT, LV, LT, MT, PL, PT, RO, SK, SL, ES, SV, RU, UK"
case .parakeetRealtime:
return "EN"
Expand All @@ -5768,7 +5793,7 @@ extension SettingsStore.SpeechModel {

var supportedLanguageNames: String? {
switch self {
case .parakeetTDT:
case .parakeetTDT, .orukeet:
return """
Bulgarian, Croatian, Czech, Danish, Dutch, English, Estonian, Finnish, French, German, Greek, Hungarian, Italian, Latvian, Lithuanian, Maltese, Polish, Portuguese, Romanian, Slovak, Slovenian, Spanish, Swedish, Russian, and Ukrainian
"""
Expand Down
4 changes: 2 additions & 2 deletions Sources/Fluid/Services/ASRService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ final class ASRService: ObservableObject {
}
case .appleSpeech:
return self.getAppleSpeechProvider()
case .parakeetTDT, .parakeetTDTv2:
case .parakeetTDT, .parakeetTDTv2, .orukeet:
return self.getFluidAudioProvider()
case .parakeetRealtime:
return self.getParakeetRealtimeProvider()
Expand Down Expand Up @@ -897,7 +897,7 @@ final class ASRService: ObservableObject {
}
case .appleSpeech:
return AppleSpeechProvider()
case .parakeetTDT, .parakeetTDTv2:
case .parakeetTDT, .parakeetTDTv2, .orukeet:
// Create a new provider configured for the specific model
return FluidAudioProvider(modelOverride: model, configureWordBoosting: false)
case .parakeetRealtime:
Expand Down
26 changes: 17 additions & 9 deletions Sources/Fluid/Services/FluidAudioProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ final class FluidAudioProvider: TranscriptionProvider {
let selectedModel = self.modelOverride ?? SettingsStore.shared.selectedSpeechModel
let asrModelVersion: AsrModelVersion = selectedModel == .parakeetTDTv2 ? .v2 : .v3
let modelVersion = selectedModel == .parakeetTDTv2 ? "v2" : "v3"
self.pronunciationModelKey = "parakeet-\(modelVersion)"
self.pronunciationModelKey = selectedModel == .orukeet ? "orukeet" : "parakeet-\(modelVersion)"
let cacheDirectory = AsrModels.defaultCacheDirectory().deletingLastPathComponent()
let modelCacheDirectory = AsrModels.defaultCacheDirectory(for: asrModelVersion)
DebugLogger.shared.info(
Expand All @@ -68,7 +68,7 @@ final class FluidAudioProvider: TranscriptionProvider {
)
DebugLogger.shared.debug("FluidAudioProvider: target cache directory=\(cacheDirectory.path)", source: "FluidAudioProvider")
try Task.checkCancellation()
if FileManager.default.fileExists(atPath: modelCacheDirectory.path), !self.modelsExistOnDisk() {
if selectedModel != .orukeet, FileManager.default.fileExists(atPath: modelCacheDirectory.path), !self.modelsExistOnDisk() {
DebugLogger.shared.warning(
"FluidAudioProvider: removing incomplete \(modelVersion) cache before download",
source: "FluidAudioProvider"
Expand All @@ -94,10 +94,14 @@ final class FluidAudioProvider: TranscriptionProvider {
// Download and load models
let models: AsrModels
do {
models = try await AsrModels.downloadAndLoad(
version: asrModelVersion,
progressHandler: fluidAudioProgressHandler
)
if selectedModel == .orukeet {
models = try await OrukeetModelStore.prepare(progress: progressRelay)
} else {
models = try await AsrModels.downloadAndLoad(
version: asrModelVersion,
progressHandler: fluidAudioProgressHandler
)
}
} catch {
let nsError = error as NSError
if Task.isCancelled
Expand Down Expand Up @@ -129,7 +133,7 @@ final class FluidAudioProvider: TranscriptionProvider {
// Shares the same underlying MLModel objects (reference types) so memory overhead
// is only the decoder state (~100KB).
let finalManager: AsrManager
if self.configureWordBoosting {
if self.configureWordBoosting && selectedModel != .orukeet {
do {
if let vocabBundle = try await ParakeetVocabularyStore.shared.loadTokenizedVocabularyBundle() {
DebugLogger.shared.debug(
Expand Down Expand Up @@ -697,7 +701,7 @@ final class FluidAudioProvider: TranscriptionProvider {
func modelsExistOnDisk() -> Bool {
let selectedModel = self.modelOverride ?? SettingsStore.shared.selectedSpeechModel
switch selectedModel {
case .parakeetTDT, .parakeetTDTv2:
case .parakeetTDT, .parakeetTDTv2, .orukeet:
return selectedModel.isInstalled
default:
return false
Expand All @@ -714,7 +718,11 @@ final class FluidAudioProvider: TranscriptionProvider {
)

let start = Date()
if selectedModel == .parakeetTDTv2 {
if selectedModel == .orukeet {
if FileManager.default.fileExists(atPath: OrukeetModelStore.directory.path) {
try FileManager.default.removeItem(at: OrukeetModelStore.directory)
}
} else if selectedModel == .parakeetTDTv2 {
// Clear v2 cache only
let v2CacheDir = baseCacheDir.appendingPathComponent("parakeet-tdt-0.6b-v2-coreml")
if FileManager.default.fileExists(atPath: v2CacheDir.path) {
Expand Down
Loading
Loading