根据提供的code differences信息,我发现没有具体的代码变更内容。因此生成一个通用的commit message:
``` chore(config): 更新项目配置文件 - 调整开发环境配置参数 - 优化构建流程设置 - 更新依赖包版本管理 ```
107
docs/release/Google赛道-重定位与改造说明.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# 康康 · Google 赛道重定位与改造说明
|
||||
|
||||
> 面向「入围 Top 30」评审表(完成度与传播 25% / Google AI 深度 25% / 创新与 Vibe Coding 20% / Tech for Good 出海 20% / 第 5 维度 ~10% 待补全)。
|
||||
> 本文是把原 MNN/SME2 赛道作品改投 Google 赛道的定位与技术映射。改造日期:2026-07-01。
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句话定位(新)
|
||||
|
||||
**A privacy-first, offline-capable health companion for low-connectivity and underserved communities — understand your medical reports and medicines even without a doctor or internet, in your own language.**
|
||||
|
||||
中文:**给弱网/缺医地区与跨国语言障碍人群的隐私优先健康助手——没有医生、没有网络,也能读懂自己的化验单和药盒,用你的母语。**
|
||||
|
||||
> 注意叙事转向:旧版讲「中国人不想把体检报告传云端」(纯国内隐私焦虑);新版讲「全球低资源人群 + 跨境语言障碍」(出海 + Tech for Good)。同一套 App、同一套 UI/数据,**主要改模型底座 + 叙事**,代码改动集中在 AI 层。
|
||||
|
||||
---
|
||||
|
||||
## 1. hybrid 架构:为什么这样用 Google 技术(评审 25% 核心)
|
||||
|
||||
评审明确要求「Google 技术参与**核心能力构建**,不是包装卖点」并「说明**为什么**用这些 Google 技术」。康康的回答:
|
||||
|
||||
| 能力 | 用的 Google 技术 | 为什么非它不可 |
|
||||
|---|---|---|
|
||||
| **端侧离线推理(默认)** | **Gemma-3n E2B**(Google 开源多模态小模型,端侧 4bit) | 弱网/无网地区要「飞行模式也能用」。Gemma-3n 是 Google 专为手机优化的开源模型,**离线、隐私、不依赖账号**——这是低资源场景的刚需,云模型做不到。 |
|
||||
| **读报告/药盒原图 → 结构化**(联网增强) | **Gemini 2.5 Flash 多模态 API** | 端侧小模型读密集化验单小字不稳;Gemini 多模态能直接读图出结构化指标 + 证据位置框。**这是产品第一卖点「拍一张→变档案」的真正承载者。** |
|
||||
| **深度解读 / 多语言** | **Gemini 2.5 Flash** | 跨国用户拿到外语报告,需要「翻译 + 大白话解读」。Gemini 的多语言与推理能力正对口出海语言障碍痛点。 |
|
||||
| **Prompt 设计 / 调试** | **Google AI Studio** | 所有 Gemini/Gemma 的 prompt 在 AI Studio 里迭代;API key 也由 AI Studio 免费签发。 |
|
||||
|
||||
**hybrid 的产品逻辑**:隐私优先——**默认端侧 Gemma-3n,数据不出设备**;只有用户在「我的 · 云端 AI」**主动开启**后,「读原图 / 深度解读 / 多语言」才走 Gemini 云端。**断网或额度耗尽自动回退端侧,功能不中断。** 这既拿满「Google 产品深度」,又守住隐私品牌,还天然适配「时有时无的网络」这一低资源现实。
|
||||
|
||||
> 代码落点:`GeminiBackend.swift`(REST 直调,SSE 流式 + 多模态)、`AIRuntime.generateCloud / analyzeReportCloud`(不进 OOM 闸门,可与端侧并发)、`CaptureService.runVL`(云端优先读图、失败回退端侧 OCR+文本)、`InferenceSettingsView`(云端开关 + key)。
|
||||
|
||||
### 生产级升级路径(写给评委看工程成熟度)
|
||||
demo 用 AI Studio 直发 key 的 REST 方案(零新增 SPM 依赖、即时可编译)。生产应升级到 **Firebase AI Logic**(Swift SDK):App Check 防盗用、客户端不裸存 key、内建端侧↔云端 hybrid 回退。迁移点已在 `GeminiBackend` 注释标注,换 SDK 不动上层 Service。
|
||||
|
||||
---
|
||||
|
||||
## 2. 重定位:目标用户 / 市场 / 影响路径(评审 Tech for Good 20% + 创新 20%)
|
||||
|
||||
评审要「真实人群 / 弱势 / 低资源 / 全球化」+「海外落地可行性,不只是善意叙事」+「目标用户、场景、影响路径、后续扩展」。
|
||||
|
||||
- **目标人群**
|
||||
1. 低资源/弱网地区居民:缺医少药、网络不稳,拿到化验单无人解读。
|
||||
2. 跨境人群:移民、留学生、外派、旅居者,面对**外语**医疗文件看不懂。
|
||||
3. 慢病/老人照护者:替家人留存报告、复查前整理重点。
|
||||
- **真实问题**:看不懂(术语+外语)、找不到(报告散落)、不敢传(隐私)、没医生在身边。
|
||||
- **海外落地可行性(不是善意叙事)**
|
||||
- 离线即可用 → 适配低带宽市场,不依赖稳定网络与数据套餐。
|
||||
- 隐私默认端侧 → 契合 GDPR 等严监管市场的健康数据合规。
|
||||
- 多语言 → 一套产品覆盖多语种,边际成本低。
|
||||
- **影响路径**:个人读懂自己 → 家庭健康档案管理 → 低资源社区的基层健康工作者辅助工具。
|
||||
- **后续扩展**:更多语种、更多文档类型(疫苗本/处方/影像)、与本地公共健康项目对接、可选导出给当地医生。
|
||||
|
||||
---
|
||||
|
||||
## 3. 逐维度自检(改造后该拿的分)
|
||||
|
||||
| 维度 | 权重 | 改造后状态 |
|
||||
|---|---|---|
|
||||
| 完成度与传播 | 25% | 可运行 App + 闭环(拍报告→识别→档案→趋势→问答→摘要);Gemini 恢复真·读图。**待补:3-5 分钟 demo 视频(见 §4)+ 重写图文(§5)** |
|
||||
| Google AI 深度 | 25% | Gemma-3n(端侧)+ Gemini 多模态(读图)+ Gemini(深度/多语言)+ AI Studio。**均在核心链路,非包装。** |
|
||||
| 创新 & Vibe Coding | 20% | hybrid 隐私优先 + 离线可用 + 多语言;Claude Code/Gemini Code Assist 全程 vibe coding。叙事已转出海。 |
|
||||
| Tech for Good 出海 | 20% | 低资源/弱网/跨国语言障碍,见 §2。 |
|
||||
| 第 5 维度(~10%) | ? | **未知,待补全(见 §6 开放项)** |
|
||||
|
||||
---
|
||||
|
||||
## 4. 现场 Demo 脚本(3-5 分钟,先证「能跑」)
|
||||
|
||||
> 评审第一条:核心功能可实际运行,3-5 分钟讲清主要价值。把最强证据放最前。
|
||||
|
||||
1. **(0:00-0:30)问题**:一句话——「全球很多人拿到化验单看不懂,身边没医生,也不敢把隐私报告传云端。」
|
||||
2. **(0:30-1:30)拍一张 → 变档案**:现场拍/导入一张报告 → Gemini 多模态读出结构化指标 + 异常高亮 → 存进档案。**这是第一卖点。**
|
||||
3. **(1:30-2:15)飞行模式离线**:开飞行模式,演示端侧 Gemma-3n 仍能做文本解读/问答——「没网也能用」。这是低资源场景的杀手锏。
|
||||
4. **(2:15-3:00)多语言**:拿一张**外语**报告 → Gemini 翻译 + 母语大白话解读。出海痛点直球。
|
||||
5. **(3:00-3:45)长期价值**:趋势页 AI 解读 + 「就诊前 30 秒整理重点」摘要。
|
||||
6. **(3:45-4:30)隐私 + 技术收尾**:「我的 · 云端 AI」开关讲 hybrid;一句话点出 Gemma-3n + Gemini + AI Studio。
|
||||
|
||||
视频素材:片头/转场可用 **Veo / Google Flow** 生成;录屏需真机配合(端侧 + 飞行模式只能真机)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 传播物料改写要点(小红书图文)
|
||||
|
||||
- 标题从「MNN/SME2 端侧」改为「不上传、断网也能用的健康 AI(Gemma + Gemini)」。
|
||||
- 9 宫格替换:把旧「MNN-SME2 性能自检」图换成「云端 AI 设置页(Gemini 开关)」+「飞行模式离线生成」+「外语报告→母语解读」。
|
||||
- 正文三个创新点改为:① 隐私优先 hybrid;② 端侧离线可用(低资源);③ 多语言出海。
|
||||
- 删除一切「100% 本地、不上云」绝对化表述,统一为「隐私优先,云端可选」。
|
||||
|
||||
---
|
||||
|
||||
## 6. 你需要做的事 + 开放项(不阻塞已完成代码)
|
||||
|
||||
**必须由你操作(我无法代办)**
|
||||
1. 去 **aistudio.google.com** 免费签发 Gemini API Key。
|
||||
2. App「我的 · 推理引擎 · 云端 AI · Gemini」打开开关、粘贴 key。
|
||||
3. 真机录 demo 视频(端侧 + 飞行模式只能真机)。
|
||||
|
||||
**开放项(需要你提供信息)**
|
||||
4. **评分表第 5 个维度(~10%)被截断**——把完整评审标准或比赛出处发我,补进 §3,别再漏考核点。
|
||||
5. **平台权衡**:当前保 iOS。若评委更看重 Android 生态(Android Studio/Flutter/Gemini Nano),可再评估,但 2-3 周内不建议重写。
|
||||
|
||||
---
|
||||
|
||||
## 7. 一句话给你自己
|
||||
|
||||
上一个比赛输在「为命题做的东西没按被考核的点证明」。这次的红线是:**Gemini 必须在核心链路里被看到在跑(读图/多语言),而不是设置页里一个没人点的开关。** demo 视频里 Gemini 读出一张报告的那 10 秒,比任何文案都值钱。
|
||||
462
docs/release/build_creative_proposal_doc.py
Normal file
@@ -0,0 +1,462 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from docx import Document
|
||||
from docx.enum.section import WD_SECTION
|
||||
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_CELL_VERTICAL_ALIGNMENT
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.oxml import OxmlElement
|
||||
from docx.oxml.ns import qn
|
||||
from docx.shared import Cm, Inches, Pt, RGBColor
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OUT = ROOT / "docs" / "release" / "康康创意方案文档.docx"
|
||||
PICS = ROOT / "docs" / "release" / "xhs-9grid"
|
||||
|
||||
BLUE = RGBColor(46, 116, 181)
|
||||
DARK_BLUE = RGBColor(31, 77, 120)
|
||||
INK = RGBColor(31, 31, 31)
|
||||
MUTED = RGBColor(95, 95, 95)
|
||||
LIGHT = "F4F6F9"
|
||||
PALE_BLUE = "E8EEF5"
|
||||
GREEN = RGBColor(58, 112, 76)
|
||||
BRICK = RGBColor(154, 78, 65)
|
||||
|
||||
|
||||
def set_run_font(run, size=None, bold=None, color=None, font="PingFang SC"):
|
||||
run.font.name = font
|
||||
run._element.rPr.rFonts.set(qn("w:ascii"), "Calibri")
|
||||
run._element.rPr.rFonts.set(qn("w:hAnsi"), "Calibri")
|
||||
run._element.rPr.rFonts.set(qn("w:eastAsia"), font)
|
||||
if size is not None:
|
||||
run.font.size = Pt(size)
|
||||
if bold is not None:
|
||||
run.bold = bold
|
||||
if color is not None:
|
||||
run.font.color.rgb = color
|
||||
|
||||
|
||||
def set_paragraph_font(paragraph, size=11, color=INK, font="PingFang SC"):
|
||||
for run in paragraph.runs:
|
||||
set_run_font(run, size=size, color=color, font=font)
|
||||
|
||||
|
||||
def set_cell_shading(cell, fill):
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
shd = tc_pr.find(qn("w:shd"))
|
||||
if shd is None:
|
||||
shd = OxmlElement("w:shd")
|
||||
tc_pr.append(shd)
|
||||
shd.set(qn("w:fill"), fill)
|
||||
|
||||
|
||||
def set_cell_margins(cell, top=80, start=120, bottom=80, end=120):
|
||||
tc = cell._tc
|
||||
tc_pr = tc.get_or_add_tcPr()
|
||||
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
||||
if tc_mar is None:
|
||||
tc_mar = OxmlElement("w:tcMar")
|
||||
tc_pr.append(tc_mar)
|
||||
for m, v in [("top", top), ("start", start), ("bottom", bottom), ("end", end)]:
|
||||
node = tc_mar.find(qn(f"w:{m}"))
|
||||
if node is None:
|
||||
node = OxmlElement(f"w:{m}")
|
||||
tc_mar.append(node)
|
||||
node.set(qn("w:w"), str(v))
|
||||
node.set(qn("w:type"), "dxa")
|
||||
|
||||
|
||||
def set_table_geometry(table, widths):
|
||||
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||
table.autofit = False
|
||||
tbl = table._tbl
|
||||
tbl_pr = tbl.tblPr
|
||||
tbl_w = tbl_pr.find(qn("w:tblW"))
|
||||
if tbl_w is None:
|
||||
tbl_w = OxmlElement("w:tblW")
|
||||
tbl_pr.append(tbl_w)
|
||||
tbl_w.set(qn("w:w"), str(sum(widths)))
|
||||
tbl_w.set(qn("w:type"), "dxa")
|
||||
tbl_grid = tbl.tblGrid
|
||||
if tbl_grid is None:
|
||||
tbl_grid = OxmlElement("w:tblGrid")
|
||||
tbl.append(tbl_grid)
|
||||
for child in list(tbl_grid):
|
||||
tbl_grid.remove(child)
|
||||
for width in widths:
|
||||
grid_col = OxmlElement("w:gridCol")
|
||||
grid_col.set(qn("w:w"), str(width))
|
||||
tbl_grid.append(grid_col)
|
||||
for row in table.rows:
|
||||
for idx, cell in enumerate(row.cells):
|
||||
tc_pr = cell._tc.get_or_add_tcPr()
|
||||
tc_w = tc_pr.find(qn("w:tcW"))
|
||||
if tc_w is None:
|
||||
tc_w = OxmlElement("w:tcW")
|
||||
tc_pr.append(tc_w)
|
||||
tc_w.set(qn("w:w"), str(widths[idx]))
|
||||
tc_w.set(qn("w:type"), "dxa")
|
||||
set_cell_margins(cell)
|
||||
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
||||
|
||||
|
||||
def table_border(table, color="DADCE0", size="6"):
|
||||
tbl_pr = table._tbl.tblPr
|
||||
borders = tbl_pr.first_child_found_in("w:tblBorders")
|
||||
if borders is None:
|
||||
borders = OxmlElement("w:tblBorders")
|
||||
tbl_pr.append(borders)
|
||||
for edge in ["top", "left", "bottom", "right", "insideH", "insideV"]:
|
||||
tag = f"w:{edge}"
|
||||
element = borders.find(qn(tag))
|
||||
if element is None:
|
||||
element = OxmlElement(tag)
|
||||
borders.append(element)
|
||||
element.set(qn("w:val"), "single")
|
||||
element.set(qn("w:sz"), size)
|
||||
element.set(qn("w:space"), "0")
|
||||
element.set(qn("w:color"), color)
|
||||
|
||||
|
||||
def add_para(doc, text="", style=None, size=11, bold=False, color=INK, align=None, after=8):
|
||||
p = doc.add_paragraph(style=style)
|
||||
p.paragraph_format.space_after = Pt(after)
|
||||
p.paragraph_format.line_spacing = 1.25
|
||||
if align is not None:
|
||||
p.alignment = align
|
||||
run = p.add_run(text)
|
||||
set_run_font(run, size=size, bold=bold, color=color)
|
||||
return p
|
||||
|
||||
|
||||
def add_heading(doc, text, level=1):
|
||||
p = doc.add_paragraph(style=f"Heading {level}")
|
||||
p.paragraph_format.keep_with_next = True
|
||||
run = p.add_run(text)
|
||||
if level == 1:
|
||||
set_run_font(run, 16, True, BLUE)
|
||||
p.paragraph_format.space_before = Pt(18)
|
||||
p.paragraph_format.space_after = Pt(10)
|
||||
elif level == 2:
|
||||
set_run_font(run, 13, True, BLUE)
|
||||
p.paragraph_format.space_before = Pt(12)
|
||||
p.paragraph_format.space_after = Pt(6)
|
||||
else:
|
||||
set_run_font(run, 12, True, DARK_BLUE)
|
||||
p.paragraph_format.space_before = Pt(8)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
return p
|
||||
|
||||
|
||||
def add_bullet(doc, text, level=0):
|
||||
p = doc.add_paragraph(style="List Bullet")
|
||||
p.paragraph_format.left_indent = Inches(0.375)
|
||||
p.paragraph_format.first_line_indent = Inches(-0.194)
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
p.paragraph_format.line_spacing = 1.208
|
||||
run = p.add_run(text)
|
||||
set_run_font(run, 10.5, color=INK)
|
||||
return p
|
||||
|
||||
|
||||
def add_callout(doc, title, body, fill=LIGHT, title_color=DARK_BLUE):
|
||||
table = doc.add_table(rows=1, cols=1)
|
||||
set_table_geometry(table, [9360])
|
||||
table_border(table, color="E1E5EA", size="4")
|
||||
cell = table.cell(0, 0)
|
||||
set_cell_shading(cell, fill)
|
||||
cell.text = ""
|
||||
p = cell.paragraphs[0]
|
||||
p.paragraph_format.space_after = Pt(4)
|
||||
r = p.add_run(title)
|
||||
set_run_font(r, 11, True, title_color)
|
||||
p2 = cell.add_paragraph()
|
||||
p2.paragraph_format.space_after = Pt(0)
|
||||
p2.paragraph_format.line_spacing = 1.22
|
||||
r2 = p2.add_run(body)
|
||||
set_run_font(r2, 10.5, color=INK)
|
||||
doc.add_paragraph().paragraph_format.space_after = Pt(4)
|
||||
return table
|
||||
|
||||
|
||||
def add_label_table(doc, rows, widths=(2300, 7060), header=None):
|
||||
table = doc.add_table(rows=0, cols=2)
|
||||
set_table_geometry(table, list(widths))
|
||||
table_border(table, color="DADCE0", size="5")
|
||||
if header:
|
||||
row = table.add_row()
|
||||
row.cells[0].merge(row.cells[1])
|
||||
cell = row.cells[0]
|
||||
set_cell_shading(cell, PALE_BLUE)
|
||||
p = cell.paragraphs[0]
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
r = p.add_run(header)
|
||||
set_run_font(r, 10.5, True, DARK_BLUE)
|
||||
for label, value in rows:
|
||||
row = table.add_row()
|
||||
for cell in row.cells:
|
||||
set_cell_shading(cell, "FFFFFF")
|
||||
p0 = row.cells[0].paragraphs[0]
|
||||
p0.paragraph_format.space_after = Pt(0)
|
||||
r0 = p0.add_run(label)
|
||||
set_run_font(r0, 10, True, DARK_BLUE)
|
||||
p1 = row.cells[1].paragraphs[0]
|
||||
p1.paragraph_format.space_after = Pt(0)
|
||||
p1.paragraph_format.line_spacing = 1.18
|
||||
r1 = p1.add_run(value)
|
||||
set_run_font(r1, 10, color=INK)
|
||||
doc.add_paragraph().paragraph_format.space_after = Pt(4)
|
||||
return table
|
||||
|
||||
|
||||
def add_matrix(doc, headers, rows, widths):
|
||||
table = doc.add_table(rows=1, cols=len(headers))
|
||||
set_table_geometry(table, widths)
|
||||
table_border(table, color="DADCE0", size="5")
|
||||
for idx, h in enumerate(headers):
|
||||
cell = table.cell(0, idx)
|
||||
set_cell_shading(cell, PALE_BLUE)
|
||||
p = cell.paragraphs[0]
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
r = p.add_run(h)
|
||||
set_run_font(r, 9.5, True, DARK_BLUE)
|
||||
for row_data in rows:
|
||||
row = table.add_row()
|
||||
for idx, text in enumerate(row_data):
|
||||
cell = row.cells[idx]
|
||||
set_cell_shading(cell, "FFFFFF")
|
||||
p = cell.paragraphs[0]
|
||||
p.paragraph_format.space_after = Pt(0)
|
||||
p.paragraph_format.line_spacing = 1.16
|
||||
r = p.add_run(text)
|
||||
set_run_font(r, 9.2, color=INK)
|
||||
doc.add_paragraph().paragraph_format.space_after = Pt(4)
|
||||
return table
|
||||
|
||||
|
||||
def setup_styles(doc):
|
||||
styles = doc.styles
|
||||
normal = styles["Normal"]
|
||||
normal.font.name = "Calibri"
|
||||
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC")
|
||||
normal.font.size = Pt(11)
|
||||
normal.font.color.rgb = INK
|
||||
normal.paragraph_format.space_after = Pt(8)
|
||||
normal.paragraph_format.line_spacing = 1.333
|
||||
normal.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
|
||||
|
||||
for style_name in ["Heading 1", "Heading 2", "Heading 3", "List Bullet"]:
|
||||
style = styles[style_name]
|
||||
style.font.name = "Calibri"
|
||||
style._element.rPr.rFonts.set(qn("w:eastAsia"), "PingFang SC")
|
||||
|
||||
|
||||
def set_header_footer(section):
|
||||
header = section.header.paragraphs[0]
|
||||
header.text = ""
|
||||
header.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||
run = header.add_run("康康 Kangkang 创意方案")
|
||||
set_run_font(run, 9, color=MUTED)
|
||||
footer = section.footer.paragraphs[0]
|
||||
footer.text = ""
|
||||
footer.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = footer.add_run("本方案仅描述健康记录与科普式解读能力,不构成医疗诊断或用药建议")
|
||||
set_run_font(run, 8.5, color=MUTED)
|
||||
|
||||
|
||||
def add_title_page(doc):
|
||||
add_para(doc, "手机上的 AI 创意方案", size=12, bold=True, color=GREEN, align=WD_ALIGN_PARAGRAPH.CENTER, after=10)
|
||||
title = doc.add_paragraph()
|
||||
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
title.paragraph_format.space_after = Pt(6)
|
||||
r = title.add_run("康康:本地优先的个人健康档案")
|
||||
set_run_font(r, 24, True, DARK_BLUE)
|
||||
subtitle = doc.add_paragraph()
|
||||
subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
subtitle.paragraph_format.space_after = Pt(18)
|
||||
r2 = subtitle.add_run("100% 本地推理 · 个人健康影像档案 · 大白话解读 · 结构化 RAG 问答")
|
||||
set_run_font(r2, 12.5, color=MUTED)
|
||||
|
||||
add_label_table(
|
||||
doc,
|
||||
[
|
||||
("项目形态", "iOS 原生 App,SwiftUI + SwiftData。"),
|
||||
("目标用户", "不愿把体检报告、化验单、症状和用药记录交给云端的普通用户。"),
|
||||
("核心主张", "健康数据默认留在手机里,本地模型负责整理、解释和检索。"),
|
||||
("技术主线", "Qwen3.5-2B + MNN + Arm SME2/NEON,MLX Swift 作为模拟器和兜底后端。"),
|
||||
("边界声明", "不做医疗诊断、剂量推荐、急诊判断、医生预约、账号系统或数据上云。"),
|
||||
],
|
||||
header="项目概览",
|
||||
)
|
||||
|
||||
add_callout(
|
||||
doc,
|
||||
"一句话创意",
|
||||
"把体检报告、化验指标、症状、日记和用药记录收进一个本地健康档案,再让手机端本地大模型用普通人能听懂的语言帮助整理、回顾和就诊前准备。",
|
||||
fill="F7FAF7",
|
||||
title_color=GREEN,
|
||||
)
|
||||
|
||||
doc.add_page_break()
|
||||
|
||||
|
||||
def add_sources(doc):
|
||||
add_heading(doc, "依据来源", 1)
|
||||
add_bullet(doc, "AGENTS.md:项目定位、技术选型、AI 链路、数据模型、隐私边界和六周 demo 目标。")
|
||||
add_bullet(doc, "康康/AI/InferenceEngine.swift 与 AIRuntime.swift:MNN/MLX 双后端、SME2 探测、actor 串行推理闸门。")
|
||||
add_bullet(doc, "康康/Services/HealthExportService.swift:本地 RAG、意图抽取、结构化检索、就诊摘要生成和失败兜底。")
|
||||
add_bullet(doc, "docs/release/app-store-metadata.md 与小红书项目介绍:用户价值、隐私表达和非医疗器械边界。")
|
||||
|
||||
|
||||
def main():
|
||||
doc = Document()
|
||||
section = doc.sections[0]
|
||||
section.page_width = Inches(8.5)
|
||||
section.page_height = Inches(11)
|
||||
section.top_margin = Inches(1)
|
||||
section.bottom_margin = Inches(1)
|
||||
section.left_margin = Inches(1)
|
||||
section.right_margin = Inches(1)
|
||||
section.header_distance = Inches(0.492)
|
||||
section.footer_distance = Inches(0.492)
|
||||
setup_styles(doc)
|
||||
set_header_footer(section)
|
||||
|
||||
add_title_page(doc)
|
||||
|
||||
add_heading(doc, "1. 应用场景", 1)
|
||||
add_para(doc, "康康面向的是普通人的日常健康信息管理,而不是医院内部系统或专业诊疗工具。产品重点放在体检后、复查前、慢性指标长期观察、看医生前准备等高频生活场景。")
|
||||
add_matrix(
|
||||
doc,
|
||||
["场景", "用户动作", "康康提供的价值"],
|
||||
[
|
||||
("体检或化验后", "拍摄报告或从相册导入。", "本地识别关键指标、参考范围和异常状态,形成可检索的报告档案。"),
|
||||
("长期指标记录", "记录血压、血糖、体重、血脂等指标。", "自动沉淀趋势曲线,用大白话说明最近变化,降低读图门槛。"),
|
||||
("症状与日记", "随手记身体感受、睡眠、疼痛、用药等。", "把零散记录整理成时间线,必要时由本地 AI 追问补全。"),
|
||||
("就诊前准备", "输入“把最近一个月整理给医生看”。", "本地检索症状、指标、报告和用药,生成可复制/分享的就诊摘要。"),
|
||||
("家庭健康管理", "为父母或自己保存报告和复查信息。", "减少纸质报告丢失、相册翻找和口头回忆遗漏。"),
|
||||
],
|
||||
[1600, 2600, 5160],
|
||||
)
|
||||
|
||||
add_heading(doc, "2. 用户痛点", 1)
|
||||
add_bullet(doc, "看不懂:体检报告和化验单充满缩写、箭头、参考范围,普通人很难快速判断哪些值得关注。")
|
||||
add_bullet(doc, "找不到:健康信息散落在纸质报告、相册截图、备忘录和聊天记录里,复查或就医时很难完整回溯。")
|
||||
add_bullet(doc, "不敢传:健康报告包含年龄、医院、检查结果、既往问题等高度敏感信息,上传给云端 AI 会带来隐私顾虑。")
|
||||
add_bullet(doc, "记不全:看医生时常常忘记症状持续时间、近期指标变化、在服药物和过敏史。")
|
||||
add_bullet(doc, "工具割裂:传统健康记录 App 重记录轻解释,AI 聊天工具会解释但不沉淀长期个人档案。")
|
||||
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "3. 技术方案", 1)
|
||||
add_heading(doc, "3.1 模型选型", 2)
|
||||
add_para(doc, "主模型选择 Qwen3.5-2B,原因是它在移动端内存和速度预算内更适合 demo 落地,同时承担文本解读和视觉识别两类任务,避免拆分多套模型造成下载、存储和加载复杂度上升。")
|
||||
add_label_table(
|
||||
doc,
|
||||
[
|
||||
("主路径模型", "Qwen3.5-2B-MNN,约 1.2GB,面向真机端侧推理。"),
|
||||
("兜底模型", "MLX Swift 对应的 Qwen3.5-2B-4bit,主要用于模拟器、调试和 MNN 不可用时的兜底。"),
|
||||
("能力覆盖", "报告/药盒等图片识别、健康文本整理、趋势解释、身体档案问答和就诊摘要。"),
|
||||
("取舍逻辑", "不用更大模型追求极限效果,而是优先保证端侧速度、内存可控、下载可接受和 demo 可复现。"),
|
||||
],
|
||||
header="模型选型摘要",
|
||||
)
|
||||
|
||||
add_heading(doc, "3.2 推理框架", 2)
|
||||
add_para(doc, "推理框架采用双后端策略:真机主路径为阿里开源 MNN,面向挑战赛的 Qwen + MNN + SME2 端侧 CPU 推理;MLX Swift 作为 Apple 官方 Metal GPU 兜底,用于模拟器和对照测试。")
|
||||
add_matrix(
|
||||
doc,
|
||||
["模块", "方案", "作用"],
|
||||
[
|
||||
("MNNBackend", "ObjC++ 桥接 MNN LLM 能力。", "在真机上加载 Qwen3.5-2B-MNN,支撑文本生成与视觉分析。"),
|
||||
("InferenceEngine", "auto / mnn / mlx 三种偏好。", "真机优先 MNN,MNN 不可用时自动回退 MLX。"),
|
||||
("AIRuntime", "actor 单例 + 推理闸门。", "串行化模型加载、文本生成和视觉推理,避免并发 OOM。"),
|
||||
("ModelStore", "Application Support/Models。", "管理模型下载、就绪校验和现场旁路导入。"),
|
||||
],
|
||||
[1900, 3200, 4260],
|
||||
)
|
||||
|
||||
add_heading(doc, "3.3 端侧适配思路", 2)
|
||||
add_bullet(doc, "设备优先级:支持 MNN 的真机默认走 MNN;支持 SME2 的 A19/iPhone17 走 SME2 加速,旧设备回退 NEON;模拟器走 MLX。")
|
||||
add_bullet(doc, "内存控制:AIRuntime 使用 actor 内信号量,保证同一时间只有一个重推理任务占用模型内存;加载 LLM 前卸载 VL,加载 MNN 前卸载 MLX 侧模型。")
|
||||
add_bullet(doc, "体验兜底:模型未就绪时 App 仍可启动,AI 入口提示前往模型管理;意图抽取失败时回退近 30 天全表扫描;识别失败时回退手动录入。")
|
||||
add_bullet(doc, "结构化 RAG:不引入 embedding 模型,先由本地模型抽取意图和关键词,再用 SwiftData 检索指标、报告、症状和日记,最后拼接上下文生成回答。")
|
||||
add_bullet(doc, "隐私保护:报告原图只保存到本地 Vault,SwiftData 存结构化记录;使用 iOS completeFileProtection、Face ID 启动锁和永久删除。")
|
||||
|
||||
add_heading(doc, "4. 创新点", 1)
|
||||
add_matrix(
|
||||
doc,
|
||||
["创新点", "说明", "差异化价值"],
|
||||
[
|
||||
("本地优先健康 AI", "AI 不依赖云端 API,核心解读和问答在手机内完成。", "解决健康数据上传焦虑,形成隐私可信的 AI 体验。"),
|
||||
("影像档案 + AI 解读闭环", "拍报告、识别、归档、趋势、问答、摘要串成一个系统。", "不是单点 OCR 或聊天框,而是长期可用的个人健康档案。"),
|
||||
("MNN + SME2 端侧推理", "将 Qwen 模型通过 MNN 接入 iPhone CPU/SME2 路径。", "展示大模型在移动 CPU 上可复现运行的技术亮点。"),
|
||||
("轻量结构化 RAG", "利用 SwiftData 已有结构化记录检索,不额外引入 embedding 模型。", "降低端侧存储和计算成本,响应更可控。"),
|
||||
("明确医疗边界", "只做记录、整理和科普式解释,不做诊断、用药和急诊判断。", "既保留实用价值,也降低合规和误导风险。"),
|
||||
],
|
||||
[1800, 3900, 3660],
|
||||
)
|
||||
|
||||
add_heading(doc, "5. 预期效果", 1)
|
||||
add_callout(
|
||||
doc,
|
||||
"用户侧效果",
|
||||
"用户可以把报告、指标、症状和用药从碎片化记录变成可检索、可回顾、可解释的个人健康档案;在看医生前,用一份摘要减少遗漏和重复描述。",
|
||||
fill="F7FAF7",
|
||||
title_color=GREEN,
|
||||
)
|
||||
add_callout(
|
||||
doc,
|
||||
"技术侧效果",
|
||||
"验证 Qwen3.5-2B 在 MNN + SME2/NEON 端侧 CPU 路径上的可用性,形成模型下载、引擎选择、性能自检、推理串行、失败兜底和本地数据检索的一体化方案。",
|
||||
fill="F4F6F9",
|
||||
title_color=DARK_BLUE,
|
||||
)
|
||||
add_callout(
|
||||
doc,
|
||||
"展示侧效果",
|
||||
"比赛或路演时可以通过飞行模式、本地模型管理页、性能自检 tok/s、报告识别前后对比和身体档案摘要,清晰证明“不是云端套壳,而是端侧 AI 创意应用”。",
|
||||
fill="FFF6F2",
|
||||
title_color=BRICK,
|
||||
)
|
||||
|
||||
add_heading(doc, "6. 方案完整性", 1)
|
||||
add_para(doc, "康康的完整性体现在产品、技术和安全边界三条线同时闭合:用户能完成从采集到回顾再到就诊摘要的闭环;技术上有模型、运行时、数据、服务层和 UI 的分层;安全上明确不引入云服务、不做账号、不做自研密码学。")
|
||||
add_matrix(
|
||||
doc,
|
||||
["层级", "已覆盖能力", "设计原则"],
|
||||
[
|
||||
("采集层", "拍报告、记录指标、写日记、记症状、药品识别。", "入口轻,失败可手动补录。"),
|
||||
("数据层", "SwiftData 模型:Indicator、Report、DiaryEntry、Symptom、Asset、ChatTurn、UserProfile 等。", "结构化保存,便于检索和趋势计算。"),
|
||||
("AI 层", "CaptureService、HealthExportService、TrendInsightService 经 AIRuntime 调用本地模型。", "UI 不直连模型,推理统一排队。"),
|
||||
("展示层", "首页、记录、趋势、我的、模型管理、身体档案摘要。", "围绕 demo 核心卖点组织信息。"),
|
||||
("隐私层", "本地 Vault、completeFileProtection、Face ID、永久删除、无账号无云。", "使用系统能力,不自造密码学。"),
|
||||
],
|
||||
[1500, 5000, 2860],
|
||||
)
|
||||
|
||||
add_heading(doc, "7. 风险与边界控制", 1)
|
||||
add_bullet(doc, "AI 输出仅作为健康记录整理与科普式解释,不作为医疗诊断、治疗、用药或剂量建议。")
|
||||
add_bullet(doc, "识别结果必须允许用户核对和编辑,避免把模型错误直接落成事实。")
|
||||
add_bullet(doc, "端侧模型受设备性能、内存和电量影响,需要模型未就绪、加载失败、低性能设备等状态提示。")
|
||||
add_bullet(doc, "健康类内容表达要避免“治疗”“诊断”“疗效”等容易误导的词汇。")
|
||||
|
||||
# Visual appendix with the generated 9-grid overview if available.
|
||||
overview = PICS / "00-9宫格总览.png"
|
||||
if overview.exists():
|
||||
doc.add_page_break()
|
||||
add_heading(doc, "附录:展示素材示意", 1)
|
||||
add_para(doc, "以下为小红书 9 宫格展示图总览,可用于说明产品闭环与技术亮点。", size=10.5, color=MUTED)
|
||||
p = doc.add_paragraph()
|
||||
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
run = p.add_run()
|
||||
run.add_picture(str(overview), width=Inches(5.6))
|
||||
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
doc.save(OUT)
|
||||
print(OUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
docs/release/pics/01-字体大小设置.png
Normal file
|
After Width: | Height: | Size: 842 KiB |
BIN
docs/release/pics/02-Xcode开发调试界面.png
Normal file
|
After Width: | Height: | Size: 955 KiB |
BIN
docs/release/pics/03-健康日记-语音输入键盘.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
docs/release/pics/04-关于页面-项目说明.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/release/pics/05-字体大小设置-副本.png
Normal file
|
After Width: | Height: | Size: 842 KiB |
BIN
docs/release/pics/06-推理引擎-MNN-SME2性能自检.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/release/pics/07-模型管理-Qwen已就绪.png
Normal file
|
After Width: | Height: | Size: 556 KiB |
BIN
docs/release/pics/08-提醒列表-服药提醒.png
Normal file
|
After Width: | Height: | Size: 437 KiB |
BIN
docs/release/pics/09-趋势详情-体重AI解读.png
Normal file
|
After Width: | Height: | Size: 737 KiB |
BIN
docs/release/pics/10-身体档案-已知背景与指标趋势.png
Normal file
|
After Width: | Height: | Size: 351 KiB |
BIN
docs/release/pics/11-身体档案-就诊摘要生成结果.png
Normal file
|
After Width: | Height: | Size: 357 KiB |
BIN
docs/release/pics/12-药品库-编辑药品.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
docs/release/pics/13-记录指标-长期监测预设.png
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
docs/release/pics/14-健康日记-AI追问补充.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
docs/release/pics/15-健康日记-本地AI整理中.png
Normal file
|
After Width: | Height: | Size: 239 KiB |
BIN
docs/release/pics/16-用药记录-记录用药表单.png
Normal file
|
After Width: | Height: | Size: 587 KiB |
BIN
docs/release/pics/17-新建菜单-记录入口列表.png
Normal file
|
After Width: | Height: | Size: 523 KiB |
BIN
docs/release/pics/18-症状开始-常见症状表单.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
docs/release/pics/19-报告归档-核对报告信息.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/release/pics/20-药品识别-核对药品.png
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
docs/release/pics/21-首页-健康日历趋势提醒.png
Normal file
|
After Width: | Height: | Size: 454 KiB |
BIN
docs/release/pics/22-框选异常指标-本地识别中.png
Normal file
|
After Width: | Height: | Size: 6.9 MiB |
83
docs/release/sensevoice-integration.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# 记录问诊 · 本地 SenseVoice 转写接入
|
||||
|
||||
> 「记录问诊」录完整段录音后,在本机用 **SenseVoice**(经 **sherpa-mnn** 跑在 **MNN 后端**)离线转写成文字,
|
||||
> 再交本地 LLM 整理成问诊小结。全程本机、无网络。
|
||||
>
|
||||
> 代码已全部就位且**默认编译通过**:未接入 sherpa-mnn 时 `SenseVoiceBridge` 走桩,问诊自动回退系统端侧识别(SFSpeech)。
|
||||
> 本文档是把真实 SenseVoice 跑起来的「构建 + 设备验证」步骤。
|
||||
|
||||
## 架构(按本项目模块边界 §3.1)
|
||||
|
||||
```
|
||||
ConsultationSheet(UI)
|
||||
→ 录音(ConsultationRecorder,m4a 落 Vault)
|
||||
→ SenseVoiceASRService.transcribe(file) // 离线整段转写,无实时字幕
|
||||
→ AIRuntime.runExclusiveForASR { … } // 进推理闸门 + 卸常驻 LLM/VL 腾内存(防 OOM)
|
||||
→ SenseVoiceBridge(ObjC++) // sherpa-mnn C-API
|
||||
→ sherpa-mnn → MNN 后端(CPU/SME2)
|
||||
→ DiaryAssistService.organizeConsultation(text) // 本地 LLM 整理成问诊小结
|
||||
→ 存为带「问诊」tag 的 DiaryEntry(录音挂 audio Asset)
|
||||
```
|
||||
|
||||
- 引擎/模型未就绪 → 自动回退 SFSpeech;再不行 → 手动文字录入。任何一步都不卡死(红线 #5)。
|
||||
- SenseVoice 是**非流式**:录音中只显示声纹动效,不显示实时字幕,结束后整段转写。
|
||||
|
||||
## 一、构建 sherpa-mnn.xcframework
|
||||
|
||||
```sh
|
||||
MNN_SRC=/Users/xuhuayong/apps/MNN-src sh scripts/build-sherpa-mnn-xcframework.sh
|
||||
```
|
||||
|
||||
- 复用 MNN 源码自带的 `apps/frameworks/sherpa-mnn/build-ios.sh`,产出 `Frameworks/sherpa-mnn.xcframework`
|
||||
(已含 device arm64 + simulator,libtool 合并好的静态库 + `Headers/`)。
|
||||
- 若 sherpa cmake 报找不到 MNN:先 `sh scripts/build-mnn-xcframework.sh` 构建 MNN,
|
||||
再 `export MNN_LIB_DIR=<含 libMNN.a + include 的目录>` 重跑。
|
||||
- Apple Silicon 上若不需要 Intel 模拟器,可在 `build-ios.sh` 里去掉 `simulator_x86_64` 段加速。
|
||||
|
||||
## 二、加进 Xcode 工程
|
||||
|
||||
1. 把 `Frameworks/sherpa-mnn.xcframework` 拖进 target → **Frameworks, Libraries, and Embedded Content**,
|
||||
选 **Do Not Embed**(静态库)。
|
||||
2. **Build Settings → Header Search Paths** 追加(recursive,Debug + Release):
|
||||
```
|
||||
$(PROJECT_DIR)/Frameworks/sherpa-mnn.xcframework/Headers
|
||||
```
|
||||
这样 `SenseVoiceBridge.mm` 里的 `#if __has_include(<sherpa-mnn/c-api/c-api.h>)` 命中,自动切到真实实现。
|
||||
3. sherpa-mnn 用到 C++ 标准库,确保 target 的 **Other Linker Flags** 含 `-lc++`(通常已隐式链接)。
|
||||
4. `MNN.xcframework` 仍需在工程里(sherpa-mnn 依赖它)。即便主 LLM 已切 Gemma-3n/MLX,
|
||||
**不要从工程移除 MNN.xcframework**,否则问诊转写退回 SFSpeech。
|
||||
|
||||
> 工程用 PBXFileSystemSynchronizedRootGroup:`SenseVoiceBridge.{h,mm}` 放在 `康康/AI/MNN/` 下已自动参与编译,
|
||||
> 桥接已在 `康康/康康-Bridging-Header.h` 暴露给 Swift,无需手改 pbxproj。
|
||||
|
||||
## 三、转换并安装 SenseVoice 模型
|
||||
|
||||
```sh
|
||||
MNN_SRC=/Users/xuhuayong/apps/MNN-src sh scripts/convert-sensevoice-mnn.sh
|
||||
```
|
||||
|
||||
产出 `build/SenseVoice/{model.mnn, tokens.txt}`(转的是 **fp32** `model.onnx`,8bit 权重量化;勿转 int8 版)。
|
||||
安装到沙盒 `Application Support/Models/SenseVoice/`:
|
||||
|
||||
- **模拟器**:拷到
|
||||
`~/Library/Developer/CoreSimulator/Devices/<id>/data/Containers/Data/Application/<id>/Library/Application Support/Models/SenseVoice/`
|
||||
- **真机**:经「我的 · 模型管理」旁路导入,或用调试构建预拷进沙盒。
|
||||
|
||||
就位判定:`SenseVoiceASRService.isModelInstalled`(model.mnn + tokens.txt 都在)。
|
||||
引擎+模型都就绪后 `SenseVoiceASRService.isAvailable == true`,问诊即走 SenseVoice。
|
||||
|
||||
## 四、设备验证
|
||||
|
||||
1. 真机打开「记一笔 → 记录问诊 → 开始录音」,说几句(含数值/药名,验数字保真)。
|
||||
2. 「结束并整理」后应看到「正在转写录音 · 本地 SenseVoice」,而非「本机识别」。
|
||||
3. 转写稿交本地 LLM 整理成问诊小结,保存后在记录详情里能回放原声、看小结。
|
||||
4. 故意不装模型 → 应自动回退「本机识别」(SFSpeech),功能仍可用。
|
||||
|
||||
## 备注:与 Gemma-3n/MLX 主线的关系
|
||||
|
||||
- 转写(SenseVoice/MNN)与文本生成(Gemma-3n/MLX)互相独立:ASR 用 MNN 后端,LLM 整理用 MLX。
|
||||
二者经 `AIRuntime` 闸门**串行**,不会同时常驻内存(§3.1)。
|
||||
- 这是目前工程里**唯一**仍依赖 MNN 的路径。若决定彻底移除 MNN,问诊转写会退回 SFSpeech;
|
||||
桥接的 `__has_include` 守卫保证那时仍能编译。
|
||||
- 若希望改走 sherpa-onnx(onnxruntime,不依赖 MNN),只需把 `SenseVoiceBridge.mm` 里的
|
||||
`SherpaMnn*` C-API 换成对应的 `SherpaOnnx*`(结构同名),其余 Swift 层不动。
|
||||
BIN
docs/release/xhs-9grid/00-9宫格总览.png
Normal file
|
After Width: | Height: | Size: 489 KiB |
BIN
docs/release/xhs-9grid/01-封面-本地健康AI档案.png
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
docs/release/xhs-9grid/02-拍报告自动变档案.png
Normal file
|
After Width: | Height: | Size: 374 KiB |
BIN
docs/release/xhs-9grid/03-Qwen-MNN-SME2性能自检.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
docs/release/xhs-9grid/04-身体档案就诊摘要.png
Normal file
|
After Width: | Height: | Size: 281 KiB |
BIN
docs/release/xhs-9grid/05-记录指标到趋势解读.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
docs/release/xhs-9grid/06-药盒识别入药品库.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
docs/release/xhs-9grid/07-语音日记AI追问.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
docs/release/xhs-9grid/08-记录入口和症状追踪.png
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
docs/release/xhs-9grid/09-开发记录端侧推理.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
86
docs/release/小红书长文-Google赛道版.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# 康康 · 小红书长文(Google 赛道版)
|
||||
|
||||
> 改写自原 MNN 版。核心转向:出海 / 低资源 / 多语言 / Google 技术(Gemma + Gemini)。
|
||||
> 发布前替换:比赛官方话题、@官方账号、真机数字、demo 视频。
|
||||
|
||||
---
|
||||
|
||||
## 标题备选
|
||||
|
||||
1. 我做了个 App:化验单看不懂?拍一下,断网也能读
|
||||
2. 把 Google 的 AI 塞进手机,帮你读懂外语体检报告
|
||||
3. Day_ | 不上传、断网也能用的健康 AI(Gemma + Gemini)
|
||||
4. 缺医、弱网、看不懂报告——我用端侧 AI 做了个解法
|
||||
|
||||
推荐主标题:**Day_ | 不上传、断网也能用的健康 AI**
|
||||
封面副字:**Gemma 端侧离线 + Gemini 读图/翻译 · 你的母语**
|
||||
|
||||
---
|
||||
|
||||
## 正文主稿
|
||||
|
||||
很多人拿到一张化验单,第一反应是:这些箭头、缩写、参考范围,到底哪项要紧?
|
||||
|
||||
如果身边没有医生,或者报告还是外语的,就更难。而最常见的「拍给云端 AI」方式,又得把姓名、年龄、医院、检查结果这些最私密的信息传上去。
|
||||
|
||||
所以我做了「康康」——一个**默认在手机本地跑、隐私优先**的健康助手。目标人群不只是国内,而是**弱网/缺医地区,和在国外看不懂医疗文件的人**。
|
||||
|
||||
它现在能完成一个完整闭环:
|
||||
|
||||
拍一张报告 → 读出指标(数值、单位、参考范围、异常)→ 存成可检索的档案 → 看趋势 → 问它「最近哪些不正常、就诊前帮我整理重点」。
|
||||
|
||||
技术上我用的是 **Google 的 AI,双路 hybrid**:
|
||||
|
||||
- **端侧默认:Gemma-3n**(Google 开源的手机端多模态小模型)。**飞行模式也能用**——这对网络不稳的地方是刚需。数据不出设备。
|
||||
- **联网增强:Gemini 2.5 Flash**。你主动开启后,**拍报告原图直接让 Gemini 读图**出结构化指标;**外语报告**也能 Gemini 翻译 + 用你的母语讲明白。
|
||||
- 断网或额度用尽,**自动回退端侧**,功能不中断。
|
||||
|
||||
为什么是这套组合?因为低资源场景同时要「离线能用」和「读得准、能翻译」两件事:前者只有端侧小模型能给(Gemma-3n),后者需要强多模态和多语言(Gemini)。一个负责隐私与可用性,一个负责能力,正好互补。
|
||||
|
||||
我特别在意一点:这不是一个「会聊天的健康机器人」,而是把健康资料变成**你自己的长期记忆**——不用再翻相册找去年的化验单,看医生前 30 秒就能整理出重点。它不替你诊断,也不给用药建议,只负责把你已有的信息收好、读懂、讲清楚。
|
||||
|
||||
整个 App 是我用 AI 辅助(vibe coding)在几周内做出来的。
|
||||
|
||||
健康数据,应该先属于你自己——哪怕没有网络,哪怕你在异国他乡。
|
||||
|
||||
#比赛官方话题# #端侧AI #Gemma #Gemini #GoogleAI #本地大模型 #健康管理 #出海 #隐私保护 #iOS开发
|
||||
|
||||
---
|
||||
|
||||
## 9 宫格图片排序(Google 赛道)
|
||||
|
||||
| 顺序 | 图片 | 图中文字 | 作用 |
|
||||
|---|---|---|---|
|
||||
| 1 | 首页 + 封面大字 | 不上传、断网也能用的健康 AI | 先讲是什么 |
|
||||
| 2 | 拍报告 → Gemini 读图结构化 | 拍一张,报告变档案 | 第一卖点(Gemini 核心) |
|
||||
| 3 | 飞行模式 + 正在生成 | 断网也能 AI 解读 | 低资源杀手锏(Gemma 端侧) |
|
||||
| 4 | 外语报告 → 母语解读 | 看不懂的外语报告,翻译+讲明白 | 出海痛点(Gemini 多语言) |
|
||||
| 5 | 我的 · 云端 AI(Gemini 开关) | 隐私优先,云端可选 | hybrid + Google 产品证据 |
|
||||
| 6 | 趋势页 AI 解读 | 长期变化看得懂 | 长期价值 |
|
||||
| 7 | 就诊摘要 | 看医生前自动整理 | 应用价值 |
|
||||
| 8 | 药盒识别 | 药盒也能入档 | 扩展场景 |
|
||||
| 9 | 架构/开发图 | Gemma + Gemini + SwiftUI | 收尾,vibe coding |
|
||||
|
||||
---
|
||||
|
||||
## 封面字方案
|
||||
|
||||
**技术向**
|
||||
- 主字:把 Google AI 塞进手机
|
||||
- 副字:Gemma 端侧离线 + Gemini 读图翻译
|
||||
- 角标:断网也能用
|
||||
|
||||
**大众向**
|
||||
- 主字:化验单看不懂?拍一下
|
||||
- 副字:不上传,断网也能读,还能翻译外语报告
|
||||
- 角标:几周独立开发 demo
|
||||
|
||||
---
|
||||
|
||||
## 仍需补的素材(对应评审「完成度与传播」)
|
||||
|
||||
1. **Gemini 读一张报告的 10 秒录屏**——最值钱的证据,务必有。
|
||||
2. 飞行模式 + 端侧生成。
|
||||
3. 外语报告 → 母语解读。
|
||||
4. 云端 AI 设置页(Gemini 开关)。
|
||||
5. 片头/转场可用 Veo / Google Flow 生成。
|
||||
318
docs/release/小红书长文-项目介绍.md
Normal file
@@ -0,0 +1,318 @@
|
||||
# 康康 · 小红书长文正文与 9 宫格方案
|
||||
|
||||
> 参考风格:「Day5|30小时,用 AI 开发的 APP 完工了」这类独立开发记录口吻。
|
||||
> 使用前替换:比赛官方话题、@官方账号、真机 tok/s 数字、开发耗时。
|
||||
|
||||
---
|
||||
|
||||
## 标题备选
|
||||
|
||||
1. Day42|我做了个不上传的健康 AI App
|
||||
2. 6 周做完:体检报告 AI 解读,但不联网
|
||||
3. 把大模型塞进 iPhone,做了个健康档案 App
|
||||
4. 体检报告别再乱拍给云端 AI 了
|
||||
5. 用 MNN 在本地跑健康 AI,我把 demo 做完了
|
||||
|
||||
推荐主标题:
|
||||
|
||||
**Day42|我做了个不上传的健康 AI App**
|
||||
|
||||
副标题可放封面小字:
|
||||
|
||||
**Qwen + MNN + SME2,体检报告在手机本地解读**
|
||||
|
||||
---
|
||||
|
||||
## 正文主稿
|
||||
|
||||
Day42,这个 6 周 demo 终于能完整跑起来了。
|
||||
|
||||
我做了一个叫「康康」的 iOS App。它的目标很简单:把体检报告、化验指标、症状、日记和用药记录整理成一个本地健康档案,再让 AI 用大白话帮你看懂。
|
||||
|
||||
但我给自己加了一条硬限制:
|
||||
|
||||
**健康数据不上传。**
|
||||
|
||||
这件事一开始听起来有点拧巴。因为现在最常见的 AI 解读方式,就是把报告拍照发给云端模型。可体检报告里有姓名、年龄、医院、检查指标、既往问题,基本是一个人最私密的数据之一。
|
||||
|
||||
所以我想做一个反过来的版本:
|
||||
|
||||
不是把报告交给云端 AI,而是把 AI 放进手机里。
|
||||
|
||||
---
|
||||
|
||||
这版「康康」现在能完成一个比较完整的闭环:
|
||||
|
||||
1. 拍一张体检报告或化验单
|
||||
2. 本地识别图片里的指标
|
||||
3. 把数值、单位、参考范围、异常状态整理成结构化档案
|
||||
4. 后续可以看趋势,比如体重、血糖、血脂、血压
|
||||
5. 也可以问它:最近哪些指标异常?就诊前帮我整理一份摘要
|
||||
|
||||
整个过程不需要账号,不接云服务,没有广告 SDK,也没有第三方分析 SDK。
|
||||
|
||||
数据存在 iPhone 本地。报告原图进本地 Vault,SwiftData 存结构化记录,Face ID 可以给 App 加启动锁。
|
||||
|
||||
---
|
||||
|
||||
技术上最核心的一点是端侧推理。
|
||||
|
||||
这次我主路径用的是:
|
||||
|
||||
**Qwen3.5-2B + MNN + iPhone CPU/SME2**
|
||||
|
||||
MNN 是阿里开源的推理框架。我的目标不是“能接上一个 API 就算 AI App”,而是让模型真的在手机本地吐 token。
|
||||
|
||||
App 里做了一个推理引擎页,可以看到当前后端:
|
||||
|
||||
- 自动模式:真机优先走 MNN
|
||||
- MNN:CPU + SME2/NEON
|
||||
- MLX:兜底和对照
|
||||
|
||||
在支持 SME2 的设备上,MNN 会走端侧 CPU 加速。性能自检页会跑固定 prompt,直接显示 prefill 和 decode 速度。这里不是 PPT 数字,是 App 内可以复现的实测。
|
||||
|
||||
目前真机结果:
|
||||
|
||||
**读入:122 tok/s,生成:33.0 tok/s,总耗时:1.1s**
|
||||
|
||||
这个速度已经足够支撑健康档案里的短问答、趋势解读和报告整理。
|
||||
|
||||
---
|
||||
|
||||
为了让它不是“套壳聊天框”,我把 AI 链路拆成了几层:
|
||||
|
||||
UI 不直接调模型。
|
||||
|
||||
拍照识别走 CaptureService,身体档案问答走 HealthExportService,趋势解读走 TrendInsightService,最后统一进入 AIRuntime。
|
||||
|
||||
AIRuntime 是 actor,里面做了推理闸门。同一时间只允许一个重推理任务占模型内存。因为端侧模型最怕的不是慢,而是多个任务同时加载,内存峰值上去以后直接被系统杀掉。
|
||||
|
||||
这也是做端侧 AI 和做云端 API 最大的区别之一:
|
||||
|
||||
云端是请求排队。
|
||||
|
||||
手机上是内存、功耗、模型加载、UI 响应一起排队。
|
||||
|
||||
---
|
||||
|
||||
我觉得这个项目真正有价值的地方,不是“AI 能不能讲两句漂亮话”,而是它把健康资料变成了自己的长期记忆。
|
||||
|
||||
比如:
|
||||
|
||||
你不用再翻相册找去年那张化验单。
|
||||
|
||||
也不用每次看医生前,临时回忆“我最近到底哪几项不正常”。
|
||||
|
||||
你可以把症状、用药、指标、报告都记在一个地方,需要时生成一份就诊摘要。它不会替医生判断,也不会给剂量建议,只负责把你已有的信息整理清楚。
|
||||
|
||||
对普通人来说,这比一个会聊天的健康机器人更实用。
|
||||
|
||||
---
|
||||
|
||||
这次我最想表达的创新点有三个:
|
||||
|
||||
第一,健康 AI 应该默认本地优先。
|
||||
|
||||
不是所有数据都适合上传。健康数据尤其应该有“不出设备也能用”的选择。
|
||||
|
||||
第二,端侧大模型已经能做完整产品闭环。
|
||||
|
||||
不是只跑一个 demo prompt,而是接进真实 App:模型管理、性能自检、拍照识别、趋势解读、问答摘要、失败兜底、隐私设置,这些都要一起工作。
|
||||
|
||||
第三,RAG 不一定非要 embedding。
|
||||
|
||||
康康里的健康记录本来就是结构化数据。指标名、时间、报告类型、症状、用药都很明确。很多问题可以先抽取意图,再从 SwiftData 里查相关记录,最后让本地模型生成回答。这样更轻,也更适合手机。
|
||||
|
||||
---
|
||||
|
||||
现在还有很多地方可以继续打磨,比如报告详情页、飞行模式演示图、Live Activity 的实时 tok/s、更多真机性能对比。
|
||||
|
||||
但这版已经证明了一件事:
|
||||
|
||||
**个人健康档案 + 本地大模型 + MNN 端侧推理,是可以组成一个完整应用的。**
|
||||
|
||||
如果说云端 AI 更像一个很聪明的外部顾问,那我希望康康更像一个安静放在手机里的私人健康档案员。
|
||||
|
||||
它不替你诊断。
|
||||
|
||||
不替医生做决定。
|
||||
|
||||
它只帮你把自己的身体记录收好、看懂、在需要的时候讲清楚。
|
||||
|
||||
这就是我这次做「康康」的原因。
|
||||
|
||||
参加 #比赛官方话题# 的作品记录。后面如果有时间,我会继续补飞行模式演示、Live Activity 和完整 demo 视频。
|
||||
|
||||
声明:康康只做健康信息记录、整理和科普式解读,不是医疗器械,不提供诊断、治疗、用药或剂量建议。任何健康决策请咨询专业医生。
|
||||
|
||||
---
|
||||
|
||||
## 精简版正文
|
||||
|
||||
我做了一个叫「康康」的 iOS App。
|
||||
|
||||
它能把体检报告、化验指标、症状、日记和用药记录整理成一个本地健康档案,再用 AI 帮你做大白话解读。
|
||||
|
||||
最关键的是:不上传。
|
||||
|
||||
我不想把体检报告这种高度隐私的数据拍给云端模型,所以这次把 Qwen3.5-2B 放进了手机本地,主推理路径用 MNN,在支持 SME2 的 iPhone 上走端侧 CPU 加速。
|
||||
|
||||
现在 App 里已经能看到完整闭环:
|
||||
|
||||
拍报告 → 本地识别 → 结构化指标 → 趋势图 → 身体档案问答 → 就诊摘要。
|
||||
|
||||
技术上做了双后端:
|
||||
|
||||
- MNN:真机主路径,CPU/SME2
|
||||
- MLX:模拟器和兜底
|
||||
|
||||
App 里还有性能自检页,当前真机实测:
|
||||
|
||||
读入 122 tok/s,生成 33.0 tok/s,总耗时 1.1s。
|
||||
|
||||
我觉得这个项目最有意思的地方,不是做了一个 AI 聊天框,而是把本地大模型真正接进了健康档案系统。
|
||||
|
||||
它不会替你诊断,也不会给用药建议。它只做三件事:
|
||||
|
||||
帮你收好记录,帮你看懂变化,帮你在看医生前把重点讲清楚。
|
||||
|
||||
健康数据应该先属于自己。
|
||||
|
||||
这就是我做「康康」的原因。
|
||||
|
||||
#比赛官方话题# #端侧AI #MNN #Qwen #本地大模型 #健康管理 #iOS开发 #SwiftUI #隐私保护 #独立开发
|
||||
|
||||
---
|
||||
|
||||
## 9 宫格图片排序
|
||||
|
||||
### 当前素材可发版
|
||||
|
||||
| 顺序 | 图片 | 封面字/图中文字 | 作用 |
|
||||
|---|---|---|---|
|
||||
| 1 | `21-首页-健康日历趋势提醒.png` | 不上传的健康 AI 档案 | 封面,先讲产品是什么 |
|
||||
| 2 | `19-报告归档-核对报告信息.png` | 拍报告,自动变档案 | 展示核心创意:报告归档 |
|
||||
| 3 | `22-框选异常指标-本地识别中.png` | 图片在本地识别 | 展示视觉识别过程 |
|
||||
| 4 | `06-推理引擎-MNN-SME2性能自检.png` | Qwen + MNN + SME2 | 技术证据和比赛亮点 |
|
||||
| 5 | `07-模型管理-Qwen已就绪.png` | 1.19GB 模型在手机里 | 证明不是云端 API |
|
||||
| 6 | `11-身体档案-就诊摘要生成结果.png` | 就诊前 30 秒整理重点 | 展示应用价值 |
|
||||
| 7 | `09-趋势详情-体重AI解读.png` | 长期趋势,AI 讲人话 | 展示长期使用价值 |
|
||||
| 8 | `20-药品识别-核对药品.png` | 药盒也能本地识别 | 展示扩展场景 |
|
||||
| 9 | `02-Xcode开发调试界面.png` | 6 周独立开发记录 | 收尾,增强真实开发感 |
|
||||
|
||||
### 如果补图后的更强版
|
||||
|
||||
| 顺序 | 建议图片 | 封面字/图中文字 |
|
||||
|---|---|---|
|
||||
| 1 | 首页 + 大字封面 | 不上传的健康 AI 档案 |
|
||||
| 2 | 飞行模式 + 正在生成 | 断网也能 AI 解读 |
|
||||
| 3 | 报告拍照前后对比 | 拍一下,报告变档案 |
|
||||
| 4 | 报告详情三 Tab | 原图、解读、指标都留存 |
|
||||
| 5 | MNN 性能自检 | Qwen + MNN + SME2 |
|
||||
| 6 | 模型管理页 | 模型真的在本机 |
|
||||
| 7 | 身体档案摘要 | 看医生前自动整理 |
|
||||
| 8 | 趋势页 | 长期变化看得懂 |
|
||||
| 9 | Xcode/架构图 | SwiftUI + SwiftData + MNN |
|
||||
|
||||
---
|
||||
|
||||
## 封面字方案
|
||||
|
||||
### 方案 A:大众向
|
||||
|
||||
主字:
|
||||
|
||||
**体检报告 AI 解读**
|
||||
|
||||
副字:
|
||||
|
||||
**不联网,不上传,只在手机里跑**
|
||||
|
||||
角标:
|
||||
|
||||
**6 周独立开发 demo**
|
||||
|
||||
### 方案 B:技术向
|
||||
|
||||
主字:
|
||||
|
||||
**把 Qwen 塞进 iPhone**
|
||||
|
||||
副字:
|
||||
|
||||
**MNN + SME2 本地推理健康档案**
|
||||
|
||||
角标:
|
||||
|
||||
**decode 33.0 tok/s**
|
||||
|
||||
### 方案 C:参考 Day 风格
|
||||
|
||||
主字:
|
||||
|
||||
**Day42 做完一个本地健康 AI**
|
||||
|
||||
副字:
|
||||
|
||||
**体检报告拍一下,但不交给云端**
|
||||
|
||||
角标:
|
||||
|
||||
**SwiftUI + MNN + Qwen**
|
||||
|
||||
推荐使用方案 C,更贴近你给的参考笔记风格,同时还能保留项目技术点。
|
||||
|
||||
---
|
||||
|
||||
## 每张图建议加字
|
||||
|
||||
1. `21-首页-健康日历趋势提醒.png`
|
||||
- 主字:Day42 做完一个本地健康 AI
|
||||
- 小字:体检报告、指标、症状、用药都存在手机里
|
||||
|
||||
2. `19-报告归档-核对报告信息.png`
|
||||
- 主字:拍报告,自动变档案
|
||||
- 小字:原图和结构化信息一起保存
|
||||
|
||||
3. `22-框选异常指标-本地识别中.png`
|
||||
- 主字:图片识别也在本地
|
||||
- 小字:异常指标可框选识别
|
||||
|
||||
4. `06-推理引擎-MNN-SME2性能自检.png`
|
||||
- 主字:Qwen + MNN + SME2
|
||||
- 小字:生成 33.0 tok/s
|
||||
|
||||
5. `07-模型管理-Qwen已就绪.png`
|
||||
- 主字:模型真的在手机里
|
||||
- 小字:Qwen3.5-2B 本地就绪
|
||||
|
||||
6. `11-身体档案-就诊摘要生成结果.png`
|
||||
- 主字:看医生前自动整理
|
||||
- 小字:症状、指标、用药一份摘要带走
|
||||
|
||||
7. `09-趋势详情-体重AI解读.png`
|
||||
- 主字:长期变化看得懂
|
||||
- 小字:折线图 + AI 大白话解读
|
||||
|
||||
8. `20-药品识别-核对药品.png`
|
||||
- 主字:药盒也能入档
|
||||
- 小字:只记录,不提供用药建议
|
||||
|
||||
9. `02-Xcode开发调试界面.png`
|
||||
- 主字:不是套壳聊天框
|
||||
- 小字:SwiftUI + SwiftData + MNN 端侧推理
|
||||
|
||||
---
|
||||
|
||||
## 仍建议补充的截图
|
||||
|
||||
1. 飞行模式打开 + App 正在流式生成,这是“不上传”的最强证据。
|
||||
2. 报告详情页三 Tab:原图 / 解读 / 指标,用于证明档案浏览闭环。
|
||||
3. Face ID 启动锁页面,用于补足隐私三件套。
|
||||
4. 如果 Live Activity 已能跑,补锁屏 tok/s,非常适合比赛记忆点。
|
||||
|
||||
---
|
||||
|
||||
## 标签
|
||||
|
||||
#比赛官方话题# #端侧AI #MNN #Qwen #本地大模型 #健康管理 #体检报告解读 #隐私保护 #iOS开发 #SwiftUI #独立开发 #数字健康
|
||||