feat: add competitor_frames to scoring and trace schema for improved diagnostics
Browse files- app/scorer.py +1 -0
- docs/trace-schema.ts +1 -0
- static/index.html +61 -50
- tests/test_scoring.py +35 -0
app/scorer.py
CHANGED
|
@@ -763,6 +763,7 @@ class Scorer:
|
|
| 763 |
"span": list(fit.al.spans[i]), "window": list(gsc.window), "curves": curves,
|
| 764 |
"p_target": p(gsc.peak), "competitor": heard_ph if not gsc.deleted else None,
|
| 765 |
"competitor_other": gsc.other, "p_competitor": p(gsc.comp_peak),
|
|
|
|
| 766 |
"gop": gsc.gop, "confidence": round(confidence, 3),
|
| 767 |
"gop_factor": round(float(np.exp(gsc.gop * g.leniency)), 3),
|
| 768 |
"confidence_factor": round(float(np.sqrt(confidence)), 3),
|
|
|
|
| 763 |
"span": list(fit.al.spans[i]), "window": list(gsc.window), "curves": curves,
|
| 764 |
"p_target": p(gsc.peak), "competitor": heard_ph if not gsc.deleted else None,
|
| 765 |
"competitor_other": gsc.other, "p_competitor": p(gsc.comp_peak),
|
| 766 |
+
"competitor_frames": gsc.comp_frames,
|
| 767 |
"gop": gsc.gop, "confidence": round(confidence, 3),
|
| 768 |
"gop_factor": round(float(np.exp(gsc.gop * g.leniency)), 3),
|
| 769 |
"confidence_factor": round(float(np.sqrt(confidence)), 3),
|
docs/trace-schema.ts
CHANGED
|
@@ -153,6 +153,7 @@ export interface TraceToken {
|
|
| 153 |
competitor: Ipa | null; // strongest other sound, neighbours and blank excluded (null: none, or unnamed)
|
| 154 |
competitor_other: boolean; // the strongest is a sound outside Arabic and Hebrew
|
| 155 |
p_competitor: number; // its peak posterior in the window
|
|
|
|
| 156 |
gop: number; // ln(p_target / max(p_target, p_competitor)) ≤ 0
|
| 157 |
confidence: number;
|
| 158 |
gop_factor: number;
|
|
|
|
| 153 |
competitor: Ipa | null; // strongest other sound, neighbours and blank excluded (null: none, or unnamed)
|
| 154 |
competitor_other: boolean; // the strongest is a sound outside Arabic and Hebrew
|
| 155 |
p_competitor: number; // its peak posterior in the window
|
| 156 |
+
competitor_frames: number; // frames where it clears deleted_comp; one frame may be an onset transient
|
| 157 |
gop: number; // ln(p_target / max(p_target, p_competitor)) ≤ 0
|
| 158 |
confidence: number;
|
| 159 |
gop_factor: number;
|
static/index.html
CHANGED
|
@@ -752,6 +752,7 @@ body.manage #manage{width:100%;max-width:none}
|
|
| 752 |
.activity-head select{font:inherit;padding:7px 10px;border:1px solid var(--line);border-radius:9px;background:var(--surface);color:var(--ink)}
|
| 753 |
.activity-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.activity-stat{padding:10px;border:1px solid var(--line);border-radius:10px;background:var(--bg)}.activity-stat b{display:block;font:400 28px/1 var(--f-display);font-variant-numeric:tabular-nums}.activity-stat small{color:var(--muted);font-size:12px}
|
| 754 |
.activity-chart{width:100%;height:190px;overflow:visible}.activity-chart .grid{stroke:var(--line);stroke-width:1}.activity-chart .bar{fill:var(--olive);opacity:.82}.activity-chart .bar:hover{opacity:1}.activity-chart text{fill:var(--muted);font:11px var(--f-body)}
|
|
|
|
| 755 |
.system-activity summary{cursor:pointer}.system-activity summary h2{display:inline;margin:0}.system-activity details[open]>summary{margin-bottom:8px}
|
| 756 |
.system-hard{margin:2px 0 4px;padding:10px;border-radius:10px;background:var(--bg)}.system-hard h3{margin:0 0 6px;font:700 15px/1.3 var(--f-body)}.system-hard-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;padding:6px 0;border-bottom:1px solid var(--line)}.system-hard-row:last-child{border-bottom:0}.system-hard-row span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.system-hard-row small{color:var(--muted);white-space:nowrap}.system-feed{display:flex;flex-direction:column;gap:14px}.system-topic{border-top:1px solid var(--line);padding-top:10px}.system-topic:first-child{border-top:0;padding-top:0}.system-topic h3{margin:0 0 3px;font:700 15px/1.3 var(--f-body)}.system-topic h3 small{color:var(--muted);font-weight:600}.system-event{display:grid;grid-template-columns:auto 1fr auto;gap:10px;align-items:center;padding:8px 0;border-bottom:1px solid var(--line)}.system-event:last-child{border-bottom:0}.system-event i{display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--olive-soft);font-style:normal}.system-event b,.system-event small{display:block}.system-event small,.system-event time{color:var(--muted);font-size:12px}.system-event time{direction:ltr;white-space:nowrap}
|
| 757 |
/* the API keys card (developers only): a row per key, where it comes from and a field for a new one; a key is never shown */
|
|
@@ -4469,60 +4470,70 @@ function drawFlags(){
|
|
| 4469 |
}
|
| 4470 |
card.append(table); box.append(card);
|
| 4471 |
}
|
| 4472 |
-
|
| 4473 |
-
|
| 4474 |
-
|
| 4475 |
-
|
| 4476 |
-
|
| 4477 |
-
|
| 4478 |
-
|
| 4479 |
-
|
| 4480 |
-
|
| 4481 |
-
|
| 4482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4483 |
try{
|
| 4484 |
-
const res=await api(`/api/flags/${f.id}
|
| 4485 |
-
|
| 4486 |
-
|
| 4487 |
-
|
| 4488 |
-
}catch(err){ audioMsg.textContent="ההשמעה נכשלה: "+err.message; play.disabled=false; }
|
| 4489 |
};
|
| 4490 |
-
|
| 4491 |
-
}
|
| 4492 |
-
const field=document.createElement("fieldset"); field.append(mark("","אילו אבחנות של המערכת היו שגויות?","legend"));
|
| 4493 |
-
for(const issue of issues){ const label=document.createElement("label"), input=document.createElement("input"); input.type="checkbox"; input.checked=true; input._issue=issue; label.append(input,issue.label); field.append(label); }
|
| 4494 |
-
if(issues.length) card.append(field); else card.append(mark("meta","בדיווח הזה אין פונמה שסומנה במפורש; עדיין אפשר לסווג ולאשר אותו.","p"));
|
| 4495 |
-
const category=document.createElement("select"); for(const [value,label] of Object.entries(FLAG_CATEGORIES)){ const o=document.createElement("option"); o.value=value; o.textContent=label; category.append(o); }
|
| 4496 |
-
const note=document.createElement("textarea"); note.maxLength=500; note.rows=2; note.placeholder="הערת בדיקה (לא חובה)";
|
| 4497 |
-
const actions=mark("row",""), diagnose=mark("btn","לוח אבחון","button"), confirm=mark("btn primary","אישור הדיווח","button"), reject=mark("btn danger","דחייה","button"), msg=mark("msg","");
|
| 4498 |
-
diagnose.disabled=!result.trace;
|
| 4499 |
-
diagnose.title=result.trace?"פתיחת האבחון המלא, כולל טבלת ההגאים":"הדיווח ישן ואינו כולל נתוני trace";
|
| 4500 |
-
diagnose.onclick=async()=>{
|
| 4501 |
-
const tab=window.open("about:blank","_blank");
|
| 4502 |
-
diagnose.disabled=true; msg.textContent=f.audio?"טוען אבחון וקול…":"פותח אבחון…";
|
| 4503 |
-
const selected=[...field.querySelectorAll("input:checked")].map(x=>({word:x._issue.word,expected:x._issue.expected,heard:x._issue.heard}));
|
| 4504 |
-
let audioUrl="";
|
| 4505 |
-
if(f.audio) try{
|
| 4506 |
-
const res=await api(`/api/flags/${f.id}/audio`); if(!res.ok) throw new Error(res.status);
|
| 4507 |
-
audioUrl=URL.createObjectURL(await res.blob());
|
| 4508 |
-
}catch(err){ msg.textContent="הקול לא נטען; לוח האבחון נפתח ללא קול."; }
|
| 4509 |
-
openTrace({...result,phrase:{he:f.he||f.phrase,ar:f.ar||""},_diagnostic:{prompt_he:f.he||f.phrase,answer_ar:f.ar,
|
| 4510 |
-
expected_ipa:f.ipa,model:f.model,version:f.version,report_note:f.note||"",review_category:category.value,
|
| 4511 |
-
reviewer_marked_false_issues:selected}},audioUrl,tab);
|
| 4512 |
-
diagnose.disabled=false; if(!msg.textContent.includes("לא נטען")) msg.textContent="";
|
| 4513 |
-
};
|
| 4514 |
-
const save=async status=>{
|
| 4515 |
-
confirm.disabled=reject.disabled=true; msg.textContent="שומר…";
|
| 4516 |
-
const false_issues=[...field.querySelectorAll("input:checked")].map(x=>({word:x._issue.word,expected:x._issue.expected,heard:x._issue.heard}));
|
| 4517 |
-
try{
|
| 4518 |
-
const res=await api(`/api/flags/${f.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({status,category:category.value,false_issues,review_note:note.value})});
|
| 4519 |
-
const data=await res.json().catch(()=>({})); if(!res.ok) throw new Error(data.detail||res.status);
|
| 4520 |
-
flagsLoaded=false; await loadFlags(); drawManage();
|
| 4521 |
-
}catch(err){ msg.textContent="השמירה נכשלה: "+err.message; confirm.disabled=reject.disabled=false; }
|
| 4522 |
-
};
|
| 4523 |
-
confirm.onclick=()=>save("confirmed"); reject.onclick=()=>save("rejected"); actions.append(category,diagnose,confirm,reject,msg); card.append(note,actions); box.append(card);
|
| 4524 |
}
|
| 4525 |
}
|
|
|
|
| 4526 |
let systemActivityOpen=false; // the recent activity starts closed; a redraw keeps it as the teacher left it
|
| 4527 |
function drawSystemActivity(box){
|
| 4528 |
const outer=mark("card system-activity",""), card=document.createElement("details"), summary=document.createElement("summary");
|
|
|
|
| 752 |
.activity-head select{font:inherit;padding:7px 10px;border:1px solid var(--line);border-radius:9px;background:var(--surface);color:var(--ink)}
|
| 753 |
.activity-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.activity-stat{padding:10px;border:1px solid var(--line);border-radius:10px;background:var(--bg)}.activity-stat b{display:block;font:400 28px/1 var(--f-display);font-variant-numeric:tabular-nums}.activity-stat small{color:var(--muted);font-size:12px}
|
| 754 |
.activity-chart{width:100%;height:190px;overflow:visible}.activity-chart .grid{stroke:var(--line);stroke-width:1}.activity-chart .bar{fill:var(--olive);opacity:.82}.activity-chart .bar:hover{opacity:1}.activity-chart text{fill:var(--muted);font:11px var(--f-body)}
|
| 755 |
+
.flag-group{display:flex;flex-direction:column;gap:10px}.flag-group>summary{cursor:pointer;padding:10px 14px;border:1px solid var(--line);border-radius:12px;background:var(--surface);font-size:16px}.flag-group>summary .meta{color:var(--muted);font-size:14px}.flag-group[open]>summary{margin-bottom:2px}.flag-group>.card{margin-inline-start:12px}
|
| 756 |
.system-activity summary{cursor:pointer}.system-activity summary h2{display:inline;margin:0}.system-activity details[open]>summary{margin-bottom:8px}
|
| 757 |
.system-hard{margin:2px 0 4px;padding:10px;border-radius:10px;background:var(--bg)}.system-hard h3{margin:0 0 6px;font:700 15px/1.3 var(--f-body)}.system-hard-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:10px;padding:6px 0;border-bottom:1px solid var(--line)}.system-hard-row:last-child{border-bottom:0}.system-hard-row span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.system-hard-row small{color:var(--muted);white-space:nowrap}.system-feed{display:flex;flex-direction:column;gap:14px}.system-topic{border-top:1px solid var(--line);padding-top:10px}.system-topic:first-child{border-top:0;padding-top:0}.system-topic h3{margin:0 0 3px;font:700 15px/1.3 var(--f-body)}.system-topic h3 small{color:var(--muted);font-weight:600}.system-event{display:grid;grid-template-columns:auto 1fr auto;gap:10px;align-items:center;padding:8px 0;border-bottom:1px solid var(--line)}.system-event:last-child{border-bottom:0}.system-event i{display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--olive-soft);font-style:normal}.system-event b,.system-event small{display:block}.system-event small,.system-event time{color:var(--muted);font-size:12px}.system-event time{direction:ltr;white-space:nowrap}
|
| 758 |
/* the API keys card (developers only): a row per key, where it comes from and a field for a new one; a key is never shown */
|
|
|
|
| 4470 |
}
|
| 4471 |
card.append(table); box.append(card);
|
| 4472 |
}
|
| 4473 |
+
const pending=FLAG_REVIEWS.pending, head=mark("card flag-head",""); box.append(head);
|
| 4474 |
+
head.append(mark("","דיווחים שממתינים לבדיקה","h2"));
|
| 4475 |
+
if(!pending.length){ head.append(mark("meta","אין כרגע דיווחים שממתינים לבדיקה.","p")); return; }
|
| 4476 |
+
const byUser=new Map; for(const f of pending){ const who=f.email||""; if(!byUser.has(who)) byUser.set(who,[]); byUser.get(who).push(f); }
|
| 4477 |
+
head.append(mark("meta",`${pending.length} ${pending.length===1?"דיווח":"דיווחים"} מ־${byUser.size} ${byUser.size===1?"מדווח/ת":"מדווחים"}, מקובצים לפי המדווח/ת.`,"p"));
|
| 4478 |
+
for(const [who,flags] of [...byUser].sort((a,b)=>b[1].length-a[1].length||a[0].localeCompare(b[0]))){ // the most reports first
|
| 4479 |
+
const group=document.createElement("details"), summary=document.createElement("summary");
|
| 4480 |
+
group.className="flag-group"; group.open=flagGroupOpen[who]!==false; group.ontoggle=()=>{ flagGroupOpen[who]=group.open; };
|
| 4481 |
+
summary.append(mark("",who||"ללא משתמש","b"),mark("meta",` · ${flags.length} ${flags.length===1?"דיווח":"דיווחים"}`)); group.append(summary); box.append(group);
|
| 4482 |
+
for(const f of flags){
|
| 4483 |
+
const card=mark("card flag-review",""), title=mark("",f.he||f.phrase,"h2"), result=f.result||{}, issues=flagIssues(f);
|
| 4484 |
+
const ar=mark("",f.ar||"","p"); ar.dir="rtl";
|
| 4485 |
+
card.append(title,ar,mark("meta",`ציון ${f.score} · המודל שמע: ${f.heard_ipa||"–"}`,"p"));
|
| 4486 |
+
if(f.note) card.append(mark("",`הערת המדווח/ת: ${f.note}`,"p"));
|
| 4487 |
+
if(f.audio){
|
| 4488 |
+
const audioRow=mark("row",""), play=mark("btn","▶ השמעת ההקלטה","button"), audio=document.createElement("audio"), audioMsg=mark("msg","");
|
| 4489 |
+
audio.controls=true; audio.preload="none"; audio.hidden=true;
|
| 4490 |
+
play.onclick=async()=>{
|
| 4491 |
+
play.disabled=true; audioMsg.textContent="טוען…";
|
| 4492 |
+
try{
|
| 4493 |
+
const res=await api(`/api/flags/${f.id}/audio`), data=res.ok?await res.blob():null;
|
| 4494 |
+
if(!res.ok) throw new Error((await res.json().catch(()=>({}))).detail||res.status);
|
| 4495 |
+
const url=URL.createObjectURL(data); flagAudioUrls.push(url); audio.src=url; audio.hidden=false; play.hidden=true; audioMsg.textContent="";
|
| 4496 |
+
audio.play().catch(()=>{});
|
| 4497 |
+
}catch(err){ audioMsg.textContent="ההשמעה נכשלה: "+err.message; play.disabled=false; }
|
| 4498 |
+
};
|
| 4499 |
+
audioRow.append(play,audio,audioMsg); card.append(audioRow);
|
| 4500 |
+
}else card.append(mark("meta","ההקלטה לא נשמרה עם הדיווח הזה.","p"));
|
| 4501 |
+
const field=document.createElement("fieldset"); field.append(mark("","אילו אבחנות של המערכת היו שגויות?","legend"));
|
| 4502 |
+
for(const issue of issues){ const label=document.createElement("label"), input=document.createElement("input"); input.type="checkbox"; input.checked=true; input._issue=issue; label.append(input,issue.label); field.append(label); }
|
| 4503 |
+
if(issues.length) card.append(field); else card.append(mark("meta","בדיווח הזה אין פונמה שסומנה במפורש; עדיין אפשר לסווג ולאשר אותו.","p"));
|
| 4504 |
+
const category=document.createElement("select"); for(const [value,label] of Object.entries(FLAG_CATEGORIES)){ const o=document.createElement("option"); o.value=value; o.textContent=label; category.append(o); }
|
| 4505 |
+
const note=document.createElement("textarea"); note.maxLength=500; note.rows=2; note.placeholder="הערת בדיקה (לא חובה)";
|
| 4506 |
+
const actions=mark("row",""), diagnose=mark("btn","לוח אבחון","button"), confirm=mark("btn primary","אישור הדיווח","button"), reject=mark("btn danger","דחייה","button"), msg=mark("msg","");
|
| 4507 |
+
diagnose.disabled=!result.trace;
|
| 4508 |
+
diagnose.title=result.trace?"פתיחת האבחון המלא, כולל טבלת ההגאים":"הדיווח ישן ואינו כולל נתוני trace";
|
| 4509 |
+
diagnose.onclick=async()=>{
|
| 4510 |
+
const tab=window.open("about:blank","_blank");
|
| 4511 |
+
diagnose.disabled=true; msg.textContent=f.audio?"טוען אבחון וקול…":"פותח אבחון…";
|
| 4512 |
+
const selected=[...field.querySelectorAll("input:checked")].map(x=>({word:x._issue.word,expected:x._issue.expected,heard:x._issue.heard}));
|
| 4513 |
+
let audioUrl="";
|
| 4514 |
+
if(f.audio) try{
|
| 4515 |
+
const res=await api(`/api/flags/${f.id}/audio`); if(!res.ok) throw new Error(res.status);
|
| 4516 |
+
audioUrl=URL.createObjectURL(await res.blob());
|
| 4517 |
+
}catch(err){ msg.textContent="הקול לא נטען; לוח האבחון נפתח ללא קול."; }
|
| 4518 |
+
openTrace({...result,phrase:{he:f.he||f.phrase,ar:f.ar||""},_diagnostic:{prompt_he:f.he||f.phrase,answer_ar:f.ar,
|
| 4519 |
+
expected_ipa:f.ipa,model:f.model,version:f.version,report_note:f.note||"",review_category:category.value,
|
| 4520 |
+
reviewer_marked_false_issues:selected}},audioUrl,tab);
|
| 4521 |
+
diagnose.disabled=false; if(!msg.textContent.includes("לא נטען")) msg.textContent="";
|
| 4522 |
+
};
|
| 4523 |
+
const save=async status=>{
|
| 4524 |
+
confirm.disabled=reject.disabled=true; msg.textContent="שומר…";
|
| 4525 |
+
const false_issues=[...field.querySelectorAll("input:checked")].map(x=>({word:x._issue.word,expected:x._issue.expected,heard:x._issue.heard}));
|
| 4526 |
try{
|
| 4527 |
+
const res=await api(`/api/flags/${f.id}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({status,category:category.value,false_issues,review_note:note.value})});
|
| 4528 |
+
const data=await res.json().catch(()=>({})); if(!res.ok) throw new Error(data.detail||res.status);
|
| 4529 |
+
flagsLoaded=false; await loadFlags(); drawManage();
|
| 4530 |
+
}catch(err){ msg.textContent="השמירה נכשלה: "+err.message; confirm.disabled=reject.disabled=false; }
|
|
|
|
| 4531 |
};
|
| 4532 |
+
confirm.onclick=()=>save("confirmed"); reject.onclick=()=>save("rejected"); actions.append(category,diagnose,confirm,reject,msg); card.append(note,actions); group.append(card);
|
| 4533 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4534 |
}
|
| 4535 |
}
|
| 4536 |
+
const flagGroupOpen={}; // reporter -> whether their group of reports is open; open until the teacher closes it
|
| 4537 |
let systemActivityOpen=false; // the recent activity starts closed; a redraw keeps it as the teacher left it
|
| 4538 |
function drawSystemActivity(box){
|
| 4539 |
const outer=mark("card system-activity",""), card=document.createElement("details"), summary=document.createElement("summary");
|
tests/test_scoring.py
CHANGED
|
@@ -361,6 +361,41 @@ def test_dropped_glottal_stop_is_not_an_error():
|
|
| 361 |
assert not r["tips"] and not any("עצירה" in i["sound"] for i in r["issues"])
|
| 362 |
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
def test_ana_explicitly_accepts_the_natural_vowell_initial_form():
|
| 365 |
from app.grammar import PRONOUNS
|
| 366 |
|
|
|
|
| 361 |
assert not r["tips"] and not any("עצירה" in i["sound"] for i in r["issues"])
|
| 362 |
|
| 363 |
|
| 364 |
+
@pytest.mark.parametrize("competitor", ["f", "s", "χ"])
|
| 365 |
+
def test_one_frame_onset_spike_does_not_become_a_glottal_substitution(competitor):
|
| 366 |
+
"""A generic microphone/breath transient must not be reported as a consonant the learner produced."""
|
| 367 |
+
syms = SYMS + ([competitor] if competitor not in SYMS else [])
|
| 368 |
+
vocab = {s: i for i, s in enumerate(syms)}
|
| 369 |
+
V = len(syms)
|
| 370 |
+
|
| 371 |
+
def row(sym, peak):
|
| 372 |
+
p = np.full(V, (1 - peak) / (V - 1))
|
| 373 |
+
p[vocab[sym]] = peak
|
| 374 |
+
return p
|
| 375 |
+
|
| 376 |
+
# No glottal stop: one weak arbitrary-phone spike, then the clean vowel-initial word.
|
| 377 |
+
rows = [row("<pad>", .95), row(competitor, .23)]
|
| 378 |
+
for ph in ["a", "h", "l", "a"]:
|
| 379 |
+
rows += [row(ph, .9)] * 3 + [row("<pad>", .95)] * 2
|
| 380 |
+
logp = np.log(np.array(rows))
|
| 381 |
+
phrase = {"variants": [("اهلا", "ʔ a h l a")]}
|
| 382 |
+
r = Scorer(lambda wav: logp, Vocab(vocab)).score_logp(logp, phrase, diagnostics=True)
|
| 383 |
+
onset = r["trace"]["tokens"][0]
|
| 384 |
+
assert onset["competitor_frames"] == 1
|
| 385 |
+
assert onset["stages"][1]["applied"] == "glottal_dropped"
|
| 386 |
+
assert r["words"][0]["score"] >= 90 and not r["tips"]
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def test_sustained_initial_consonant_is_still_a_glottal_substitution():
|
| 390 |
+
phrase = {"variants": [("اهلا", "ʔ a h l a")]}
|
| 391 |
+
r = scorer_for(emissions(["s", "a", "h", "l", "a"])).score(None, phrase, diagnostics=True)
|
| 392 |
+
onset = r["trace"]["tokens"][0]
|
| 393 |
+
assert onset["competitor_frames"] >= 2
|
| 394 |
+
assert onset["stages"][1]["applied"] is None
|
| 395 |
+
assert r["words"][0]["phonemes"][0]["heard"] == "s"
|
| 396 |
+
assert r["tips"][0]["key"] == "ʔ>s"
|
| 397 |
+
|
| 398 |
+
|
| 399 |
def test_ana_explicitly_accepts_the_natural_vowell_initial_form():
|
| 400 |
from app.grammar import PRONOUNS
|
| 401 |
|