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
15 changes: 13 additions & 2 deletions apps/dreamverse/dreamverse/mock_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
MOCK_DURATION_SECONDS = 5.0
MOCK_STREAM_CHUNK_SIZE_BYTES = 256 * 1024
MOCK_STREAM_CHUNK_DELAY_MS = 15
MOCK_AV_MIME = 'video/mp4; codecs="avc1.42E01E"'
MOCK_AV_MIME = 'video/mp4; codecs="avc1.42E01E,mp4a.40.2"'
FFMPEG_BIN = shutil.which(os.getenv("FASTVIDEO_FFMPEG_BIN", "ffmpeg"))
MOCK_SEGMENT_BYTES: bytes | None = None

Expand Down Expand Up @@ -75,9 +75,14 @@ def _build_mock_segment_bytes() -> bytes:
"lavfi",
"-i",
f"testsrc2=size={MOCK_FRAME_WIDTH}x{MOCK_FRAME_HEIGHT}:rate={MOCK_FPS}",
# Silent stereo AAC track. We don't need audible content — the FE's
# audio-decode coverage only needs a real AAC track present
"-f",
"lavfi",
"-i",
"anullsrc=channel_layout=stereo:sample_rate=48000",
"-t",
f"{MOCK_DURATION_SECONDS}",
"-an",
"-c:v",
"libx264",
"-preset",
Expand All @@ -90,6 +95,12 @@ def _build_mock_segment_bytes() -> bytes:
"baseline",
"-level",
"3.0",
"-c:a",
"aac",
"-b:a",
"128k",
"-ar",
"48000",
"-movflags",
"+empty_moov+default_base_moof+frag_keyframe",
"-frag_duration",
Expand Down
268 changes: 267 additions & 1 deletion apps/dreamverse/web/e2e/mock-backed-generation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { stat } from 'node:fs/promises';

import { test, expect, type WebSocket as PWWebSocket } from '@playwright/test';

test.describe('mock-backed generation smoke', () => {
Expand All @@ -18,7 +20,6 @@ test.describe('mock-backed generation smoke', () => {
const parsed = JSON.parse(payload);
if (typeof parsed?.type === 'string') wsEventTypes.push(parsed.type);
} catch {
// Ignore non-JSON frames; binary media chunks are asserted through UI state.
}
});
});
Expand All @@ -42,4 +43,269 @@ test.describe('mock-backed generation smoke', () => {
expect.arrayContaining(['ltx2_stream_start', 'ltx2_segment_start', 'media_init']),
);
});

test('streams, plays, and surfaces a downloadable clip', async ({ page, request }) => {
const health = await request.get('/healthz');
const body = health.ok() ? await health.json() : {};
test.skip(
body.service !== 'ltx2-streaming-mock-server',
'Mock-backed playback assertions require dreamverse.mock_server on BACKEND_PORT (default 8009).',
);

await page.addInitScript(() => {
(window as unknown as { __sharedFiles: unknown }).__sharedFiles = null;
const stub = async (data: { files?: File[] }) => {
const files = Array.isArray(data?.files) ? data.files : [];
(window as unknown as { __sharedFiles: unknown }).__sharedFiles = files.map((f) => ({
name: f.name,
type: f.type,
size: f.size,
}));
};
Object.defineProperty(Navigator.prototype, 'share', {
value: stub,
configurable: true,
writable: true,
});
Object.defineProperty(Navigator.prototype, 'canShare', {
value: (data: { files?: File[] }) => Array.isArray(data?.files),
configurable: true,
writable: true,
});
});

await page.goto('/');

const continuation = page.getByLabel('Continuation prompt');
await expect(continuation).toBeVisible();
await continuation.fill('A glass whale swims above a neon forest');
await page.getByRole('button', { name: /^generate$/i }).click();

const liveVideo = page.locator('video:not(.hidden)');
await expect(liveVideo).toHaveCount(1);

await test.step('MSE pipeline attaches the live <video>', async () => {
// Chromium/Firefox attach via blob: URL; Safari uses srcObject with ManagedMediaSource.
await expect
.poll(
async () =>
liveVideo.evaluate(
(v: HTMLVideoElement) =>
v.src.startsWith('blob:') || v.srcObject !== null,
),
{ timeout: 30_000 },
)
.toBe(true);
});

await test.step('SourceBuffer accepts fMP4 and decoder produces frames', async () => {
await expect
.poll(
async () =>
liveVideo.evaluate((v: HTMLVideoElement) =>
v.buffered.length > 0 ? v.buffered.end(0) : 0,
),
{ timeout: 60_000 },
)
.toBeGreaterThan(0);

// readyState >= 3 = HAVE_FUTURE_DATA.
await expect
.poll(
async () => liveVideo.evaluate((v: HTMLVideoElement) => v.readyState),
{ timeout: 60_000 },
)
.toBeGreaterThanOrEqual(3);
});

await test.step('<video> playback advances past the first second', async () => {
await liveVideo.evaluate(async (v: HTMLVideoElement) => {
if (v.paused) {
try {
await v.play();
} catch {
}
}
});

await expect
.poll(
async () => liveVideo.evaluate((v: HTMLVideoElement) => v.currentTime),
{ timeout: 30_000 },
)
.toBeGreaterThan(0);

await expect
.poll(
async () =>
liveVideo.evaluate((v: HTMLVideoElement) =>
v.ended ? v.duration : v.currentTime,
),
{ timeout: 10_000 },
)
.toBeGreaterThanOrEqual(1.0);
});

await test.step('the AAC audio track is demuxed and decoded', async () => {
// The mock emits AAC (mp4a.40.2), matching the real backend
// (av_streaming.py). Proves the FE decoded the audio track.
await expect
.poll(
async () =>
liveVideo.evaluate((el: HTMLVideoElement) => {
const v = el as HTMLVideoElement & {
audioTracks?: { length: number };
mozHasAudio?: boolean;
webkitAudioDecodedByteCount?: number;
};
return (
(v.audioTracks?.length ?? 0) > 0 ||
v.mozHasAudio === true ||
(v.webkitAudioDecodedByteCount ?? 0) > 0
);
}),
{ timeout: 10_000 },
)
.toBe(true);
});

await test.step('completed clip surfaces a working download', async () => {
const downloadButton = page.getByRole('button', { name: /download video|share video/i });
await expect(downloadButton).toBeVisible({ timeout: 30_000 });

const buttonLabel = (await downloadButton.getAttribute('aria-label')) ?? '';
const isShareFlow = /share video/i.test(buttonLabel);

if (isShareFlow) {
await downloadButton.click();
await expect
.poll(
async () => page.evaluate(() => (window as { __sharedFiles?: unknown }).__sharedFiles),
{ timeout: 10_000 },
)
.not.toBeNull();
const shared = (await page.evaluate(
() => (window as { __sharedFiles?: Array<{ name: string; type: string; size: number }> }).__sharedFiles,
)) ?? [];
expect(shared).toHaveLength(1);
expect(shared[0].name).toMatch(/\.(mp4|webm)$/);
expect(shared[0].size).toBeGreaterThan(0);
} else {
const downloadPromise = page.waitForEvent('download');
await downloadButton.click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toMatch(/\.(mp4|webm)$/);

const savedPath = await download.path();
expect(savedPath).not.toBeNull();
const { size } = await stat(savedPath!);
expect(size).toBeGreaterThan(0);
}
});

await test.step('project history sidebar lists the current session', async () => {
const sidebar = page.getByRole('complementary', { name: 'Project history' });
await page.getByRole('button', { name: 'Toggle sidebar' }).click();

await expect(sidebar).toBeInViewport();

await expect(sidebar.getByText('Current', { exact: true })).toBeVisible();
await expect(sidebar.getByText('Active', { exact: true })).toBeVisible();
});

const lastError = await liveVideo.evaluate((v: HTMLVideoElement) =>
v.error ? `${v.error.code}: ${v.error.message ?? ''}` : null,
);
expect(lastError).toBeNull();
});

test('starts a new project and switches back to the prior session', async ({ page, request }) => {
const health = await request.get('/healthz');
const body = health.ok() ? await health.json() : {};
test.skip(
body.service !== 'ltx2-streaming-mock-server',
'Mock-backed project lifecycle assertions require dreamverse.mock_server on BACKEND_PORT (default 8009).',
);

await page.goto('/');

await test.step('complete a generation so there is a project to save', async () => {
const continuation = page.getByLabel('Continuation prompt');
await expect(continuation).toBeVisible();
await continuation.fill('Aurora over a frozen lake');
await page.getByRole('button', { name: /^generate$/i }).click();
await expect(
page.getByRole('button', { name: /download video|share video/i }),
).toBeVisible({ timeout: 60_000 });
});

const sidebar = page.getByRole('complementary', { name: 'Project history' });

await test.step('"New project" closes the sidebar and resets the composer', async () => {
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await sidebar.getByRole('button', { name: /^new project$/i }).click();
await expect(sidebar).not.toBeInViewport();
await expect(page.getByRole('button', { name: /^generate$/i })).toBeVisible({ timeout: 30_000 });
const continuation = page.getByLabel('Continuation prompt');
await expect(continuation).toBeEnabled();
await expect(continuation).toHaveValue('');
});

await test.step('the prior session appears under timeline', async () => {
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await expect(sidebar.getByText('Previous', { exact: true })).toBeVisible({ timeout: 30_000 });
await expect(sidebar.getByText(/^(just now|\d+m ago)$/).first()).toBeVisible();
});

await test.step('clicking the prior session enters viewing mode', async () => {
const priorRow = sidebar.locator('div[role="button"]').filter({ hasText: /just now|\d+m ago/ }).first();
await priorRow.click();
await expect(sidebar).not.toBeInViewport();
await expect(page.locator('video[autoplay][loop]')).toBeVisible({ timeout: 30_000 });
});
});

test('saved projects persist across a page reload', async ({ page, request }) => {
const health = await request.get('/healthz');
const body = health.ok() ? await health.json() : {};
test.skip(
body.service !== 'ltx2-streaming-mock-server',
'Mock-backed persistence assertions require dreamverse.mock_server on BACKEND_PORT (default 8009).',
);

await page.goto('/');

await test.step('complete a generation', async () => {
const continuation = page.getByLabel('Continuation prompt');
await expect(continuation).toBeVisible();
await continuation.fill('Aurora over a frozen lake');
await page.getByRole('button', { name: /^generate$/i }).click();
await expect(
page.getByRole('button', { name: /download video|share video/i }),
).toBeVisible({ timeout: 60_000 });
});

const sidebar = page.getByRole('complementary', { name: 'Project history' });

await test.step('persist via "New project" and confirm it lands under Previous', async () => {
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await sidebar.getByRole('button', { name: /^new project$/i }).click();
await expect(page.getByRole('button', { name: /^generate$/i })).toBeVisible({ timeout: 30_000 });
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await expect(sidebar.getByText('Previous', { exact: true })).toBeVisible({ timeout: 30_000 });
await expect(sidebar.getByText(/^(just now|\d+m ago)$/).first()).toBeVisible();
});

await test.step('after page reload, the prior project is still in Previous', async () => {
await page.reload();
await page.getByRole('button', { name: 'Toggle sidebar' }).click();
await expect(sidebar).toBeInViewport();
await expect(sidebar.getByText('Previous', { exact: true })).toBeVisible({ timeout: 30_000 });
await expect(sidebar.getByText(/^(just now|\d+m ago)$/).first()).toBeVisible();
});
});
});
11 changes: 7 additions & 4 deletions apps/dreamverse/web/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,15 @@ export default defineConfig({
viewport: { width: 1280, height: 720 },
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
video: process.env.CI ? 'retain-on-failure' : 'on',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'msedge', use: { ...devices['Desktop Edge'], channel: 'msedge' } },
{ name: 'mobile-safari', use: { ...devices['iPhone 14'] } },
{ name: 'mobile-chromium', use: { ...devices['Pixel 7'] } },
],
webServer: process.env.PLAYWRIGHT_SKIP_WEBSERVER
? undefined
Expand Down
1 change: 1 addition & 0 deletions apps/dreamverse/web/src/components/VideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export default function VideoPlayer({
}}
size="icon"
variant="outline"
aria-label={canShare ? "Share video" : "Download video"}
className="absolute top-3 left-3 z-10 cursor-pointer bg-slate-800/50 text-white/90 shadow-md backdrop-blur-sm transition-all border-white/30 hover:bg-slate-800/85 hover:border-white/50 hover:text-white hover:scale-105"
>
{canShare ? <Share className="size-5" /> : <Download className="size-5" />}
Expand Down
7 changes: 2 additions & 5 deletions fastvideo/configs/models/encoders/gemma.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@
def _is_feature_extractor_linear(n: str, m) -> bool:
# LTX-2.3 (caption_proj_before_connector) introduces separate
# video/audio feature extractor linears; keep the LTX-2.0 name too.
return (
n.endswith("feature_extractor_linear")
or n.endswith("video_feature_extractor_linear")
or n.endswith("audio_feature_extractor_linear")
)
return (n.endswith("feature_extractor_linear") or n.endswith("video_feature_extractor_linear")
or n.endswith("audio_feature_extractor_linear"))


def _is_embeddings(n: str, m) -> bool:
Expand Down
4 changes: 2 additions & 2 deletions fastvideo/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ def _register_configs() -> None:
"FastVideo/LTX2-Diffusers",
],
model_detectors=[
lambda path: ("ltx2" in path.lower() or "ltx-2" in path.lower())
and "distilled" not in path.lower() and "2.3" not in path.lower(),
lambda path: ("ltx2" in path.lower() or "ltx-2" in path.lower()) and "distilled" not in path.lower() and
"2.3" not in path.lower(),
],
model_family="ltx2",
default_preset="ltx2_base",
Expand Down
10 changes: 8 additions & 2 deletions fastvideo/tests/modal/pr_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def run_dreamverse_app_tests():
npm ci &&
npm run typecheck &&
npm test &&
npx playwright install --with-deps chromium &&
npx playwright install --with-deps chromium webkit firefox &&
bash -c '
set -e
BACKEND_PORT="${BACKEND_PORT:-8009}"
Expand All @@ -260,7 +260,13 @@ def run_dreamverse_app_tests():
sleep 1
done
curl -fsS "http://127.0.0.1:$BACKEND_PORT/healthz"
BACKEND_HOST=127.0.0.1 BACKEND_PORT="$BACKEND_PORT" CI=1 npm run e2e
BACKEND_HOST=127.0.0.1 BACKEND_PORT="$BACKEND_PORT" CI=1 \
npm run e2e -- \
--project=chromium \
--project=webkit \
--project=firefox \
--project=mobile-safari \
--project=mobile-chromium
'
""",
build_kernel=False)
Expand Down
Loading