TT-7666 TT-7432 fix: tell one take of a phrase segment from another - #567
TT-7666 TT-7432 fix: tell one take of a phrase segment from another#567nabalone wants to merge 4 commits into
Conversation
Failing tests first, for two defects that both come of a take not being told apart from another take of the same segment. TT-7666: a take records the slice of vernacular it covers in sourceSegments. Adjusting a segment boundary rewrites the slices, so takes made before the adjustment answer to a segment that no longer exists - and recording the moved segments again leaves both generations attached to the same vernacular. The record step only shows takes matching the boundaries it is reading, but the Transcribe task list is built from every take there is: two segments, four tasks. TT-7432: segment index, source version and step language are the whole of a take's name, and they are identical for two takes of one segment in one step. Deleting a recording and recording it again therefore uploaded the replacement under the name the deleted take is cached on, and since dataPath resolves a mediafile's audioUrl to `<offlineData>/media/ <basename>`, the deleted audio is what played back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TT-7666 - scope the Transcribe task list to the segment boundaries the step is reading. `selectCurrentPhraseTakes` keeps the newest take of each current segment and drops the takes that name a segment the boundaries have moved away from, so adjusting a boundary and recording again leaves one task per segment instead of one per take ever recorded. It hides rather than deletes: a take is only dropped where boundaries exist to judge it against. With no boundaries to read - a vernacular whose segments cannot be parsed, an artifact that records none, the plan-level task list that has no step context - the takes come through untouched, as do takes with no `sourceSegments` at all (Retell, and anything older than segment maps). Hiding audio on a guess would be worse than a duplicate row. The segment-matching and newest-take rules now live in one place, crud/phraseTakes, reached by both the step (carefulSpeechCompletion, matchesGuidedOutputRow) and the context layer that builds the task list. TT-7432 - give every take its own upload name. A take now carries a token minted where recording begins, so a segment re-recorded after its first take was deleted no longer resolves to the cached file of the take that was deleted. Takes already uploaded keep their names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
newTakeToken() can repeat tokens if Date.now() moves backward, risking filename collisions and incorrect offline playback.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes two take-identity defects in the renderer by (1) scoping Transcribe task generation to the current phrase-segment boundaries (dropping stale takes left behind by boundary edits) and (2) ensuring each newly recorded take gets a unique filename token so offline cache resolution can’t accidentally replay a prior/deleted recording.
Changes:
- Add shared phrase-segment matching + “newest take wins” selection logic (
crud/phraseTakes) and use it when building Transcribe task lists. - Thread current phrase-segment regions from
PassageDetailTranscribeintoTranscriberProviderso stale takes don’t become tasks (TT-7666). - Add per-take filename tokens for guided phrase recording so re-recorded takes don’t collide in the offline media cache (TT-7432), with accompanying tests.
File summaries
| File | Description |
|---|---|
| src/renderer/src/crud/phraseTakes.ts | New helpers to parse take regions, compare recency, and select current (non-stale) takes per region. |
| src/renderer/src/crud/phraseTakes.test.ts | Unit coverage for stale-boundary hiding, newest-take selection, tie-break stability, tolerance, and pass-through cases. |
| src/renderer/src/context/TranscriberContext.tsx | Applies step-language filtering plus phrase-region scoping when computing plan/passage transcribe media tasks. |
| src/renderer/src/components/PassageDetail/PassageDetailTranscribe.tsx | Computes phrase regions for phrase artifacts and passes them into TranscriberProvider. |
| src/renderer/src/components/PassageDetail/PassageDetailTranscribe.test.tsx | Verifies phrase boundaries are passed only for phrase artifacts; non-phrase artifacts remain unscoped. |
| src/renderer/src/components/PassageDetail/PassageDetailGuidedPhraseRecord.tsx | Mints a per-take token at recording start and includes it in the filename postfix. |
| src/renderer/src/components/PassageDetail/PassageDetailGuidedPhraseRecord.stepScope.test.tsx | Adds coverage that re-recording the same segment produces a different filename (TT-7432). |
| src/renderer/src/components/PassageDetail/guidedPhraseRecord/types.ts | Extends filename postfix builder to accept an optional take token; adds newTakeToken() helper. |
| src/renderer/src/components/PassageDetail/guidedPhraseRecord/types.test.ts | Tests tokenized postfix behavior + newTakeToken uniqueness/ordering expectations. |
| src/renderer/src/components/PassageDetail/carefulSpeech/matchesGuidedOutputRow.ts | Reuses the shared “newest take” comparator instead of duplicating sort logic. |
| src/renderer/src/components/PassageDetail/carefulSpeech/carefulSpeechCompletion.ts | Reuses shared region parsing/matching logic for completion checks. |
Review details
- Files reviewed: 11/11 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| export function newTakeToken(now: number = Date.now()): string { | ||
| if (now === lastTokenMs) { | ||
| tokenSeq += 1; | ||
| } else { | ||
| lastTokenMs = now; | ||
| tokenSeq = 0; | ||
| } | ||
| const stamp = now.toString(36); | ||
| return tokenSeq === 0 ? stamp : `${stamp}${tokenSeq.toString(36)}`; | ||
| } |
The same defect one step over. Clause index and source version are the whole of an LWC Translation take's name and neither changes when the clause is recorded again, so clearing a recording and recording it again uploads the replacement under the name the deleted take is cached on - and `dataPath` resolves a mediafile's audioUrl to `<offlineData>/media/<basename>`, so the deleted audio is what plays. The step's own test harness stubbed the filename to a constant, so it could not have caught this; it now echoes the postfix the step builds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same fix as the Careful Speech / Phrase BT steps: the take carries a token minted where recording begins, so re-recording a clause - with or without clearing the old take first - no longer resolves to the cached file of the take it replaced. The token generator is shared with the guided phrase-record steps rather than duplicated, so the whole session mints from one monotonic sequence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
newTakeToken as implemented can re-issue tokens if the system clock moves backwards (and lacks per-session entropy as described), which risks reintroducing filename collisions and incorrect cached-audio playback.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
src/renderer/src/components/PassageDetail/guidedPhraseRecord/types.ts:87
- newTakeToken() claims to be unique/ascending, but if the system clock moves backwards (or two app sessions record in the same millisecond) this implementation can re-issue a previously used token, which can reintroduce filename collisions and cached-audio playback bugs (TT-7432). Consider using a high-water-mark timestamp and a per-session salt so tokens remain unique and monotonic even with a backwards/stalled clock.
let lastTokenMs = 0;
let tokenSeq = 0;
/**
* A token for one take, unique and ascending. The clock alone would do, but two
* calls can land in the same millisecond, so same-millisecond calls get a
* counter appended rather than the same token.
*/
export function newTakeToken(now: number = Date.now()): string {
if (now === lastTokenMs) {
tokenSeq += 1;
} else {
lastTokenMs = now;
tokenSeq = 0;
}
const stamp = now.toString(36);
return tokenSeq === 0 ? stamp : `${stamp}${tokenSeq.toString(36)}`;
}
- Files reviewed: 13/13 changed files
- Comments generated: 1
- Review effort level: Lite
| describe('newTakeToken', () => { | ||
| it('never repeats a token, even inside one millisecond', () => { | ||
| const now = 1767225600000; | ||
| expect(newTakeToken(now)).not.toEqual(newTakeToken(now)); | ||
| }); | ||
|
|
||
| it('grows with the clock so a later take sorts after an earlier one', () => { | ||
| expect(newTakeToken(1767225600000) < newTakeToken(1767225700000)).toBe( | ||
| true | ||
| ); | ||
| }); | ||
| }); |
Two defects that both come of a take not being told apart from another take of the same segment. Stacked on #561 (TT-7643) — base is
TT-7643_pbt-cross-language-audio, retarget todeveloponce that merges.TT-7666 — PBT Transcribe displays duplicate audio after segment boundary adjustment
A take records the slice of vernacular it covers in
sourceSegments. That is its only link back to a segment: segments are boundaries stored on the vernacular's named regions, not records, and moving a boundary rewrites them in place. Takes recorded before the move are left answering to a segment that no longer exists, and recording the moved segments again leaves both generations attached to the same vernacular. The record step only shows takes matching the boundaries it is reading, but the Transcribe task list was built from every take there was — two segments, four tasks.The task list is now scoped to the boundaries the step is reading:
selectCurrentPhraseTakeskeeps the newest take of each current segment and drops takes naming a segment the boundaries have moved away from.It hides rather than deletes, and only where there are boundaries to judge against. With none to read — a vernacular whose segments cannot be parsed, an artifact that records none, the plan-level task list that has no step context — the takes come through untouched, as do takes with no
sourceSegmentsat all (Retell, and anything older than segment maps). Hiding audio on a guess would be worse than a duplicate row.TT-7432 — Careful Speech: deleted recording continues to play after re-recording
Segment index, source version and step language were the whole of a take's name, and they are identical for two takes of one segment in one step. Deleting a recording and recording it again therefore uploaded the replacement under the name the deleted take is cached on, and
dataPathresolves a mediafile's audioUrl to<offlineData>/media/<basename>— so the file already on disk for the deleted take is what played. Same mechanism as TT-7643, one step over.Every take now carries a token minted where recording begins, so no two takes of a segment share a name. Takes already uploaded keep the names they have. The token is a per-session monotonic stamp: a high-water-mark clock reading plus a counter for calls that do not advance it (so a stalled or backwards-set clock cannot reissue one), plus a per-session salt (so two sessions recording the same segment in the same millisecond still differ).
LWC Translation had the same defect
lwctranslation<n>_v<v>was the whole of an LWC Translation take's name, and neither part changes when the clause is recorded again — and that step has a Clear button that deletes the take, so the same clear-and-re-record path plays the deleted audio. Fixed the same way, sharing the one token generator. Its own test harness had stubbed the filename to a constant, so it could not have caught this; it now echoes the postfix the step builds.Also
crud/phraseTakes, reached by both the step (carefulSpeechCompletion,matchesGuidedOutputRow) and the context layer that builds the task list.Test plan
TDD — each fix's failing tests are committed before it.
crud/phraseTakes.test.ts— stale boundaries dropped, newest take of a re-recorded segment kept, stable tie-break, tolerance, and the three cases that must pass through untouched.PassageDetailTranscribe.test.tsx— the step hands its current boundaries to the transcriber provider, and a non-phrase artifact stays unscoped.guidedPhraseRecord/types.test.ts— a take token separates two takes of one segment; names without one are unchanged;newTakeTokennever repeats — same millisecond, or a clock set backwards — and carries session entropy.PassageDetailGuidedPhraseRecord.stepScope.test.tsxandPassageDetailLwcTranslation.test.tsx— record, delete, record again: the second take is not named like the first.tsc --noEmitgreen.Not covered by an automated test: the Cypress PBT harness has no Transcribe step, so the four-tasks-to-two symptom is covered at the unit level only. Manual verification of both tickets in the app still wanted.
Open questions / follow-ups
🤖 Generated with Claude Code