3v324v23 Claude Opus 5.5 commited on
Commit
b6579f5
·
1 Parent(s): ee6bcf6

feat(scoring): measure against فدوى too, and show every native baseline in the trace dashboard

Browse files

- The native baseline now uses two TTS voices: ראאף (generated on demand) and فدوى (used when cached).
Each sound is still measured against the lowest score any voice got on it.
- References are read with wav_for_voice: wav() turned any voice outside playback into ראאף.
- Each Reference carries its voice name; the trace keeps every voice's score per sound
(baseline stage `refs`, references[].voice).
- Trace dashboard: a row per voice under each sound (the voice used in bold), a marker per
voice on the waterfall's baseline step, per-voice tooltip and table columns.
- The mock trace has two voices.

Bench on 2,828 lab recordings (all said right): false rejections 3.6% -> 2.9%
(ħ 6.0 -> 2.8%, h 9.7 -> 5.4%, ɣ 20.6 -> 12.5%, ʕ 12.5 -> 9.5%). Detection is not measured:
the lab has no recordings with a planted mistake yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

README.md CHANGED
@@ -361,7 +361,7 @@ insert into storage.buckets (id, name) values ('lab', 'lab') on conflict (id) do
361
  1. the frames around the sound: target, competitor and neighbour posteriors, frame energy, and the window it was read from (the end of the sound before to the start of the one after)
362
  2. its score stage by stage: raw GOP → exemptions → cluster cap → native baseline → teacher floor → final
363
  3. the readings the audio was aligned to, with the winning CTC path, and the edit-distance gauge for a different sentence
364
- 4. the sentence by word and sound, coloured by score, with how sounds add up to words and words to the sentence
365
 
366
  The data's schema is [docs/trace-schema.ts](docs/trace-schema.ts). `python scripts/mock_trace.py` regenerates the example (`static/assets/trace-mock.json`, the "דוגמה" button) by running the real scorer on synthetic posteriors, with a substitution, a teacher floor, a native baseline and a noise spike beside a short vowel planted in it (the spike shows how a sound's window takes in its neighbours' blank frames). `?src=mock` opens the example, `?i=6` pins a sound.
367
 
 
361
  1. the frames around the sound: target, competitor and neighbour posteriors, frame energy, and the window it was read from (the end of the sound before to the start of the one after)
362
  2. its score stage by stage: raw GOP → exemptions → cluster cap → native baseline → teacher floor → final
363
  3. the readings the audio was aligned to, with the winning CTC path, and the edit-distance gauge for a different sentence
364
+ 4. the sentence by word and sound, coloured by score, with how sounds add up to words and words to the sentence. When the recording was scored against native baselines, each TTS voice gets a row of its own scores under the sounds; the voice a sound was measured against (the lowest) is in bold. The waterfall marks every voice on the baseline step, and the table has a column per voice
365
 
366
  The data's schema is [docs/trace-schema.ts](docs/trace-schema.ts). `python scripts/mock_trace.py` regenerates the example (`static/assets/trace-mock.json`, the "דוגמה" button) by running the real scorer on synthetic posteriors, with a substitution, a teacher floor, a native baseline and a noise spike beside a short vowel planted in it (the spike shows how a sound's window takes in its neighbours' blank frames). `?src=mock` opens the example, `?i=6` pins a sound.
367
 
app/main.py CHANGED
@@ -28,6 +28,7 @@ import urllib.parse
28
  import uuid
29
  import zipfile
30
  from contextlib import asynccontextmanager
 
31
  from datetime import date, datetime, timedelta, timezone
32
  from pathlib import Path
33
  from queue import Queue
@@ -54,7 +55,7 @@ from .rules import TOGGLES, check_detectable
54
  from .scorer import SAMPLE_RATE, Scorer, decode_audio, decode_audio_with_timing, frame_energy, load_scorer
55
  from .speech_evaluation import SpeechEvaluationService
56
  from .texts import read_line, text_phrases
57
- from .tts import LINE_VOICES, RAEF_VOICE_ID, VOICES, SawtakTTS, TTSUnavailable, load_tts
58
  from .tutor import (BY_ID, DAILY_LIMIT, MAX_TEXT, MAX_TURNS, SCENARIOS, Tutor, TutorLimit,
59
  DEFAULT_MODEL as DEFAULT_TUTOR_MODEL, TutorUnavailable, load_tutor, tutor_client)
60
  from .verb_game import VerbExerciseGenerator
@@ -490,7 +491,7 @@ def create_app(scorer: Scorer | None = None, tts: SawtakTTS | None = None,
490
  app.state.tts_warm_stop = threading.Event() # set by POST /api/tts-cache/warm/stop
491
  app.state.tutor = tutor or load_tutor(app.state.keys.get("ANTHROPIC_API_KEY"), app.state.keys.get("TUTOR_MODEL"))
492
  # the playback voices double as the native baseline of the scores (see reference.py)
493
- app.state.refs = References(app.state.scorer, app.state.tts, list(VOICES.values())) if app.state.tts else None
494
  if app.state.auth and hasattr(app.state.auth, "calibrations"):
495
  try:
496
  rows = app.state.auth.calibrations("applied")
@@ -1572,7 +1573,7 @@ def create_app(scorer: Scorer | None = None, tts: SawtakTTS | None = None,
1572
  try:
1573
  if not refs.tts.cached(text, voice):
1574
  continue
1575
- data, _ = refs.tts.wav(text, voice)
1576
  except Exception as e: # provider down: score without this voice, as References does
1577
  log.warning("sweep: no %s voice for %r: %s", voice, text, e)
1578
  continue
@@ -1586,7 +1587,8 @@ def create_app(scorer: Scorer | None = None, tts: SawtakTTS | None = None,
1586
  key = (*emitted, phrase["key"], setting, scorer.disabled)
1587
  if key not in sweep_refs:
1588
  logp, energy = sweep_emits[emitted]
1589
- sweep_refs[key] = scorer.reference_logp(logp, phrase, energy)
 
1590
  if sweep_refs[key] is not None:
1591
  out.append(sweep_refs[key])
1592
  return out
@@ -2869,7 +2871,7 @@ def create_app(scorer: Scorer | None = None, tts: SawtakTTS | None = None,
2869
  app.state.tts.api_key = value
2870
  else:
2871
  app.state.tts = load_tts(value)
2872
- app.state.refs = References(app.state.scorer, app.state.tts, list(VOICES.values())) if app.state.tts else None
2873
  elif app.state.tutor is not None and value:
2874
  app.state.tutor.client = tutor_client(value)
2875
  else:
 
28
  import uuid
29
  import zipfile
30
  from contextlib import asynccontextmanager
31
+ from dataclasses import replace as dc_replace
32
  from datetime import date, datetime, timedelta, timezone
33
  from pathlib import Path
34
  from queue import Queue
 
55
  from .scorer import SAMPLE_RATE, Scorer, decode_audio, decode_audio_with_timing, frame_energy, load_scorer
56
  from .speech_evaluation import SpeechEvaluationService
57
  from .texts import read_line, text_phrases
58
+ from .tts import BASELINE_VOICES, LINE_VOICES, RAEF_VOICE_ID, VOICES, SawtakTTS, TTSUnavailable, load_tts, voice_name
59
  from .tutor import (BY_ID, DAILY_LIMIT, MAX_TEXT, MAX_TURNS, SCENARIOS, Tutor, TutorLimit,
60
  DEFAULT_MODEL as DEFAULT_TUTOR_MODEL, TutorUnavailable, load_tutor, tutor_client)
61
  from .verb_game import VerbExerciseGenerator
 
491
  app.state.tts_warm_stop = threading.Event() # set by POST /api/tts-cache/warm/stop
492
  app.state.tutor = tutor or load_tutor(app.state.keys.get("ANTHROPIC_API_KEY"), app.state.keys.get("TUTOR_MODEL"))
493
  # the playback voices double as the native baseline of the scores (see reference.py)
494
+ app.state.refs = References(app.state.scorer, app.state.tts, list(BASELINE_VOICES.values())) if app.state.tts else None
495
  if app.state.auth and hasattr(app.state.auth, "calibrations"):
496
  try:
497
  rows = app.state.auth.calibrations("applied")
 
1573
  try:
1574
  if not refs.tts.cached(text, voice):
1575
  continue
1576
+ data, _ = refs.tts.wav_for_voice(text, voice, source="baseline")
1577
  except Exception as e: # provider down: score without this voice, as References does
1578
  log.warning("sweep: no %s voice for %r: %s", voice, text, e)
1579
  continue
 
1587
  key = (*emitted, phrase["key"], setting, scorer.disabled)
1588
  if key not in sweep_refs:
1589
  logp, energy = sweep_emits[emitted]
1590
+ ref = scorer.reference_logp(logp, phrase, energy)
1591
+ sweep_refs[key] = dc_replace(ref, voice=voice_name(voice)) if ref is not None else None
1592
  if sweep_refs[key] is not None:
1593
  out.append(sweep_refs[key])
1594
  return out
 
2871
  app.state.tts.api_key = value
2872
  else:
2873
  app.state.tts = load_tts(value)
2874
+ app.state.refs = References(app.state.scorer, app.state.tts, list(BASELINE_VOICES.values())) if app.state.tts else None
2875
  elif app.state.tutor is not None and value:
2876
  app.state.tutor.client = tutor_client(value)
2877
  else:
app/reference.py CHANGED
@@ -14,10 +14,12 @@ from __future__ import annotations
14
  import logging
15
  import threading
16
  import time
 
17
  from concurrent.futures import Future, ThreadPoolExecutor, wait
18
 
19
  from .arabic_diacritics import point_arabic
20
  from .scorer import Reference, Scorer, decode_audio
 
21
 
22
  log = logging.getLogger("ihki.reference")
23
 
@@ -32,11 +34,13 @@ class References:
32
  self._lock = threading.Lock()
33
 
34
  def _build(self, phrase: dict, text: str, voice: str) -> Reference | None:
35
- data, _ = self.tts.wav(text, voice)
 
36
  ref = self.scorer.reference(decode_audio(data), phrase)
37
  if ref is None:
38
  log.info("reference not usable: %r by voice %s", text, voice)
39
- return ref
 
40
 
41
  def get(self, phrase_id: int, phrase: dict) -> list[Reference]:
42
  """The references of a phrase that are ready within the wait."""
 
14
  import logging
15
  import threading
16
  import time
17
+ from dataclasses import replace
18
  from concurrent.futures import Future, ThreadPoolExecutor, wait
19
 
20
  from .arabic_diacritics import point_arabic
21
  from .scorer import Reference, Scorer, decode_audio
22
+ from .tts import voice_name
23
 
24
  log = logging.getLogger("ihki.reference")
25
 
 
34
  self._lock = threading.Lock()
35
 
36
  def _build(self, phrase: dict, text: str, voice: str) -> Reference | None:
37
+ # wav() would turn a voice it does not offer for playback (فدوى) into Raef's
38
+ data, _ = self.tts.wav_for_voice(text, voice, source="baseline", upload=True)
39
  ref = self.scorer.reference(decode_audio(data), phrase)
40
  if ref is None:
41
  log.info("reference not usable: %r by voice %s", text, voice)
42
+ return None
43
+ return replace(ref, voice=voice_name(voice))
44
 
45
  def get(self, phrase_id: int, phrase: dict) -> list[Reference]:
46
  """The references of a phrase that are ready within the wait."""
app/scorer.py CHANGED
@@ -192,6 +192,7 @@ class Reference:
192
  """How the model scored a native voice saying a phrase: the baseline a learner is measured against."""
193
  sounds: list[tuple[int, str]] # (word, sound) of each sound of the variant the voice said (Q, not ʔ / k)
194
  scores: list[float] # their scores, same order
 
195
 
196
 
197
  @dataclass
@@ -361,12 +362,22 @@ class Scorer:
361
  References may have said another accepted variant: only the sounds the two variants share are matched.
362
  Sounds are compared by symbol, so a voice that said ق as q is a baseline for a learner who says ʔ.
363
  """
364
- sounds = [(t.word, t.sound) for t in toks]
365
  out: list[float | None] = [None] * len(toks)
 
 
 
 
 
 
 
 
 
366
  for ref in refs:
 
367
  for a, b, n in SequenceMatcher(None, ref.sounds, sounds, autojunk=False).get_matching_blocks():
368
  for k in range(n):
369
- out[b + k] = ref.scores[a + k] if out[b + k] is None else min(out[b + k], ref.scores[a + k])
 
370
  return out
371
 
372
  def _carriers(self, ipa: str) -> list[list[list[int]]]:
@@ -736,6 +747,7 @@ class Scorer:
736
  """Every stage of the classification with its numbers, weights and thresholds, for the teacher's view
737
  (the 'stages' part of the phoneme analysis). Nothing here changes the result."""
738
  cfg, g, i2t = self.cfg, self.cfg.gop, self.vocab.i2t
 
739
  p = lambda lp: round(float(np.exp(lp)), 4) if lp > -1e20 else 0.0
740
  max_tips = self._max_tips()
741
  n, T = len(fit.toks), len(fit.logp)
@@ -759,7 +771,7 @@ class Scorer:
759
  {"stage": "exemption", "score": fit.exempt_scores[i].score, "applied": fit.exempted[i]},
760
  {"stage": "insertion", "score": fit.scores[i].score, "applied": fit.inserted.get(i)},
761
  {"stage": "baseline", "score": after_baseline[i].score, "ref": b,
762
- "applied": b is not None and b < 100},
763
  {"stage": "calibration", "score": sc.score, "floor": floors[i],
764
  "applied": floors[i] is not None and after_baseline[i].score < floors[i]},
765
  ]
@@ -812,7 +824,8 @@ class Scorer:
812
  "unrelated": {"heard": heard, "distance": fit.distance, "tokens": n,
813
  "ratio": round(fit.distance / max(1, n), 3), "threshold": cfg.unrelated_ratio,
814
  "unrelated": fit.unrelated},
815
- "references": [{"mean": round(float(np.mean(r.scores)), 1), "sounds": len(r.sounds)} for r in refs],
 
816
  "tokens": tokens,
817
  "words": [{"tr": w["tr"], "scores": [ph["score"] for ph in w["phonemes"]],
818
  "min": min((ph["score"] for ph in w["phonemes"]), default=0.0),
 
192
  """How the model scored a native voice saying a phrase: the baseline a learner is measured against."""
193
  sounds: list[tuple[int, str]] # (word, sound) of each sound of the variant the voice said (Q, not ʔ / k)
194
  scores: list[float] # their scores, same order
195
+ voice: str = "" # who said it (the TTS voice's name), for the trace dashboard
196
 
197
 
198
  @dataclass
 
362
  References may have said another accepted variant: only the sounds the two variants share are matched.
363
  Sounds are compared by symbol, so a voice that said ق as q is a baseline for a learner who says ʔ.
364
  """
 
365
  out: list[float | None] = [None] * len(toks)
366
+ for per_ref in Scorer._ref_scores(toks, refs):
367
+ out = [x if y is None else y if x is None else min(x, y) for x, y in zip(out, per_ref)]
368
+ return out
369
+
370
+ @staticmethod
371
+ def _ref_scores(toks: list[TargetToken], refs: list[Reference]) -> list[list[float | None]]:
372
+ """Per reference, per target token, the score that reference got on the same sound (None: it does not have it)."""
373
+ sounds = [(t.word, t.sound) for t in toks]
374
+ out = []
375
  for ref in refs:
376
+ row: list[float | None] = [None] * len(toks)
377
  for a, b, n in SequenceMatcher(None, ref.sounds, sounds, autojunk=False).get_matching_blocks():
378
  for k in range(n):
379
+ row[b + k] = ref.scores[a + k]
380
+ out.append(row)
381
  return out
382
 
383
  def _carriers(self, ipa: str) -> list[list[list[int]]]:
 
747
  """Every stage of the classification with its numbers, weights and thresholds, for the teacher's view
748
  (the 'stages' part of the phoneme analysis). Nothing here changes the result."""
749
  cfg, g, i2t = self.cfg, self.cfg.gop, self.vocab.i2t
750
+ per_ref = self._ref_scores(fit.toks, refs)
751
  p = lambda lp: round(float(np.exp(lp)), 4) if lp > -1e20 else 0.0
752
  max_tips = self._max_tips()
753
  n, T = len(fit.toks), len(fit.logp)
 
771
  {"stage": "exemption", "score": fit.exempt_scores[i].score, "applied": fit.exempted[i]},
772
  {"stage": "insertion", "score": fit.scores[i].score, "applied": fit.inserted.get(i)},
773
  {"stage": "baseline", "score": after_baseline[i].score, "ref": b,
774
+ "refs": [row[i] for row in per_ref], "applied": b is not None and b < 100},
775
  {"stage": "calibration", "score": sc.score, "floor": floors[i],
776
  "applied": floors[i] is not None and after_baseline[i].score < floors[i]},
777
  ]
 
824
  "unrelated": {"heard": heard, "distance": fit.distance, "tokens": n,
825
  "ratio": round(fit.distance / max(1, n), 3), "threshold": cfg.unrelated_ratio,
826
  "unrelated": fit.unrelated},
827
+ "references": [{"voice": r.voice or f"קול {k + 1}", "mean": round(float(np.mean(r.scores)), 1),
828
+ "sounds": len(r.sounds)} for k, r in enumerate(refs)],
829
  "tokens": tokens,
830
  "words": [{"tr": w["tr"], "scores": [ph["score"] for ph in w["phonemes"]],
831
  "min": min((ph["score"] for ph in w["phonemes"]), default=0.0),
app/tts.py CHANGED
@@ -45,6 +45,16 @@ RAEF_VOICE_ID = "273d81b2-991b-5b76-8525-eeacc6a3d759"
45
  VOICES = {"רַאאִף": RAEF_VOICE_ID}
46
  # A female voice, only for the lines a dialogue gives her (workbook.LINE_VOICES); never offered on the page.
47
  LINE_VOICES = {"חנאן": "e41781f5-127c-5eb0-b83c-50bf52a64caf"}
 
 
 
 
 
 
 
 
 
 
48
 
49
  class TTSUnavailable(Exception):
50
  """No key configured, or the provider refused / failed. The message is safe to show."""
@@ -424,9 +434,10 @@ class SawtakTTS:
424
  voice = voice if voice in LINE_VOICES.values() else RAEF_VOICE_ID
425
  return self._wav(text, voice, upload=True, source="playback")
426
 
427
- def wav_for_voice(self, text: str, voice: str, source: str = "sweep") -> tuple[bytes, str]:
428
- """(WAV, voice id) for a teacher-selected, validated Sawtak voice."""
429
- return self._wav(text, voice, source=source)
 
430
 
431
 
432
  def load_tts(key: str | None = None) -> SawtakTTS | None:
 
45
  VOICES = {"רַאאִף": RAEF_VOICE_ID}
46
  # A female voice, only for the lines a dialogue gives her (workbook.LINE_VOICES); never offered on the page.
47
  LINE_VOICES = {"חנאן": "e41781f5-127c-5eb0-b83c-50bf52a64caf"}
48
+ FADWA_VOICE_ID = "39a7e0e0-087e-52b3-afff-5203e430fc39" # فدوى: Palestinian, female, conversational
49
+ # The native voices the scores are measured against (reference.py), first one first: only it is generated for
50
+ # the baseline; the others count when their recording is already cached (the cache-warm sweep made them).
51
+ BASELINE_VOICES = {"ראאף": RAEF_VOICE_ID, "פדוא": FADWA_VOICE_ID}
52
+
53
+
54
+ def voice_name(voice_id: str) -> str:
55
+ """A voice id's display name, without vowel points (ראאף, חנאן); the id itself when it is not one of ours."""
56
+ names = {v: k for k, v in {**VOICES, **LINE_VOICES, **BASELINE_VOICES}.items()}
57
+ return names.get(voice_id, voice_id).translate({c: None for c in range(0x591, 0x5c8)})
58
 
59
  class TTSUnavailable(Exception):
60
  """No key configured, or the provider refused / failed. The message is safe to show."""
 
434
  voice = voice if voice in LINE_VOICES.values() else RAEF_VOICE_ID
435
  return self._wav(text, voice, upload=True, source="playback")
436
 
437
+ def wav_for_voice(self, text: str, voice: str, source: str = "sweep", upload: bool = False) -> tuple[bytes, str]:
438
+ """(WAV, voice id) for a teacher-selected, validated Sawtak voice. Unlike wav(), any voice id is kept.
439
+ `upload`: a new recording also goes to the bucket in the background."""
440
+ return self._wav(text, voice, upload=upload, source=source)
441
 
442
 
443
  def load_tts(key: str | None = None) -> SawtakTTS | None:
docs/trace-schema.ts CHANGED
@@ -75,7 +75,8 @@ export interface Trace {
75
  frames: TraceFrames;
76
  aipa: string; // the chosen reading with archi symbols
77
  unrelated: TraceUnrelated;
78
- references: { mean: number; sounds: number }[];
 
79
  /** Stages 1–5 per sound, in order: gop → exemption → insertion → baseline → calibration → final. */
80
  tokens: TraceToken[];
81
  words: TraceWord[];
@@ -137,7 +138,9 @@ export type Stage =
137
  | { stage: "gop"; score: number }
138
  | { stage: "exemption"; score: number; applied: TraceConfig["exemptions"][number] | null }
139
  | { stage: "insertion"; score: number; applied: Ipa | null } // the vowel heard inside the cluster
140
- | { stage: "baseline"; score: number; ref: number | null; applied: boolean } // ref: the native voice's score
 
 
141
  | { stage: "calibration"; score: number; floor: number | null; applied: boolean };
142
 
143
  export interface TraceToken {
 
75
  frames: TraceFrames;
76
  aipa: string; // the chosen reading with archi symbols
77
  unrelated: TraceUnrelated;
78
+ /** The native baselines, one per TTS voice; each sound is measured against the lowest of them (Stage "baseline"). */
79
+ references: { voice: string; mean: number; sounds: number }[];
80
  /** Stages 1–5 per sound, in order: gop → exemption → insertion → baseline → calibration → final. */
81
  tokens: TraceToken[];
82
  words: TraceWord[];
 
138
  | { stage: "gop"; score: number }
139
  | { stage: "exemption"; score: number; applied: TraceConfig["exemptions"][number] | null }
140
  | { stage: "insertion"; score: number; applied: Ipa | null } // the vowel heard inside the cluster
141
+ | { stage: "baseline"; score: number; ref: number | null; refs: (number | null)[]; applied: boolean }
142
+ // ref: the lowest native score on this sound (what it is measured against); refs: each voice's, in
143
+ // Trace.references order (null: that voice's reading has no such sound)
144
  | { stage: "calibration"; score: number; floor: number | null; applied: boolean };
145
 
146
  export interface TraceToken {
scripts/bench.py CHANGED
@@ -30,7 +30,7 @@ from app import lab # noqa: E402
30
  from app.auth import AuthUnavailable, load_auth # noqa: E402
31
  from app.reference import References # noqa: E402
32
  from app.scorer import decode_audio, load_scorer # noqa: E402
33
- from app.tts import VOICES, load_tts # noqa: E402
34
 
35
 
36
  def pct(x: float | None) -> str:
@@ -68,7 +68,7 @@ def main() -> int:
68
  model = os.environ.get("MODEL_ID", "facebook/wav2vec2-xlsr-53-espeak-cv-ft")
69
  scorer = load_scorer(model)
70
  tts = None if args.no_baseline else load_tts()
71
- refs = References(scorer, tts, list(VOICES.values())) if tts else None
72
 
73
  def score(audio: bytes, phrase: dict, key) -> dict:
74
  return scorer.score(decode_audio(audio), phrase, refs.get(key, phrase) if refs else [])
 
30
  from app.auth import AuthUnavailable, load_auth # noqa: E402
31
  from app.reference import References # noqa: E402
32
  from app.scorer import decode_audio, load_scorer # noqa: E402
33
+ from app.tts import BASELINE_VOICES, load_tts # noqa: E402
34
 
35
 
36
  def pct(x: float | None) -> str:
 
68
  model = os.environ.get("MODEL_ID", "facebook/wav2vec2-xlsr-53-espeak-cv-ft")
69
  scorer = load_scorer(model)
70
  tts = None if args.no_baseline else load_tts()
71
+ refs = References(scorer, tts, list(BASELINE_VOICES.values())) if tts else None
72
 
73
  def score(audio: bytes, phrase: dict, key) -> dict:
74
  return scorer.score(decode_audio(audio), phrase, refs.get(key, phrase) if refs else [])
scripts/import_minerva.py CHANGED
@@ -29,7 +29,7 @@ from app.auth import AuthUnavailable, load_auth # noqa: E402
29
  from app.main import MODEL_ID, build_version # noqa: E402
30
  from app.reference import References # noqa: E402
31
  from app.scorer import decode_audio, load_scorer # noqa: E402
32
- from app.tts import VOICES, load_tts # noqa: E402
33
 
34
  SPEAKER = "מינרווה"
35
 
@@ -117,7 +117,7 @@ def main() -> int:
117
  model = os.environ.get("MODEL_ID", MODEL_ID)
118
  scorer = load_scorer(model)
119
  tts = None if args.no_baseline else load_tts()
120
- refs = References(scorer, tts, list(VOICES.values())) if tts else None
121
  saved = skipped = 0
122
  for n, (video, segment, phrase) in enumerate(entries, 1):
123
  item, start, end = phrase["key"], segment["start"], segment["end"]
 
29
  from app.main import MODEL_ID, build_version # noqa: E402
30
  from app.reference import References # noqa: E402
31
  from app.scorer import decode_audio, load_scorer # noqa: E402
32
+ from app.tts import BASELINE_VOICES, load_tts # noqa: E402
33
 
34
  SPEAKER = "מינרווה"
35
 
 
117
  model = os.environ.get("MODEL_ID", MODEL_ID)
118
  scorer = load_scorer(model)
119
  tts = None if args.no_baseline else load_tts()
120
+ refs = References(scorer, tts, list(BASELINE_VOICES.values())) if tts else None
121
  saved = skipped = 0
122
  for n, (video, segment, phrase) in enumerate(entries, 1):
123
  item, start, end = phrase["key"], segment["start"], segment["end"]
scripts/mock_trace.py CHANGED
@@ -10,7 +10,9 @@ readings: سيدي / جدّي, بيت / بيت with the diphthong), with what th
10
  - ع of قعد said as a glottal stop (ء): flagged, with a severity-3 tip, and raised by a teacher's calibration floor
11
  - a spike of h in a near-silent frame two frames after the short a of سيدك, inside the a's window and the k's
12
  (the window runs from the end of the sound before to the start of the one after)
13
- - k at the end of سيدك weak, as the native TTS voice's is too: the native baseline raises it
 
 
14
  """
15
  from __future__ import annotations
16
 
@@ -67,11 +69,12 @@ def main() -> int:
67
  logp, energy = emissions()
68
  scorer = Scorer(lambda wav: logp, Vocab(VOCAB))
69
  scorer.set_calibrations([{"expected": "ʕ", "heard": "ʔ", "floor": 45, "scope": "global"}])
70
- # the native voice: everything right but its own weak k at the end of سيدك
71
  toks = scorer._variants(PHRASE)[0][3]
72
- ref = Reference([(t.word, t.sound) for t in toks],
73
- [55.0 if (t.word, t.sound) == (1, "K") else 100.0 for t in toks])
74
- result = scorer.score_logp(logp, PHRASE, [ref], diagnostics=True, duration_s=len(logp) * 0.02, energy=energy)
 
75
  result["phrase"] = {"he": PHRASE["he"], "ar": PHRASE["variants"][0][0]}
76
  args.out.write_text(json.dumps(result, ensure_ascii=False, indent=1), encoding="utf-8")
77
  print(f"{args.out}: score {result['score']}, {len(result['trace']['tokens'])} sounds, "
 
10
  - ع of قعد said as a glottal stop (ء): flagged, with a severity-3 tip, and raised by a teacher's calibration floor
11
  - a spike of h in a near-silent frame two frames after the short a of سيدك, inside the a's window and the k's
12
  (the window runs from the end of the sound before to the start of the one after)
13
+ - k at the end of سيدك weak, as the native TTS voices' is too: the native baseline raises it. Two voices: the
14
+ male one (ראאף) weaker on that k (55) than the female one (פדוא, 70), so the score is measured against the lower; the female
15
+ voice also has a weak ع (80), which the calibration floor then still decides
16
  """
17
  from __future__ import annotations
18
 
 
69
  logp, energy = emissions()
70
  scorer = Scorer(lambda wav: logp, Vocab(VOCAB))
71
  scorer.set_calibrations([{"expected": "ʕ", "heard": "ʔ", "floor": 45, "scope": "global"}])
72
+ # the native voices: everything right but their own weak k at the end of سيدك (and the female voice's ع)
73
  toks = scorer._variants(PHRASE)[0][3]
74
+ weak = {"ראאף": {(1, "K"): 55.0}, "פדוא": {(1, "K"): 70.0, (3, "ʕ"): 80.0}}
75
+ refs = [Reference([(t.word, t.sound) for t in toks], [w.get((t.word, t.sound), 100.0) for t in toks], voice)
76
+ for voice, w in weak.items()]
77
+ result = scorer.score_logp(logp, PHRASE, refs, diagnostics=True, duration_s=len(logp) * 0.02, energy=energy)
78
  result["phrase"] = {"he": PHRASE["he"], "ar": PHRASE["variants"][0][0]}
79
  args.out.write_text(json.dumps(result, ensure_ascii=False, indent=1), encoding="utf-8")
80
  print(f"{args.out}: score {result['score']}, {len(result['trace']['tokens'])} sounds, "
static/assets/trace-mock.json CHANGED
@@ -1,12 +1,12 @@
1
  {
2
- "score": 94,
3
  "variant": {
4
  "ar": "وين سيدك؟ سيدي قعد في بيته.",
5
  "tr": "וֵין סִידַכּ סִידִי קַעַד פִי בֵּיתוֹ",
6
  "ipa": "w eː n | s iː d a k | s iː d i | ʔ a ʕ a d | f i | b eː t o"
7
  },
8
  "heard_ipa": "w e n s i d a h k s i d i ʔ a ʔ a d f i b e t o",
9
- "heard_tr": "וֵין סִידַהְכּ סִידִי קַקַד פִי בֵּיתוֹ",
10
  "unrelated": false,
11
  "words": [
12
  {
@@ -51,8 +51,8 @@
51
  {
52
  "ar": "سيدك؟",
53
  "tr": "סִידַכּ",
54
- "heard_tr": "סִידַהְכּ",
55
- "score": 81.1,
56
  "letters": [
57
  {
58
  "t": "סִי",
@@ -60,8 +60,7 @@
60
  },
61
  {
62
  "t": "דַ",
63
- "score": 68.5,
64
- "why": "תנועת אַ נשמעה כמו ה (ه)"
65
  },
66
  {
67
  "t": "כּ",
@@ -93,7 +92,7 @@
93
  {
94
  "ipa": "a",
95
  "word": 1,
96
- "score": 68.5,
97
  "flagged": false,
98
  "heard": null
99
  },
@@ -158,7 +157,7 @@
158
  "ar": "قعد",
159
  "tr": "קַעַד",
160
  "heard_tr": "קַקַד",
161
- "score": 67.0,
162
  "letters": [
163
  {
164
  "t": "קַ",
@@ -301,22 +300,14 @@
301
  "score": 45
302
  }
303
  ],
304
- "issues": [
305
- {
306
- "sound": "תנועת אַ",
307
- "word_tr": "סִידַכּ",
308
- "score": 68,
309
- "missing": false,
310
- "heard": "ה (ه)"
311
- }
312
- ],
313
- "references": 1,
314
  "dialect": "urban",
315
  "calibrated": [
316
  {
317
  "expected": "ʕ",
318
  "heard": "ʔ",
319
- "from": 2,
320
  "to": 45
321
  }
322
  ],
@@ -2899,14 +2890,16 @@
2899
  },
2900
  "trace": {
2901
  "config": {
2902
- "leniency": 0.5,
2903
  "confident_peak": 0.3,
 
2904
  "deleted_peak": 0.05,
2905
  "deleted_comp": 0.2,
2906
  "flag_below": 50.0,
 
2907
  "issue_below": 90.0,
2908
  "near_vowel_floor": 90.0,
2909
- "word_min_weight": 0.5,
2910
  "vowel_weight": 0.5,
2911
  "unrelated_ratio": 0.65,
2912
  "unrelated_cap": 30.0,
@@ -2916,6 +2909,7 @@
2916
  "ref_min_mean": 60.0,
2917
  "margin_default": 0.0,
2918
  "ayin_deleted_floor": 60.0,
 
2919
  "margins": [
2920
  {
2921
  "target": "dˤ",
@@ -3055,8 +3049,12 @@
3055
  ],
3056
  "exemptions": [
3057
  "glottal_dropped",
 
3058
  "article_vowel_elided",
3059
- "near_vowel"
 
 
 
3060
  ],
3061
  "insertion": {
3062
  "score_cap": 60,
@@ -3727,8 +3725,14 @@
3727
  },
3728
  "references": [
3729
  {
 
3730
  "mean": 98.0,
3731
  "sounds": 23
 
 
 
 
 
3732
  }
3733
  ],
3734
  "tokens": [
@@ -3811,6 +3815,7 @@
3811
  "competitor": "i",
3812
  "competitor_other": false,
3813
  "p_competitor": 0.0107,
 
3814
  "gop": 0.0,
3815
  "confidence": 1.0,
3816
  "gop_factor": 1.0,
@@ -3835,6 +3840,10 @@
3835
  "stage": "baseline",
3836
  "score": 100.0,
3837
  "ref": 100.0,
 
 
 
 
3838
  "applied": false
3839
  },
3840
  {
@@ -3969,6 +3978,7 @@
3969
  "competitor": "i",
3970
  "competitor_other": false,
3971
  "p_competitor": 0.0071,
 
3972
  "gop": 0.0,
3973
  "confidence": 1.0,
3974
  "gop_factor": 1.0,
@@ -3993,6 +4003,10 @@
3993
  "stage": "baseline",
3994
  "score": 100.0,
3995
  "ref": 100.0,
 
 
 
 
3996
  "applied": false
3997
  },
3998
  {
@@ -4032,7 +4046,7 @@
4032
  ],
4033
  "window": [
4034
  12,
4035
- 22
4036
  ],
4037
  "curves": {
4038
  "start": 8,
@@ -4052,9 +4066,7 @@
4052
  0.001,
4053
  0.001,
4054
  0.004,
4055
- 0.004,
4056
- 0.004,
4057
- 0.002
4058
  ],
4059
  "competitor": [
4060
  0.007,
@@ -4072,9 +4084,7 @@
4072
  0.002,
4073
  0.002,
4074
  0.007,
4075
- 0.007,
4076
- 0.007,
4077
- 0.004
4078
  ],
4079
  "prev": [
4080
  0.904,
@@ -4092,9 +4102,7 @@
4092
  0.002,
4093
  0.002,
4094
  0.007,
4095
- 0.007,
4096
- 0.007,
4097
- 0.004
4098
  ],
4099
  "next": [
4100
  0.004,
@@ -4112,9 +4120,7 @@
4112
  0.001,
4113
  0.001,
4114
  0.9,
4115
- 0.9,
4116
- 0.9,
4117
- 0.002
4118
  ],
4119
  "blank": [
4120
  0.004,
@@ -4132,15 +4138,14 @@
4132
  0.97,
4133
  0.97,
4134
  0.004,
4135
- 0.004,
4136
- 0.004,
4137
- 0.95
4138
  ]
4139
  },
4140
  "p_target": 0.8001,
4141
  "competitor": "i",
4142
  "competitor_other": false,
4143
  "p_competitor": 0.0143,
 
4144
  "gop": 0.0,
4145
  "confidence": 1.0,
4146
  "gop_factor": 1.0,
@@ -4165,6 +4170,10 @@
4165
  "stage": "baseline",
4166
  "score": 100.0,
4167
  "ref": 100.0,
 
 
 
 
4168
  "applied": false
4169
  },
4170
  {
@@ -4199,14 +4208,12 @@
4199
  25
4200
  ],
4201
  "window": [
4202
- 16,
4203
  27
4204
  ],
4205
  "curves": {
4206
- "start": 12,
4207
  "target": [
4208
- 0.002,
4209
- 0.002,
4210
  0.007,
4211
  0.007,
4212
  0.001,
@@ -4226,8 +4233,6 @@
4226
  0.004
4227
  ],
4228
  "competitor": [
4229
- 0.004,
4230
- 0.004,
4231
  0.014,
4232
  0.014,
4233
  0.002,
@@ -4247,8 +4252,6 @@
4247
  0.009
4248
  ],
4249
  "prev": [
4250
- 0.002,
4251
- 0.002,
4252
  0.8,
4253
  0.8,
4254
  0.001,
@@ -4268,8 +4271,6 @@
4268
  0.004
4269
  ],
4270
  "next": [
4271
- 0.004,
4272
- 0.004,
4273
  0.014,
4274
  0.014,
4275
  0.002,
@@ -4289,8 +4290,6 @@
4289
  0.884
4290
  ],
4291
  "blank": [
4292
- 0.95,
4293
- 0.95,
4294
  0.007,
4295
  0.007,
4296
  0.97,
@@ -4314,6 +4313,7 @@
4314
  "competitor": "e",
4315
  "competitor_other": false,
4316
  "p_competitor": 0.0071,
 
4317
  "gop": 0.0,
4318
  "confidence": 1.0,
4319
  "gop_factor": 1.0,
@@ -4338,6 +4338,10 @@
4338
  "stage": "baseline",
4339
  "score": 100.0,
4340
  "ref": 100.0,
 
 
 
 
4341
  "applied": false
4342
  },
4343
  {
@@ -4472,6 +4476,7 @@
4472
  "competitor": "e",
4473
  "competitor_other": false,
4474
  "p_competitor": 0.0086,
 
4475
  "gop": 0.0,
4476
  "confidence": 1.0,
4477
  "gop_factor": 1.0,
@@ -4496,6 +4501,10 @@
4496
  "stage": "baseline",
4497
  "score": 100.0,
4498
  "ref": 100.0,
 
 
 
 
4499
  "applied": false
4500
  },
4501
  {
@@ -4620,6 +4629,7 @@
4620
  "competitor": "e",
4621
  "competitor_other": false,
4622
  "p_competitor": 0.0143,
 
4623
  "gop": 0.0,
4624
  "confidence": 1.0,
4625
  "gop_factor": 1.0,
@@ -4644,6 +4654,10 @@
4644
  "stage": "baseline",
4645
  "score": 100.0,
4646
  "ref": 100.0,
 
 
 
 
4647
  "applied": false
4648
  },
4649
  {
@@ -4700,20 +4714,20 @@
4700
  0.001
4701
  ],
4702
  "competitor": [
4703
- 0.002,
4704
- 0.002,
4705
- 0.007,
4706
- 0.007,
4707
- 0.002,
4708
- 0.002,
4709
- 0.02,
4710
  0.004,
4711
- 0.96,
4712
  0.004,
4713
- 0.025,
4714
- 0.025,
 
 
 
4715
  0.007,
4716
- 0.001
 
 
 
 
 
4717
  ],
4718
  "prev": [
4719
  0.002,
@@ -4765,46 +4779,51 @@
4765
  ]
4766
  },
4767
  "p_target": 0.4498,
4768
- "competitor": "h",
4769
  "competitor_other": false,
4770
- "p_competitor": 0.9598,
4771
- "gop": -0.758,
 
4772
  "confidence": 1.0,
4773
- "gop_factor": 0.685,
4774
  "confidence_factor": 1.0,
4775
  "deleted": false,
4776
  "stages": [
4777
  {
4778
  "stage": "gop",
4779
- "score": 68.5
4780
  },
4781
  {
4782
  "stage": "exemption",
4783
- "score": 68.5,
4784
- "applied": null
4785
  },
4786
  {
4787
  "stage": "insertion",
4788
- "score": 68.5,
4789
  "applied": null
4790
  },
4791
  {
4792
  "stage": "baseline",
4793
- "score": 68.5,
4794
  "ref": 100.0,
 
 
 
 
4795
  "applied": false
4796
  },
4797
  {
4798
  "stage": "calibration",
4799
- "score": 68.5,
4800
  "floor": null,
4801
  "applied": false
4802
  }
4803
  ],
4804
- "final": 68.5,
4805
  "margin": {
4806
  "needed": 0.0,
4807
- "got": 0.758,
4808
  "beyond": true
4809
  },
4810
  "vowel": true,
@@ -4812,7 +4831,7 @@
4812
  "flagged": false,
4813
  "rule": null,
4814
  "tip": false,
4815
- "issue": true
4816
  },
4817
  {
4818
  "i": 7,
@@ -4830,7 +4849,7 @@
4830
  ],
4831
  "window": [
4832
  38,
4833
- 50
4834
  ],
4835
  "curves": {
4836
  "start": 34,
@@ -4851,10 +4870,7 @@
4851
  0.002,
4852
  0.002,
4853
  0.002,
4854
- 0.007,
4855
- 0.007,
4856
- 0.007,
4857
- 0.004
4858
  ],
4859
  "competitor": [
4860
  0.007,
@@ -4873,10 +4889,7 @@
4873
  0.001,
4874
  0.001,
4875
  0.001,
4876
- 0.004,
4877
- 0.004,
4878
- 0.004,
4879
- 0.002
4880
  ],
4881
  "prev": [
4882
  0.007,
@@ -4895,10 +4908,7 @@
4895
  0.001,
4896
  0.001,
4897
  0.001,
4898
- 0.004,
4899
- 0.004,
4900
- 0.004,
4901
- 0.002
4902
  ],
4903
  "next": [
4904
  0.007,
@@ -4917,10 +4927,7 @@
4917
  0.001,
4918
  0.001,
4919
  0.001,
4920
- 0.9,
4921
- 0.9,
4922
- 0.9,
4923
- 0.002
4924
  ],
4925
  "blank": [
4926
  0.007,
@@ -4939,40 +4946,42 @@
4939
  0.97,
4940
  0.97,
4941
  0.97,
4942
- 0.004,
4943
- 0.004,
4944
- 0.004,
4945
- 0.95
4946
  ]
4947
  },
4948
  "p_target": 0.325,
4949
  "competitor": "h",
4950
  "competitor_other": false,
4951
  "p_competitor": 0.9598,
 
4952
  "gop": -1.083,
4953
  "confidence": 1.0,
4954
- "gop_factor": 0.582,
4955
  "confidence_factor": 1.0,
4956
  "deleted": false,
4957
  "stages": [
4958
  {
4959
  "stage": "gop",
4960
- "score": 58.2
4961
  },
4962
  {
4963
  "stage": "exemption",
4964
- "score": 58.2,
4965
  "applied": null
4966
  },
4967
  {
4968
  "stage": "insertion",
4969
- "score": 58.2,
4970
  "applied": null
4971
  },
4972
  {
4973
  "stage": "baseline",
4974
  "score": 100.0,
4975
  "ref": 55.0,
 
 
 
 
4976
  "applied": true
4977
  },
4978
  {
@@ -5007,15 +5016,12 @@
5007
  53
5008
  ],
5009
  "window": [
5010
- 43,
5011
  55
5012
  ],
5013
  "curves": {
5014
- "start": 39,
5015
  "target": [
5016
- 0.001,
5017
- 0.004,
5018
- 0.025,
5019
  0.025,
5020
  0.007,
5021
  0.001,
@@ -5035,9 +5041,6 @@
5035
  0.004
5036
  ],
5037
  "competitor": [
5038
- 0.003,
5039
- 0.007,
5040
- 0.05,
5041
  0.05,
5042
  0.014,
5043
  0.002,
@@ -5057,9 +5060,6 @@
5057
  0.007
5058
  ],
5059
  "prev": [
5060
- 0.003,
5061
- 0.007,
5062
- 0.325,
5063
  0.325,
5064
  0.014,
5065
  0.002,
@@ -5079,9 +5079,6 @@
5079
  0.007
5080
  ],
5081
  "next": [
5082
- 0.003,
5083
- 0.007,
5084
- 0.05,
5085
  0.05,
5086
  0.014,
5087
  0.002,
@@ -5101,9 +5098,6 @@
5101
  0.904
5102
  ],
5103
  "blank": [
5104
- 0.001,
5105
- 0.9,
5106
- 0.025,
5107
  0.025,
5108
  0.8,
5109
  0.97,
@@ -5126,7 +5120,8 @@
5126
  "p_target": 0.9003,
5127
  "competitor": "e",
5128
  "competitor_other": false,
5129
- "p_competitor": 0.0143,
 
5130
  "gop": 0.0,
5131
  "confidence": 1.0,
5132
  "gop_factor": 1.0,
@@ -5151,6 +5146,10 @@
5151
  "stage": "baseline",
5152
  "score": 100.0,
5153
  "ref": 100.0,
 
 
 
 
5154
  "applied": false
5155
  },
5156
  {
@@ -5285,6 +5284,7 @@
5285
  "competitor": "e",
5286
  "competitor_other": false,
5287
  "p_competitor": 0.0071,
 
5288
  "gop": 0.0,
5289
  "confidence": 1.0,
5290
  "gop_factor": 1.0,
@@ -5309,6 +5309,10 @@
5309
  "stage": "baseline",
5310
  "score": 100.0,
5311
  "ref": 100.0,
 
 
 
 
5312
  "applied": false
5313
  },
5314
  {
@@ -5433,6 +5437,7 @@
5433
  "competitor": "e",
5434
  "competitor_other": false,
5435
  "p_competitor": 0.0107,
 
5436
  "gop": 0.0,
5437
  "confidence": 1.0,
5438
  "gop_factor": 1.0,
@@ -5457,6 +5462,10 @@
5457
  "stage": "baseline",
5458
  "score": 100.0,
5459
  "ref": 100.0,
 
 
 
 
5460
  "applied": false
5461
  },
5462
  {
@@ -5601,6 +5610,7 @@
5601
  "competitor": "e",
5602
  "competitor_other": false,
5603
  "p_competitor": 0.0143,
 
5604
  "gop": 0.0,
5605
  "confidence": 1.0,
5606
  "gop_factor": 1.0,
@@ -5625,6 +5635,10 @@
5625
  "stage": "baseline",
5626
  "score": 100.0,
5627
  "ref": 100.0,
 
 
 
 
5628
  "applied": false
5629
  },
5630
  {
@@ -5663,14 +5677,12 @@
5663
  75
5664
  ],
5665
  "window": [
5666
- 67,
5667
  77
5668
  ],
5669
  "curves": {
5670
- "start": 63,
5671
  "target": [
5672
- 0.005,
5673
- 0.005,
5674
  0.021,
5675
  0.021,
5676
  0.003,
@@ -5689,8 +5701,6 @@
5689
  0.005
5690
  ],
5691
  "competitor": [
5692
- 0.004,
5693
- 0.004,
5694
  0.014,
5695
  0.014,
5696
  0.002,
@@ -5709,8 +5719,6 @@
5709
  0.004
5710
  ],
5711
  "prev": [
5712
- 0.004,
5713
- 0.004,
5714
  0.807,
5715
  0.807,
5716
  0.002,
@@ -5729,8 +5737,6 @@
5729
  0.004
5730
  ],
5731
  "next": [
5732
- 0.002,
5733
- 0.002,
5734
  0.007,
5735
  0.007,
5736
  0.001,
@@ -5749,8 +5755,6 @@
5749
  0.002
5750
  ],
5751
  "blank": [
5752
- 0.95,
5753
- 0.95,
5754
  0.007,
5755
  0.007,
5756
  0.97,
@@ -5773,6 +5777,7 @@
5773
  "competitor": "e",
5774
  "competitor_other": false,
5775
  "p_competitor": 0.0129,
 
5776
  "gop": 0.0,
5777
  "confidence": 1.0,
5778
  "gop_factor": 1.0,
@@ -5797,6 +5802,10 @@
5797
  "stage": "baseline",
5798
  "score": 100.0,
5799
  "ref": 100.0,
 
 
 
 
5800
  "applied": false
5801
  },
5802
  {
@@ -5921,6 +5930,7 @@
5921
  "competitor": "i",
5922
  "competitor_other": false,
5923
  "p_competitor": 0.0107,
 
5924
  "gop": 0.0,
5925
  "confidence": 1.0,
5926
  "gop_factor": 1.0,
@@ -5934,7 +5944,7 @@
5934
  {
5935
  "stage": "exemption",
5936
  "score": 100.0,
5937
- "applied": null
5938
  },
5939
  {
5940
  "stage": "insertion",
@@ -5945,6 +5955,10 @@
5945
  "stage": "baseline",
5946
  "score": 100.0,
5947
  "ref": 100.0,
 
 
 
 
5948
  "applied": false
5949
  },
5950
  {
@@ -6069,31 +6083,36 @@
6069
  "competitor": "ʔ",
6070
  "competitor_other": false,
6071
  "p_competitor": 0.8001,
 
6072
  "gop": -4.718,
6073
  "confidence": 0.024,
6074
- "gop_factor": 0.095,
6075
  "confidence_factor": 0.154,
6076
  "deleted": false,
6077
  "stages": [
6078
  {
6079
  "stage": "gop",
6080
- "score": 1.5
6081
  },
6082
  {
6083
  "stage": "exemption",
6084
- "score": 1.5,
6085
  "applied": null
6086
  },
6087
  {
6088
  "stage": "insertion",
6089
- "score": 1.5,
6090
  "applied": null
6091
  },
6092
  {
6093
  "stage": "baseline",
6094
- "score": 1.5,
6095
- "ref": 100.0,
6096
- "applied": false
 
 
 
 
6097
  },
6098
  {
6099
  "stage": "calibration",
@@ -6226,6 +6245,7 @@
6226
  "competitor": "ʔ",
6227
  "competitor_other": false,
6228
  "p_competitor": 0.8001,
 
6229
  "gop": 0.0,
6230
  "confidence": 1.0,
6231
  "gop_factor": 1.0,
@@ -6239,7 +6259,7 @@
6239
  {
6240
  "stage": "exemption",
6241
  "score": 100.0,
6242
- "applied": null
6243
  },
6244
  {
6245
  "stage": "insertion",
@@ -6250,6 +6270,10 @@
6250
  "stage": "baseline",
6251
  "score": 100.0,
6252
  "ref": 100.0,
 
 
 
 
6253
  "applied": false
6254
  },
6255
  {
@@ -6285,7 +6309,7 @@
6285
  ],
6286
  "window": [
6287
  87,
6288
- 97
6289
  ],
6290
  "curves": {
6291
  "start": 83,
@@ -6305,9 +6329,7 @@
6305
  0.001,
6306
  0.001,
6307
  0.005,
6308
- 0.005,
6309
- 0.005,
6310
- 0.002
6311
  ],
6312
  "competitor": [
6313
  0.004,
@@ -6325,9 +6347,7 @@
6325
  0.002,
6326
  0.002,
6327
  0.011,
6328
- 0.011,
6329
- 0.011,
6330
- 0.004
6331
  ],
6332
  "prev": [
6333
  0.002,
@@ -6345,9 +6365,7 @@
6345
  0.001,
6346
  0.001,
6347
  0.005,
6348
- 0.005,
6349
- 0.005,
6350
- 0.002
6351
  ],
6352
  "next": [
6353
  0.002,
@@ -6365,9 +6383,7 @@
6365
  0.001,
6366
  0.001,
6367
  0.85,
6368
- 0.85,
6369
- 0.85,
6370
- 0.002
6371
  ],
6372
  "blank": [
6373
  0.95,
@@ -6385,15 +6401,14 @@
6385
  0.97,
6386
  0.97,
6387
  0.005,
6388
- 0.005,
6389
- 0.005,
6390
- 0.95
6391
  ]
6392
  },
6393
  "p_target": 0.8496,
6394
  "competitor": "i",
6395
  "competitor_other": false,
6396
  "p_competitor": 0.0107,
 
6397
  "gop": 0.0,
6398
  "confidence": 1.0,
6399
  "gop_factor": 1.0,
@@ -6418,6 +6433,10 @@
6418
  "stage": "baseline",
6419
  "score": 100.0,
6420
  "ref": 100.0,
 
 
 
 
6421
  "applied": false
6422
  },
6423
  {
@@ -6452,14 +6471,12 @@
6452
  100
6453
  ],
6454
  "window": [
6455
- 91,
6456
  102
6457
  ],
6458
  "curves": {
6459
- "start": 87,
6460
  "target": [
6461
- 0.002,
6462
- 0.002,
6463
  0.005,
6464
  0.005,
6465
  0.001,
@@ -6479,8 +6496,6 @@
6479
  0.001
6480
  ],
6481
  "competitor": [
6482
- 0.004,
6483
- 0.004,
6484
  0.011,
6485
  0.011,
6486
  0.002,
@@ -6500,8 +6515,6 @@
6500
  0.002
6501
  ],
6502
  "prev": [
6503
- 0.002,
6504
- 0.002,
6505
  0.85,
6506
  0.85,
6507
  0.001,
@@ -6521,8 +6534,6 @@
6521
  0.001
6522
  ],
6523
  "next": [
6524
- 0.004,
6525
- 0.004,
6526
  0.011,
6527
  0.011,
6528
  0.002,
@@ -6542,8 +6553,6 @@
6542
  0.002
6543
  ],
6544
  "blank": [
6545
- 0.95,
6546
- 0.95,
6547
  0.005,
6548
  0.005,
6549
  0.97,
@@ -6567,6 +6576,7 @@
6567
  "competitor": "e",
6568
  "competitor_other": false,
6569
  "p_competitor": 0.0107,
 
6570
  "gop": 0.0,
6571
  "confidence": 1.0,
6572
  "gop_factor": 1.0,
@@ -6591,6 +6601,10 @@
6591
  "stage": "baseline",
6592
  "score": 100.0,
6593
  "ref": 100.0,
 
 
 
 
6594
  "applied": false
6595
  },
6596
  {
@@ -6735,6 +6749,7 @@
6735
  "competitor": "e",
6736
  "competitor_other": false,
6737
  "p_competitor": 0.0107,
 
6738
  "gop": 0.0,
6739
  "confidence": 1.0,
6740
  "gop_factor": 1.0,
@@ -6759,6 +6774,10 @@
6759
  "stage": "baseline",
6760
  "score": 100.0,
6761
  "ref": 100.0,
 
 
 
 
6762
  "applied": false
6763
  },
6764
  {
@@ -6793,14 +6812,12 @@
6793
  112
6794
  ],
6795
  "window": [
6796
- 104,
6797
  114
6798
  ],
6799
  "curves": {
6800
- "start": 100,
6801
  "target": [
6802
- 0.002,
6803
- 0.002,
6804
  0.005,
6805
  0.005,
6806
  0.001,
@@ -6819,8 +6836,6 @@
6819
  0.004
6820
  ],
6821
  "competitor": [
6822
- 0.002,
6823
- 0.002,
6824
  0.005,
6825
  0.005,
6826
  0.001,
@@ -6839,8 +6854,6 @@
6839
  0.004
6840
  ],
6841
  "prev": [
6842
- 0.004,
6843
- 0.004,
6844
  0.855,
6845
  0.855,
6846
  0.002,
@@ -6859,8 +6872,6 @@
6859
  0.007
6860
  ],
6861
  "next": [
6862
- 0.004,
6863
- 0.004,
6864
  0.011,
6865
  0.011,
6866
  0.002,
@@ -6879,8 +6890,6 @@
6879
  0.904
6880
  ],
6881
  "blank": [
6882
- 0.95,
6883
- 0.95,
6884
  0.005,
6885
  0.005,
6886
  0.97,
@@ -6903,6 +6912,7 @@
6903
  "competitor": "w",
6904
  "competitor_other": false,
6905
  "p_competitor": 0.0054,
 
6906
  "gop": 0.0,
6907
  "confidence": 1.0,
6908
  "gop_factor": 1.0,
@@ -6927,6 +6937,10 @@
6927
  "stage": "baseline",
6928
  "score": 100.0,
6929
  "ref": 100.0,
 
 
 
 
6930
  "applied": false
6931
  },
6932
  {
@@ -7061,6 +7075,7 @@
7061
  "competitor": "i",
7062
  "competitor_other": false,
7063
  "p_competitor": 0.0071,
 
7064
  "gop": 0.0,
7065
  "confidence": 1.0,
7066
  "gop_factor": 1.0,
@@ -7085,6 +7100,10 @@
7085
  "stage": "baseline",
7086
  "score": 100.0,
7087
  "ref": 100.0,
 
 
 
 
7088
  "applied": false
7089
  },
7090
  {
@@ -7213,6 +7232,7 @@
7213
  "competitor": "i",
7214
  "competitor_other": false,
7215
  "p_competitor": 0.0107,
 
7216
  "gop": 0.0,
7217
  "confidence": 1.0,
7218
  "gop_factor": 1.0,
@@ -7237,6 +7257,10 @@
7237
  "stage": "baseline",
7238
  "score": 100.0,
7239
  "ref": 100.0,
 
 
 
 
7240
  "applied": false
7241
  },
7242
  {
@@ -7342,6 +7366,7 @@
7342
  "competitor": "i",
7343
  "competitor_other": false,
7344
  "p_competitor": 0.0107,
 
7345
  "gop": 0.0,
7346
  "confidence": 1.0,
7347
  "gop_factor": 1.0,
@@ -7355,7 +7380,7 @@
7355
  {
7356
  "stage": "exemption",
7357
  "score": 100.0,
7358
- "applied": null
7359
  },
7360
  {
7361
  "stage": "insertion",
@@ -7366,6 +7391,10 @@
7366
  "stage": "baseline",
7367
  "score": 100.0,
7368
  "ref": 100.0,
 
 
 
 
7369
  "applied": false
7370
  },
7371
  {
@@ -7407,12 +7436,12 @@
7407
  100.0,
7408
  100.0,
7409
  100.0,
7410
- 68.5,
7411
  100.0
7412
  ],
7413
- "min": 68.5,
7414
- "mean": 93.7,
7415
- "score": 81.1
7416
  },
7417
  {
7418
  "tr": "סִידִי",
@@ -7437,7 +7466,7 @@
7437
  ],
7438
  "min": 45.0,
7439
  "mean": 89.0,
7440
- "score": 67.0
7441
  },
7442
  {
7443
  "tr": "פִי",
@@ -7463,12 +7492,12 @@
7463
  }
7464
  ],
7465
  "sentence": {
7466
- "phone_mean": 96.1,
7467
- "word_mean": 91.4,
7468
  "phone_weight": 0.5,
7469
  "word_weight": 0.5,
7470
- "before_cap": 93.7,
7471
- "final": 94,
7472
  "capped": false
7473
  },
7474
  "tips": [
 
1
  {
2
+ "score": 96,
3
  "variant": {
4
  "ar": "وين سيدك؟ سيدي قعد في بيته.",
5
  "tr": "וֵין סִידַכּ סִידִי קַעַד פִי בֵּיתוֹ",
6
  "ipa": "w eː n | s iː d a k | s iː d i | ʔ a ʕ a d | f i | b eː t o"
7
  },
8
  "heard_ipa": "w e n s i d a h k s i d i ʔ a ʔ a d f i b e t o",
9
+ "heard_tr": "וֵין סִידַכּ סִידִי קַקַד פִי בֵּיתוֹ",
10
  "unrelated": false,
11
  "words": [
12
  {
 
51
  {
52
  "ar": "سيدك؟",
53
  "tr": "סִידַכּ",
54
+ "heard_tr": "סִידַכּ",
55
+ "score": 100.0,
56
  "letters": [
57
  {
58
  "t": "סִי",
 
60
  },
61
  {
62
  "t": "דַ",
63
+ "score": 100.0
 
64
  },
65
  {
66
  "t": "כּ",
 
92
  {
93
  "ipa": "a",
94
  "word": 1,
95
+ "score": 100.0,
96
  "flagged": false,
97
  "heard": null
98
  },
 
157
  "ar": "قعد",
158
  "tr": "קַעַד",
159
  "heard_tr": "קַקַד",
160
+ "score": 73.6,
161
  "letters": [
162
  {
163
  "t": "קַ",
 
300
  "score": 45
301
  }
302
  ],
303
+ "issues": [],
304
+ "references": 2,
 
 
 
 
 
 
 
 
305
  "dialect": "urban",
306
  "calibrated": [
307
  {
308
  "expected": "ʕ",
309
  "heard": "ʔ",
310
+ "from": 4,
311
  "to": 45
312
  }
313
  ],
 
2890
  },
2891
  "trace": {
2892
  "config": {
2893
+ "leniency": 0.35,
2894
  "confident_peak": 0.3,
2895
+ "consonant_window": 4,
2896
  "deleted_peak": 0.05,
2897
  "deleted_comp": 0.2,
2898
  "flag_below": 50.0,
2899
+ "flag_below_sound": {},
2900
  "issue_below": 90.0,
2901
  "near_vowel_floor": 90.0,
2902
+ "word_min_weight": 0.35,
2903
  "vowel_weight": 0.5,
2904
  "unrelated_ratio": 0.65,
2905
  "unrelated_cap": 30.0,
 
2909
  "ref_min_mean": 60.0,
2910
  "margin_default": 0.0,
2911
  "ayin_deleted_floor": 60.0,
2912
+ "onset_transient_floor": 90.0,
2913
  "margins": [
2914
  {
2915
  "target": "dˤ",
 
3049
  ],
3050
  "exemptions": [
3051
  "glottal_dropped",
3052
+ "onset_transient",
3053
  "article_vowel_elided",
3054
+ "near_vowel",
3055
+ "final_b_devoicing",
3056
+ "word_final_vowel_linking",
3057
+ "unmarked_short_vowel"
3058
  ],
3059
  "insertion": {
3060
  "score_cap": 60,
 
3725
  },
3726
  "references": [
3727
  {
3728
+ "voice": "ראאף",
3729
  "mean": 98.0,
3730
  "sounds": 23
3731
+ },
3732
+ {
3733
+ "voice": "פדוא",
3734
+ "mean": 97.8,
3735
+ "sounds": 23
3736
  }
3737
  ],
3738
  "tokens": [
 
3815
  "competitor": "i",
3816
  "competitor_other": false,
3817
  "p_competitor": 0.0107,
3818
+ "competitor_frames": 0,
3819
  "gop": 0.0,
3820
  "confidence": 1.0,
3821
  "gop_factor": 1.0,
 
3840
  "stage": "baseline",
3841
  "score": 100.0,
3842
  "ref": 100.0,
3843
+ "refs": [
3844
+ 100.0,
3845
+ 100.0
3846
+ ],
3847
  "applied": false
3848
  },
3849
  {
 
3978
  "competitor": "i",
3979
  "competitor_other": false,
3980
  "p_competitor": 0.0071,
3981
+ "competitor_frames": 0,
3982
  "gop": 0.0,
3983
  "confidence": 1.0,
3984
  "gop_factor": 1.0,
 
4003
  "stage": "baseline",
4004
  "score": 100.0,
4005
  "ref": 100.0,
4006
+ "refs": [
4007
+ 100.0,
4008
+ 100.0
4009
+ ],
4010
  "applied": false
4011
  },
4012
  {
 
4046
  ],
4047
  "window": [
4048
  12,
4049
+ 20
4050
  ],
4051
  "curves": {
4052
  "start": 8,
 
4066
  0.001,
4067
  0.001,
4068
  0.004,
4069
+ 0.004
 
 
4070
  ],
4071
  "competitor": [
4072
  0.007,
 
4084
  0.002,
4085
  0.002,
4086
  0.007,
4087
+ 0.007
 
 
4088
  ],
4089
  "prev": [
4090
  0.904,
 
4102
  0.002,
4103
  0.002,
4104
  0.007,
4105
+ 0.007
 
 
4106
  ],
4107
  "next": [
4108
  0.004,
 
4120
  0.001,
4121
  0.001,
4122
  0.9,
4123
+ 0.9
 
 
4124
  ],
4125
  "blank": [
4126
  0.004,
 
4138
  0.97,
4139
  0.97,
4140
  0.004,
4141
+ 0.004
 
 
4142
  ]
4143
  },
4144
  "p_target": 0.8001,
4145
  "competitor": "i",
4146
  "competitor_other": false,
4147
  "p_competitor": 0.0143,
4148
+ "competitor_frames": 0,
4149
  "gop": 0.0,
4150
  "confidence": 1.0,
4151
  "gop_factor": 1.0,
 
4170
  "stage": "baseline",
4171
  "score": 100.0,
4172
  "ref": 100.0,
4173
+ "refs": [
4174
+ 100.0,
4175
+ 100.0
4176
+ ],
4177
  "applied": false
4178
  },
4179
  {
 
4208
  25
4209
  ],
4210
  "window": [
4211
+ 18,
4212
  27
4213
  ],
4214
  "curves": {
4215
+ "start": 14,
4216
  "target": [
 
 
4217
  0.007,
4218
  0.007,
4219
  0.001,
 
4233
  0.004
4234
  ],
4235
  "competitor": [
 
 
4236
  0.014,
4237
  0.014,
4238
  0.002,
 
4252
  0.009
4253
  ],
4254
  "prev": [
 
 
4255
  0.8,
4256
  0.8,
4257
  0.001,
 
4271
  0.004
4272
  ],
4273
  "next": [
 
 
4274
  0.014,
4275
  0.014,
4276
  0.002,
 
4290
  0.884
4291
  ],
4292
  "blank": [
 
 
4293
  0.007,
4294
  0.007,
4295
  0.97,
 
4313
  "competitor": "e",
4314
  "competitor_other": false,
4315
  "p_competitor": 0.0071,
4316
+ "competitor_frames": 0,
4317
  "gop": 0.0,
4318
  "confidence": 1.0,
4319
  "gop_factor": 1.0,
 
4338
  "stage": "baseline",
4339
  "score": 100.0,
4340
  "ref": 100.0,
4341
+ "refs": [
4342
+ 100.0,
4343
+ 100.0
4344
+ ],
4345
  "applied": false
4346
  },
4347
  {
 
4476
  "competitor": "e",
4477
  "competitor_other": false,
4478
  "p_competitor": 0.0086,
4479
+ "competitor_frames": 0,
4480
  "gop": 0.0,
4481
  "confidence": 1.0,
4482
  "gop_factor": 1.0,
 
4501
  "stage": "baseline",
4502
  "score": 100.0,
4503
  "ref": 100.0,
4504
+ "refs": [
4505
+ 100.0,
4506
+ 100.0
4507
+ ],
4508
  "applied": false
4509
  },
4510
  {
 
4629
  "competitor": "e",
4630
  "competitor_other": false,
4631
  "p_competitor": 0.0143,
4632
+ "competitor_frames": 0,
4633
  "gop": 0.0,
4634
  "confidence": 1.0,
4635
  "gop_factor": 1.0,
 
4654
  "stage": "baseline",
4655
  "score": 100.0,
4656
  "ref": 100.0,
4657
+ "refs": [
4658
+ 100.0,
4659
+ 100.0
4660
+ ],
4661
  "applied": false
4662
  },
4663
  {
 
4714
  0.001
4715
  ],
4716
  "competitor": [
 
 
 
 
 
 
 
4717
  0.004,
 
4718
  0.004,
4719
+ 0.014,
4720
+ 0.014,
4721
+ 0.004,
4722
+ 0.004,
4723
+ 0.039,
4724
  0.007,
4725
+ 0.003,
4726
+ 0.007,
4727
+ 0.05,
4728
+ 0.05,
4729
+ 0.014,
4730
+ 0.002
4731
  ],
4732
  "prev": [
4733
  0.002,
 
4779
  ]
4780
  },
4781
  "p_target": 0.4498,
4782
+ "competitor": "i",
4783
  "competitor_other": false,
4784
+ "p_competitor": 0.0393,
4785
+ "competitor_frames": 0,
4786
+ "gop": 0.0,
4787
  "confidence": 1.0,
4788
+ "gop_factor": 1.0,
4789
  "confidence_factor": 1.0,
4790
  "deleted": false,
4791
  "stages": [
4792
  {
4793
  "stage": "gop",
4794
+ "score": 100.0
4795
  },
4796
  {
4797
  "stage": "exemption",
4798
+ "score": 100.0,
4799
+ "applied": "unmarked_short_vowel"
4800
  },
4801
  {
4802
  "stage": "insertion",
4803
+ "score": 100.0,
4804
  "applied": null
4805
  },
4806
  {
4807
  "stage": "baseline",
4808
+ "score": 100.0,
4809
  "ref": 100.0,
4810
+ "refs": [
4811
+ 100.0,
4812
+ 100.0
4813
+ ],
4814
  "applied": false
4815
  },
4816
  {
4817
  "stage": "calibration",
4818
+ "score": 100.0,
4819
  "floor": null,
4820
  "applied": false
4821
  }
4822
  ],
4823
+ "final": 100.0,
4824
  "margin": {
4825
  "needed": 0.0,
4826
+ "got": -0.0,
4827
  "beyond": true
4828
  },
4829
  "vowel": true,
 
4831
  "flagged": false,
4832
  "rule": null,
4833
  "tip": false,
4834
+ "issue": false
4835
  },
4836
  {
4837
  "i": 7,
 
4849
  ],
4850
  "window": [
4851
  38,
4852
+ 47
4853
  ],
4854
  "curves": {
4855
  "start": 34,
 
4870
  0.002,
4871
  0.002,
4872
  0.002,
4873
+ 0.007
 
 
 
4874
  ],
4875
  "competitor": [
4876
  0.007,
 
4889
  0.001,
4890
  0.001,
4891
  0.001,
4892
+ 0.004
 
 
 
4893
  ],
4894
  "prev": [
4895
  0.007,
 
4908
  0.001,
4909
  0.001,
4910
  0.001,
4911
+ 0.004
 
 
 
4912
  ],
4913
  "next": [
4914
  0.007,
 
4927
  0.001,
4928
  0.001,
4929
  0.001,
4930
+ 0.9
 
 
 
4931
  ],
4932
  "blank": [
4933
  0.007,
 
4946
  0.97,
4947
  0.97,
4948
  0.97,
4949
+ 0.004
 
 
 
4950
  ]
4951
  },
4952
  "p_target": 0.325,
4953
  "competitor": "h",
4954
  "competitor_other": false,
4955
  "p_competitor": 0.9598,
4956
+ "competitor_frames": 1,
4957
  "gop": -1.083,
4958
  "confidence": 1.0,
4959
+ "gop_factor": 0.685,
4960
  "confidence_factor": 1.0,
4961
  "deleted": false,
4962
  "stages": [
4963
  {
4964
  "stage": "gop",
4965
+ "score": 68.4
4966
  },
4967
  {
4968
  "stage": "exemption",
4969
+ "score": 68.4,
4970
  "applied": null
4971
  },
4972
  {
4973
  "stage": "insertion",
4974
+ "score": 68.4,
4975
  "applied": null
4976
  },
4977
  {
4978
  "stage": "baseline",
4979
  "score": 100.0,
4980
  "ref": 55.0,
4981
+ "refs": [
4982
+ 55.0,
4983
+ 70.0
4984
+ ],
4985
  "applied": true
4986
  },
4987
  {
 
5016
  53
5017
  ],
5018
  "window": [
5019
+ 46,
5020
  55
5021
  ],
5022
  "curves": {
5023
+ "start": 42,
5024
  "target": [
 
 
 
5025
  0.025,
5026
  0.007,
5027
  0.001,
 
5041
  0.004
5042
  ],
5043
  "competitor": [
 
 
 
5044
  0.05,
5045
  0.014,
5046
  0.002,
 
5060
  0.007
5061
  ],
5062
  "prev": [
 
 
 
5063
  0.325,
5064
  0.014,
5065
  0.002,
 
5079
  0.007
5080
  ],
5081
  "next": [
 
 
 
5082
  0.05,
5083
  0.014,
5084
  0.002,
 
5098
  0.904
5099
  ],
5100
  "blank": [
 
 
 
5101
  0.025,
5102
  0.8,
5103
  0.97,
 
5120
  "p_target": 0.9003,
5121
  "competitor": "e",
5122
  "competitor_other": false,
5123
+ "p_competitor": 0.0071,
5124
+ "competitor_frames": 0,
5125
  "gop": 0.0,
5126
  "confidence": 1.0,
5127
  "gop_factor": 1.0,
 
5146
  "stage": "baseline",
5147
  "score": 100.0,
5148
  "ref": 100.0,
5149
+ "refs": [
5150
+ 100.0,
5151
+ 100.0
5152
+ ],
5153
  "applied": false
5154
  },
5155
  {
 
5284
  "competitor": "e",
5285
  "competitor_other": false,
5286
  "p_competitor": 0.0071,
5287
+ "competitor_frames": 0,
5288
  "gop": 0.0,
5289
  "confidence": 1.0,
5290
  "gop_factor": 1.0,
 
5309
  "stage": "baseline",
5310
  "score": 100.0,
5311
  "ref": 100.0,
5312
+ "refs": [
5313
+ 100.0,
5314
+ 100.0
5315
+ ],
5316
  "applied": false
5317
  },
5318
  {
 
5437
  "competitor": "e",
5438
  "competitor_other": false,
5439
  "p_competitor": 0.0107,
5440
+ "competitor_frames": 0,
5441
  "gop": 0.0,
5442
  "confidence": 1.0,
5443
  "gop_factor": 1.0,
 
5462
  "stage": "baseline",
5463
  "score": 100.0,
5464
  "ref": 100.0,
5465
+ "refs": [
5466
+ 100.0,
5467
+ 100.0
5468
+ ],
5469
  "applied": false
5470
  },
5471
  {
 
5610
  "competitor": "e",
5611
  "competitor_other": false,
5612
  "p_competitor": 0.0143,
5613
+ "competitor_frames": 0,
5614
  "gop": 0.0,
5615
  "confidence": 1.0,
5616
  "gop_factor": 1.0,
 
5635
  "stage": "baseline",
5636
  "score": 100.0,
5637
  "ref": 100.0,
5638
+ "refs": [
5639
+ 100.0,
5640
+ 100.0
5641
+ ],
5642
  "applied": false
5643
  },
5644
  {
 
5677
  75
5678
  ],
5679
  "window": [
5680
+ 69,
5681
  77
5682
  ],
5683
  "curves": {
5684
+ "start": 65,
5685
  "target": [
 
 
5686
  0.021,
5687
  0.021,
5688
  0.003,
 
5701
  0.005
5702
  ],
5703
  "competitor": [
 
 
5704
  0.014,
5705
  0.014,
5706
  0.002,
 
5719
  0.004
5720
  ],
5721
  "prev": [
 
 
5722
  0.807,
5723
  0.807,
5724
  0.002,
 
5737
  0.004
5738
  ],
5739
  "next": [
 
 
5740
  0.007,
5741
  0.007,
5742
  0.001,
 
5755
  0.002
5756
  ],
5757
  "blank": [
 
 
5758
  0.007,
5759
  0.007,
5760
  0.97,
 
5777
  "competitor": "e",
5778
  "competitor_other": false,
5779
  "p_competitor": 0.0129,
5780
+ "competitor_frames": 0,
5781
  "gop": 0.0,
5782
  "confidence": 1.0,
5783
  "gop_factor": 1.0,
 
5802
  "stage": "baseline",
5803
  "score": 100.0,
5804
  "ref": 100.0,
5805
+ "refs": [
5806
+ 100.0,
5807
+ 100.0
5808
+ ],
5809
  "applied": false
5810
  },
5811
  {
 
5930
  "competitor": "i",
5931
  "competitor_other": false,
5932
  "p_competitor": 0.0107,
5933
+ "competitor_frames": 0,
5934
  "gop": 0.0,
5935
  "confidence": 1.0,
5936
  "gop_factor": 1.0,
 
5944
  {
5945
  "stage": "exemption",
5946
  "score": 100.0,
5947
+ "applied": "unmarked_short_vowel"
5948
  },
5949
  {
5950
  "stage": "insertion",
 
5955
  "stage": "baseline",
5956
  "score": 100.0,
5957
  "ref": 100.0,
5958
+ "refs": [
5959
+ 100.0,
5960
+ 100.0
5961
+ ],
5962
  "applied": false
5963
  },
5964
  {
 
6083
  "competitor": "ʔ",
6084
  "competitor_other": false,
6085
  "p_competitor": 0.8001,
6086
+ "competitor_frames": 2,
6087
  "gop": -4.718,
6088
  "confidence": 0.024,
6089
+ "gop_factor": 0.192,
6090
  "confidence_factor": 0.154,
6091
  "deleted": false,
6092
  "stages": [
6093
  {
6094
  "stage": "gop",
6095
+ "score": 3.0
6096
  },
6097
  {
6098
  "stage": "exemption",
6099
+ "score": 3.0,
6100
  "applied": null
6101
  },
6102
  {
6103
  "stage": "insertion",
6104
+ "score": 3.0,
6105
  "applied": null
6106
  },
6107
  {
6108
  "stage": "baseline",
6109
+ "score": 3.8,
6110
+ "ref": 80.0,
6111
+ "refs": [
6112
+ 100.0,
6113
+ 80.0
6114
+ ],
6115
+ "applied": true
6116
  },
6117
  {
6118
  "stage": "calibration",
 
6245
  "competitor": "ʔ",
6246
  "competitor_other": false,
6247
  "p_competitor": 0.8001,
6248
+ "competitor_frames": 1,
6249
  "gop": 0.0,
6250
  "confidence": 1.0,
6251
  "gop_factor": 1.0,
 
6259
  {
6260
  "stage": "exemption",
6261
  "score": 100.0,
6262
+ "applied": "unmarked_short_vowel"
6263
  },
6264
  {
6265
  "stage": "insertion",
 
6270
  "stage": "baseline",
6271
  "score": 100.0,
6272
  "ref": 100.0,
6273
+ "refs": [
6274
+ 100.0,
6275
+ 100.0
6276
+ ],
6277
  "applied": false
6278
  },
6279
  {
 
6309
  ],
6310
  "window": [
6311
  87,
6312
+ 95
6313
  ],
6314
  "curves": {
6315
  "start": 83,
 
6329
  0.001,
6330
  0.001,
6331
  0.005,
6332
+ 0.005
 
 
6333
  ],
6334
  "competitor": [
6335
  0.004,
 
6347
  0.002,
6348
  0.002,
6349
  0.011,
6350
+ 0.011
 
 
6351
  ],
6352
  "prev": [
6353
  0.002,
 
6365
  0.001,
6366
  0.001,
6367
  0.005,
6368
+ 0.005
 
 
6369
  ],
6370
  "next": [
6371
  0.002,
 
6383
  0.001,
6384
  0.001,
6385
  0.85,
6386
+ 0.85
 
 
6387
  ],
6388
  "blank": [
6389
  0.95,
 
6401
  0.97,
6402
  0.97,
6403
  0.005,
6404
+ 0.005
 
 
6405
  ]
6406
  },
6407
  "p_target": 0.8496,
6408
  "competitor": "i",
6409
  "competitor_other": false,
6410
  "p_competitor": 0.0107,
6411
+ "competitor_frames": 0,
6412
  "gop": 0.0,
6413
  "confidence": 1.0,
6414
  "gop_factor": 1.0,
 
6433
  "stage": "baseline",
6434
  "score": 100.0,
6435
  "ref": 100.0,
6436
+ "refs": [
6437
+ 100.0,
6438
+ 100.0
6439
+ ],
6440
  "applied": false
6441
  },
6442
  {
 
6471
  100
6472
  ],
6473
  "window": [
6474
+ 93,
6475
  102
6476
  ],
6477
  "curves": {
6478
+ "start": 89,
6479
  "target": [
 
 
6480
  0.005,
6481
  0.005,
6482
  0.001,
 
6496
  0.001
6497
  ],
6498
  "competitor": [
 
 
6499
  0.011,
6500
  0.011,
6501
  0.002,
 
6515
  0.002
6516
  ],
6517
  "prev": [
 
 
6518
  0.85,
6519
  0.85,
6520
  0.001,
 
6534
  0.001
6535
  ],
6536
  "next": [
 
 
6537
  0.011,
6538
  0.011,
6539
  0.002,
 
6553
  0.002
6554
  ],
6555
  "blank": [
 
 
6556
  0.005,
6557
  0.005,
6558
  0.97,
 
6576
  "competitor": "e",
6577
  "competitor_other": false,
6578
  "p_competitor": 0.0107,
6579
+ "competitor_frames": 0,
6580
  "gop": 0.0,
6581
  "confidence": 1.0,
6582
  "gop_factor": 1.0,
 
6601
  "stage": "baseline",
6602
  "score": 100.0,
6603
  "ref": 100.0,
6604
+ "refs": [
6605
+ 100.0,
6606
+ 100.0
6607
+ ],
6608
  "applied": false
6609
  },
6610
  {
 
6749
  "competitor": "e",
6750
  "competitor_other": false,
6751
  "p_competitor": 0.0107,
6752
+ "competitor_frames": 0,
6753
  "gop": 0.0,
6754
  "confidence": 1.0,
6755
  "gop_factor": 1.0,
 
6774
  "stage": "baseline",
6775
  "score": 100.0,
6776
  "ref": 100.0,
6777
+ "refs": [
6778
+ 100.0,
6779
+ 100.0
6780
+ ],
6781
  "applied": false
6782
  },
6783
  {
 
6812
  112
6813
  ],
6814
  "window": [
6815
+ 106,
6816
  114
6817
  ],
6818
  "curves": {
6819
+ "start": 102,
6820
  "target": [
 
 
6821
  0.005,
6822
  0.005,
6823
  0.001,
 
6836
  0.004
6837
  ],
6838
  "competitor": [
 
 
6839
  0.005,
6840
  0.005,
6841
  0.001,
 
6854
  0.004
6855
  ],
6856
  "prev": [
 
 
6857
  0.855,
6858
  0.855,
6859
  0.002,
 
6872
  0.007
6873
  ],
6874
  "next": [
 
 
6875
  0.011,
6876
  0.011,
6877
  0.002,
 
6890
  0.904
6891
  ],
6892
  "blank": [
 
 
6893
  0.005,
6894
  0.005,
6895
  0.97,
 
6912
  "competitor": "w",
6913
  "competitor_other": false,
6914
  "p_competitor": 0.0054,
6915
+ "competitor_frames": 0,
6916
  "gop": 0.0,
6917
  "confidence": 1.0,
6918
  "gop_factor": 1.0,
 
6937
  "stage": "baseline",
6938
  "score": 100.0,
6939
  "ref": 100.0,
6940
+ "refs": [
6941
+ 100.0,
6942
+ 100.0
6943
+ ],
6944
  "applied": false
6945
  },
6946
  {
 
7075
  "competitor": "i",
7076
  "competitor_other": false,
7077
  "p_competitor": 0.0071,
7078
+ "competitor_frames": 0,
7079
  "gop": 0.0,
7080
  "confidence": 1.0,
7081
  "gop_factor": 1.0,
 
7100
  "stage": "baseline",
7101
  "score": 100.0,
7102
  "ref": 100.0,
7103
+ "refs": [
7104
+ 100.0,
7105
+ 100.0
7106
+ ],
7107
  "applied": false
7108
  },
7109
  {
 
7232
  "competitor": "i",
7233
  "competitor_other": false,
7234
  "p_competitor": 0.0107,
7235
+ "competitor_frames": 0,
7236
  "gop": 0.0,
7237
  "confidence": 1.0,
7238
  "gop_factor": 1.0,
 
7257
  "stage": "baseline",
7258
  "score": 100.0,
7259
  "ref": 100.0,
7260
+ "refs": [
7261
+ 100.0,
7262
+ 100.0
7263
+ ],
7264
  "applied": false
7265
  },
7266
  {
 
7366
  "competitor": "i",
7367
  "competitor_other": false,
7368
  "p_competitor": 0.0107,
7369
+ "competitor_frames": 0,
7370
  "gop": 0.0,
7371
  "confidence": 1.0,
7372
  "gop_factor": 1.0,
 
7380
  {
7381
  "stage": "exemption",
7382
  "score": 100.0,
7383
+ "applied": "unmarked_short_vowel"
7384
  },
7385
  {
7386
  "stage": "insertion",
 
7391
  "stage": "baseline",
7392
  "score": 100.0,
7393
  "ref": 100.0,
7394
+ "refs": [
7395
+ 100.0,
7396
+ 100.0
7397
+ ],
7398
  "applied": false
7399
  },
7400
  {
 
7436
  100.0,
7437
  100.0,
7438
  100.0,
7439
+ 100.0,
7440
  100.0
7441
  ],
7442
+ "min": 100.0,
7443
+ "mean": 100.0,
7444
+ "score": 100.0
7445
  },
7446
  {
7447
  "tr": "סִידִי",
 
7466
  ],
7467
  "min": 45.0,
7468
  "mean": 89.0,
7469
+ "score": 73.6
7470
  },
7471
  {
7472
  "tr": "פִי",
 
7492
  }
7493
  ],
7494
  "sentence": {
7495
+ "phone_mean": 96.9,
7496
+ "word_mean": 95.6,
7497
  "phone_weight": 0.5,
7498
  "word_weight": 0.5,
7499
+ "before_cap": 96.3,
7500
+ "final": 96,
7501
  "capped": false
7502
  },
7503
  "tips": [
static/index.html CHANGED
@@ -4990,7 +4990,7 @@ function drawTtsActivity(card){
4990
  const details=document.createElement("details"), summary=mark("","מה קורה עכשיו","summary"), pace=mark("msg","","div"), stats=mark("msg","","div"), active=mark("","","div"), wrap=mark("","","div"), table=document.createElement("table");
4991
  wrap.style.cssText="overflow-x:auto;max-height:340px;overflow-y:auto"; table.style.cssText="width:100%;border-collapse:collapse;font-size:.85em";
4992
  details.style.marginTop="12px"; summary.style.cssText="cursor:pointer;font-weight:600"; wrap.append(table); details.append(summary,pace,stats,active,wrap);
4993
- const SOURCE={sweep:"הכנה",playback:"השמעה",import:"טעינה"}, secs=ms=>ms==null?"–":ms<1000?`${ms} ms`:`${(ms/1000).toFixed(1)} שנ׳`;
4994
  const result=e=>e.status===200||e.status==="ok"?"✓":e.status===429?"429 · הגבלת קצב":typeof e.status==="number"&&e.status>=500?`${e.status} · תקלה אצל Sawtak`:typeof e.status==="number"?`${e.status} · נדחה`:"שגיאה";
4995
  const what=e=>e.kind==="upload"?(e.files>1?`${e.files} קבצים · ${(e.bytes/1048576).toFixed(1)} MB`:(e.text||"קובץ אחד")):(e.text||"");
4996
  const cell=(tr,text,head)=>{ const c=document.createElement(head?"th":"td"); c.textContent=text; c.dir="auto"; c.style.cssText="padding:4px 6px;border-bottom:1px solid #8884;text-align:start;white-space:nowrap"; tr.append(c); return c; };
 
4990
  const details=document.createElement("details"), summary=mark("","מה קורה עכשיו","summary"), pace=mark("msg","","div"), stats=mark("msg","","div"), active=mark("","","div"), wrap=mark("","","div"), table=document.createElement("table");
4991
  wrap.style.cssText="overflow-x:auto;max-height:340px;overflow-y:auto"; table.style.cssText="width:100%;border-collapse:collapse;font-size:.85em";
4992
  details.style.marginTop="12px"; summary.style.cssText="cursor:pointer;font-weight:600"; wrap.append(table); details.append(summary,pace,stats,active,wrap);
4993
+ const SOURCE={sweep:"הכנה",playback:"השמעה",baseline:"קו בסיס",import:"טעינה"}, secs=ms=>ms==null?"–":ms<1000?`${ms} ms`:`${(ms/1000).toFixed(1)} שנ׳`;
4994
  const result=e=>e.status===200||e.status==="ok"?"✓":e.status===429?"429 · הגבלת קצב":typeof e.status==="number"&&e.status>=500?`${e.status} · תקלה אצל Sawtak`:typeof e.status==="number"?`${e.status} · נדחה`:"שגיאה";
4995
  const what=e=>e.kind==="upload"?(e.files>1?`${e.files} קבצים · ${(e.bytes/1048576).toFixed(1)} MB`:(e.text||"קובץ אחד")):(e.text||"");
4996
  const cell=(tr,text,head)=>{ const c=document.createElement(head?"th":"td"); c.textContent=text; c.dir="auto"; c.style.cssText="padding:4px 6px;border-bottom:1px solid #8884;text-align:start;white-space:nowrap"; tr.append(c); return c; };
static/trace.html CHANGED
@@ -14,7 +14,8 @@
14
  (the end of the sound before to the start of the one after, where a substitution peaks)
15
  2. its score stage by stage (GOP → exemption → cluster cap → native baseline → teacher floor)
16
  3. the readings the audio was aligned to, and the different-sentence gauge
17
- 4. the sentence by word and sound, and how sounds add up to words and words to the sentence
 
18
  -->
19
  <style>
20
  :root{
@@ -110,6 +111,19 @@ svg .strong{fill:var(--ink);font-weight:700}
110
  .ph.good{box-shadow:inset 0 -3px 0 var(--good)} .ph.warn{box-shadow:inset 0 -3px 0 var(--warn)} .ph.crit{box-shadow:inset 0 -3px 0 var(--crit)}
111
  .ph.flag::after{content:"!";color:var(--crit-ink);margin-inline-start:2px}
112
  .ph.sel{outline:2px solid var(--s1);outline-offset:1px}
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  .tree{display:flex;flex-direction:column;gap:8px}
114
  .tree .node{background:var(--raise);border-radius:10px;padding:6px 10px;font-size:13px}
115
  .tree .node b{font-size:15px}
@@ -159,6 +173,7 @@ details summary{cursor:pointer;color:var(--ink-2);font-size:12px}
159
 
160
  <section class="panel" id="p4" aria-labelledby="p4h">
161
  <div class="head"><h2 id="p4h">המשפט, מילה אחר מילה</h2><span class="sub" id="wordHint">▶ לחצו על מילה כדי לשמוע אותה · מעבר עם העכבר על הגה מציג אותו בכל הלוחות; לחיצה מקבעת</span></div>
 
162
  <div class="sentence" id="sentence"></div>
163
  <div class="legend"><span><span class="dot good"></span>75 ומעלה</span><span><span class="dot warn"></span>50–74</span><span><span class="dot crit"></span>מתחת ל־50</span><span>! סומן כטעות</span></div>
164
  <details><summary>עץ הצבירה: הגאים ← מילים ← משפט</summary><div class="tree" id="tree"></div></details>
@@ -230,6 +245,39 @@ const PHONE_HEBREW={
230
  };
231
  const phoneHebrew=p=>PHONE_HEBREW[p]||p;
232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  let R=null, T=null, PH=[], sel=0, pinned=false, traceSource="";
234
  let wordAudio=null, wordAudioEnd=0, wordAudioNode=null;
235
  let frameAudio=null, frameRaf=0, frameAudioTimer=0, frameCursor=null, frameX=null, frameCurve=null;
@@ -287,7 +335,7 @@ function step(d){ if(!T) return; sel=(sel+d+T.tokens.length)%T.tokens.length; pi
287
  /* ---------- selection: one sound everywhere ---------- */
288
  function hover(i){ if(pinned||i===sel) return; sel=i; drawSelected(); }
289
  function pick(i){ pinned=!(pinned&&sel===i); sel=i; drawSelected(); }
290
- function drawAll(){ drawSentence(); drawTree(); drawPaths(); drawGauge(); drawTable(); drawSelected(); }
291
  function drawSelected(){
292
  $("pos").textContent=`${sel+1}/${T.tokens.length}`+(pinned?" 📌":"");
293
  for(const b of document.querySelectorAll(".ph")) b.classList.toggle("sel",+b.dataset.i===sel);
@@ -310,6 +358,8 @@ function soundTip(i){
310
  out.push(trow("צפוי",el("span",{class:"ipa",text:`${t.ipa} (${pct(t.p_target)})`})));
311
  out.push(trow("המתחרה החזק",el("span",{class:"ipa",text:heard})));
312
  out.push(trow("ציון",`${fmt(t.final)} · ${BAND_NAME[band(t.final)]}`+(t.flagged?" · סומן כטעות":"")));
 
 
313
  out.push(trow("מרווח (נדרש / בפועל)",el("span",{class:"ltr",text:`${fmt(t.margin.needed,2)} / ${fmt(t.margin.got,2)}`+(t.margin.beyond?" ✓ עבר":" ✗ לא עבר")})));
314
  out.push(trow("חלון",el("span",{class:"ltr",text:`${t.window[0]}–${t.window[1]}`})));
315
  if(t.rule) out.push(trow("כלל",`${t.rule.title} · חומרה ${t.rule.severity}`));
@@ -331,13 +381,24 @@ function drawSentence(){
331
  word.onkeydown=e=>{ if(e.key==="Enter"||e.key===" "){ e.preventDefault(); playWord(wi,word); } };
332
  }
333
  word.append(el("span",{class:"tr",text:w.tr}),el("span",{class:"ar",text:w.ar}),el("span",{class:"ws",text:`${fmt(w.score,0)} · ${BAND_NAME[b]}`}));
334
- const phones=el("div",{class:"phones"});
 
 
 
335
  T.tokens.forEach(t=>{ if(t.word!==wi) return;
336
  const c=el("button",{class:`ph ${band(t.final)}${t.flagged?" flag":""}`,"data-i":t.i,
337
  "aria-label":`${phoneHebrew(t.ipa)}, IPA ${t.ipa}, ציון ${fmt(t.final,0)}`,title:`IPA: ${t.ipa}`,text:phoneHebrew(t.ipa)});
338
  c.onmouseenter=e=>{ hover(t.i); tipShow(e,soundTip(t.i)); }; c.onmousemove=tipMove; c.onmouseleave=tipHide;
339
  c.onfocus=()=>hover(t.i); c.onclick=e=>{ e.stopPropagation(); pick(t.i); };
340
- phones.append(c); });
 
 
 
 
 
 
 
 
341
  word.append(phones);
342
  if(traceContext.sample!=null){
343
  const toggle=el("button",{class:"word-exclude",text:excluded?"החזרה לציון":"לא תקינה · החרגה"});
@@ -554,7 +615,10 @@ function drawWaterfall(){
554
  let note="";
555
  if(s.stage==="exemption"&&s.applied) note=EXEMPT_NAME[s.applied]||s.applied;
556
  if(s.stage==="insertion"&&s.applied) note=`תנועה ${s.applied} בצרור`;
557
- if(s.stage==="baseline") note=s.ref==null?"אין קול ילידי":`הקול הילידי: ${fmt(s.ref,0)}`;
 
 
 
558
  if(s.stage==="calibration") note=s.floor==null?"אין רצפה":`רצפה ${fmt(s.floor,0)}`;
559
  steps.push({name:s.stage,from:cur,to:s.score,note}); cur=s.score;
560
  }
@@ -575,13 +639,19 @@ function drawWaterfall(){
575
  if(k&&!total) svg.append(sv("line",{x1:x(s.from),x2:x(s.from),y1:y-rowH+7,y2:y+7,stroke:"var(--axis)"}));
576
  const d=s.to-s.from, label=total?fmt(s.to):d?`${d>0?"+":""}${fmt(d)} → ${fmt(s.to)}`:"±0";
577
  svg.append(sv("text",{x:Math.min(W-2,x(hi)+5),y:y+rowH/2+4,class:total?"strong":"lbl"},label));
 
 
 
 
 
578
  const hit=sv("rect",{x:0,y,width:W,height:rowH,fill:"transparent"});
579
  hit.onmouseenter=ev=>tipShow(ev,[el("div",{class:"t",text:STAGE_NAME[s.name]}),trow("לפני",fmt(s.from)),trow("אחרי",fmt(s.to)),s.note?el("div",{class:"tiptext ltr",text:s.note}):null].filter(Boolean));
580
  hit.onmousemove=tipMove; hit.onmouseleave=tipHide; svg.append(hit);
581
  });
582
  box.replaceChildren(svg);
583
  $("p2sub").replaceChildren(el("span",{class:"ipa",text:t.ipa})," · ",el("span",{class:"dot "+band(t.final)}),`${fmt(t.final,0)} · ${BAND_NAME[band(t.final)]}`);
584
- $("p2facts").replaceChildren(...[fact("סומן",t.flagged?"כן":"לא"),fact("מרווח נדרש",fmt(t.margin.needed,2)),fact("בפועל",fmt(t.margin.got,2)),
 
585
  t.rule?fact("כלל",`${t.rule.title} (${t.rule.severity})`):null, fact("טיפ",t.tip?"כן":"לא")].filter(Boolean));
586
  }
587
 
@@ -631,11 +701,12 @@ function drawGauge(){
631
  /* ---------- the table: every number ---------- */
632
  function drawTable(){
633
  const tb=$("table"); tb.replaceChildren();
634
- const head=["#","הגה","מילה","span","חלון","p_target","מתחרה","p_comp","GOP","gop","פטור","צרור","בסיס","כיול","סופי","סומן"];
 
635
  tb.append(el("tr",{},...head.map(h=>el("th",{text:h}))));
636
  for(const t of T.tokens){
637
  const st=Object.fromEntries(t.stages.map(s=>[s.stage,s.score]));
638
- const cells=[t.i,t.ipa,t.word,t.span.join("–"),t.window.join("–"),pct(t.p_target),t.competitor||"—",pct(t.p_competitor),fmt(t.gop,3),fmt(st.gop),fmt(st.exemption),fmt(st.insertion),fmt(st.baseline),fmt(st.calibration),fmt(t.final),t.flagged?"כן":""];
639
  const tr=el("tr",{},...cells.map((v,k)=>el("td",{class:[1,3,4,6].includes(k)?"ipa":"",text:String(v)})));
640
  tr.onmouseenter=()=>hover(t.i); tr.onclick=()=>pick(t.i); tb.append(tr);
641
  }
 
14
  (the end of the sound before to the start of the one after, where a substitution peaks)
15
  2. its score stage by stage (GOP → exemption → cluster cap → native baseline → teacher floor)
16
  3. the readings the audio was aligned to, and the different-sentence gauge
17
+ 4. the sentence by word and sound, and how sounds add up to words and words to the sentence; under each sound,
18
+ one row per native baseline (TTS voice), the voice it was measured against (the lowest) in bold
19
  -->
20
  <style>
21
  :root{
 
111
  .ph.good{box-shadow:inset 0 -3px 0 var(--good)} .ph.warn{box-shadow:inset 0 -3px 0 var(--warn)} .ph.crit{box-shadow:inset 0 -3px 0 var(--crit)}
112
  .ph.flag::after{content:"!";color:var(--crit-ink);margin-inline-start:2px}
113
  .ph.sel{outline:2px solid var(--s1);outline-offset:1px}
114
+ /* the native baselines under each sound: one row per voice, the one the score was measured against in bold */
115
+ .phcol{display:flex;flex-direction:column;align-items:stretch;gap:2px}
116
+ .phcol.lab{justify-content:flex-end}
117
+ .phcol.lab .ph{visibility:hidden}
118
+ .reflab{font:10.5px var(--font);color:var(--muted);line-height:16px;white-space:nowrap;text-align:start}
119
+ .refcell{font:10.5px var(--font);font-variant-numeric:tabular-nums;line-height:16px;text-align:center;color:var(--ink-2);background:var(--raise);border-radius:4px;padding:0 2px;cursor:default}
120
+ .refcell.good{box-shadow:inset 0 -2px 0 var(--good)} .refcell.warn{box-shadow:inset 0 -2px 0 var(--warn)} .refcell.crit{box-shadow:inset 0 -2px 0 var(--crit)}
121
+ .refcell.used{color:var(--ink);font-weight:700;outline:1px solid var(--ink-2)}
122
+ .refcell.none{color:var(--muted);background:none}
123
+ .refbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px;font-size:12px;color:var(--ink-2)}
124
+ .refbar .chip{background:var(--raise);border-radius:8px;padding:3px 8px}
125
+ .refbar .chip b{color:var(--ink)}
126
+ .refbar label{display:inline-flex;align-items:center;gap:5px;cursor:pointer}
127
  .tree{display:flex;flex-direction:column;gap:8px}
128
  .tree .node{background:var(--raise);border-radius:10px;padding:6px 10px;font-size:13px}
129
  .tree .node b{font-size:15px}
 
173
 
174
  <section class="panel" id="p4" aria-labelledby="p4h">
175
  <div class="head"><h2 id="p4h">המשפט, מילה אחר מילה</h2><span class="sub" id="wordHint">▶ לחצו על מילה כדי לשמוע אותה · מעבר עם העכבר על הגה מציג אותו בכל הלוחות; לחיצה מקבעת</span></div>
176
+ <div class="refbar" id="refbar"></div>
177
  <div class="sentence" id="sentence"></div>
178
  <div class="legend"><span><span class="dot good"></span>75 ומעלה</span><span><span class="dot warn"></span>50–74</span><span><span class="dot crit"></span>מתחת ל־50</span><span>! סומן כטעות</span></div>
179
  <details><summary>עץ הצבירה: הגאים ← מילים ← משפט</summary><div class="tree" id="tree"></div></details>
 
245
  };
246
  const phoneHebrew=p=>PHONE_HEBREW[p]||p;
247
 
248
+ /* ---------- native baselines: one or more TTS voices scored by the same model ---------- */
249
+ let showRefs=true; try{ showRefs=localStorage.getItem("ihki.trace.refs")!=="0"; }catch{}
250
+ const refList=()=>(T&&T.references)||[];
251
+ const refName=k=>(refList()[k]||{}).voice||`קול ${k+1}`;
252
+ const refTag=k=>{ const n=refName(k); return n.length<=5?n:n.slice(0,4)+"…"; };
253
+ /* each voice's score on sound t (null: that voice has no such sound); older traces carry only the lowest, as `ref` */
254
+ function refScores(t){
255
+ const b=t.stages.find(s=>s.stage==="baseline")||{};
256
+ if(Array.isArray(b.refs)) return b.refs;
257
+ return refList().length===1?[b.ref??null]:[];
258
+ }
259
+ /* the voice the sound was measured against: the lowest score, as Scorer._baseline takes it */
260
+ function refUsed(t){
261
+ const xs=refScores(t); let best=-1;
262
+ xs.forEach((x,k)=>{ if(x!=null&&(best<0||x<xs[best])) best=k; });
263
+ return best>=0&&xs[best]<100?best:-1; // at 100 the baseline changes nothing: no voice was measured against
264
+ }
265
+ function drawRefBar(){
266
+ const box=$("refbar"), n=refList().length; box.replaceChildren();
267
+ if(!n){ box.append(el("span",{text:"קו בסיס ילידי: אין. הציון לא נמדד מול קול יליד (אין מפתח TTS, הקול לא היה מוכן בזמן, או מבחן TTS שבו הבסיס כבוי)."})); return; }
268
+ box.append(el("span",{text:n===1?"קו בסיס ילידי:":`${n} קווי בסיס ילידיים (ההגה נמדד מול הנמוך שבהם):`}));
269
+ refList().forEach((r,k)=>box.append(el("span",{class:"chip"},el("b",{text:refName(k)}),` · ממוצע ${fmt(r.mean,0)} · ${r.sounds} הגאים`)));
270
+ const cb=el("input",{type:"checkbox"}); cb.checked=showRefs;
271
+ cb.onchange=()=>{ showRefs=cb.checked; try{ localStorage.setItem("ihki.trace.refs",showRefs?"1":"0"); }catch{} drawSentence(); drawSelected(); };
272
+ box.append(el("label",{},cb,"הצגה מתחת לכל הגה"));
273
+ }
274
+ function refTip(i){
275
+ const t=T.tokens[i], xs=refScores(t), used=refUsed(t);
276
+ return [el("div",{class:"t",text:`קו בסיס ילידי · ${t.ipa}`}),
277
+ ...xs.map((x,k)=>trow(refName(k),x==null?"אין הגה כזה בקריאה שלו":`${fmt(x,0)}`+(k===used?" · נמדד מולו":""))),
278
+ trow("הלומד: לפני → אחרי",(()=>{ const st=Object.fromEntries(t.stages.map(s=>[s.stage,s.score])); return `${fmt(st.insertion)} → ${fmt(st.baseline)}`; })())];
279
+ }
280
+
281
  let R=null, T=null, PH=[], sel=0, pinned=false, traceSource="";
282
  let wordAudio=null, wordAudioEnd=0, wordAudioNode=null;
283
  let frameAudio=null, frameRaf=0, frameAudioTimer=0, frameCursor=null, frameX=null, frameCurve=null;
 
335
  /* ---------- selection: one sound everywhere ---------- */
336
  function hover(i){ if(pinned||i===sel) return; sel=i; drawSelected(); }
337
  function pick(i){ pinned=!(pinned&&sel===i); sel=i; drawSelected(); }
338
+ function drawAll(){ drawRefBar(); drawSentence(); drawTree(); drawPaths(); drawGauge(); drawTable(); drawSelected(); }
339
  function drawSelected(){
340
  $("pos").textContent=`${sel+1}/${T.tokens.length}`+(pinned?" 📌":"");
341
  for(const b of document.querySelectorAll(".ph")) b.classList.toggle("sel",+b.dataset.i===sel);
 
358
  out.push(trow("צפוי",el("span",{class:"ipa",text:`${t.ipa} (${pct(t.p_target)})`})));
359
  out.push(trow("המתחרה החזק",el("span",{class:"ipa",text:heard})));
360
  out.push(trow("ציון",`${fmt(t.final)} · ${BAND_NAME[band(t.final)]}`+(t.flagged?" · סומן כטעות":"")));
361
+ const rx=refScores(t), ru=refUsed(t);
362
+ if(rx.length) out.push(trow("קו בסיס",rx.map((x,k)=>`${refName(k)} ${x==null?"—":fmt(x,0)}${k===ru&&rx.length>1?"*":""}`).join(" · ")));
363
  out.push(trow("מרווח (נדרש / בפועל)",el("span",{class:"ltr",text:`${fmt(t.margin.needed,2)} / ${fmt(t.margin.got,2)}`+(t.margin.beyond?" ✓ עבר":" ✗ לא עבר")})));
364
  out.push(trow("חלון",el("span",{class:"ltr",text:`${t.window[0]}–${t.window[1]}`})));
365
  if(t.rule) out.push(trow("כלל",`${t.rule.title} · חומרה ${t.rule.severity}`));
 
381
  word.onkeydown=e=>{ if(e.key==="Enter"||e.key===" "){ e.preventDefault(); playWord(wi,word); } };
382
  }
383
  word.append(el("span",{class:"tr",text:w.tr}),el("span",{class:"ar",text:w.ar}),el("span",{class:"ws",text:`${fmt(w.score,0)} · ${BAND_NAME[b]}`}));
384
+ const phones=el("div",{class:"phones"}), nref=showRefs?refList().length:0;
385
+ if(nref){ const lab=el("div",{class:"phcol lab"},el("span",{class:"ph",text:"·"}));
386
+ for(let k=0;k<nref;k++) lab.append(el("span",{class:"reflab",title:refName(k),text:refTag(k)}));
387
+ phones.append(lab); }
388
  T.tokens.forEach(t=>{ if(t.word!==wi) return;
389
  const c=el("button",{class:`ph ${band(t.final)}${t.flagged?" flag":""}`,"data-i":t.i,
390
  "aria-label":`${phoneHebrew(t.ipa)}, IPA ${t.ipa}, ציון ${fmt(t.final,0)}`,title:`IPA: ${t.ipa}`,text:phoneHebrew(t.ipa)});
391
  c.onmouseenter=e=>{ hover(t.i); tipShow(e,soundTip(t.i)); }; c.onmousemove=tipMove; c.onmouseleave=tipHide;
392
  c.onfocus=()=>hover(t.i); c.onclick=e=>{ e.stopPropagation(); pick(t.i); };
393
+ if(!nref){ phones.append(c); return; }
394
+ const col=el("div",{class:"phcol"},c), xs=refScores(t), used=refUsed(t);
395
+ for(let k=0;k<nref;k++){ const x=xs[k];
396
+ const cell=el("span",{class:`refcell ${x==null?"none":band(x)}${k===used?" used":""}`,text:x==null?"—":fmt(x,0),
397
+ "aria-label":`${refName(k)}: ${x==null?"אין":fmt(x,0)}`});
398
+ cell.onmouseenter=e=>{ hover(t.i); tipShow(e,refTip(t.i)); }; cell.onmousemove=tipMove; cell.onmouseleave=tipHide;
399
+ cell.onclick=e=>{ e.stopPropagation(); pick(t.i); };
400
+ col.append(cell); }
401
+ phones.append(col); });
402
  word.append(phones);
403
  if(traceContext.sample!=null){
404
  const toggle=el("button",{class:"word-exclude",text:excluded?"החזרה לציון":"לא תקינה · החרגה"});
 
615
  let note="";
616
  if(s.stage==="exemption"&&s.applied) note=EXEMPT_NAME[s.applied]||s.applied;
617
  if(s.stage==="insertion"&&s.applied) note=`תנועה ${s.applied} בצרור`;
618
+ if(s.stage==="baseline"){ const xs=refScores(t), u=refUsed(t);
619
+ note=s.ref==null?(refList().length?"לאף קול ילידי אין את ההגה הזה":"אין קול ילידי")
620
+ :xs.length>1?xs.map((x,k)=>`${refName(k)} ${x==null?"—":fmt(x,0)}`).join(" · ")+(u>=0?` → נמדד מול ${refName(u)} (${fmt(s.ref,0)}): 100·s/${fmt(s.ref,0)}`:" (100: לא משנה)")
621
+ :`${refName(0)}: ${fmt(s.ref,0)}`+(s.ref<100?` → 100·s/${fmt(s.ref,0)}`:" (100: לא משנה)"); }
622
  if(s.stage==="calibration") note=s.floor==null?"אין רצפה":`רצפה ${fmt(s.floor,0)}`;
623
  steps.push({name:s.stage,from:cur,to:s.score,note}); cur=s.score;
624
  }
 
639
  if(k&&!total) svg.append(sv("line",{x1:x(s.from),x2:x(s.from),y1:y-rowH+7,y2:y+7,stroke:"var(--axis)"}));
640
  const d=s.to-s.from, label=total?fmt(s.to):d?`${d>0?"+":""}${fmt(d)} → ${fmt(s.to)}`:"±0";
641
  svg.append(sv("text",{x:Math.min(W-2,x(hi)+5),y:y+rowH/2+4,class:total?"strong":"lbl"},label));
642
+ if(s.name==="baseline"){ const at=new Map(), u=refUsed(t); // voices with the same score share one marker
643
+ refScores(t).forEach((r,k)=>{ if(r!=null) at.set(r,[...(at.get(r)||[]),k]); });
644
+ for(const [r,ks] of at){ const cx=x(r), cy=y+rowH/2, d=5;
645
+ svg.append(sv("path",{d:`M${cx} ${cy-d}L${cx+d} ${cy}L${cx} ${cy+d}L${cx-d} ${cy}Z`,fill:ks.includes(u)?"var(--ink)":"var(--surface)",stroke:"var(--ink)","stroke-width":1.3}));
646
+ svg.append(sv("text",{x:cx,y:y+4,"text-anchor":"middle",style:"font-size:9.5px"},ks.map(refTag).join(" · "))); } }
647
  const hit=sv("rect",{x:0,y,width:W,height:rowH,fill:"transparent"});
648
  hit.onmouseenter=ev=>tipShow(ev,[el("div",{class:"t",text:STAGE_NAME[s.name]}),trow("לפני",fmt(s.from)),trow("אחרי",fmt(s.to)),s.note?el("div",{class:"tiptext ltr",text:s.note}):null].filter(Boolean));
649
  hit.onmousemove=tipMove; hit.onmouseleave=tipHide; svg.append(hit);
650
  });
651
  box.replaceChildren(svg);
652
  $("p2sub").replaceChildren(el("span",{class:"ipa",text:t.ipa})," · ",el("span",{class:"dot "+band(t.final)}),`${fmt(t.final,0)} · ${BAND_NAME[band(t.final)]}`);
653
+ const rx=refScores(t), ru=refUsed(t);
654
+ $("p2facts").replaceChildren(...[...rx.map((x,k)=>fact(`בסיס ${refName(k)}`,(x==null?"—":fmt(x,0))+(k===ru&&rx.length>1?" ◆":""))),fact("סומן",t.flagged?"כן":"לא"),fact("מרווח נדרש",fmt(t.margin.needed,2)),fact("בפועל",fmt(t.margin.got,2)),
655
  t.rule?fact("כלל",`${t.rule.title} (${t.rule.severity})`):null, fact("טיפ",t.tip?"כן":"לא")].filter(Boolean));
656
  }
657
 
 
701
  /* ---------- the table: every number ---------- */
702
  function drawTable(){
703
  const tb=$("table"); tb.replaceChildren();
704
+ const nref=refList().length;
705
+ const head=["#","הגה","מילה","span","חלון","p_target","מתחרה","p_comp","GOP","gop","פטור","צרור",...Array.from({length:nref},(_,k)=>`בסיס ${refName(k)}`),"בסיס","כיול","סופי","סומן"];
706
  tb.append(el("tr",{},...head.map(h=>el("th",{text:h}))));
707
  for(const t of T.tokens){
708
  const st=Object.fromEntries(t.stages.map(s=>[s.stage,s.score]));
709
+ const cells=[t.i,t.ipa,t.word,t.span.join("–"),t.window.join("–"),pct(t.p_target),t.competitor||"—",pct(t.p_competitor),fmt(t.gop,3),fmt(st.gop),fmt(st.exemption),fmt(st.insertion),...Array.from({length:nref},(_,k)=>{ const x=refScores(t)[k]; return x==null?"—":fmt(x,0); }),fmt(st.baseline),fmt(st.calibration),fmt(t.final),t.flagged?"כן":""];
710
  const tr=el("tr",{},...cells.map((v,k)=>el("td",{class:[1,3,4,6].includes(k)?"ipa":"",text:String(v)})));
711
  tr.onmouseenter=()=>hover(t.i); tr.onclick=()=>pick(t.i); tb.append(tr);
712
  }
tests/test_scoring.py CHANGED
@@ -774,6 +774,27 @@ def test_native_reference_keeps_real_errors():
774
  assert scorer_for(emissions(["ʕ", "χ", "l", "χ", "i", "l", "χ", "a", "χ", "aː", "χ"])).reference(None, ALA) is None
775
 
776
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
  def test_references_use_one_generated_voice_and_any_cached_one():
778
  from app.reference import References
779
  from app.tts import TTSUnavailable
@@ -786,7 +807,7 @@ def test_references_use_one_generated_voice_and_any_cached_one():
786
  def cached(self, text, voice):
787
  return voice == "played"
788
 
789
- def wav(self, text, voice=""):
790
  calls.append(voice)
791
  if self.down:
792
  raise TTSUnavailable("down")
@@ -798,6 +819,7 @@ def test_references_use_one_generated_voice_and_any_cached_one():
798
  try:
799
  refs = References(s, tts, ["first", "never-played", "played"])
800
  assert len(refs.get(7, ALA)) == 2 and calls == ["first", "played"]
 
801
  assert len(refs.get(7, ALA)) == 2 and len(calls) == 2 # kept in memory
802
  assert refs.invalidate(7, "first") == 1
803
  assert len(refs.get(7, ALA)) == 2 and calls == ["first", "played", "first"]
 
774
  assert scorer_for(emissions(["ʕ", "χ", "l", "χ", "i", "l", "χ", "a", "χ", "aː", "χ"])).reference(None, ALA) is None
775
 
776
 
777
+ def test_baseline_voices_are_raef_then_fadwa_by_name():
778
+ from app.tts import BASELINE_VOICES, FADWA_VOICE_ID, RAEF_VOICE_ID, voice_name
779
+ assert list(BASELINE_VOICES.values()) == [RAEF_VOICE_ID, FADWA_VOICE_ID] # Raef first: the one generated
780
+ assert voice_name(RAEF_VOICE_ID) == "ראאף" and voice_name(FADWA_VOICE_ID) == "פדוא" and voice_name("x") == "x"
781
+
782
+
783
+ def test_trace_keeps_every_voices_score_and_measures_against_the_lowest():
784
+ from app.scorer import Reference
785
+ s = scorer_for(emissions(MISHEARD))
786
+ toks = s._variants(ALA)[0][3]
787
+ weak = lambda score_on: Reference([(t.word, t.sound) for t in toks], [score_on.get(t.sound, 100.0) for t in toks])
788
+ a, b = replace(weak({"l": 40.0}), voice="A"), replace(weak({"l": 60.0, "ʕ": 70.0}), voice="B")
789
+ r = s.score_logp(s.emit(None), ALA, [a, b], diagnostics=True)
790
+ assert [x["voice"] for x in r["trace"]["references"]] == ["A", "B"]
791
+ for t in r["trace"]["tokens"]:
792
+ st = next(x for x in t["stages"] if x["stage"] == "baseline")
793
+ assert len(st["refs"]) == 2 and st["ref"] == min(x for x in st["refs"] if x is not None)
794
+ if t["sound"] == "ʕ":
795
+ assert st["refs"] == [100.0, 70.0]
796
+
797
+
798
  def test_references_use_one_generated_voice_and_any_cached_one():
799
  from app.reference import References
800
  from app.tts import TTSUnavailable
 
807
  def cached(self, text, voice):
808
  return voice == "played"
809
 
810
+ def wav_for_voice(self, text, voice, source="", upload=False): # keeps the voice id (wav() would not)
811
  calls.append(voice)
812
  if self.down:
813
  raise TTSUnavailable("down")
 
819
  try:
820
  refs = References(s, tts, ["first", "never-played", "played"])
821
  assert len(refs.get(7, ALA)) == 2 and calls == ["first", "played"]
822
+ assert [r.voice for r in refs.get(7, ALA)] == ["first", "played"] # named for the trace dashboard
823
  assert len(refs.get(7, ALA)) == 2 and len(calls) == 2 # kept in memory
824
  assert refs.invalidate(7, "first") == 1
825
  assert len(refs.get(7, ALA)) == 2 and calls == ["first", "played", "first"]