Голосовой ввод показателей в дневник пациента (Web Speech API, распознавание русских числительных)

This commit is contained in:
Dauren777 2026-06-30 08:59:13 +00:00
parent 242766ad0b
commit 40e6344a10

View File

@ -727,6 +727,87 @@ function readDevice(kind){
}
}
/* ============ ГОЛОСОВОЙ ВВОД (Web Speech API) ============ */
var RU_ONES={ноль:0,один:1,одна:1,два:2,две:2,три:3,четыре:4,пять:5,шесть:6,семь:7,восемь:8,девять:9};
var RU_TEENS={десять:10,одиннадцать:11,двенадцать:12,тринадцать:13,четырнадцать:14,пятнадцать:15,шестнадцать:16,семнадцать:17,восемнадцать:18,девятнадцать:19};
var RU_TENS={двадцать:20,тридцать:30,сорок:40,пятьдесят:50,шестьдесят:60,семьдесят:70,восемьдесят:80,девяносто:90};
var RU_HUND={сто:100,двести:200,триста:300,четыреста:400,пятьсот:500};
function isRuNum(w){ return RU_ONES[w]!=null||RU_TEENS[w]!=null||RU_TENS[w]!=null||RU_HUND[w]!=null; }
function parseRuNum(tokens, start){
var total=0, found=false, i=start;
while(i<tokens.length){
var w=tokens[i].toLowerCase().replace(/[.,!?;:]/g,"");
if(RU_HUND[w]!=null){ total+=RU_HUND[w]; found=true; i++; }
else if(RU_TENS[w]!=null){ total+=RU_TENS[w]; found=true; i++; }
else if(RU_TEENS[w]!=null){ total+=RU_TEENS[w]; found=true; i++; break; }
else if(RU_ONES[w]!=null){ total+=RU_ONES[w]; found=true; i++; break; }
else break;
}
return {num:total, next:i, found:found};
}
function wordsToDigits(text){
var tokens=text.split(/\s+/); var out=[], i=0;
while(i<tokens.length){
var w=tokens[i].toLowerCase().replace(/[.,!?;:]/g,"");
if(isRuNum(w)){ var r=parseRuNum(tokens,i); out.push(String(r.num)); i=r.next; }
else { out.push(tokens[i]); i++; }
}
return out.join(" ");
}
function parseVitals(transcript){
var text=wordsToDigits((transcript||"").toLowerCase());
var res={};
var m;
if(/(сатурац|кислород|spo|эспио|эспо|сату)/.test(text)){
m=text.match(/(?:сатурац\w*|кислород\w*|spo\w*|эспио\w*|эспо\w*|сату\w*)\D{0,6}(\d{2,3})/);
if(m) res.spo2=+m[1];
}
if(/(давлен|pressure|ад\b|давление)/.test(text)){
var bm=text.match(/(?:давлен\w*|pressure|ад)\D{0,8}(\d{2,3})\D{1,6}(\d{2,3})/);
if(bm){ res.sys=+bm[1]; res.dia=+bm[2]; }
else { var bm2=text.match(/(?:давлен\w*|pressure|ад)\D{0,6}(\d{2,3})/); if(bm2) res.sys=+bm2[1]; }
}
if(/(пульс|сердц|чсс|частота пульс)/.test(text)){
m=text.match(/(?:пульс\w*|сердц\w*|чсс)\D{0,6}(\d{2,3})/);
if(m) res.hr=+m[1];
}
if(/(дыхан|чдд|частота дых)/.test(text)){
m=text.match(/(?:дыхан\w*|чдд)\D{0,6}(\d{2})/);
if(m) res.rr=+m[1];
}
return res;
}
function setFieldIf(id,val){ var f=$("#"+id); if(f && val!=null && !isNaN(val)){ f.value=val; return true; } return false; }
function startVoiceInput(btn){
var SR=window.SpeechRecognition||window.webkitSpeechRecognition;
if(!SR){ toast("Голосовой ввод не поддерживается. Откройте в Chrome/Edge."); return; }
if(btn.dataset.listening==="1"){ window.__rec && window.__rec.stop(); return; }
var rec=new SR();
window.__rec=rec;
rec.lang="ru-RU"; rec.continuous=false; rec.interimResults=false;
var stat=$("#voiceStat");
btn.dataset.listening="1"; btn.textContent="⏹ Стоп"; btn.style.background="var(--red)"; btn.style.color="#fff";
if(stat){ stat.style.display="inline"; stat.textContent="🔴 Слушаю… скажите: «сатурация 95, давление 130 на 85, пульс 78, дыхание 18»"; }
rec.onresult=function(ev){
var tr=ev.results[0][0].transcript;
var v=parseVitals(tr);
var parts=[];
if(v.spo2!=null){ setFieldIf("spo2",v.spo2); parts.push("SpO₂ "+v.spo2); }
if(v.sys!=null){ setFieldIf("bpSys",v.sys); parts.push("АД сис "+v.sys); }
if(v.dia!=null){ setFieldIf("bpDia",v.dia); parts.push("АД диа "+v.dia); }
if(v.hr!=null){ setFieldIf("hr",v.hr); parts.push("ЧСС "+v.hr); }
if(v.rr!=null){ setFieldIf("rr",v.rr); parts.push("ЧДД "+v.rr); }
toast(parts.length?("Распознано: "+parts.join(", ")):"Не удалось распознать показатели. Скажите чётче.");
if(stat) stat.textContent="«"+tr+"»";
};
rec.onerror=function(ev){ toast("Ошибка распознавания: "+(ev.error||"")); };
rec.onend=function(){
btn.dataset.listening="0"; btn.textContent="🎤 Голосом"; btn.style.background=""; btn.style.color="";
if(stat){ setTimeout(function(){ stat.style.display="none"; }, 2500); }
};
try{ rec.start(); }catch(e){ toast("Не удалось запустить микрофон"); rec.onend(); }
}
function historyTable(p){
var t = el("table","hist");
var thead = el("thead"); thead.innerHTML='<tr><th>Дата</th><th>SpO₂</th><th>АД</th><th>ЧСС</th><th>ЧДД</th><th>mMRC</th><th>CAT</th><th>Зона</th></tr>';
@ -773,6 +854,12 @@ function openDiaryModal(p){
var bBp = el("button","btn ghost sm","🩺 Тонометр");
bBp.onclick=function(){ readDevice("bp"); };
devBar.appendChild(bOxy); devBar.appendChild(bBp);
var bVoice = el("button","btn ghost sm","🎤 Голосом");
bVoice.id="voiceBtn";
bVoice.onclick=function(){ startVoiceInput(bVoice); };
devBar.appendChild(bVoice);
var voiceStat = el("span","devlbl","",{id:"voiceStat",style:"display:none"});
devBar.appendChild(voiceStat);
body.appendChild(devBar);
var fg = el("div","form-grid");
fg.appendChild(field('spo2','number','🩸 Сатурация (SpO₂)','90',true,"%, 50100"));