diff --git a/:memory:.ses b/:memory:.ses new file mode 100644 index 00000000..e22cf5bb --- /dev/null +++ b/:memory:.ses @@ -0,0 +1,2 @@ +1788271637990 +d8d64392-e276-e11d-730f-432cfd53e5f2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf42b70..29210c51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/frontend/src/pages/Transcriptions.jsx b/frontend/src/pages/Transcriptions.jsx index 9ab3237e..608fcebc 100644 --- a/frontend/src/pages/Transcriptions.jsx +++ b/frontend/src/pages/Transcriptions.jsx @@ -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)]" > - {seg.start.toFixed(1)}s – {seg.end.toFixed(1)}s + {segTimeRange(seg)} {seg.text} diff --git a/frontend/src/pages/Transcriptions.test.jsx b/frontend/src/pages/Transcriptions.test.jsx index fd4cd3de..55bb28fb 100644 --- a/frontend/src/pages/Transcriptions.test.jsx +++ b/frontend/src/pages/Transcriptions.test.jsx @@ -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(); + 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(''); + }); +});