chore: adiciona .gitignore e commit.command
This commit is contained in:
+214
@@ -0,0 +1,214 @@
|
||||
import Foundation
|
||||
import HexCore
|
||||
|
||||
#if canImport(FluidAudio)
|
||||
import FluidAudio
|
||||
|
||||
actor ParakeetClient {
|
||||
private var asr: AsrManager?
|
||||
private var models: AsrModels?
|
||||
private var currentVariant: ParakeetModel?
|
||||
private let logger = HexLog.parakeet
|
||||
private let vendorDirs = [
|
||||
// Our app-specific cache path convention (under XDG or com.kitlangton.Hex/cache)
|
||||
"fluidaudio/Models",
|
||||
"FluidAudio/Models"
|
||||
]
|
||||
|
||||
func isModelAvailable(_ modelName: String) async -> Bool {
|
||||
guard let variant = ParakeetModel(rawValue: modelName) else {
|
||||
logger.error("Unknown Parakeet variant requested: \(modelName)")
|
||||
return false
|
||||
}
|
||||
if currentVariant == variant, asr != nil { return true }
|
||||
|
||||
let directory = AsrModels.defaultCacheDirectory(for: variant.asrVersion)
|
||||
migrateLegacyCacheIfNeeded(variant, to: directory)
|
||||
let available = AsrModels.modelsExist(
|
||||
at: directory,
|
||||
version: variant.asrVersion
|
||||
)
|
||||
if available {
|
||||
logger.notice("Found Parakeet cache at \(directory.path)")
|
||||
} else {
|
||||
logger.debug("No Parakeet cache detected variant=\(variant.identifier) path=\(directory.path)")
|
||||
}
|
||||
return available
|
||||
}
|
||||
|
||||
func ensureLoaded(modelName: String, progress: @escaping (Progress) -> Void) async throws {
|
||||
guard let variant = ParakeetModel(rawValue: modelName) else {
|
||||
throw NSError(
|
||||
domain: "Parakeet",
|
||||
code: -4,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Unsupported Parakeet variant: \(modelName)"]
|
||||
)
|
||||
}
|
||||
if currentVariant == variant, asr != nil { return }
|
||||
if currentVariant != variant {
|
||||
asr = nil
|
||||
models = nil
|
||||
}
|
||||
migrateLegacyCacheIfNeeded(
|
||||
variant,
|
||||
to: AsrModels.defaultCacheDirectory(for: variant.asrVersion)
|
||||
)
|
||||
let t0 = Date()
|
||||
logger.notice("Starting Parakeet load variant=\(variant.identifier)")
|
||||
let p = Progress(totalUnitCount: 100)
|
||||
p.completedUnitCount = 1
|
||||
progress(p)
|
||||
|
||||
// Best-effort progress polling while FluidAudio downloads
|
||||
let fm = FileManager.default
|
||||
let support = try? fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
let faDir = support?.appendingPathComponent("FluidAudio/Models/\(variant.identifier)", isDirectory: true)
|
||||
let pollTask = Task {
|
||||
while p.completedUnitCount < 95 {
|
||||
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||
if let dir = faDir, let size = directorySize(dir) {
|
||||
let target: Double = 650 * 1024 * 1024 // ~650MB
|
||||
let frac = max(0.0, min(1.0, Double(size) / target))
|
||||
p.completedUnitCount = Int64(5 + frac * 90)
|
||||
progress(p)
|
||||
}
|
||||
if Task.isCancelled { break }
|
||||
}
|
||||
}
|
||||
defer { pollTask.cancel() }
|
||||
|
||||
// Download + load the requested variant (returns when all assets are present)
|
||||
let models = try await AsrModels.downloadAndLoad(version: variant.asrVersion)
|
||||
self.models = models
|
||||
let manager = AsrManager(config: .init(), models: models)
|
||||
self.asr = manager
|
||||
self.currentVariant = variant
|
||||
p.completedUnitCount = 100
|
||||
progress(p)
|
||||
logger.notice("Parakeet ensureLoaded completed in \(String(format: "%.2f", Date().timeIntervalSince(t0)))s")
|
||||
}
|
||||
|
||||
private func directorySize(_ dir: URL) -> UInt64? {
|
||||
let fm = FileManager.default
|
||||
guard let en = fm.enumerator(at: dir, includingPropertiesForKeys: [.isRegularFileKey, .fileSizeKey], options: .skipsHiddenFiles) else { return nil }
|
||||
var total: UInt64 = 0
|
||||
for case let url as URL in en {
|
||||
if let vals = try? url.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey]), vals.isRegularFile == true {
|
||||
total &+= UInt64(vals.fileSize ?? 0)
|
||||
}
|
||||
}
|
||||
return total
|
||||
}
|
||||
|
||||
private func migrateLegacyCacheIfNeeded(_ variant: ParakeetModel, to directory: URL) {
|
||||
let legacyDirectory = directory
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent(variant.identifier, isDirectory: true)
|
||||
|
||||
do {
|
||||
if try LegacyModelCacheMigrator.migrate(
|
||||
from: legacyDirectory,
|
||||
to: directory,
|
||||
isValid: {
|
||||
AsrModels.modelsExist(at: $0, version: variant.asrVersion)
|
||||
}
|
||||
) {
|
||||
logger.notice("Migrated legacy Parakeet cache from \(legacyDirectory.path) to \(directory.path)")
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to migrate legacy Parakeet cache: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func transcribe(_ url: URL) async throws -> String {
|
||||
guard let asr else { throw NSError(domain: "Parakeet", code: -1, userInfo: [NSLocalizedDescriptionKey: "Parakeet not initialized"]) }
|
||||
let t0 = Date()
|
||||
logger.notice("Transcribing with Parakeet file=\(url.lastPathComponent)")
|
||||
var decoderState = TdtDecoderState.make(decoderLayers: await asr.decoderLayerCount)
|
||||
let result = try await asr.transcribe(url, decoderState: &decoderState)
|
||||
logger.info("Parakeet transcription finished in \(String(format: "%.2f", Date().timeIntervalSince(t0)))s")
|
||||
return result.text
|
||||
}
|
||||
|
||||
// Delete cached Parakeet models from known locations and reset state
|
||||
func deleteCaches(modelName: String) async throws {
|
||||
guard let variant = ParakeetModel(rawValue: modelName) else { return }
|
||||
let fm = FileManager.default
|
||||
|
||||
var removedAny = false
|
||||
for dir in modelDirectories(variant) {
|
||||
if fm.fileExists(atPath: dir.path) {
|
||||
try? fm.removeItem(at: dir)
|
||||
removedAny = true
|
||||
}
|
||||
}
|
||||
|
||||
// Reset live objects so a future download can proceed cleanly
|
||||
if removedAny {
|
||||
self.asr = nil
|
||||
self.models = nil
|
||||
if currentVariant == variant {
|
||||
currentVariant = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all candidate directories where a Parakeet model might be cached.
|
||||
/// Includes both exact matches and prefixed directories (e.g. versioned folders).
|
||||
private func modelDirectories(_ variant: ParakeetModel) -> [URL] {
|
||||
let fm = FileManager.default
|
||||
var result: [URL] = [AsrModels.defaultCacheDirectory(for: variant.asrVersion)]
|
||||
|
||||
for root in candidateRoots() {
|
||||
for vendor in vendorDirs {
|
||||
let base = root.appendingPathComponent(vendor, isDirectory: true)
|
||||
// Exact match directory
|
||||
let direct = base.appendingPathComponent(variant.identifier, isDirectory: true)
|
||||
result.append(direct)
|
||||
// Prefixed directories (e.g. versioned folders)
|
||||
if let items = try? fm.contentsOfDirectory(at: base, includingPropertiesForKeys: [.isDirectoryKey], options: .skipsHiddenFiles) {
|
||||
for item in items where item.lastPathComponent.hasPrefix(variant.identifier) && item != direct {
|
||||
result.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var seen = Set<String>()
|
||||
return result.filter { seen.insert($0.standardizedFileURL.path).inserted }
|
||||
}
|
||||
|
||||
private func candidateRoots() -> [URL] {
|
||||
let fm = FileManager.default
|
||||
let xdg = ProcessInfo.processInfo.environment["XDG_CACHE_HOME"].flatMap { URL(fileURLWithPath: $0, isDirectory: true) }
|
||||
let appSupport = try? fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
||||
let appCache = try? URL.hexApplicationSupport.appendingPathComponent("cache", isDirectory: true)
|
||||
let userCache = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".cache", isDirectory: true)
|
||||
return [xdg, appCache, appSupport, userCache].compactMap { $0 }
|
||||
}
|
||||
}
|
||||
|
||||
private extension ParakeetModel {
|
||||
var asrVersion: AsrModelVersion {
|
||||
switch self {
|
||||
case .englishV2: return .v2
|
||||
case .multilingualV3: return .v3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
actor ParakeetClient {
|
||||
func isModelAvailable(_ modelName: String) async -> Bool { false }
|
||||
func ensureLoaded(modelName: String, progress: @escaping (Progress) -> Void) async throws {
|
||||
throw NSError(
|
||||
domain: "Parakeet",
|
||||
code: -2,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Parakeet support not linked. Add Swift Package: https://github.com/FluidInference/FluidAudio.git and link FluidAudio to Hex."]
|
||||
)
|
||||
}
|
||||
func transcribe(_ url: URL) async throws -> String { throw NSError(domain: "Parakeet", code: -3, userInfo: [NSLocalizedDescriptionKey: "Parakeet not available"]) }
|
||||
func deleteCaches(modelName: String) async throws {}
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user