feat(polish): allow mood emoji on custom styles and ship Flow/ASR fixes

Custom polish styles can opt in to emotion-matched emoji (default off), with
prompt-level opt-in detection so paste-only styles keep model-added emoji.
Also include Volcengine API-Key ASR auth, voice-processing capture, PiP flash
fix, and related keyboard Shift/haptics reliability work.
This commit is contained in:
Rocky
2026-08-06 15:11:12 +08:00
parent 2ed16e1fbf
commit a8d58d8f0c
47 changed files with 1467 additions and 258 deletions
@@ -0,0 +1,108 @@
// FlowCaptureVoiceProcessing.swift
// OSGKeyboard · Host Support
//
// Capture-side speech front-end for local ASR: Apple Voice Processing
// (noise suppression / AGC / AEC) plus near-talk built-in mic preference.
// `.measurement` delivers near-raw PCM and is a poor fit for competing
// talkers in the same room; `.voiceChat` + VP is the system path that
// also unlocks Control Center Mic Modes (incl. Voice Isolation).
import AVFoundation
import Foundation
import OSGKeyboardShared
public enum FlowCaptureVoiceProcessing {
/// Speech-oriented session mode. Prefer over `.measurement` for ASR.
public static let captureMode: AVAudioSession.Mode = .voiceChat
public static let captureOptions: AVAudioSession.CategoryOptions = [
.defaultToSpeaker, .allowBluetoothHFP, .mixWithOthers
]
/// Enable Apple Voice Processing on a stopped engine. Format may change
/// afterward callers must re-read `inputNode` formats before `installTap`.
@discardableResult
public static func enableVoiceProcessing(on engine: AVAudioEngine) -> Bool {
let input = engine.inputNode
if input.isVoiceProcessingEnabled { return true }
do {
try input.setVoiceProcessingEnabled(true)
OSGDiag.log("voiceProcessing enabled", category: "flow")
logActiveMicrophoneMode()
return true
} catch {
OSGDiag.log(
"voiceProcessing enable failed: \(error.localizedDescription)",
category: "flow"
)
return false
}
}
/// Prefer a near-field built-in data source (front/lower + cardioid when
/// available). No-op while Bluetooth HFP is preferred or active keep
/// the existing headset route.
public static func preferNearTalkBuiltInMic(on session: AVAudioSession) {
if session.currentRoute.inputs.first?.portType == .bluetoothHFP { return }
if session.preferredInput?.portType == .bluetoothHFP { return }
guard let builtIn = session.availableInputs?.first(where: {
$0.portType == .builtInMic
}) else { return }
let sources = builtIn.dataSources ?? []
let preferred =
sources.first(where: { $0.orientation == .front })
?? sources.first(where: { $0.location == .lower })
?? sources.first
if let preferred {
if preferred.supportedPolarPatterns?.contains(.cardioid) == true {
do {
try preferred.setPreferredPolarPattern(.cardioid)
} catch {
OSGDiag.log(
"cardioid polar pattern failed: \(error.localizedDescription)",
category: "flow"
)
}
}
do {
try builtIn.setPreferredDataSource(preferred)
} catch {
OSGDiag.log(
"preferred data source failed: \(error.localizedDescription)",
category: "flow"
)
}
}
do {
try session.setPreferredInput(builtIn)
} catch {
OSGDiag.log(
"preferred built-in mic failed: \(error.localizedDescription)",
category: "flow"
)
}
}
public static func logActiveMicrophoneMode() {
let preferred = micModeLabel(AVCaptureDevice.preferredMicrophoneMode)
let active = micModeLabel(AVCaptureDevice.activeMicrophoneMode)
OSGDiag.log(
"micMode preferred=\(preferred) active=\(active)",
category: "flow"
)
}
private static func micModeLabel(_ mode: AVCaptureDevice.MicrophoneMode) -> String {
switch mode {
case .standard: return "standard"
case .wideSpectrum: return "wideSpectrum"
case .voiceIsolation: return "voiceIsolation"
@unknown default: return "unknown(\(mode.rawValue))"
}
}
}