缺少代码差异信息,无法生成具体的commit message。请提供code differences内容以便分析并生成符合Angular规范的提交信息。
当您提供代码差异后,我将按照以下格式生成: ``` <type>(<scope>): <subject> <body> ``` 其中type会根据更改类型选择(feat、fix、docs、style、refactor等),scope表示影响范围,subject简要描述变更内容,body详细说明修改内容。
This commit is contained in:
@@ -21,6 +21,11 @@ struct ParsedReport: Sendable {
|
||||
var unit: String
|
||||
var range: String
|
||||
var status: IndicatorStatus
|
||||
var sourcePageIndex: Int?
|
||||
var sourceBoxX: Double?
|
||||
var sourceBoxY: Double?
|
||||
var sourceBoxWidth: Double?
|
||||
var sourceBoxHeight: Double?
|
||||
}
|
||||
|
||||
/// 一项都没识别出来 = 视作失败,UI 走手动录入回退。
|
||||
@@ -100,11 +105,16 @@ actor CaptureService {
|
||||
do {
|
||||
raw = try await AIRuntime.shared.analyzeReport(
|
||||
imageURLs: [tmpURL],
|
||||
prompt: VLPrompts.regionExtraction()
|
||||
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) {
|
||||
@@ -114,6 +124,56 @@ actor CaptureService {
|
||||
}
|
||||
}
|
||||
|
||||
/// 「拍照识别」OCR 链路:把 Vision OCR 出的纯文本交给 LLM(Qwen3-1.7B)结构化抽指标。
|
||||
/// 不建 Report、不留图;失败抛 `CaptureError`,UI 回退手动录入(§3.2)。
|
||||
/// 调用方(MainActor)先做 OCR,再把文本传进来——OCR 不需进 actor,也避免 UIImage 跨 actor。
|
||||
func recognizeIndicators(fromOCRText text: String) async throws -> [ParsedReport.ParsedIndicator] {
|
||||
do {
|
||||
try await AIRuntime.shared.prepare() // 载 LLM(会先卸 VL,OOM 闸门已处理)
|
||||
} catch {
|
||||
throw CaptureError.modelNotReady
|
||||
}
|
||||
|
||||
let prompt = VLPrompts.indicatorsFromText(text)
|
||||
var collected = ""
|
||||
do {
|
||||
// 整张化验单十余项,给足 token;LLM 解码与任何 VL 解码由 AIRuntime 闸门串行。
|
||||
let stream = await AIRuntime.shared.generate(prompt: prompt, maxTokens: 2048)
|
||||
for try await chunk in stream {
|
||||
collected += chunk.text
|
||||
}
|
||||
} catch {
|
||||
throw CaptureError.inferenceFailed("\(error)")
|
||||
}
|
||||
|
||||
// Qwen3 可能吐 <think>…</think>,先剥掉再抠 JSON。
|
||||
let cleaned = CaptureService.stripThink(collected)
|
||||
#if DEBUG
|
||||
print("🧠 [recognizeIndicators] LLM cleaned output:\n\(cleaned)\n--- end LLM ---")
|
||||
#endif
|
||||
do {
|
||||
return try CaptureService.parseIndicatorsJSON(cleaned)
|
||||
} catch let CaptureError.parseFailed(msg) {
|
||||
throw CaptureError.parseFailed(msg)
|
||||
} catch {
|
||||
throw CaptureError.parseFailed("\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// 剥掉 Qwen3 的 <think>…</think>(配对块 / 未闭合开标签 / 孤立闭标签),再 trim 顶部空白。
|
||||
/// 与 HealthExportService.stripThinkBlocks 同逻辑,但本类是非 MainActor actor,放一份 nonisolated 版避免跨隔离调用。
|
||||
nonisolated static func stripThink(_ raw: String) -> String {
|
||||
var s = raw
|
||||
while let openR = s.range(of: "<think>"),
|
||||
let closeR = s.range(of: "</think>", range: openR.upperBound..<s.endIndex) {
|
||||
s.removeSubrange(openR.lowerBound..<closeR.upperBound)
|
||||
}
|
||||
if let openR = s.range(of: "<think>") { s = String(s[..<openR.lowerBound]) }
|
||||
if let closeR = s.range(of: "</think>") { s = String(s[closeR.upperBound...]) }
|
||||
while let first = s.first, first.isWhitespace { s.removeFirst() }
|
||||
return s
|
||||
}
|
||||
|
||||
/// VL 推理 + JSON 解析的纯阶段。assets 必须已写入 Vault。
|
||||
private func runVL(on assets: [FileVault.SavedAsset]) async throws -> ParsedReport {
|
||||
do {
|
||||
@@ -344,7 +404,36 @@ actor CaptureService {
|
||||
let range = stringValue(d, keys: ["range", "reference", "reference_range", "ref", "参考", "参考值", "参考范围", "正常范围"]) ?? ""
|
||||
let statusRaw = stringValue(d, keys: ["status", "flag", "abnormal", "异常", "提示", "标记"])
|
||||
let status = parseIndicatorStatus(raw: statusRaw, value: value, range: range)
|
||||
return .init(name: name, value: value, unit: unit, range: range, status: status)
|
||||
let evidence = parseEvidenceLocation(d)
|
||||
return .init(
|
||||
name: name,
|
||||
value: value,
|
||||
unit: unit,
|
||||
range: range,
|
||||
status: status,
|
||||
sourcePageIndex: evidence?.pageIndex,
|
||||
sourceBoxX: evidence?.box.x,
|
||||
sourceBoxY: evidence?.box.y,
|
||||
sourceBoxWidth: evidence?.box.width,
|
||||
sourceBoxHeight: evidence?.box.height
|
||||
)
|
||||
}
|
||||
|
||||
private static func parseEvidenceLocation(_ d: [String: Any]) -> (pageIndex: Int, box: (x: Double, y: Double, width: Double, height: Double))? {
|
||||
guard let page = intValue(d, keys: ["source_page", "sourcePage", "page", "页码", "来源页码"]),
|
||||
page >= 1,
|
||||
let box = numberArrayValue(d, keys: ["source_box", "sourceBox", "box", "bbox", "位置", "来源位置"]),
|
||||
box.count == 4 else {
|
||||
return nil
|
||||
}
|
||||
let x = box[0]
|
||||
let y = box[1]
|
||||
let width = box[2]
|
||||
let height = box[3]
|
||||
guard x >= 0, y >= 0, width > 0, height > 0, x + width <= 1, y + height <= 1 else {
|
||||
return nil
|
||||
}
|
||||
return (page - 1, (x, y, width, height))
|
||||
}
|
||||
|
||||
private static func stringValue(_ d: [String: Any], keys: [String]) -> String? {
|
||||
@@ -359,6 +448,44 @@ actor CaptureService {
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func intValue(_ d: [String: Any], keys: [String]) -> Int? {
|
||||
for key in keys {
|
||||
if let i = d[key] as? Int {
|
||||
return i
|
||||
}
|
||||
if let n = d[key] as? NSNumber {
|
||||
return n.intValue
|
||||
}
|
||||
if let s = d[key] as? String, let i = Int(s.trimmingCharacters(in: .whitespacesAndNewlines)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func numberArrayValue(_ d: [String: Any], keys: [String]) -> [Double]? {
|
||||
for key in keys {
|
||||
if let arr = d[key] as? [Double] {
|
||||
return arr
|
||||
}
|
||||
if let arr = d[key] as? [NSNumber] {
|
||||
return arr.map(\.doubleValue)
|
||||
}
|
||||
if let arr = d[key] as? [Any] {
|
||||
let values = arr.compactMap { item -> Double? in
|
||||
if let d = item as? Double { return d }
|
||||
if let n = item as? NSNumber { return n.doubleValue }
|
||||
if let s = item as? String { return Double(s.trimmingCharacters(in: .whitespacesAndNewlines)) }
|
||||
return nil
|
||||
}
|
||||
if values.count == arr.count {
|
||||
return values
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func arrayValue(_ d: [String: Any], keys: [String]) -> [[String: Any]] {
|
||||
for key in keys {
|
||||
if let arr = d[key] as? [[String: Any]] {
|
||||
@@ -480,7 +607,12 @@ extension Report {
|
||||
status: p.status,
|
||||
capturedAt: reportDate,
|
||||
report: self,
|
||||
source: .report
|
||||
source: .report,
|
||||
sourcePageIndex: p.sourcePageIndex,
|
||||
sourceBoxX: p.sourceBoxX,
|
||||
sourceBoxY: p.sourceBoxY,
|
||||
sourceBoxWidth: p.sourceBoxWidth,
|
||||
sourceBoxHeight: p.sourceBoxHeight
|
||||
)
|
||||
ctx.insert(i)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user