From 850a6423248704a0c67d24c620b6f9e61ba690d6 Mon Sep 17 00:00:00 2001 From: Kevin Lenzo Date: Thu, 23 Apr 2026 13:35:26 -0400 Subject: [PATCH 1/3] Don't advance active successor's frame in phone_transition When phone_transition fires and the successor HMM is already active (hmm_frame >= frame_idx), calling hmm_enter() overwrites hmm_frame with nf even though the phone is already being tracked at the current frame. Split the entry into two cases: inactive successors get a full hmm_enter() with the new frame assignment; active ones only have their in_score and in_history updated if the incoming score is better. This is the correct Viterbi competition rule. --- src/state_align_search.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/state_align_search.c b/src/state_align_search.c index 391d2f931..25163067c 100644 --- a/src/state_align_search.c +++ b/src/state_align_search.c @@ -121,11 +121,14 @@ phone_transition(state_align_search_t *sas, int frame_idx) continue; newphone_score = hmm_out_score(hmm); - /* Transition into next phone using the usual Viterbi rule. */ nhmm = hmm + 1; - if (hmm_frame(nhmm) < frame_idx - || newphone_score BETTER_THAN hmm_in_score(nhmm)) { + if (hmm_frame(nhmm) < frame_idx) { hmm_enter(nhmm, newphone_score, hmm_out_history(hmm), nf); + continue; + } + if (newphone_score BETTER_THAN hmm_in_score(nhmm)) { + hmm_in_score(nhmm) = newphone_score; + hmm_in_history(nhmm) = hmm_out_history(hmm); } } } From ceee17cfb43ec37e5d61645b9eae1311b0e54285 Mon Sep 17 00:00:00 2001 From: Kevin Lenzo Date: Thu, 23 Apr 2026 13:36:34 -0400 Subject: [PATCH 2/3] Fix ef boundary handling in state_align_search prune_hmms used nf > ef[i] to gate frame advancement, where ef[i] is the exclusive end frame (start + duration). This allowed a phone to advance to ef[i] and be scored there, one frame past its window. phone_transition used hmm_frame(hmm) != nf to decide whether to fire, which meant only phones advanced by prune_hmms could transition. Phones stopped at the ef boundary had hmm_frame == frame_idx rather than nf, so the check silently suppressed transitions. In practice this meant the last phone of a word could not enter the first phone of the following word, causing two-pass alignment to fail to reach the final state. Change the prune gate to nf >= ef[i] so a phone's last active frame is ef[i]-1. Change the transition condition to hmm_frame(hmm) < frame_idx so any phone active this frame can fire, whether or not it was advanced by prune. --- src/state_align_search.c | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/state_align_search.c b/src/state_align_search.c index 25163067c..efbcf1736 100644 --- a/src/state_align_search.c +++ b/src/state_align_search.c @@ -94,10 +94,12 @@ prune_hmms(state_align_search_t *sas, int frame_idx) hmm_t *hmm = sas->hmms + i; if (hmm_frame(hmm) < frame_idx) continue; - /* Enforce alignment constraint: due to non-emitting states, - * previous phone's HMM remains active in first frame of its - * successor. */ - if (nf > sas->ef[i]) + /* ef[i] is the exclusive end frame (= start + duration). + * Once nf reaches ef[i] the phone has used its full window; + * stop advancing so it is not scored in the next frame. + * phone_transition is allowed to fire at this last active + * frame via the complementary check in phone_transition. */ + if (nf >= sas->ef[i]) continue; hmm_frame(hmm) = nf; } @@ -114,7 +116,13 @@ phone_transition(state_align_search_t *sas, int frame_idx) int32 newphone_score; hmm = sas->hmms + i; - if (hmm_frame(hmm) != nf) + /* Allow transition from phones that were active this frame, + * whether they were advanced to nf (normal case) or left at + * frame_idx by prune_hmms because they hit their ef boundary. + * The original check (hmm_frame != nf) silently blocked + * cross-word transitions when the phone was evaluated at + * exactly its last allowed frame. */ + if (hmm_frame(hmm) < frame_idx) continue; /* Enforce alignment constraint for initial state of each phone. */ if (nf < sas->sf[i + 1]) @@ -123,9 +131,12 @@ phone_transition(state_align_search_t *sas, int frame_idx) newphone_score = hmm_out_score(hmm); nhmm = hmm + 1; if (hmm_frame(nhmm) < frame_idx) { + /* Successor is inactive: enter it fresh with the new frame. */ hmm_enter(nhmm, newphone_score, hmm_out_history(hmm), nf); continue; } + /* Successor is already active: update score/history only (Viterbi + * competition), never bump its hmm_frame to a later value. */ if (newphone_score BETTER_THAN hmm_in_score(nhmm)) { hmm_in_score(nhmm) = newphone_score; hmm_in_history(nhmm) = hmm_out_history(hmm); @@ -461,7 +472,7 @@ state_align_search_init(const char *name, ++i, itor = ps_alignment_iter_next(itor)) { ps_alignment_entry_t *ent = ps_alignment_iter_get(itor); int min_nframes; - + hmm_init(sas->hmmctx, &sas->hmms[i], FALSE, ent->id.pid.ssid, ent->id.pid.tmatid); /* Can't align less than the number of frames in an HMM! */ From 1c18c6ac166a3d01efc3bc3857bdb59f60a9f3d5 Mon Sep 17 00:00:00 2001 From: David Huggins-Daines Date: Mon, 4 May 2026 18:33:23 -0400 Subject: [PATCH 3/3] refactor: remove a stray space --- src/state_align_search.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state_align_search.c b/src/state_align_search.c index efbcf1736..6af846824 100644 --- a/src/state_align_search.c +++ b/src/state_align_search.c @@ -257,7 +257,7 @@ state_align_search_finish(ps_search_t *search) ent = ps_alignment_iter_get(itor); ent->start = cur_frame + 1; ent->duration = last_frame - ent->start; - ent->score = last.score - cur.score; + ent->score = last.score - cur.score; E_DEBUG("state %d start %d end %d\n", last.id, ent->start, last_frame); last = cur;