Repository navigation
feat: place phrase boundaries by hand, by dragging or by tapping along (M11, 5/4) - #39
Conversation
PR Summary by QodoAdd waveform-based phrase boundary editing and manual tap-along
AI Description
Diagram
High-Level Assessment
Files changed (15)
|
Code Review by Qodo
1. Taps get pinned to placeholder ends, not the playhead
|
…g (M11, 5/4) M11, a fifth part the plan had folded into the fourth. The practice screen shipped without it, so M11 stayed unticked; this is what ticks it. Forced alignment is right most of the time and wrong in a way the reader can hear: a boundary lands half a syllable late and the next phrase opens with the tail of the last word. Correcting that is a drag, so the editor draws the recording's shape around the phrase — the only waveform in the app, and drawn only here, because everywhere else the question is "how far through am I" and here it is "does that syllable belong inside". Two operations, deliberately different. A **drag** corrects one phrase and stops at its neighbour, leaving any silence alignment excluded where it is; where two phrases already meet it moves both, because the line between them is one thing and not two. A **tap** is a statement about both phrases made while the recording plays, so it closes whatever gap was there. A reader tapping sentences in has no way to say "leave 300 milliseconds between them", and no reason to want to. Tapping along is also the whole answer for a system that cannot listen: a Mac older than macOS 26, a system with no Chinese speech model, or a reader who will not download several hundred megabytes. The text is split into sentences with no times worth having, the recording plays once, and ⌘↩ places each boundary. A few dozen lines, and always exactly right — which is why it is here rather than a third alignment path guessing from silence. That option is now offered from the invitation screen as well as after a failure, since a reader who already knows the recogniser cannot follow their recording should not have to watch it fail first. What a reader places is marked `manual` and survives re-alignment. What was merely shuffled out of their way is not, so a later listen is free to replace it — which is why `movePhrase` now takes an origin instead of assuming one. Three things found on the running app rather than in the tests: - Splitting without listening opened the editor on a flat line, because it set the flag instead of going through `startEditing`, which is what reads the shape. - Stopping part-way through tapping left every phrase ahead collapsed to the minimum length, stacked up behind the playhead. They are now spread over the recording that is left — not right, but evenly wrong, which is what a placeholder split is. - Mark was on ⌘M, which is the system's Minimize. A view's shortcut competing with a menu command is a fight to lose, so it is ⌘↩: free while the writing field is off screen, and already "commit this" elsewhere on the screen. Verified against 听力64 and its narration: a drag moved both sides of a shared boundary and wrote them down as `manual`; splitting without listening gave 27 placeholders over the full 133 seconds at zero confidence; tapping placed boundaries at the playhead and advanced; and stopping after one tap left the remaining twenty-five sharing 4.75 seconds each rather than a quarter of a second each, with the last ending exactly at the end of the recording. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c22e670 to
b28c69a
Compare
Eight findings, all real. Two of them matter. **A tap was pinned to a placeholder rather than to the playhead.** `place` clamped the boundary to the opened phrase's end minus the minimum, and during tap-along that end is wherever an equal-slice split happened to put it. A spoken sentence running past its placeholder is the ordinary case, not the exception, so the boundary was recorded somewhere the reader did not press — and the phrase they were opening collapsed to a quarter of a second. The tap now decides, held off only by the phrases either side needing a length each. **Stopping tap-along could replace a good alignment with equal slices.** `settleUntapped` spread from the last opened phrase without distinguishing a placeholder from a boundary somebody had placed, and with nothing tapped yet that was the first phrase — so opening tap-along and pressing Stop rewrote the lot. Each tap now lays out only the phrases ahead of it that nobody has placed, stopping at the first `manual` one, which leaves the text consistent at every moment and leaves nothing to settle. `settleUntapped` and `respread` are gone, and so is the hole. **Six more.** Two quick Marks either side of a database round trip both read the same upcoming phrase and placed the same boundary twice; the counter now advances before the write. `movePhrase` wrote times and origin but not confidence, so a corrected phrase came back from the database still flagged uncertain and the warning the reader had just answered reappeared. A failed write left the moved boundary on screen as though it had been kept; the screen is now taken back from the database. Dismissing the sheet part-way through tapping cleared the flag directly instead of going through the path Stop takes. `select` cleared `isRevealed` while the editor stayed open, so the editor showed each new phrase's text with the model believing nobody had looked. And `WaveformReader` bucketed by a fixed integer stride while `resampled` assumed `duration / count`, so the far end of a long recording was drawn out of step with what plays there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stacked on #38 — based on
feat/m11-practice-screen, so this diff is only the new work. Merge #38 first.The piece the plan folded into PR 4 and I split out. M11 is ticked here.
Why
Forced alignment is right most of the time and wrong in a way the reader can
hear: a boundary lands half a syllable late and the next phrase opens with the
tail of the last word. Correcting that is a drag — so the editor draws the
recording's shape around the phrase, the only waveform in the app. Everywhere
else the question is "how far through am I"; here it is "does that syllable
belong inside", and that is a question about shape.
Two operations, deliberately different
The path for a system that cannot listen
A Mac older than macOS 26, a system with no Chinese speech model, or a reader
who will not download several hundred megabytes. The text is split into
sentences with no times worth having, the recording plays once, and ⌘↩ places
each boundary. A few dozen lines, always exactly right — which is why it is
here rather than a third alignment path guessing from silence.
Now offered from the invitation screen as well as after a failure: a reader
who already knows the recogniser cannot follow their recording should not have
to watch it fail first.
What survives re-alignment
What a reader places is
manualand is kept. What was merely shuffled out oftheir way is not, so a later listen is free to replace it — which is why
movePhrasenow takes an origin instead of assuming one.Shape
HanReaderCore/Listening/BoundaryEditing.swift—move,place(boundaryBefore:),respread,window. Pure, Foundation only.HanReaderPlayback/WaveformPeaks.swift—AVAssetReaderdown to one peak perbucket, 12,000 over a file;
resampled(over:count:)takes the maximum peroutput bucket so a short transient stays visible when zoomed out.
HanReaderUI/Screens/BoundaryEditor.swift— aCanvasstrip, the phrase's ownbars in the accent colour, two draggable edges with grab areas far wider than
the lines they draw.
PracticeModel+Boundaries.swift— opening the editor, dragging, splittingwithout listening, tapping along.
Three things the tests did not find
Found on the running app:
flag instead of going through
startEditing, which is what reads the shape.minimum, stacked up behind the playhead. They are now spread over the
recording that is left — not right, but evenly wrong, which is what a
placeholder split is.
competing with a menu command is a fight to lose. It is ⌘↩ now: free while the
writing field is off screen, and already "commit this" elsewhere on the screen.
Verification
— 31 on the core, 15 on the model, 10 on the waveform.
recording built a range whose lower bound was above its upper one, and
Rangetraps on that. The editor asks for exactly that the moment a phrase's times
outlive the file they were measured against.
them already make unreachable. Those clamps were removed rather than papered
over with tests that cannot fail.
of a shared boundary and wrote them as
manual; splitting without listeninggave 27 placeholders over the full 133 s at zero confidence; tapping placed
boundaries at the playhead and advanced the counter; stopping after one tap left
the remaining twenty-five sharing 4.75 s each rather than a quarter-second
each, ending exactly at the end of the recording.
swift testgreen (493),make lintclean, both app targets built.🤖 Generated with Claude Code