Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ build/
# Environment
# .env
.venv/
node_modules/
env/
venv/
ENV/
Expand Down Expand Up @@ -87,3 +88,5 @@ pyproject.toml.bkp
# Local scratch notes, scan outputs, anything not meant to ship
.local/
examples/assorted_checks/test_silence/out/*
playwright-report/
test-results/
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Notable changes to this project will be documented in this file.

Per-PR attribution and contributor credits are published automatically on the corresponding GitHub release page; this file is the curated, human-readable summary.

## [Unreleased]
### Fixed
- Web UI long-playback bugfix around the 10-minute mark; in-browser audio buffer is now bounded ahead of `currentTime` with trailing eviction behind it, so long generations stop overflowing the SourceBuffer.
- Web UI stays responsive on extended sessions; waveform animation is transition-gated and `PlayerState` short-circuits no-op updates, so controls don't drift into lag after 10+ minutes of playback.

### Notes
- Scrubbing may not be fully not supported in current state on MP3 streamed playback. WAV etc, plays back fine on completion.

## [v0.4.0] - 2026-05-24
### Added
- GPU image variants for Blackwell / RTX 50-series (`:latest-cu128`, `:vX.Y.Z-cu128`, amd64 only) with PyTorch cu128 wheels (#443). Default `:latest` and new `:latest-cu126` alias stay on cu126 for Maxwell/Pascal compatibility.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.4.0
0.4.1-rc1
75 changes: 75 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"type": "module",
"scripts": {
"test:web": "node web/tests/unit/index.test.mjs",
"test:e2e": "playwright test",
"gpu:build": "docker compose -f docker/gpu/docker-compose.yml build",
"gpu:up": "docker compose -f docker/gpu/docker-compose.yml up",
"gpu:down": "docker compose -f docker/gpu/docker-compose.yml down",
"cpu:build": "docker compose -f docker/cpu/docker-compose.yml build",
"cpu:up": "docker compose -f docker/cpu/docker-compose.yml up",
"cpu:down": "docker compose -f docker/cpu/docker-compose.yml down",
"rocm:build": "docker compose -f docker/rocm/docker-compose.yml build",
"rocm:up": "docker compose -f docker/rocm/docker-compose.yml up",
"rocm:down": "docker compose -f docker/rocm/docker-compose.yml down"
},
"devDependencies": {
"@playwright/test": "^1.57.0"
}
}
15 changes: 15 additions & 0 deletions playwright.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineConfig } from '@playwright/test';

export default defineConfig({
testDir: './web/tests/e2e',
timeout: 30_000,
use: {
baseURL: 'http://127.0.0.1:4173',
},
webServer: {
command: 'node web/tests/e2e/fixtures/static-server.mjs',
url: 'http://127.0.0.1:4173',
reuseExistingServer: true,
timeout: 10_000,
},
});
3 changes: 3 additions & 0 deletions web/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ export class App {
const voice = this.voiceService.getSelectedVoiceString();
const speed = this.playerState.getState().speed;

this.playerState.setReady(false);
this.playerState.setPlaying(false);
this.playerState.setTime(0, 0);
this.setGenerating(true);
this._playbackFailed = false;
this.elements.downloadBtn.classList.remove('ready');
Expand Down
17 changes: 13 additions & 4 deletions web/src/components/PlayerControls.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export class PlayerControls {
}

formatTime(secs) {
if (!Number.isFinite(secs) || secs < 0) {
return '0:00';
}

const minutes = Math.floor(secs / 60);
const seconds = Math.floor(secs % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
Expand Down Expand Up @@ -47,7 +51,7 @@ export class PlayerControls {
`${this.formatTime(currentTime)} / ${this.formatTime(duration || 0)}`;

// Update seek slider
if (duration > 0 && !this.elements.seekSlider.dragging) {
if (Number.isFinite(duration) && duration > 0 && !this.elements.seekSlider.dragging) {
this.elements.seekSlider.value = (currentTime / duration) * 100;
}

Expand Down Expand Up @@ -123,6 +127,11 @@ export class PlayerControls {
this.stopTimeUpdate();
});

this.audioService.addEventListener('ready', () => {
this.playerState.setReady(true);
this.updateTimeDisplay();
});

// Initial time display
this.updateTimeDisplay();
}
Expand All @@ -133,8 +142,8 @@ export class PlayerControls {

updateControls(state) {
// Update button states
this.elements.playPauseBtn.disabled = !state.duration && !state.isGenerating;
this.elements.seekSlider.disabled = !state.duration;
this.elements.playPauseBtn.disabled = !state.isReady && !state.isGenerating;
this.elements.seekSlider.disabled = !state.isReady || !Number.isFinite(state.duration) || state.duration <= 0;
this.elements.cancelBtn.style.display = state.isGenerating ? 'block' : 'none';

// Update volume and speed if changed externally
Expand Down Expand Up @@ -165,4 +174,4 @@ export class PlayerControls {
}
}

export default PlayerControls;
export default PlayerControls;
9 changes: 6 additions & 3 deletions web/src/components/WaveVisualizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class WaveVisualizer {
}

setupStateSubscription() {
this.wasPlaying = false;
this.playerState.subscribe(state => {
// Handle generation progress
if (state.isGenerating) {
Expand All @@ -53,12 +54,14 @@ export class WaveVisualizer {
}, 500);
}

// Only animate when playing, stop otherwise
if (state.isPlaying) {
// SiriWave.start() is not idempotent — each call spawns a new RAF
// loop. Only call start/stop on isPlaying transitions.
if (state.isPlaying && !this.wasPlaying) {
this.wave.start();
} else {
} else if (!state.isPlaying && this.wasPlaying) {
this.wave.stop();
}
this.wasPlaying = state.isPlaying;
});
}

Expand Down
Loading
Loading