Skip to content

Studio: Video tab frontend#6875

Open
danielhanchen wants to merge 32 commits into
video-inferencefrom
video-tab
Open

Studio: Video tab frontend#6875
danielhanchen wants to merge 32 commits into
video-inferencefrom
video-tab

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Stacked on #6874. Adds the Video tab UI over the video inference API.

What this adds

  • New Video sidebar entry with the same keep-alive mount pattern as Images, so an in-flight generation survives tab switches.
  • video-page.tsx: curated video model picker (LTX-2.3 GGUF variants first), load progress polling, phase-based generation progress with ETA (load, encode prompt, denoise, decode, export) so the bar never looks stuck outside the denoise callback window.
  • Controls: resolution presets per family (including vertical), frames, fps, steps, guidance, seed; Advanced panel reuses the auto policy badges fed by the backend resolved record.
  • Gallery grid with the first video player in Studio: mp4 fetched from the auth-protected endpoint into a blob URL, inline playback, download, delete.
  • i18n strings for en and zh-CN; video models excluded from the Chat picker.

Verification

Add a Video generation page that mirrors the Images feature's create
workflow. It loads a text-to-video model, generates a clip, and plays it
back inline with the gallery of past clips.

- src/features/video/api.ts: typed client for the /api/inference/video
  routes (load, load-progress, generate, generate-progress, cancel,
  status, unload, gallery CRUD, and an auth-protected MP4 blob fetch).
- src/features/video/video-page.tsx: the page. Curated model picker
  (LTX 2.3 distilled GGUF, LTX 2 base pipeline), prompt and negative
  prompt, resolution preset select, duration select over the family's
  temporal lattice, fixed fps display, steps and guidance sliders seeded
  from per-model defaults, seed box. Generate polls per-step progress
  with a phase label and ETA and a Cancel button, then plays the result
  in a video player with a download button and an audio badge. Gallery
  strip below with per-card delete and clear all. Right-docked Advanced
  panel for memory, speed, attention, and step-cache with Auto badges
  fed from the resolved status.
- Register the page: router child, /video route, sidebar nav item with a
  video icon after Images, and the __root keep-alive mount so an
  in-flight generation survives leaving the tab.
- Add the text-to-video task to the model picker so video models never
  appear in the chat picker.
- Add the video nav label to the en and zh-CN locales.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new Video generation feature to the Studio frontend, adding a dedicated /video route, sidebar navigation, API integration, and a comprehensive VideoPage component with advanced load-time tuning, generation parameters, and a persistent video gallery. Feedback on the changes suggests refining the model tag detection in defaultsFor by splitting repository names into segments to avoid false positives, and checking isMounted.current before updating state in the asynchronous ensureSrc callback to prevent React state updates on unmounted components.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +107 to +110
function defaultsFor(repoId: string): { steps: number; guidance: number } {
const id = repoId.toLowerCase();
return MODEL_DEFAULTS.find((d) => id.includes(d.match)) ?? DEFAULT_GEN;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using simple raw substring checks (id.includes(d.match)) to detect model types or tags, as this can lead to false positives (e.g., matching 'ltxify' or 'distilledmodel'). Instead, split the repository name into segments using delimiters like dashes, underscores, dots, and slashes, and verify if the tag appears as a whole segment.

Suggested change
function defaultsFor(repoId: string): { steps: number; guidance: number } {
const id = repoId.toLowerCase();
return MODEL_DEFAULTS.find((d) => id.includes(d.match)) ?? DEFAULT_GEN;
}
function defaultsFor(repoId: string): { steps: number; guidance: number } {
const segments = repoId.toLowerCase().split(/[-_./\\]/);
return MODEL_DEFAULTS.find((d) => segments.includes(d.match)) ?? DEFAULT_GEN;
}
References
  1. When statically detecting model types or tags from repository IDs or names, avoid simple raw substring checks to prevent false positives. Instead, split the repository name into segments using delimiters like dashes, underscores, and dots, and verify if the tag appears as a whole segment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The substring match is deliberate: "wan" has to match Wan2.2 repo ids, and the suggested segment split would stop it (segment "wan2" is not "wan"). These defaults only prefill steps and guidance for the curated picker entries, so a false positive costs nothing.

Comment on lines +602 to +614
const ensureSrc = useCallback(async (video: GalleryVideo) => {
if (galleryCache.srcById.has(video.id) || galleryCache.inflight.has(video.id)) return;
galleryCache.inflight.add(video.id);
try {
const url = await fetchGalleryVideoObjectUrl(video.url);
galleryCache.srcById.set(video.id, url);
setSrcById((prev) => ({ ...prev, [video.id]: url }));
} catch {
// Leave it without a src; the card shows a placeholder.
} finally {
galleryCache.inflight.delete(video.id);
}
}, []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent state updates on unmounted components, check isMounted.current before calling setSrcById inside the asynchronous ensureSrc callback.

  const ensureSrc = useCallback(async (video: GalleryVideo) => {
    if (galleryCache.srcById.has(video.id) || galleryCache.inflight.has(video.id)) return;
    galleryCache.inflight.add(video.id);
    try {
      const url = await fetchGalleryVideoObjectUrl(video.url);
      galleryCache.srcById.set(video.id, url);
      if (isMounted.current) {
        setSrcById((prev) => ({ ...prev, [video.id]: url }));
      }
    } catch {
      // Leave it without a src; the card shows a placeholder.
    } finally {
      galleryCache.inflight.delete(video.id);
    }
  }, []);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the isMounted guard to match the other async callbacks in the file.

@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

The object URL still lands in the module cache either way; the setState call
now checks isMounted like the other async callbacks in the file.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f35bac673

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +900 to +902
const dq = defaultsFor(id);
setSteps(dq.steps);
setGuidance(dq.guidance);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the selected GGUF filename for defaults

When a user picks a GGUF variant from unsloth/LTX-2.3-GGUF, defaultsFor(id) only sees the repo id, so it never matches distilled even though the chosen filename does. The backend derives defaults from state.gguf_filename, but these 40-step/CFG-4 values are then sent explicitly during generation, overriding the intended distilled 8-step/CFG-1 schedule and making default runs much slower and different from the loaded model's defaults. Use meta.ggufFilename here or apply status.defaults after load.

Useful? React with 👍 / 👎.

// Video-generation pipeline tasks: handled by the Video page, never loadable as
// chat models. The backend reports "text-to-video" for video-diffusion GGUFs. The
// Video page reuses this as its picker's `task` filter, so it lives here.
export const VIDEO_GEN_TASKS = ["text-to-video"] as const;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align the video task filter with backend tags

The Video picker filters all cached/local On Device entries through passesTaskGate(..., VIDEO_GEN_TASKS), but the current backend cache classifier in routes/models.py::_arch_to_task does not emit text-to-video for supported LTX GGUFs; it tags ltxv as image-diffusion-unsupported. As a result, users who already have an LTX video GGUF in the HF cache, LM Studio, ./models, or a custom folder will not see it in the Video picker even though the new page is supposed to load on-device video GGUFs. Either emit text-to-video for supported video families or include the backend's current tag here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All five landed: ltxv and wan GGUF archs are now tagged text-to-video so cached checkpoints surface in the picker, the loaded family default clip length applies, the picked GGUF filename drives the steps and guidance prefill, Cancel no longer toasts as an error, and the Video nav item is disabled with a hint on chat-only hosts.

Comment on lines +593 to +597
setNumFrames((cur) => {
if (durationOptions.some((o) => o.frames === cur)) return cur;
// Prefer the ~3s preset (index 2) as a sensible default, else the first.
return durationOptions[2]?.frames ?? durationOptions[0]?.frames ?? cur;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply the loaded model's duration default

After a model loads, numFrames is still the fallback 25-frame value because it is already a valid LTX lattice option, so this effect keeps it and every generation sends num_frames: 25. That prevents the backend from using status.defaults.num_frames (121 frames for LTX-2), making the default Video run about 1s instead of the model's intended ~5s default unless the user manually changes the duration. Reset from status.defaults.num_frames when the loaded family changes.

Useful? React with 👍 / 👎.

setSelectedId(res.video.id);
void ensureSrc(res.video);
} catch (err) {
toast.error(err instanceof Error ? err.message : "Video generation failed");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Treat user-cancelled generations as non-errors

When the user presses Cancel, the cancel endpoint only signals the backend; the in-flight generateVideo request then rejects with the backend's cancellation sentinel, and this catch displays it as an error toast. That makes the normal Cancel path look like a generation failure, so suppress or special-case the known cancellation message/status here while still reporting real failures.

Useful? React with 👍 / 👎.

Comment on lines +1215 to +1218
onClick={() => {
navigate({ to: "/video" });
closeMobileIfOpen();
}}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Disable the video nav item on chat-only hosts

On chat-only hosts, __root.tsx does not include /video in the allowlist, so clicking this enabled sidebar item immediately hits the root guard and redirects back to /chat. In that environment the tab appears selectable but can never open; mirror the Train item by disabling/hiding it with a tooltip, or allow /video if it is meant to work there.

Useful? React with 👍 / 👎.

danielhanchen and others added 16 commits July 5, 2026 00:28
…ncel and chat-only polish

Tag the ltxv and wan GGUF archs text-to-video so cached video checkpoints
actually surface in the Video picker (they were classed unsupported and
hidden everywhere). Adopt the loaded family's default clip length instead
of silently keeping the 25-frame pre-load fallback, and derive steps and
guidance from the picked GGUF filename so a distilled variant gets its
few-step schedule. Suppress the error toast for the user's own Cancel and
disable the Video nav item on chat-only hosts with a hint, matching Train.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Jul 6, 2026
@danielhanchen

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35c1bc2135

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# Video GGUF archs the video backend CAN load (LTX-2.x ships as "ltxv"; the Wan
# community GGUFs as "wan"). Tagged text-to-video so they surface in the Video
# picker (VIDEO_GEN_TASKS) and stay out of chat (NON_CHAT_TASKS).
_VIDEO_GGUF_ARCHS = frozenset({"ltxv", "wan"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Wan GGUFs out until backend support exists

When a cached Wan GGUF is present, _arch_to_task now labels its wan architecture as text-to-video, so the Video picker will offer it as loadable. I checked the video backend family registry (core/inference/video_families.py), and it currently only registers ltx-2; validate_load_request will therefore reject Wan picks as an unsupported text-to-video family. Please leave wan under the unsupported diffusion task until a Wan VideoFamily and loader are actually added.

Useful? React with 👍 / 👎.

galleryCache.hasMore = page.has_more;
setVideos(page.videos);
setHasMore(page.has_more);
page.videos.forEach((video) => void ensureSrc(video));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid eagerly downloading every gallery MP4

When the Video tab opens with existing clips, this calls ensureSrc for every first-page record, and ensureSrc uses authFetch(url).blob(), so up to 50 full MP4s are downloaded into browser memory before the user plays or selects them. Generated video files can be large, so galleries with many clips can stall the tab and consume hundreds of MB/GB immediately; fetch object URLs lazily for the selected/visible items instead.

Useful? React with 👍 / 👎.

const dq2 = defaultsFor(id);
setSteps(dq2.steps);
setGuidance(dq2.guidance);
void handleLoad(dir, { kind: "gguf", filename }).then((started) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve family metadata for local GGUF loads

For direct local GGUF picks, the picker admits files based on their GGUF architecture, but this load request sends only the parent directory and filename. The video backend resolves the family from path/name tokens (_detect_load_family) rather than the GGUF metadata, so a valid ltxv file renamed to something like /models/video/model.gguf appears in the Video picker and then always fails validation as an unsupported text-to-video model. Pass a family override or have the backend infer the family from the selected GGUF metadata on this path.

Useful? React with 👍 / 👎.

{videos.length > 0 && (
<button
type="button"
onClick={() => void handleClearAll()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Confirm before clearing the whole video gallery

When a user reaches the tail of the thumbnail strip and clicks this trash tile, handleClearAll() permanently deletes every saved video through the gallery DELETE endpoint with no confirmation or undo. Because this control is adjacent to normal gallery navigation/actions and its scope is the entire gallery rather than one clip, an accidental click can wipe all generated MP4s; add a confirmation step before invoking the clear-all request.

Useful? React with 👍 / 👎.

onEject={status?.loaded ? handleUnload : undefined}
variant="ghost"
className="!h-[34px]"
task={VIDEO_GEN_TASKS}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block deleting loaded video GGUFs

With this task-scoped selector, the shared On Device GGUF rows expose their existing delete action for cached video variants. I checked /api/models/delete-cached: it refuses loaded chat and image/diffusion models, but it does not check the video backend, so a user can delete the GGUF backing the currently loaded Video model from this picker. Please disable model deletion while the video model is loaded or add the same loaded-model guard for get_video_backend().status().

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant