Skip to content

TT-7666 TT-7432 fix: tell one take of a phrase segment from another - #567

Closed
nabalone wants to merge 4 commits into
TT-7643_pbt-cross-language-audiofrom
TT-7666_TT-7432_pbt-takes
Closed

TT-7666 TT-7432 fix: tell one take of a phrase segment from another#567
nabalone wants to merge 4 commits into
TT-7643_pbt-cross-language-audiofrom
TT-7666_TT-7432_pbt-takes

Conversation

@nabalone

@nabalone nabalone commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

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 to develop once 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: selectCurrentPhraseTakes keeps 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 sourceSegments at 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 dataPath resolves 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

  • 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.

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; newTakeToken never repeats — same millisecond, or a clock set backwards — and carries session entropy.
  • PassageDetailGuidedPhraseRecord.stepScope.test.tsx and PassageDetailLwcTranslation.test.tsx — record, delete, record again: the second take is not named like the first.
  • Full jest suite (177 suites / 1416 tests) and tsc --noEmit green.

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

  • Stale takes are hidden, not deleted — they still sync and download. If they should be reaped, that is a separate change (and a destructive one).
  • The plan-level Transcriber task list (no step, so no boundaries) still lists every take.
  • LWC Translation has no language in its take names. If a project can configure more than one LWC Translation step over the same audio, that is TT-7643's defect there too — not addressed here.

🤖 Generated with Claude Code

nabalone and others added 2 commits September 1, 2026 21:32
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 PassageDetailTranscribe into TranscriberProvider so 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.

Comment on lines +78 to +87
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)}`;
}
nabalone and others added 2 commits September 1, 2026 22:03
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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment on lines +114 to 125
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
);
});
});
@nabalone

nabalone commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Split by ticket. TT-7666 (stale takes after a boundary adjustment) is now #570, on top of #561. The TT-7432 take-naming work continues on its own branch and gets its own PR.

@nabalone nabalone closed this Sep 2, 2026
@nabalone
nabalone deleted the TT-7666_TT-7432_pbt-takes branch September 2, 2026 19:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants