Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
887ef4e
video_output support
Aug 7, 2025
cc7dd79
rough rough POC of video tab
Aug 8, 2025
89aca84
split out RunwayVideoOutput from VideoOutput
Aug 11, 2025
7e146d4
update VideoField
Aug 11, 2025
5c2cdee
push up updates for VideoField
Aug 11, 2025
d519af9
build out adhoc video saving graph
Aug 11, 2025
d5107ba
combine nodes that generate and save videos
Aug 12, 2025
8943af9
add video models
Aug 14, 2025
d3d6494
add noop video router
Aug 14, 2025
dfa3cbc
integrating video into gallery - thinking maybe a new category of ima…
Aug 14, 2025
0ac61f7
add duration and aspect ratio to video settings
Aug 15, 2025
741139b
feat(ui): add dnd target for video start frame
psychedelicious Aug 18, 2025
39fe97e
feat(nodes): update VideoField & VideoOutput
psychedelicious Aug 19, 2025
ea9ac91
chore: ruff
psychedelicious Aug 19, 2025
f672cfa
feat(ui): fiddle w/ video stuff
psychedelicious Aug 19, 2025
d21a460
feat(ui): fiddle w/ video stuff
psychedelicious Aug 19, 2025
1b95021
feat(ui): more video stuff
psychedelicious Aug 19, 2025
a7fafee
add readiness logic to video tab
Aug 19, 2025
d405c94
hook up starring, unstarring, and deleting single videos (no multisel…
Aug 19, 2025
217ed44
stubbing out change board functionality
Aug 19, 2025
4598489
fix(ui): panel names on video tab
psychedelicious Aug 20, 2025
990fec4
feat(ui): gallery optimistic updates for video
psychedelicious Aug 20, 2025
12b70bc
feat(ui): consolidated gallery (wip)
psychedelicious Aug 20, 2025
6972618
add Veo3 model support to backend
Aug 20, 2025
38fa20c
replace runway with veo, build out veo3 model support
Aug 20, 2025
05ad2f0
add resolution as a generation setting
Aug 20, 2025
0c92022
add videos to change board modal
Aug 20, 2025
dced665
Revert "feat(ui): consolidated gallery (wip)"
psychedelicious Aug 21, 2025
360c817
gallery
psychedelicious Aug 21, 2025
888df49
lint the dang thing
Aug 21, 2025
d703d40
tsc
Aug 21, 2025
4b0d52a
lint
Aug 21, 2025
604105d
update redux selection to have a list of images and/or videos, update…
Aug 21, 2025
5d33419
add runway to backend
Aug 21, 2025
6dfe3f9
add runway back as a model and allow runway and veo3 to live together…
Aug 21, 2025
c6bfe99
fix video styling
Aug 21, 2025
7a83146
remove reference images on video tab
Aug 21, 2025
6f6d4b5
feat(ui): simpler layout for video player
psychedelicious Aug 22, 2025
734e6e8
tidy(ui): remove unused VideoAtPosition component
psychedelicious Aug 22, 2025
571aa69
fix(ui): fetching imageDTO for video
psychedelicious Aug 22, 2025
26f22a3
fix(ui): missing tranlsation
psychedelicious Aug 22, 2025
85f6ca3
fix(ui): iterations works for video models
psychedelicious Aug 22, 2025
ab3a877
fix(ui): generate tab graph builder
psychedelicious Aug 22, 2025
f9536fb
feat(ui): video dnd
psychedelicious Aug 22, 2025
34fc50f
chore(ui): dpdm
psychedelicious Aug 22, 2025
0acb9c8
chore(ui): lint
psychedelicious Aug 22, 2025
aa8af79
fix(ui): locate in gallery, galleryview when selecting image/video
psychedelicious Aug 22, 2025
60755e8
fix(ui): use correct placeholder for vidoes
psychedelicious Aug 22, 2025
93833c0
docs(ui): add note about visual jank in gallery
psychedelicious Aug 22, 2025
df8d2a7
add Video as new model type
Aug 22, 2025
b43c4cf
add UI support for new model type Video
Aug 22, 2025
70c5915
Checkpoint before follow-up message
cursoragent Aug 22, 2025
3bc0dd6
Refactor video tab layout and model picker with improved flexibility
cursoragent Aug 22, 2025
5e119c9
Update Launchpad Panel
hipsterusername Aug 22, 2025
b47f124
updating to latest vibe
hipsterusername Aug 22, 2025
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
55 changes: 55 additions & 0 deletions getItemsPerRow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Calculate how many images fit in a row based on the current grid layout.
*
* TODO(psyche): We only need to do this when the gallery width changes, or when the galleryImageMinimumWidth value
* changes. Cache this calculation.
*/
export const getItemsPerRow = (rootEl: HTMLDivElement): number => {
// Start from root and find virtuoso grid elements
const gridElement = rootEl.querySelector('.virtuoso-grid-list');

if (!gridElement) {
return 0;
}

const firstGridItem = gridElement.querySelector('.virtuoso-grid-item');

if (!firstGridItem) {
return 0;
}

const itemRect = firstGridItem.getBoundingClientRect();
const containerRect = gridElement.getBoundingClientRect();

// Get the computed gap from CSS
const gridStyle = window.getComputedStyle(gridElement);
const gapValue = gridStyle.gap;
const gap = parseFloat(gapValue);

if (isNaN(gap) || !itemRect.width || !itemRect.height || !containerRect.width || !containerRect.height) {
return 0;
}

/**
* You might be tempted to just do some simple math like:
* const itemsPerRow = Math.floor(containerRect.width / itemRect.width);
*
* But floating point precision can cause issues with this approach, causing it to be off by 1 in some cases.
*
* Instead, we use a more robust approach that iteratively calculates how many items fit in the row.
*/
let itemsPerRow = 0;
let spaceUsed = 0;

// Floating point precision can cause imagesPerRow to be 1 too small. Adding 1px to the container size fixes
// this, without the possibility of accidentally adding an extra column.
while (spaceUsed + itemRect.width <= containerRect.width + 1) {
itemsPerRow++; // Increment the number of items
spaceUsed += itemRect.width; // Add image size to the used space
if (spaceUsed + gap <= containerRect.width) {
spaceUsed += gap; // Add gap size to the used space after each item except after the last item
}
}

return Math.max(1, itemsPerRow);
};
38 changes: 38 additions & 0 deletions invokeai/app/api/routers/board_videos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from fastapi import Body, HTTPException
from fastapi.routing import APIRouter

from invokeai.app.services.videos_common import AddVideosToBoardResult, RemoveVideosFromBoardResult

board_videos_router = APIRouter(prefix="/v1/board_videos", tags=["boards"])

@board_videos_router.post(
"/batch",
operation_id="add_videos_to_board",
responses={
201: {"description": "Videos were added to board successfully"},
},
status_code=201,
response_model=AddVideosToBoardResult,
)
async def add_videos_to_board(
board_id: str = Body(description="The id of the board to add to"),
video_ids: list[str] = Body(description="The ids of the videos to add", embed=True),
) -> AddVideosToBoardResult:
"""Adds a list of videos to a board"""
raise HTTPException(status_code=501, detail="Not implemented")


@board_videos_router.post(
"/batch/delete",
operation_id="remove_videos_from_board",
responses={
201: {"description": "Videos were removed from board successfully"},
},
status_code=201,
response_model=RemoveVideosFromBoardResult,
)
async def remove_videos_from_board(
video_ids: list[str] = Body(description="The ids of the videos to remove", embed=True),
) -> RemoveVideosFromBoardResult:
"""Removes a list of videos from their board, if they had one"""
raise HTTPException(status_code=501, detail="Not implemented")
119 changes: 119 additions & 0 deletions invokeai/app/api/routers/videos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from typing import Optional

from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter

from invokeai.app.services.shared.pagination import OffsetPaginatedResults
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
from invokeai.app.services.videos_common import (
DeleteVideosResult,
StarredVideosResult,
UnstarredVideosResult,
VideoDTO,
VideoIdsResult,
VideoRecordChanges,
)

videos_router = APIRouter(prefix="/v1/videos", tags=["videos"])


@videos_router.patch(
"/i/{video_id}",
operation_id="update_video",
response_model=VideoDTO,
)
async def update_video(
video_id: str = Path(description="The id of the video to update"),
video_changes: VideoRecordChanges = Body(description="The changes to apply to the video"),
) -> VideoDTO:
"""Updates a video"""

raise HTTPException(status_code=501, detail="Not implemented")


@videos_router.get(
"/i/{video_id}",
operation_id="get_video_dto",
response_model=VideoDTO,
)
async def get_video_dto(
video_id: str = Path(description="The id of the video to get"),
) -> VideoDTO:
"""Gets a video's DTO"""

raise HTTPException(status_code=501, detail="Not implemented")


@videos_router.post("/delete", operation_id="delete_videos_from_list", response_model=DeleteVideosResult)
async def delete_videos_from_list(
video_ids: list[str] = Body(description="The list of ids of videos to delete", embed=True),
) -> DeleteVideosResult:
raise HTTPException(status_code=501, detail="Not implemented")


@videos_router.post("/star", operation_id="star_videos_in_list", response_model=StarredVideosResult)
async def star_videos_in_list(
video_ids: list[str] = Body(description="The list of ids of videos to star", embed=True),
) -> StarredVideosResult:
raise HTTPException(status_code=501, detail="Not implemented")


@videos_router.post("/unstar", operation_id="unstar_videos_in_list", response_model=UnstarredVideosResult)
async def unstar_videos_in_list(
video_ids: list[str] = Body(description="The list of ids of videos to unstar", embed=True),
) -> UnstarredVideosResult:
raise HTTPException(status_code=501, detail="Not implemented")


@videos_router.delete("/uncategorized", operation_id="delete_uncategorized_videos", response_model=DeleteVideosResult)
async def delete_uncategorized_videos() -> DeleteVideosResult:
"""Deletes all videos that are uncategorized"""

raise HTTPException(status_code=501, detail="Not implemented")


@videos_router.get("/", operation_id="list_video_dtos", response_model=OffsetPaginatedResults[VideoDTO])
async def list_video_dtos(
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate videos."),
board_id: Optional[str] = Query(
default=None,
description="The board id to filter by. Use 'none' to find videos without a board.",
),
offset: int = Query(default=0, description="The page offset"),
limit: int = Query(default=10, description="The number of videos per page"),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
starred_first: bool = Query(default=True, description="Whether to sort by starred videos first"),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
) -> OffsetPaginatedResults[VideoDTO]:
"""Lists video DTOs"""

raise HTTPException(status_code=501, detail="Not implemented")


@videos_router.get("/ids", operation_id="get_video_ids")
async def get_video_ids(
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate videos."),
board_id: Optional[str] = Query(
default=None,
description="The board id to filter by. Use 'none' to find videos without a board.",
),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
starred_first: bool = Query(default=True, description="Whether to sort by starred videos first"),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
) -> VideoIdsResult:
"""Gets ordered list of video ids with metadata for optimistic updates"""

raise HTTPException(status_code=501, detail="Not implemented")


@videos_router.post(
"/videos_by_ids",
operation_id="get_videos_by_ids",
responses={200: {"model": list[VideoDTO]}},
)
async def get_videos_by_ids(
video_ids: list[str] = Body(embed=True, description="Object containing list of video ids to fetch DTOs for"),
) -> list[VideoDTO]:
"""Gets video DTOs for the specified video ids. Maintains order of input ids."""

raise HTTPException(status_code=501, detail="Not implemented")
4 changes: 4 additions & 0 deletions invokeai/app/api_app.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
import asyncio
import logging
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi_events.handlers.local import local_handler
from fastapi_events.middleware import EventHandlerASGIMiddleware
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint

import invokeai.frontend.web as web_dir
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.no_cache_staticfiles import NoCacheStaticFiles
from invokeai.app.api.routers import (
app_info,
board_images,
boards,
client_state,
board_videos,
download_queue,
images,
model_manager,
model_relationships,
session_queue,
style_presets,
utilities,
videos,
workflows,
)
from invokeai.app.api.sockets import SocketIO
from invokeai.app.services.config.config_default import get_config
from invokeai.app.util.custom_openapi import get_openapi_func
from invokeai.backend.util.logging import InvokeAILogger

Check failure on line 37 in invokeai/app/api_app.py

View workflow job for this annotation

GitHub Actions / python-checks

Ruff (I001)

invokeai/app/api_app.py:1:1: I001 Import block is un-sorted or un-formatted

app_config = get_config()
logger = InvokeAILogger.get_logger(config=app_config)
Expand Down Expand Up @@ -125,8 +127,10 @@
app.include_router(model_manager.model_manager_router, prefix="/api")
app.include_router(download_queue.download_queue_router, prefix="/api")
app.include_router(images.images_router, prefix="/api")
app.include_router(videos.videos_router, prefix="/api")
app.include_router(boards.boards_router, prefix="/api")
app.include_router(board_images.board_images_router, prefix="/api")
app.include_router(board_videos.board_videos_router, prefix="/api")
app.include_router(model_relationships.model_relationships_router, prefix="/api")
app.include_router(app_info.app_router, prefix="/api")
app.include_router(session_queue.session_queue_router, prefix="/api")
Expand Down
9 changes: 9 additions & 0 deletions invokeai/app/invocations/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,14 @@ class UIType(str, Enum, metaclass=MetaEnum):
Imagen4Model = "Imagen4ModelField"
ChatGPT4oModel = "ChatGPT4oModelField"
FluxKontextModel = "FluxKontextModelField"
Veo3Model = "Veo3ModelField"
RunwayModel = "RunwayModelField"
# endregion

# region Misc Field Types
Scheduler = "SchedulerField"
Any = "AnyField"
Video = "VideoField"
# endregion

# region Internal Field Types
Expand Down Expand Up @@ -224,6 +227,12 @@ class ImageField(BaseModel):
image_name: str = Field(description="The name of the image")


class VideoField(BaseModel):
"""A video primitive field"""

video_id: str = Field(description="The id of the video")


class BoardField(BaseModel):
"""A board primitive field"""

Expand Down
24 changes: 24 additions & 0 deletions invokeai/app/invocations/primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
SD3ConditioningField,
TensorField,
UIComponent,
VideoField,
)
from invokeai.app.services.images.images_common import ImageDTO
from invokeai.app.services.shared.invocation_context import InvocationContext
Expand Down Expand Up @@ -287,6 +288,29 @@ def invoke(self, context: InvocationContext) -> ImageCollectionOutput:
return ImageCollectionOutput(collection=self.collection)


# endregion

# region Video

@invocation_output("video_output")
class VideoOutput(BaseInvocationOutput):
"""Base class for nodes that output a video"""

video: VideoField = OutputField(description="The output video")
width: int = OutputField(description="The width of the video in pixels")
height: int = OutputField(description="The height of the video in pixels")
duration_seconds: float = OutputField(description="The duration of the video in seconds")

@classmethod
def build(cls, video_id: str, width: int, height: int, duration_seconds: float) -> "VideoOutput":
return cls(
video=VideoField(video_id=video_id),
width=width,
height=height,
duration_seconds=duration_seconds,
)


# endregion

# region DenoiseMask
Expand Down
Loading
Loading