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:
Rocky
2026-07-11 19:10:20 +08:00
parent cdf833935a
commit cc8dd1070a
116 changed files with 6659 additions and 2634 deletions
+23 -2
View File
@@ -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 {