fix(transcriptions): render segments that have no timings

An OpenAI-compatible ASR answering in json/text format returns no
timestamps, and services/asr_backend.py records that honestly as
`end: None` rather than inventing a number. The segment list called
`.toFixed()` on it unconditionally, so the render threw and the whole
Transcriptions view went blank — a transcript that merely lacked timings
became one the user could not read at all.

Show whichever bound is known and nothing when neither is, so the text
stays readable either way. Non-finite values are treated as unknown too,
so a bad timing prints nothing rather than NaN.

Fixes #1798.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HR6J9zKQop9TGGVUwjypnF
This commit is contained in:
Palash Debnath
2026-09-04 03:26:51 +05:30
co-authored by Claude Opus 5
parent f2302e8c95
commit 4e6c36848c
4 changed files with 59 additions and 2 deletions
+2
View File
@@ -0,0 +1,2 @@
1788271637990
d8d64392-e276-e11d-730f-432cfd53e5f2
+2
View File
@@ -58,6 +58,8 @@ the frozen-backend fallback mirror it for their toolchains.
### Fixed
- The Transcriptions view no longer goes blank on a transcript whose segments have no timings — an ASR backend that returns text without timestamps is rendered as text instead of crashing the page (#1798)
- The generation compute-time budget is now a Settings control (Performance & Device) instead of an env-var-only setting the timeout error recommended with no UI path — the error copy points there too, and long CPU/MPS renders get an upfront heads-up before they start (#1787)
- Windows: the backend can now start when the install path contains non-English characters (e.g. a CJK username) on a non-UTF-8 system code page — a new or broken Python environment now builds at an ASCII-safe path automatically (a healthy existing one is never relocated), and a specific error message names the cause and a working fix if the interpreter still crashes in `site` (#1783)
- Exports and other native-picker actions no longer 403 with "Invalid or expired desktop authorization" when the desktop app and backend resolve different data directories, e.g. dev mode or a custom data folder (#1781)
+18 -1
View File
@@ -26,6 +26,23 @@ function saveTranscriptions(list) {
localStorage.setItem(TRANSCRIPTIONS_KEY, JSON.stringify(list));
}
/** A segment's "12.0s 15.5s" label, tolerant of missing timings (#1798).
*
* Not every ASR path produces a timed segment: an OpenAI-compatible backend
* answering in `json`/`text` format has no timings at all, and
* `services/asr_backend.py` records that honestly as `end: None` rather than
* inventing a number. Calling `.toFixed()` on it threw during render and took
* the whole Transcriptions view down, so a transcript that merely lacked
* timings became one the user could not read at all. Render whichever half is
* known, and nothing when neither is. */
export function segTimeRange(seg) {
const known = (v) => typeof v === 'number' && Number.isFinite(v);
const start = known(seg?.start) ? `${seg.start.toFixed(1)}s` : null;
const end = known(seg?.end) ? `${seg.end.toFixed(1)}s` : null;
if (start && end) return `${start} ${end}`;
return start || end || '';
}
export function addTranscription(entry) {
const list = loadTranscriptions();
const newEntry = {
@@ -288,7 +305,7 @@ export default function TranscriptionsPage() {
className="txn-detail__seg flex gap-[8px] py-[3px] text-[var(--text-xs)]"
>
<span className="txn-detail__seg-time shrink-0 font-mono text-fg-subtle min-w-[80px]">
{seg.start.toFixed(1)}s {seg.end.toFixed(1)}s
{segTimeRange(seg)}
</span>
<span className="txn-detail__seg-text text-fg">{seg.text}</span>
</div>
+37 -1
View File
@@ -19,7 +19,7 @@ vi.mock('../hooks/useEffectiveDictationShortcut', () => ({
}));
vi.mock('react-hot-toast', () => ({ toast }));
import TranscriptionsPage, { addTranscription } from './Transcriptions';
import TranscriptionsPage, { addTranscription, segTimeRange } from './Transcriptions';
describe('Transcriptions capture entry point', () => {
beforeEach(() => {
@@ -67,3 +67,39 @@ describe('Transcriptions capture entry point', () => {
expect(await screen.findByText('The shared capture path works.')).toBeInTheDocument();
});
});
// #1798: an OpenAI-compatible ASR answering in json/text format returns no
// timings, and services/asr_backend.py records that honestly as `end: null`
// rather than inventing a number. The segment list called `.toFixed()` on it
// unconditionally, which threw during render and took the whole
// Transcriptions view down — a transcript that merely lacked timings became
// one the user could not read at all.
describe('segments without timings (#1798)', () => {
beforeEach(() => {
localStorage.clear();
});
it('renders a segment whose end is null instead of crashing the view', async () => {
addTranscription({
text: 'hello from an untimed backend',
language: 'en',
segments: [{ text: 'hello from an untimed backend', start: 0, end: null }],
});
render(<TranscriptionsPage />);
fireEvent.click(await screen.findByText('hello from an untimed backend'));
// The transcript itself must still be readable — this is the regression:
// before the guard, the null `end` threw and nothing rendered at all.
expect(screen.getAllByText('hello from an untimed backend').length).toBeGreaterThan(0);
});
it('formats what is known and never prints NaN', () => {
expect(segTimeRange({ start: 12, end: 15.55 })).toBe('12.0s 15.6s');
expect(segTimeRange({ start: 0, end: null })).toBe('0.0s');
expect(segTimeRange({ start: null, end: 4 })).toBe('4.0s');
expect(segTimeRange({ text: 'no timings' })).toBe('');
expect(segTimeRange(undefined)).toBe('');
expect(segTimeRange({ start: NaN, end: Infinity })).toBe('');
});
});