Add voice notes: on-device transcription, multi-language, watch capture
- 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
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user