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,84 @@
|
||||
import SwiftUI
|
||||
|
||||
/// The watch app's single screen: a big record/stop button, an elapsed timer
|
||||
/// while recording, and a footer counting recordings still waiting to reach the
|
||||
/// iPhone. Shows a permission-denied state when the mic is refused.
|
||||
struct RecordView: View {
|
||||
@Environment(WatchAppServices.self) private var services
|
||||
|
||||
var body: some View {
|
||||
let recorder = services.recorder
|
||||
VStack(spacing: 10) {
|
||||
if recorder.permissionDenied {
|
||||
permissionDenied
|
||||
} else {
|
||||
recordButton(isRecording: recorder.isRecording)
|
||||
if recorder.isRecording {
|
||||
Text(Self.elapsedString(recorder.elapsed))
|
||||
.font(.system(.title3, design: .rounded).monospacedDigit())
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text("Tap to record")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
pendingFooter
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.containerBackground(.red.gradient, for: .navigation)
|
||||
.navigationTitle("Contextful")
|
||||
}
|
||||
|
||||
private func recordButton(isRecording: Bool) -> some View {
|
||||
Button {
|
||||
services.toggleRecording()
|
||||
} label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(.red)
|
||||
.frame(width: 96, height: 96)
|
||||
Image(systemName: isRecording ? "stop.fill" : "mic.fill")
|
||||
.font(.system(size: 40))
|
||||
.foregroundStyle(.white)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(isRecording ? "Stop recording" : "Start recording")
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var pendingFooter: some View {
|
||||
let count = services.transfer.pendingCount
|
||||
if count > 0 {
|
||||
Label(
|
||||
count == 1 ? "1 waiting for iPhone" : "\(count) waiting for iPhone",
|
||||
systemImage: "arrow.up.circle"
|
||||
)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
private var permissionDenied: some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: "mic.slash.fill")
|
||||
.font(.title)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Microphone access is off")
|
||||
.font(.headline)
|
||||
.multilineTextAlignment(.center)
|
||||
Text("Enable it in the Watch app on iPhone under Contextful.")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
|
||||
/// mm:ss for the elapsed readout.
|
||||
static func elapsedString(_ interval: TimeInterval) -> String {
|
||||
let total = Int(interval)
|
||||
return String(format: "%02d:%02d", total / 60, total % 60)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user