feat(iOS): 更新MNN后端模型配置优化性能

将MNN主模型从Qwen3.5-4B(~2.64GiB)降级为Qwen3.5-2B(~1.1GiB),因为4B版本
实测运行过慢,影响用户体验。iPhone17+/SME2设备使用2B模型,保留MLX
兜底方案用于模拟器和备用场景,确保AI推理性能和存储效率的平衡。
```
This commit is contained in:
link2026
2026-06-09 22:20:07 +08:00
parent ca5a3fa38b
commit b79ae54b7b
40 changed files with 1327 additions and 452 deletions

View File

@@ -77,53 +77,6 @@ actor CaptureService {
try await runVL(on: assets)
}
/// :****(JPEG data) VL, indicators, Report
/// - `NSTemporaryDirectory`(`.completeFileProtectionUnlessOpen`), `defer`
/// (§ )线(§6), Vault Asset
/// - `CaptureError`,UI 退(§3.2 退线)
/// (MainActor) Indicator
func recognizeRegion(imageData: Data) async throws -> [ParsedReport.ParsedIndicator] {
do {
try await AIRuntime.shared.prepareVL()
} catch {
throw CaptureError.modelNotReady
}
let tmpURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("region-\(UUID().uuidString).jpg")
do {
// .completeFileProtectionUnlessOpen .complete:VL ,
// ,.complete / EPERM 使;
// unlessOpen 访, Vault(completeUnlessOpen)
try imageData.write(to: tmpURL, options: [.completeFileProtectionUnlessOpen, .atomic])
} catch {
throw CaptureError.inferenceFailed("临时图片写入失败:\(error.localizedDescription)")
}
defer { try? FileManager.default.removeItem(at: tmpURL) }
let raw: String
do {
raw = try await AIRuntime.shared.analyzeReport(
imageURLs: [tmpURL],
prompt: VLPrompts.regionExtraction(),
// ,512 token
maxTokens: 2048
)
} catch {
throw CaptureError.inferenceFailed("\(error)")
}
#if DEBUG
print("🔎 [recognizeRegion] image bytes=\(imageData.count), VL raw output:\n\(raw)\n--- end VL raw ---")
#endif
do {
return try CaptureService.parseIndicatorsJSON(raw)
} catch let CaptureError.parseFailed(msg) {
throw CaptureError.parseFailed(msg)
} catch {
throw CaptureError.parseFailed("\(error)")
}
}
/// OCR : Vision OCR LLM(Qwen3-1.7B)
/// Report; `CaptureError`,UI 退(§3.2)
/// (MainActor) OCR,OCR actor, UIImage actor
@@ -149,12 +102,19 @@ actor CaptureService {
// Qwen3 <think></think>, JSON
let cleaned = CaptureService.stripThink(collected)
#if DEBUG
print("🧠 [recognizeIndicators] LLM cleaned output:\n\(cleaned)\n--- end LLM ---")
// :( <think>)+ strip ,/ JSON
// NSLog() print(stdout Xcode lldb ,idevicesyslog )
NSLog("KKDBG-VL RAW LLM output (%d chars):\n%@\n--- end RAW ---", collected.count, collected)
NSLog("KKDBG-VL cleaned (%d chars):\n%@\n--- end cleaned ---", cleaned.count, cleaned)
#endif
do {
return try CaptureService.parseIndicatorsJSON(cleaned)
} catch let CaptureError.parseFailed(msg) {
throw CaptureError.parseFailed(msg)
// ,便( / strip / )
let rawLen = collected.count
let cleanLen = cleaned.count
let preview = cleaned.isEmpty ? "(strip 后为空)" : String(cleaned.prefix(60))
throw CaptureError.parseFailed("\(msg)raw \(rawLen)字/clean \(cleanLen)字·前缀:\(preview)")
} catch {
throw CaptureError.parseFailed("\(error)")
}
@@ -213,7 +173,7 @@ actor CaptureService {
// extractBalancedJSON( {} extractJSONObject):VL
// [{...},{...}], { , indicator
// indicators
let jsonString = extractBalancedJSON(from: raw)
let jsonString = repairJSON(extractBalancedJSON(from: raw))
guard let data = jsonString.data(using: .utf8) else {
throw CaptureError.parseFailed("非 UTF-8 输出")
}
@@ -259,7 +219,7 @@ actor CaptureService {
/// `extractJSONObject` + `parseIndicator` indicator (),
/// UI ,JSON `parseFailed`
static func parseIndicatorsJSON(_ raw: String) throws -> [ParsedReport.ParsedIndicator] {
let jsonString = extractBalancedJSON(from: raw)
let jsonString = repairJSON(extractBalancedJSON(from: raw))
guard let data = jsonString.data(using: .utf8) else {
throw CaptureError.parseFailed("非 UTF-8 输出")
}
@@ -324,6 +284,21 @@ actor CaptureService {
return String(s[start...])
}
/// (2B) JSON , JSONSerialization :
/// - "( key/value )
/// - /(`,}` / `,]` `}` / `]`)
/// ;,
static func repairJSON(_ s: String) -> String {
var t = s
t = t.replacingOccurrences(of: "\u{201C}", with: "\"") //
t = t.replacingOccurrences(of: "\u{201D}", with: "\"") //
if let re = try? NSRegularExpression(pattern: ",\\s*([}\\]])") {
t = re.stringByReplacingMatches(
in: t, range: NSRange(t.startIndex..., in: t), withTemplate: "$1")
}
return t
}
/// JSON ,`{...}` `[...]`
/// ( `{"indicators":[...]}` `[...]`)
/// ( JSONSerialization )

View File

@@ -74,8 +74,8 @@ struct DiaryAssistService {
// 1. <think>...</think>( HealthExportService )
let stripped = HealthExportService.stripThinkBlocks(collected)
// 2. JSON( CaptureService.extractJSONObject)
let jsonStr = CaptureService.extractJSONObject(from: stripped)
// 2. JSON( CaptureService.extractJSONObject)+
let jsonStr = CaptureService.repairJSON(CaptureService.extractJSONObject(from: stripped))
guard let data = jsonStr.data(using: .utf8),
let obj = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]),
let dict = obj as? [String: Any] else {