Two frontend bugs reported on v0.3.7, both Windows/Chromium-flavoured: 1. The PLAY button on the dubbed-video preview did nothing. WaveSurfer builds its AudioContext at mount (before any user gesture), so on Windows WebView2 / Linux FF/Chrome it stays "suspended" and playPause() resolves with no sound. This is the same autoplay-policy trap #510 fixed for WaveformPlayer, but the dub timeline player was missed. togglePlay and the per-segment playRange now await the shared unlockAudio() on the click before starting playback, and swallowed play() rejections are logged. A source-contract regression test pins the invariant (fail-before/pass-after verified). 2. The designer Script text field couldn't be expanded. It was a `flex: 1` item in a flex column, so flex-grow recomputed its height each reflow and snapped the resize-drag back — `resize: vertical` is ignored on a flex-grown item in Chromium/WebView2. The textarea now owns its height (flex: 0 1 auto + a taller min-height) so the corner grip grows it reliably on every platform. Gates: `bun run build` and `bunx vitest run` (563 tests) both pass. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
mergetest
Claude Opus 4.8
parent
2767e2995d
commit
2ad83f37fd
@@ -27,6 +27,24 @@ The bundled TTS model package (`pyproject.toml`) is versioned independently.
|
||||
contact — less wall-of-text, faster to act on.
|
||||
### Fixed
|
||||
|
||||
- **Dubbing: the PLAY button on the dubbed-video preview did nothing.** Same
|
||||
autoplay-policy trap that #510 fixed for the standalone audio player, but the
|
||||
dub editor's timeline player was missed. WaveSurfer builds its `AudioContext`
|
||||
at mount — before any user gesture — so on Windows WebView2 (and Linux
|
||||
Firefox/Chrome, Android Chrome) it stays `"suspended"`; `playPause()` then
|
||||
resolves with no sound and the preview just sits there. Every playback entry
|
||||
point in the dub timeline (the toolbar Play button and the per-segment "play
|
||||
this slot") now resumes the context via the shared `unlockAudio()` on the
|
||||
click before starting playback, and swallowed play() rejections are logged
|
||||
instead of hidden. A source-contract regression test pins the invariant so a
|
||||
future refactor can't quietly reintroduce a silent play path. macOS is
|
||||
unaffected (its context was never blocked). (#595)
|
||||
- **Voice design: the script text field couldn't be expanded.** The Script
|
||||
textarea was a `flex: 1` item inside a flex column, so flex-grow recomputed
|
||||
its height on every reflow and snapped the user's drag back — `resize:
|
||||
vertical` is silently ignored on a flex-grown item in Chromium/WebView2. The
|
||||
field now owns its own height (starts taller, and the corner grip grows it
|
||||
reliably on every platform). (#595)
|
||||
- **An interrupted model download now self-repairs instead of dead-ending.**
|
||||
When the OmniVoice TTS cache was missing weight shards (the usual aftermath of
|
||||
an interrupted first download), the next synthesize failed with a 500 and a
|
||||
|
||||
@@ -5,6 +5,7 @@ import TimelinePlugin from 'wavesurfer.js/dist/plugins/timeline.esm.js';
|
||||
import { Play, Pause, ZoomIn, ZoomOut, SkipBack, Loader, Keyboard, AlertTriangle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppStore } from '../store';
|
||||
import { unlockAudio } from '../utils/audioUnlock';
|
||||
import SegmentTrack from './SegmentTrack';
|
||||
import './WaveformErrorBoundary.css';
|
||||
|
||||
@@ -489,7 +490,10 @@ function WaveformTimeline({
|
||||
// timeupdate watcher wired in the init effect. Used by the SegmentTrack
|
||||
// ("play this slot") on whatever media the player currently holds, so it
|
||||
// respects the original/dubbed preview toggle for free.
|
||||
const playRange = useCallback((start, end) => {
|
||||
const playRange = useCallback(async (start, end) => {
|
||||
// Same autoplay-policy unlock as togglePlay (#595): resume the suspended
|
||||
// AudioContext on this user gesture before kicking off playback.
|
||||
try { await unlockAudio(); } catch { /* ignore */ }
|
||||
playRangeEndRef.current = end;
|
||||
const ws = wsRef.current;
|
||||
if (ws) {
|
||||
@@ -517,14 +521,22 @@ function WaveformTimeline({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
const togglePlay = useCallback(async () => {
|
||||
// Browser autoplay policy (Linux FF/Chrome, Windows WebView2, Android
|
||||
// Chrome): WaveSurfer's AudioContext is constructed at mount — before any
|
||||
// user gesture — and stays "suspended", so playPause() resolves without a
|
||||
// sound and the dub video preview just sits there (#595, same class as
|
||||
// #510 already fixed in WaveformPlayer). This click IS the gesture —
|
||||
// explicitly resume before play. No-op on macOS where it never blocked.
|
||||
try { await unlockAudio(); } catch { /* play() will surface real errors */ }
|
||||
if (wsRef.current) {
|
||||
wsRef.current.playPause();
|
||||
try { await wsRef.current.playPause(); }
|
||||
catch (e) { console.warn('WaveformTimeline: play failed:', e); }
|
||||
} else if (mediaElRef.current) {
|
||||
// Fallback: control the native media element directly
|
||||
const el = mediaElRef.current;
|
||||
if (el.paused) {
|
||||
el.play().catch(() => {});
|
||||
el.play().catch((e) => console.warn('WaveformTimeline: play failed:', e));
|
||||
} else {
|
||||
el.pause();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Regression guard for #595 — the dubbed-video PLAY button did nothing.
|
||||
//
|
||||
// WaveSurfer constructs its AudioContext at mount (before any user gesture), so
|
||||
// on Windows WebView2 / Linux FF / Android Chrome it stays "suspended" and
|
||||
// playPause() resolves silently with no sound. WaveformPlayer was already fixed
|
||||
// for this in #510 by awaiting unlockAudio() on the click; WaveformTimeline (the
|
||||
// dub editor's player) was missed — that's this bug.
|
||||
//
|
||||
// Driving WaveSurfer + a real AudioContext through jsdom is brittle, so this is
|
||||
// a source-level contract guard: every playback entry point in WaveformTimeline
|
||||
// must resume the AudioContext via unlockAudio() before it kicks off playback.
|
||||
// It fails-before (no import / no await) and passes-after the fix, and pins the
|
||||
// invariant so a future refactor can't quietly reintroduce a silent play path.
|
||||
|
||||
// Vitest runs with cwd = frontend/, so resolve the component from there.
|
||||
const src = readFileSync(
|
||||
path.resolve(process.cwd(), 'src/components/WaveformTimeline.jsx'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
describe('WaveformTimeline autoplay-unlock wiring (#595)', () => {
|
||||
it('imports the shared unlockAudio helper', () => {
|
||||
expect(src).toMatch(/import\s*\{\s*unlockAudio\s*\}\s*from\s*['"]\.\.\/utils\/audioUnlock['"]/);
|
||||
});
|
||||
|
||||
it('awaits unlockAudio() before playing in every playback entry point', () => {
|
||||
// Each playback handler must resume the context first. Grab the body of
|
||||
// each handler and assert the unlock await precedes the play/playPause call.
|
||||
const handlers = {
|
||||
togglePlay: /const togglePlay = useCallback\(async \(\) => \{([\s\S]*?)\}, \[\]\);/.exec(src)?.[1],
|
||||
playRange: /const playRange = useCallback\(async \(start, end\) => \{([\s\S]*?)\}, \[\]\);/.exec(src)?.[1],
|
||||
};
|
||||
for (const [name, body] of Object.entries(handlers)) {
|
||||
expect(body, `${name} handler not found`).toBeTruthy();
|
||||
const unlockAt = body.indexOf('await unlockAudio()');
|
||||
expect(unlockAt, `${name} must await unlockAudio()`).toBeGreaterThanOrEqual(0);
|
||||
const playAt = body.search(/\.play(Pause)?\(/);
|
||||
expect(playAt, `${name} must call play`).toBeGreaterThanOrEqual(0);
|
||||
expect(unlockAt, `${name} must unlock before play`).toBeLessThan(playAt);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -272,12 +272,16 @@
|
||||
|
||||
/* Text input + textarea tweaks */
|
||||
.clone-text-area {
|
||||
flex: 1;
|
||||
/* Re-enable the corner grip (matches base textarea.input-base). The ⊕ Insert
|
||||
button is lifted off the bottom-right so it no longer covers the handle
|
||||
(#481). */
|
||||
/* #595: the script field "couldn't be expanded". It was `flex: 1` inside a
|
||||
flex column, so flex-grow recomputed its height on every reflow and
|
||||
snapped the user's drag back — `resize: vertical` is silently ignored on a
|
||||
flex-grown item in Chromium/WebView2 (Windows). Drop flex-grow (`flex: 0 1
|
||||
auto`) so the textarea owns its height: it starts at min-height and the
|
||||
corner grip now grows it reliably on every platform. The ⊕ Insert button is
|
||||
lifted off the bottom-right so it doesn't cover the grip (#481). */
|
||||
flex: 0 1 auto;
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
min-height: 160px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.clone-auto-extract-btn { border-color: #b8bb26; color: #b8bb26; }
|
||||
|
||||
Reference in New Issue
Block a user