- Schema v2: Note.AudioInfo, .m4a sidecar next to the note JSON in iCloud - AudioRecorderService (iOS/macOS) + SpeechAnalyzer transcription pipeline with enabled-language set and confidence-based auto-pick, re-transcribe menu - Settings > Transcription > Languages with asset download/reserve handling - watchOS app + accessory complication: one-button recorder, WCSession transferFile to phone, WatchInbox staging, direct-upsert ingestion - Player UI, transcription status chips, quick-capture mic in notes list - Version 0.2, changelog + README updated
120 lines
4.1 KiB
Swift
120 lines
4.1 KiB
Swift
import SwiftUI
|
|
|
|
/// Settings screen for choosing which languages voice notes can be transcribed
|
|
/// in. Enabling a language reserves it and downloads its on-device model; the
|
|
/// system caps how many can be reserved at once, so the list disables further
|
|
/// toggles at capacity and explains the limit in the footer.
|
|
struct TranscriptionLanguagesView: View {
|
|
@Environment(TranscriptionSettings.self) private var settings
|
|
@State private var errorMessage: String?
|
|
|
|
var body: some View {
|
|
List {
|
|
if let errorMessage {
|
|
Section {
|
|
Label(errorMessage, systemImage: "exclamationmark.triangle")
|
|
.font(.footnote)
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
|
|
if !enabledLocales.isEmpty {
|
|
Section("Enabled") {
|
|
ForEach(enabledLocales, id: \.identifier) { row($0) }
|
|
}
|
|
}
|
|
|
|
Section {
|
|
ForEach(otherLocales, id: \.identifier) { row($0) }
|
|
} header: {
|
|
Text("All Languages")
|
|
} footer: {
|
|
Text(footerText)
|
|
}
|
|
}
|
|
.navigationTitle("Languages")
|
|
.overlay {
|
|
if settings.supportedLocales.isEmpty {
|
|
ProgressView()
|
|
}
|
|
}
|
|
.task { await settings.prepare() }
|
|
}
|
|
|
|
private var enabledLocales: [Locale] {
|
|
settings.supportedLocales.filter { settings.isEnabled($0) }
|
|
}
|
|
|
|
private var otherLocales: [Locale] {
|
|
settings.supportedLocales.filter { !settings.isEnabled($0) }
|
|
}
|
|
|
|
private var footerText: String {
|
|
guard settings.maximumReserved > 0 else {
|
|
return "Each new note is transcribed on device in your enabled languages."
|
|
}
|
|
return "Enable up to \(settings.maximumReserved) languages. Each new voice note is sampled in your enabled languages and transcribed in whichever fits best."
|
|
}
|
|
|
|
private func row(_ locale: Locale) -> some View {
|
|
let enabled = settings.isEnabled(locale)
|
|
return HStack {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(TranscriptionSettings.displayName(for: locale))
|
|
stateSubtitle(for: locale)
|
|
}
|
|
Spacer()
|
|
Toggle("", isOn: Binding(
|
|
get: { settings.isEnabled(locale) },
|
|
set: { toggle(locale, on: $0) }
|
|
))
|
|
.labelsHidden()
|
|
.disabled(!enabled && settings.isAtCapacity)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func stateSubtitle(for locale: Locale) -> some View {
|
|
switch settings.assetStates[TranscriptionSettings.canonicalID(locale)] {
|
|
case .installed:
|
|
Label("Installed", systemImage: "checkmark.circle.fill")
|
|
.font(.caption)
|
|
.foregroundStyle(.green)
|
|
case .downloading(let fraction):
|
|
HStack(spacing: 6) {
|
|
ProgressView().controlSize(.small)
|
|
if let fraction {
|
|
Text("Downloading \(Int(fraction * 100))%")
|
|
} else {
|
|
Text("Downloading…")
|
|
}
|
|
}
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
case .notInstalled:
|
|
Label("Available to download", systemImage: "icloud.and.arrow.down")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
case .unsupported, .none:
|
|
EmptyView()
|
|
}
|
|
}
|
|
|
|
private func toggle(_ locale: Locale, on: Bool) {
|
|
errorMessage = nil
|
|
Task {
|
|
if on {
|
|
do {
|
|
try await settings.enable(locale)
|
|
} catch TranscriptionSettings.SettingsError.atCapacity {
|
|
errorMessage = "You can enable up to \(settings.maximumReserved) languages. Turn one off first."
|
|
} catch {
|
|
errorMessage = "Couldn't enable \(TranscriptionSettings.displayName(for: locale)): \(error.localizedDescription)"
|
|
}
|
|
} else {
|
|
await settings.disable(locale)
|
|
}
|
|
}
|
|
}
|
|
}
|