Голосовой ввод показателей в дневник пациента (Web Speech API, распознавание русских числительных)
This commit is contained in:
parent
242766ad0b
commit
40e6344a10
87
index.html
87
index.html
@ -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,"%, 50–100"));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user