feat: macOS architecture, cloud ASR/LLM providers, and 6-step iOS onboarding
- Add macOS menu-bar dictation app with local ASR models (SenseVoice/Qwen3),
global Option hotkey, and bottom overlay
- Add cloud ASR/LLM providers (Anthropic, Volcengine, Bailian, and more) with
provider logos, model listing, and connection checks
- Add shared 7-day usage stats UI (UsageStatsCluster / SevenDayUsageChart)
- Add iOS onboarding step 6 for polish LLM setup; hide custom-language-model
diagnostic toggle behind DEBUG
- Unify iOS onboarding tagline with the macOS brand line ("开口即文字。")
- Rewrite README (Chinese-first, product-oriented) and refresh GitHub Pages
This commit is contained in:
@@ -92,7 +92,9 @@ final class MacDictationViewModel: ObservableObject {
|
||||
self.config = ProviderConfig(defaults: defaults)
|
||||
self.usageStatistics = UsageStatisticsStore(defaults: defaults)
|
||||
self.autoPasteEnabled = defaults.object(forKey: StoredKeys.autoPaste) as? Bool ?? true
|
||||
self.hotkeyEnabled = defaults.object(forKey: StoredKeys.hotkey) as? Bool ?? true
|
||||
// Global hotkey is always on — the settings toggle was removed, so any
|
||||
// previously-stored "off" value is intentionally ignored.
|
||||
self.hotkeyEnabled = true
|
||||
self.hotkeyTrigger = MacHotkeyTrigger(
|
||||
rawValue: defaults.string(forKey: StoredKeys.hotkeyTrigger) ?? ""
|
||||
) ?? .rightOption
|
||||
@@ -284,7 +286,7 @@ final class MacDictationViewModel: ObservableObject {
|
||||
do {
|
||||
let result: MacDictationResult
|
||||
if let liveTask {
|
||||
let capture = await liveTask.value
|
||||
let capture = await Self.awaitLiveCapture(liveTask)
|
||||
let trimmedLive = capture.raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !capture.shouldFallbackToBatch, !trimmedLive.isEmpty {
|
||||
if self.transcript.isEmpty {
|
||||
@@ -362,6 +364,25 @@ final class MacDictationViewModel: ObservableObject {
|
||||
isStreamingPartial = false
|
||||
}
|
||||
|
||||
/// Hard timeout so a hung realtime ASR drain cannot leave `isProcessing` stuck.
|
||||
private static func awaitLiveCapture(
|
||||
_ task: Task<MacLiveASRCaptureResult, Never>
|
||||
) async -> MacLiveASRCaptureResult {
|
||||
await HardTimeout.value(
|
||||
seconds: FlowSessionKeys.cloudASRWaitTimeout,
|
||||
operation: { await task.value },
|
||||
onTimeout: {
|
||||
task.cancel()
|
||||
return MacLiveASRCaptureResult(
|
||||
raw: "",
|
||||
chunkWarning: nil,
|
||||
localBias: nil,
|
||||
shouldFallbackToBatch: true
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Stops an in-flight prepare, or finishes an active recording.
|
||||
private func cancelOrFinishRecording() {
|
||||
if isRecording {
|
||||
|
||||
Reference in New Issue
Block a user