Files
2026-08-11 17:41:36 +08:00

311 lines
8.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
const CN_DIGIT = {
零: 0,
"〇": 0,
一: 1,
壹: 1,
幺: 1,
二: 2,
贰: 2,
两: 2,
三: 3,
叁: 3,
四: 4,
肆: 4,
五: 5,
伍: 5,
六: 6,
陆: 6,
七: 7,
柒: 7,
八: 8,
捌: 8,
九: 9,
玖: 9
};
const CN_UNIT = { 十: 10, 拾: 10, 百: 100, 佰: 100, 千: 1e3, 仟: 1e3, 万: 1e4, 亿: 1e8 };
function cnIntToNumber(s) {
let total = 0;
let section = 0;
let number = 0;
let hadUnit = false;
let lastUnit = 0;
let sawZeroAfterUnit = false;
for (const ch of s) {
if (CN_DIGIT[ch] !== void 0) {
number = CN_DIGIT[ch];
if (number === 0)
sawZeroAfterUnit = true;
} else if (CN_UNIT[ch] !== void 0) {
hadUnit = true;
const unit = CN_UNIT[ch];
lastUnit = unit;
sawZeroAfterUnit = false;
if (unit >= 1e4) {
section = (section + number) * unit;
total += section;
section = 0;
} else {
if (number === 0)
number = 1;
section += number * unit;
}
number = 0;
}
}
if (!hadUnit && s.length > 1) {
let joined = "";
for (const ch of s) {
if (CN_DIGIT[ch] !== void 0)
joined += CN_DIGIT[ch];
}
if (joined)
return Number(joined);
}
if (number > 0 && lastUnit >= 100 && !sawZeroAfterUnit) {
number = number * (lastUnit / 10);
}
return total + section + number;
}
function cnSeqToNumber(seq) {
if (seq.includes("点")) {
const parts = seq.split("点");
const intPart = parts[0];
const decPart = parts.slice(1).join("");
const intVal = intPart ? cnIntToNumber(intPart) : 0;
let decStr = "";
for (const ch of decPart) {
if (CN_DIGIT[ch] !== void 0)
decStr += CN_DIGIT[ch];
}
if (!decStr)
return intVal;
return Number(`${intVal}.${decStr}`);
}
return cnIntToNumber(seq);
}
function hasCnDigit(seq) {
for (const ch of seq) {
if (CN_DIGIT[ch] !== void 0 || CN_UNIT[ch] !== void 0)
return true;
}
return false;
}
function normalizeNumbers(text) {
const raw = String(text || "");
return raw.replace(/[零〇一壹幺二贰两三叁四肆五伍六陆七柒八捌九玖十拾百佰千仟万亿点]+/g, (m) => {
if (!hasCnDigit(m))
return m;
const n = cnSeqToNumber(m);
return Number.isFinite(n) ? String(n) : m;
});
}
function numAfter(text, keys) {
for (const k of keys) {
const i = text.indexOf(k);
if (i >= 0) {
const rest = text.slice(i + k.length);
const m = rest.match(/-?\d+(?:\.\d+)?/);
if (m)
return m[0];
}
}
return "";
}
function textAfter(text, keys, stopKeys = []) {
for (const k of keys) {
const i = text.indexOf(k);
if (i >= 0) {
let rest = text.slice(i + k.length);
rest = rest.replace(/^[是为吃了喝了吃的喝的有打了用了::,,、。\s]+/, "");
let end = rest.length;
const mStop = rest.match(/[,,。.;;!!??\n]/);
if (mStop && mStop.index < end)
end = mStop.index;
for (const sk of stopKeys) {
const si = rest.indexOf(sk);
if (si >= 0 && si < end)
end = si;
}
const seg = rest.slice(0, end).trim();
if (seg)
return seg;
}
}
return "";
}
function parseGlucose(raw) {
const t = normalizeNumbers(raw);
const result = {};
const fasting = numAfter(t, ["空腹"]);
const post = numAfter(t, ["餐后", "饭后"]);
const other = numAfter(t, ["其他", "随机", "睡前", "凌晨", "夜间", "晚上"]);
if (fasting)
result.fasting_blood_sugar = fasting;
if (post)
result.postprandial_blood_sugar = post;
if (other)
result.other_blood_sugar = other;
if (!fasting && !post && !other) {
const nums = t.match(/\d+(?:\.\d+)?/g);
if (nums && nums.length === 1)
result.other_blood_sugar = nums[0];
}
return result;
}
function parseBloodPressure(raw) {
const t = normalizeNumbers(raw);
const result = {};
let sys = numAfter(t, ["高压", "收缩压", "收缩"]);
let dia = numAfter(t, ["低压", "舒张压", "舒张"]);
if (!sys || !dia) {
const nums = (t.match(/\d{2,3}/g) || []).map(Number).filter((n) => n >= 30 && n <= 300);
if (!sys && !dia && nums.length >= 2) {
sys = String(nums[0]);
dia = String(nums[1]);
} else if (!sys && nums.length === 1 && nums[0] >= 90) {
sys = String(nums[0]);
}
}
if (sys && dia && Number(sys) < Number(dia)) {
const tmp = sys;
sys = dia;
dia = tmp;
}
if (sys)
result.systolic_pressure = sys;
if (dia)
result.diastolic_pressure = dia;
const insulin = textAfter(raw, ["胰岛素"], ["高压", "低压", "血压", "西药", "备注"]);
if (insulin)
result.insulin = insulin;
const western = textAfter(raw, ["西药", "降糖药", "口服药"], ["胰岛素", "高压", "低压", "血压", "备注"]);
if (western)
result.western_medicine = western;
return result;
}
const DIET_MARKERS = [
{ keys: ["早餐", "早饭", "早上", "早点", "早晨"], field: "breakfast_foods" },
{ keys: ["午餐", "午饭", "中午"], field: "lunch_foods" },
{ keys: ["晚餐", "晚饭", "晚上", "夜宵", "夜里"], field: "dinner_foods" }
];
function cleanFoodSeg(seg) {
return String(seg || "").replace(/^[是为吃了吃的喝了喝的有::,,、。\s]+/, "").replace(/[。.\s]+$/, "").trim();
}
function parseDiet(raw) {
const t = String(raw || "").trim();
const result = {};
const hits = [];
DIET_MARKERS.forEach((m) => {
for (const k of m.keys) {
const idx = t.indexOf(k);
if (idx >= 0) {
hits.push({ idx, field: m.field, klen: k.length });
break;
}
}
});
if (!hits.length) {
if (t)
result.note = t;
return result;
}
hits.sort((a, b) => a.idx - b.idx);
hits.forEach((h, i) => {
const start = h.idx + h.klen;
const end = i + 1 < hits.length ? hits[i + 1].idx : t.length;
const seg = cleanFoodSeg(t.slice(start, end));
if (seg && !result[h.field])
result[h.field] = seg;
});
const head = cleanFoodSeg(t.slice(0, hits[0].idx));
if (head)
result.note = head;
return result;
}
const EXERCISE_FILLERS = [
"今天",
"我",
"做了",
"做",
"进行了",
"进行",
"运动了",
"运动",
"锻炼了",
"锻炼",
"了",
"大概",
"左右",
"持续",
"差不多",
"一共",
"总共",
"的",
"走了",
"打了",
"跑了"
];
function parseExercise(raw) {
const t = normalizeNumbers(raw);
const result = {};
const durMatch = t.match(/(\d+(?:\.\d+)?)\s*(个小时|小时|钟头|时|分钟|分)/);
if (durMatch) {
const val = parseFloat(durMatch[1]);
const isHour = /个小时|小时|钟头|时/.test(durMatch[2]);
result.duration = String(Math.round(isHour ? val * 60 : val));
}
if (/高强度|剧烈|很累|大汗|气喘/.test(t))
result.intensity = 3;
else if (/中强度|中等强度|中等|有点累|微微出汗|微汗/.test(t))
result.intensity = 2;
else if (/低强度|轻松|轻微|溜达|缓慢/.test(t))
result.intensity = 1;
let type = t;
if (durMatch)
type = type.replace(durMatch[0], "");
type = type.replace(/高强度|中强度|中等强度|低强度|剧烈|很累|大汗|气喘|有点累|微微出汗|微汗|轻松|轻微|缓慢/g, "").replace(/\d+(?:\.\d+)?/g, "").replace(/[,,。.;;、\s]+/g, "");
EXERCISE_FILLERS.forEach((f) => {
type = type.split(f).join("");
});
type = type.trim();
if (type && type.length <= 20)
result.exercise_type = type;
return result;
}
const FIELD_LABELS = {
fasting_blood_sugar: "空腹",
postprandial_blood_sugar: "餐后",
other_blood_sugar: "其他血糖",
systolic_pressure: "高压",
diastolic_pressure: "低压",
western_medicine: "西药",
insulin: "胰岛素",
breakfast_foods: "早餐",
lunch_foods: "午餐",
dinner_foods: "晚餐",
note: "备注",
exercise_type: "运动",
duration: "时长",
intensity: "强度"
};
const INTENSITY_LABELS = { 1: "低强度", 2: "中强度", 3: "高强度" };
function summarizeParsed(parsed) {
const parts = [];
Object.keys(parsed).forEach((k) => {
const label = FIELD_LABELS[k] || k;
let val = parsed[k];
if (k === "intensity")
val = INTENSITY_LABELS[val] || val;
parts.push(`${label}${val}`);
});
return parts.join(" · ");
}
exports.normalizeNumbers = normalizeNumbers;
exports.parseBloodPressure = parseBloodPressure;
exports.parseDiet = parseDiet;
exports.parseExercise = parseExercise;
exports.parseGlucose = parseGlucose;
exports.summarizeParsed = summarizeParsed;
//# sourceMappingURL=../../../.sourcemap/mp-weixin/tongji/utils/voiceParse.js.map