Files
kangkang/康康/Services/VoiceIntentService.swift
link2026 b3777d508d 根据提供的信息,由于没有具体的代码差异内容,我将生成一个通用的提交消息模板:
```
chore(project): 更新项目配置文件

移除未使用的依赖项并优化构建配置,
提升项目整体性能和可维护性。
```
2026-06-16 00:01:48 +08:00

118 lines
6.0 KiB
Swift

import Foundation
/// + rawValue IntentPrompts token
enum VoiceIntent: String, CaseIterable, Sendable {
case diary, medication, symptom, indicator, archive, export, reminder
}
/// :LLM(MNN/SME2 ),6 退(§3.2)
/// diary(), diary
/// , OCRService enum ;UI AIRuntime(§3.1)
/// nonisolated: MainActor, + await,线()
nonisolated enum VoiceIntentService {
static func classify(_ utterance: String) async -> VoiceIntent? {
let text = utterance.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { return nil }
// ,:6s
if let intent = try? await withTimeout(seconds: 6, operation: {
try await classifyWithLLM(text)
}) {
return intent
}
return keywordMatch(text)
}
// MARK: - LLM
private static func classifyWithLLM(_ text: String) async throws -> VoiceIntent {
try await AIRuntime.shared.prepare()
let stream = await AIRuntime.shared.generate(prompt: IntentPrompts.classify(text),
maxTokens: 48)
var collected = ""
for try await chunk in stream {
collected += chunk.text
}
guard let intent = parseIntent(from: collected) else {
throw CaptureError.parseFailed("intent")
}
return intent
}
/// `{"intent":""}`:think "unknown"/ nil
static func parseIntent(from raw: String) -> VoiceIntent? {
let cleaned = CaptureService.stripThink(raw)
let jsonString = CaptureService.repairJSON(CaptureService.extractBalancedJSON(from: cleaned))
if let data = jsonString.data(using: .utf8),
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let token = obj["intent"] as? String {
return VoiceIntent(rawValue: token.trimmingCharacters(in: .whitespaces).lowercased())
}
// :(diary / symptom )
let bare = cleaned.trimmingCharacters(in: .whitespacesAndNewlines)
.trimmingCharacters(in: CharacterSet(charactersIn: "\"'`。."))
.lowercased()
return VoiceIntent(rawValue: bare)
}
// MARK: - 退(,)
/// : reminder, reminder
/// symptom ****(),///
/// , diary
static func keywordMatch(_ text: String) -> VoiceIntent {
let t = text.lowercased()
// archive ****( / / ),
// , archive
let rules: [(VoiceIntent, [String])] = [
(.reminder, ["提醒", "别忘", "闹钟"]),
(.medication, ["药盒", "用药", "吃药", "吃了药", "服药", "药品", "降压药", "胰岛素"]),
(.archive, ["化验单", "化验报告", "检查报告", "检验报告", "体检报告", "归档", "存档"]),
(.export, ["身体档案", "给医生", "健康总结", "导出"]),
(.indicator, ["血压", "血糖", "体重", "心率", "体温", "尿酸", "血脂", "指标",
"高压", "低压"]),
(.symptom, ["症状", "头疼", "头痛", "肚子疼", "胃疼", "牙疼", "嗓子疼",
"咳嗽", "发烧", "发热", "头晕", "恶心", "拉肚子"]),
]
for (intent, keys) in rules {
for key in keys where t.contains(key) {
// ( / )****:
// , diary
if intent == .medication || intent == .archive, isNegated(t, keyword: key) {
continue
}
return intent
}
}
// :
return .diary
}
/// /,,
/// ,
private static let negationMarkers: Set<Character> = ["", "", "", "", "", "", ""]
static func isNegated(_ text: String, keyword: String) -> Bool {
guard let range = text.range(of: keyword) else { return false }
let preceding = text[..<range.lowerBound].suffix(2)
return preceding.contains { negationMarkers.contains($0) }
}
}
/// :operation sleep , CancellationError
nonisolated private func withTimeout<T: Sendable>(
seconds: Double,
operation: @escaping @Sendable () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask { try await operation() }
group.addTask {
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
throw CancellationError()
}
guard let result = try await group.next() else { throw CancellationError() }
group.cancelAll()
return result
}
}