TT-7643 fix: name a Phrase BT take for its step language so it cannot collide - #561
Merged
Conversation
… step
A team can configure one Phrase Back Translation step per language, and
every one of them renders the same PassageDetailGuidedPhraseRecord
against the same vernacular audio. The mobile route rendered that
component unkeyed, so moving between two PBT steps reused the instance -
and the component resets its per-step state only when the vernacular
mediafile changes, which it does not here. The recording pass, the
segment it was parked on, the baseline boundaries, the optimistic greens
and the take mounted in its recorder all carried into the next
language's step, which then played the previous language's audio.
The route now keys the guided-record steps on currentstep, the way the
desktop grid already does with <Paper key={currentstep}> and the way the
adjacent transcription steps were keyed for TT-7503. The component also
scopes its own reset to the step as well as the mediafile, so a reused
instance still opens the next step from scratch.
The legacy claim had a second cross-language path: it stamped every
untagged take of its artifact type with the opening step's language.
That premise only holds for data recorded before takes carried a
language, in a project working in one language. Once the passage has
Phrase BT boundaries for another language an untagged take could belong
to either step, so the claim is now skipped there rather than handing
the take to whichever step happened to open.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rs in the PBT harness parseMediaLanguageBcp47 and phraseBtBoundaryRegionName already own the `Name|bcp47` parse and the `BT:<bcp47>` bucket name; the harness had grown its own copies of both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…t changes useGuidedPhraseSegments guarded its own reset on the mediafile id, so a step change on the same vernacular was rejected as a no-op: bootstrapped stayed true and ensureSegments short-circuited on the phrase string already loaded. The next language's step opened on the previous language's boundaries even though the parent had asked for a reset. The bucket is part of that scope, not just the audio - each Phrase BT step reads its own BT:<bcp47> regions off the same vernacular - so the guard now keys on both, and the function is named resetForScope to say so. Covered by a test against the real hook rather than the mocked one (Devin review). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes cross-language state/audio bleed when teams configure multiple Phrase Back Translation (PBT) guided-record steps (one per language) against the same vernacular audio. It ensures guided-record UI state and mounted takes are scoped to the current step/language, particularly on mobile where the route previously reused an unkeyed component instance.
Changes:
- Key mobile guided-record steps by
currentstepso navigating between adjacent per-language steps remounts the step UI instance. - Scope guided-record reset logic to
${mediafileId}|${currentstep}(not justmediafileId) and reset legacy-claim state per step. - Prevent
claimLegacyPhraseBtfrom stamping untagged takes with a language once multi-language PBT boundaries exist, and add unit + Cypress coverage to lock in behavior.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/renderer/src/routes/PassageDetail.tsx | Adds key={currentstep} to guided-record mobile steps to force remount between adjacent per-language steps. |
| src/renderer/src/components/PassageDetail/PassageDetailGuidedPhraseRecord.tsx | Scopes reset behavior to step+mediafile and releases per-step legacy-claim gating when step changes. |
| src/renderer/src/components/PassageDetail/carefulSpeech/useGuidedPhraseSegments.ts | Renames/reset logic to resetForScope and keys the boundary reset on (mediafileId, namedRegion) (language bucket). |
| src/renderer/src/components/PassageDetail/carefulSpeech/claimLegacyPhraseBt.ts | Skips language-claiming untagged takes when other-language boundary buckets are present with regions. |
| src/renderer/src/components/PassageDetail/PassageDetailGuidedPhraseRecord.stepScope.test.tsx | New test verifying a reused guided-record instance resets cleanly when currentstep changes. |
| src/renderer/src/components/PassageDetail/carefulSpeech/useGuidedPhraseSegments.test.tsx | New test verifying boundary reset re-reads when only the bucket/language changes on the same audio. |
| src/renderer/src/components/PassageDetail/carefulSpeech/claimLegacyPhraseBt.test.ts | Adds cases covering multi-language boundary presence and empty/own-bucket scenarios. |
| src/renderer/src/components/PassageDetail/PassageDetailCarefulSpeech.test.tsx | Updates mocks to the new resetForScope API. |
| src/renderer/src/components/PassageDetail/PassageDetailPhraseBackTranslate.cy.tsx | Adds CT coverage that only this step’s language take is fetched/played. |
| src/renderer/cypress/support/pbtHarness.tsx | Extends the PBT harness to seed per-language takes/step language and record fileurl fetch IDs for assertions. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… collide The real cause of PBT audio from one language playing in another: on the desktop app a take's audio is resolved by FILE NAME, not by id. useFetchMediaUrl calls dataPath(mediafile.audioUrl, PathType.MEDIA), which maps to <offlineData>/media/<basename> and hands back whatever file is already sitting there. The upload name is passageDefaultFilename + buildFilenamePostfix, which carried the passage, the segment index and the source version but not the step language. A team can configure one Phrase BT step per language, and every one of them records the same segment of the same vernacular - so Hebrew segment 1 uploaded under exactly the name Sena segment 1 had already cached, and the first file cached is what played for both. buildFilenamePostfix now takes the step bcp47 and appends it. Steps with no configured language (Careful Speech) keep the names they always had, so existing files are untouched. Also fixes a harness defect that hid this: the fake server stamped every uploaded take English|en rather than the language that was posted. Note the earlier commits on this branch do not fix the reported defect - the desktop route already remounted the step on currentstep. They stay because they are correct on their own terms: the mobile route lacked that parity, and the segment hook's reset had to key on the language bucket as well as the audio. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
nabalone
marked this pull request as ready for review
September 1, 2026 20:59
This was referenced Sep 1, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
TT-7643 — with a Phrase Back Translation step per language (Sena, Hebrew, English), audio recorded in one language plays in another language's PBT step.
On the desktop app a take's audio is resolved by file name, not by id.
useFetchMediaUrlcallsdataPath(mediafile.attributes.audioUrl, PathType.MEDIA), which maps to<offlineData>/media/<basename(audioUrl)>and returns that local file if it exists — the mediafile id is never consulted.store/upload/actions.tsxalso stages every online upload into that same folder, so the cache is populated as you record.The upload name is
passageDefaultFilename(...) + config.buildFilenamePostfix(unitIndex, sourceVersion). It carried the passage, the segment index and the source version — but not the step language. A team configures one PBT step per language and every one of them records the same segment of the same vernacular, so Hebrew segment 1 uploaded under exactly the name Sena segment 1 had already cached. First file cached wins, and that is Sena.The naming is visible in a real cache:
One file per segment, no language anywhere in the name.
What this supersedes
The first two commits on this branch were aimed at component reuse, on the reading that the step instance carried its take across a step change. That is not what the ticket reports: the ticket video is the desktop layout, and
PassageDetailGridshas had<Paper key={currentstep}>all along, so the step was already remounting. Those commits stay because they are correct on their own terms — the mobile route lacked that parity, and the segment hook's reset had to key on the language bucket as well as the audio (Devin's finding) — but they are hardening, not the fix. The row-level language filter from #490 is likewise sound: a CT test on the real step shows a Hebrew step never fetches a Sena take.Changes
guidedPhraseRecord/types.ts—buildFilenamePostfixtakes the step bcp47 and appends it:backtranslation1_v1_sehvsbacktranslation1_v1_he. Steps with no configured language (Careful Speech) keep the names they always had, so existing files are untouched.PassageDetailGuidedPhraseRecord.tsx— passesstepLanguageBcp47into the postfix builder.cypress/support/pbtHarness.tsx— the fake server stamped every uploaded takeEnglish|eninstead of the language that was posted, which hid whether the step language reached the upload at all.Retained from the earlier commits (hardening, not the fix):
routes/PassageDetail.tsx—key={currentstep}on the guided-record steps inMobileStep, matching desktop and the TT-7503 keyed transcription step.PassageDetailGuidedPhraseRecord.tsx— per-step reset gated on`${mediafileId}|${currentstep}`;claimRanRefreleased with it.carefulSpeech/useGuidedPhraseSegments.ts—resetForScopekeys on theBT:<bcp47>bucket as well as the mediafile, so a step change is not rejected as a no-op.carefulSpeech/claimLegacyPhraseBt.ts— the legacy language claim is skipped once anotherBT:<bcp47>bucket has regions, instead of stamping every untagged take with whichever step opened first.Test plan
Failing-before / passing-after, all against unfixed source:
guidedPhraseRecord/types.test.ts—buildFilenamePostfixseparates the languages of the same segment.PassageDetailGuidedPhraseRecord.stepScope.test.tsx— "names a take so it cannot collide with another language of the same segment": drives the real component through a Sena step then a Hebrew step and reads the filename it handsMediaRecord. Pre-fix both steps produceGEN001_014-019backtranslation1_v1_plan.PassageDetailPhraseBackTranslate.cy.tsx— "uploads a take under a name no other language can take": records a real take on a Hebrew step and asserts the postedoriginal-filecarries the language.Also on this branch: the step-scope reset tests,
useGuidedPhraseSegments.test.tsx(real hook, bucket change on the same audio), the "PBT language scoping" CT pair, and 3 newclaimLegacyPhraseBtcases.npx jest(full renderer suite) → 1389 pass, 3 skipped; 1 pre-existing unrelated failure (EditReferenceDropdown.test.tsx, MUIDrawermodule resolution) present ondeveloptoo.tsc --noEmitclean; Prettier clean on the touched files.Known, not fixed here
Re-recording the same segment in the same language reuses the name too, so the earlier take's cached audio can play for the new one. Same defect class, outside this ticket — flagging rather than folding in, since a per-take unique token changes every filename users see in exports.
🤖 Generated with Claude Code