From 9863bd9db1a6f5179f4920d802ceb73f53f2ab74 Mon Sep 17 00:00:00 2001 From: Kelly Jerrell Date: Wed, 14 Jan 2026 14:02:59 -0700 Subject: [PATCH 01/70] feat: Implement video clip generation from frames and display clip details in the image overlay. --- src-tauri/src/lib.rs | 5 +- src-tauri/src/projects_db/commands.rs | 9 ++ src-tauri/src/projects_db/dt_project.rs | 43 +++++-- src-tauri/src/projects_db/mod.rs | 2 +- src-tauri/src/projects_db/projects_db.rs | 24 +++- src-tauri/src/projects_db/tensor_history.rs | 4 +- src-tauri/src/vid.rs | 97 +++++++++++++++ src/commands/index.ts | 3 +- src/commands/projects.ts | 3 + src/commands/urls.ts | 41 ++++--- src/commands/vid.ts | 5 + src/components/VideoFrames.tsx | 82 +++++++++++++ src/dtProjects/detailsOverlay/Clip.tsx | 110 ++++++++++++++++++ .../detailsOverlay/DTImageContext.tsx | 2 + .../detailsOverlay/DTImageProvider.tsx | 3 +- .../detailsOverlay/DetailsContent.tsx | 12 ++ .../detailsOverlay/DetailsImages.tsx | 31 ++--- 17 files changed, 431 insertions(+), 45 deletions(-) create mode 100644 src-tauri/src/vid.rs create mode 100644 src/commands/vid.ts create mode 100644 src/components/VideoFrames.tsx create mode 100644 src/dtProjects/detailsOverlay/Clip.tsx diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2bcfef4..933cd1d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,7 @@ use tauri_plugin_window_state::StateFlags; mod clipboard; mod projects_db; +mod vid; use once_cell::sync::Lazy; use tokio::runtime::Runtime; @@ -142,6 +143,7 @@ pub fn run() { projects_db_project_update_exclude, projects_db_image_count, // #unused projects_db_image_list, + projects_db_get_clip, projects_db_image_rebuild_fts, projects_db_watch_folder_list, projects_db_watch_folder_add, @@ -156,7 +158,8 @@ pub fn run() { dt_project_find_predecessor_candidates, dt_project_get_tensor_raw, // #unused dt_project_get_tensor_size, - dt_project_decode_tensor + dt_project_decode_tensor, + vid::create_video_from_frames ]) .register_asynchronous_uri_scheme_protocol("dtm", |_ctx, request, responder| { std::thread::spawn(move || { diff --git a/src-tauri/src/projects_db/commands.rs b/src-tauri/src/projects_db/commands.rs index 31bc84b..9762d9f 100644 --- a/src-tauri/src/projects_db/commands.rs +++ b/src-tauri/src/projects_db/commands.rs @@ -206,6 +206,15 @@ pub async fn projects_db_image_list( Ok(projects_db.list_images(opts).await.unwrap()) } +#[tauri::command] +pub async fn projects_db_get_clip( + app_handle: tauri::AppHandle, + image_id: i64, +) -> Result, String> { + let projects_db = ProjectsDb::get_or_init(&app_handle).await?; + projects_db.get_clip(image_id).await +} + #[tauri::command] pub async fn projects_db_image_rebuild_fts(app: tauri::AppHandle) -> Result<(), String> { let projects_db = ProjectsDb::get_or_init(&app).await?; diff --git a/src-tauri/src/projects_db/dt_project.rs b/src-tauri/src/projects_db/dt_project.rs index d1e842e..911a5bc 100644 --- a/src-tauri/src/projects_db/dt_project.rs +++ b/src-tauri/src/projects_db/dt_project.rs @@ -17,7 +17,10 @@ use tokio::sync::OnceCell; static PROJECT_CACHE: Lazy>> = Lazy::new(|| { Cache::builder() .max_capacity(16) - .time_to_idle(std::time::Duration::from_secs(300)) // 10 min idle timeout + // caching database connections for 3 seconds, so images can be loaded in bulk + // from separate requests. Closing them early to avoid locks, in case project + // is renamed in DT + .time_to_idle(std::time::Duration::from_secs(3)) .build() }); @@ -171,24 +174,21 @@ impl DTProject { fn map_import(&self, row: SqliteRow) -> TensorHistoryImport { let row_id: i64 = row.get(0); let p: &[u8] = row.get(1); - // let image_id: i64 = row.get(2); + let tensor_id: String = row.get(2); // These are booleans from the query (MAX(val) > 0) - let has_mask: bool = row.get(2); - let has_depth: bool = row.get(3); - let has_scribble: bool = row.get(4); - let has_pose: bool = row.get(5); - let has_color: bool = row.get(6); - let has_custom: bool = row.get(7); + let has_mask: bool = row.get(3); + let has_depth: bool = row.get(4); + let has_scribble: bool = row.get(5); + let has_pose: bool = row.get(6); + let has_color: bool = row.get(7); + let has_custom: bool = row.get(8); let has_shuffle: bool = row.get(8); - // We aren't using has_mask in TensorHistoryImport yet, but it's part of the query. - // TensorHistoryImport::new expects the boolean flags. - TensorHistoryImport::new( p, row_id, - // image_id, + tensor_id, has_depth, has_pose, has_color, @@ -351,6 +351,22 @@ impl DTProject { Ok(thumbnail) } + pub async fn get_histories_from_clip( + &self, + node_id: i64, + ) -> Result, Error> { + self.check_table(&DTProjectTable::TensorHistory).await?; + + // get_history_full + let history = self.get_history_full(node_id).await?; + // find out num_frames and clip_id from the history, + // let clip_id = history.history.clip_id; + let num_frames = history.history.num_frames; + println!("num_frames: {}, {}", num_frames, self.path); + // and return get_histories(node_id, num_frames) + self.get_histories(node_id, num_frames as i64).await + } + pub async fn get_history_full(&self, row_id: i64) -> Result { self.check_table(&DTProjectTable::TensorHistory).await?; let mut item: TensorHistoryExtra = query(&full_query_where("thn.rowid == ?1")) @@ -562,6 +578,7 @@ fn import_query(has_moodboard: bool) -> String { thn.rowid, thn.p AS data_blob, + MAX('tensor_history_' || NULLIF(td.f20, 0)) AS tensor_id, MAX(td.f22) > 0 AS has_mask, MAX(td.f24) > 0 AS has_depth, MAX(td.f26) > 0 AS has_scribble, @@ -578,6 +595,7 @@ fn import_query(has_moodboard: bool) -> String { td.rowid, td.__pk0, td.__pk1, + f20.f20 AS f20, f22.f22 AS f22, f24.f24 AS f24, f26.f26 AS f26, @@ -585,6 +603,7 @@ fn import_query(has_moodboard: bool) -> String { f30.f30 AS f30, f32.f32 AS f32 FROM tensordata AS td + LEFT JOIN tensordata__f20 AS f20 ON f20.rowid = td.rowid LEFT JOIN tensordata__f22 AS f22 ON f22.rowid = td.rowid LEFT JOIN tensordata__f24 AS f24 ON f24.rowid = td.rowid LEFT JOIN tensordata__f26 AS f26 ON f26.rowid = td.rowid diff --git a/src-tauri/src/projects_db/mod.rs b/src-tauri/src/projects_db/mod.rs index 7085162..78da073 100644 --- a/src-tauri/src/projects_db/mod.rs +++ b/src-tauri/src/projects_db/mod.rs @@ -11,7 +11,7 @@ pub mod tensor_history_generated; pub mod commands; mod dtm_dtproject; -pub use dtm_dtproject::dtm_dtproject_protocol; +pub use dtm_dtproject::{dtm_dtproject_protocol, extract_jpeg_slice}; mod tensor_history_mod; diff --git a/src-tauri/src/projects_db/projects_db.rs b/src-tauri/src/projects_db/projects_db.rs index a275b69..74dcd26 100644 --- a/src-tauri/src/projects_db/projects_db.rs +++ b/src-tauri/src/projects_db/projects_db.rs @@ -26,7 +26,7 @@ static CELL: OnceCell = OnceCell::const_new(); #[derive(Clone, Debug)] pub struct ProjectsDb { - db: DatabaseConnection, + pub db: DatabaseConnection, } fn get_path(app_handle: &tauri::AppHandle) -> String { @@ -736,6 +736,28 @@ impl ProjectsDb { Ok(dt_project::DTProject::get(&project_path).await.unwrap()) } + pub async fn get_clip(&self, image_id: i64) -> Result, String> { + let result: Option<(String, i64)> = images::Entity::find_by_id(image_id) + .join(JoinType::InnerJoin, images::Relation::Projects.def()) + .select_only() + .column(entity::projects::Column::Path) + .column(images::Column::NodeId) + .into_tuple() + .one(&self.db) + .await + .map_err(|e| e.to_string())?; + + let (project_path, node_id) = result.ok_or("Image or Project not found")?; + + let dt_project = DTProject::get(&project_path) + .await + .map_err(|e| e.to_string())?; + dt_project + .get_histories_from_clip(node_id) + .await + .map_err(|e| e.to_string()) + } + pub async fn update_models( &self, mut models: HashMap, diff --git a/src-tauri/src/projects_db/tensor_history.rs b/src-tauri/src/projects_db/tensor_history.rs index a3a2e5c..39ac482 100644 --- a/src-tauri/src/projects_db/tensor_history.rs +++ b/src-tauri/src/projects_db/tensor_history.rs @@ -16,6 +16,7 @@ pub struct ModelAndWeight { pub struct TensorHistoryImport { pub lineage: i64, pub logical_time: i64, + pub tensor_id: String, pub width: u16, pub height: u16, pub seed: u32, @@ -52,7 +53,6 @@ pub struct TensorHistoryImport { pub has_scribble: bool, pub has_shuffle: bool, pub has_mask: bool, - // pub tensor_id: i64, pub text_edits: i64, pub text_lineage: i64, // pub batch_size: u32, @@ -132,6 +132,7 @@ impl TensorHistoryImport { pub fn new( blob: &[u8], row_id: i64, + tensor_id: String, has_depth: bool, has_pose: bool, has_color: bool, @@ -174,6 +175,7 @@ impl TensorHistoryImport { model: node.model().unwrap_or("").trim().to_string(), lineage: node.lineage(), preview_id: node.preview_id(), + tensor_id, row_id, controls, loras, diff --git a/src-tauri/src/vid.rs b/src-tauri/src/vid.rs new file mode 100644 index 0000000..db50bc9 --- /dev/null +++ b/src-tauri/src/vid.rs @@ -0,0 +1,97 @@ +use sea_orm::{ + ColumnTrait, EntityTrait, JoinType, QuerySelect, RelationTrait, +}; +use std::fs; +use std::process::Command; +use tauri::Manager; + +use crate::projects_db::{DTProject, ProjectsDb}; + +#[tauri::command] +pub async fn create_video_from_frames( + app: tauri::AppHandle, + image_id: i64, +) -> Result { + let projects_db = ProjectsDb::get_or_init(&app).await?; + + // 1. Resolve Project and Node ID (similar to get_clip) + let result: Option<(String, i64, i64)> = entity::images::Entity::find_by_id(image_id) + .join(JoinType::InnerJoin, entity::images::Relation::Projects.def()) + .select_only() + .column(entity::projects::Column::Path) + .column(entity::images::Column::NodeId) + .column(entity::images::Column::ProjectId) + .into_tuple() + .one(&projects_db.db) + .await + .map_err(|e| e.to_string())?; + + let (project_path, node_id, _project_db_id) = result.ok_or("Image or Project not found")?; + + // 2. Fetch Clip Frames + let dt_project = DTProject::get(&project_path) + .await + .map_err(|e| e.to_string())?; + let frames = dt_project + .get_histories_from_clip(node_id) + .await + .map_err(|e| e.to_string())?; + + if frames.is_empty() { + return Err("No frames found for this clip".to_string()); + } + + // 3. Prepare Temp Directory + let app_data_dir = app.path().app_data_dir().unwrap(); + let temp_dir = app_data_dir.join("temp_video_frames"); + if temp_dir.exists() { + fs::remove_dir_all(&temp_dir).map_err(|e| e.to_string())?; + } + fs::create_dir_all(&temp_dir).map_err(|e| e.to_string())?; + + // 4. Save Thumbnails + for (i, frame) in frames.iter().enumerate() { + let thumb_data = dt_project + .get_thumb(frame.preview_id) + .await + .map_err(|e| e.to_string())?; + + let thumb_data = crate::projects_db::extract_jpeg_slice(&thumb_data) + .ok_or("Failed to extract JPEG slice".to_string())?; + + let file_path = temp_dir.join(format!("frame_{:04}.jpg", i)); + fs::write(&file_path, thumb_data).map_err(|e| e.to_string())?; + } + + // 5. Generate Video with FFmpeg + let output_file = temp_dir.join("output.mp4"); + // Ensure output file doesn't exist + if output_file.exists() { + fs::remove_file(&output_file).map_err(|e| e.to_string())?; + } + + let status = Command::new("ffmpeg") + .args(&[ + "-framerate", + "10", // Adjust framerate as needed, maybe make it an argument? + "-i", + temp_dir.join("frame_%04d.jpg").to_str().unwrap(), + "-c:v", + "libx264", + "-pix_fmt", + "yuv420p", + output_file.to_str().unwrap(), + ]) + .status() + .map_err(|e| format!("Failed to execute ffmpeg: {}", e))?; + + if !status.success() { + return Err("FFmpeg failed to generate video".to_string()); + } + + // 6. Return Path + // For now, let's move it to a more permanent location or just return the temp path. + // Returning temp path is fine for now, frontend can move it or display it. + // Actually, let's ensure the path is absolute and accessible. + Ok(output_file.to_string_lossy().to_string()) +} diff --git a/src/commands/index.ts b/src/commands/index.ts index 4836163..426d2fb 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1 +1,2 @@ -export * from './projects' \ No newline at end of file +export * from './projects' +export * from './vid' \ No newline at end of file diff --git a/src/commands/projects.ts b/src/commands/projects.ts index 0b4b2c8..649a955 100644 --- a/src/commands/projects.ts +++ b/src/commands/projects.ts @@ -283,6 +283,9 @@ export const pdb = { else return { items: [], total: 0 } }, + getClip: async (imageId: number): Promise => + invoke("projects_db_get_clip", { imageId }), + /** * ignores projectIds, returns count of image matches in each project. */ diff --git a/src/commands/urls.ts b/src/commands/urls.ts index 0759b12..bbb72b0 100644 --- a/src/commands/urls.ts +++ b/src/commands/urls.ts @@ -1,20 +1,33 @@ import type { ImageExtra } from "./projects" +function thumb(image: ImageExtra): string +function thumb(projectId: number, previewId: number): string +function thumb(arg: ImageExtra | number, previewId?: number): string { + if (typeof arg === "number") return `dtm://dtproject/thumb/${arg}/${previewId}` + return `dtm://dtproject/thumb/${arg.project_id}/${arg.preview_id}` +} + +function thumbHalf(image: ImageExtra): string +function thumbHalf(projectId: number, previewId: number): string +function thumbHalf(arg: ImageExtra | number, previewId?: number): string { + if (typeof arg === "number") return `dtm://dtproject/thumbhalf/${arg}/${previewId}` + return `dtm://dtproject/thumbhalf/${arg.project_id}/${arg.preview_id}` +} + const urls = { - thumb: (image: ImageExtra) => `dtm://dtproject/thumb/${image.project_id}/${image.preview_id}`, - thumbHalf: (image: ImageExtra) => - `dtm://dtproject/thumbhalf/${image.project_id}/${image.preview_id}`, - tensor: ( - projectId: number, - name: string, - opts?: { nodeId?: number | null; size?: number | null; invert?: boolean }, - ) => { - const url = new URL(`dtm://dtproject/tensor/${projectId}/${name}`) - if (opts?.nodeId) url.searchParams.set("node", opts.nodeId.toString()) - if (opts?.size) url.searchParams.set("s", opts.size.toString()) - if (opts?.invert) url.searchParams.set("mask", "invert") - return url.toString() - }, + thumb, + thumbHalf, + tensor: ( + projectId: number, + name: string, + opts?: { nodeId?: number | null; size?: number | null; invert?: boolean }, + ) => { + const url = new URL(`dtm://dtproject/tensor/${projectId}/${name}`) + if (opts?.nodeId) url.searchParams.set("node", opts.nodeId.toString()) + if (opts?.size) url.searchParams.set("s", opts.size.toString()) + if (opts?.invert) url.searchParams.set("mask", "invert") + return url.toString() + }, } export default urls diff --git a/src/commands/vid.ts b/src/commands/vid.ts new file mode 100644 index 0000000..9eb30ca --- /dev/null +++ b/src/commands/vid.ts @@ -0,0 +1,5 @@ +import { invoke } from '@tauri-apps/api/core' + +export async function createVideoFromFrames(imageId: number): Promise { + return await invoke('create_video_from_frames', { imageId }) +} diff --git a/src/components/VideoFrames.tsx b/src/components/VideoFrames.tsx new file mode 100644 index 0000000..ca8d004 --- /dev/null +++ b/src/components/VideoFrames.tsx @@ -0,0 +1,82 @@ +import { pdb, TensorHistoryNode } from "@/commands" +import urls from "@/commands/urls" +import { UIControllerState } from "@/dtProjects/state/uiState" +import { useProxyRef } from "@/hooks/valtioHooks" +import { Box } from "@chakra-ui/react" +import { useEffect, useRef } from "react" +import { Snapshot } from "valtio" + +interface VideoFramesProps extends ChakraProps { + image: Snapshot +} + +function VideoFrames(props: VideoFramesProps) { + const { image, ...restProps } = props + + const { state, snap } = useProxyRef(() => ({ + data: [] as TensorHistoryNode[], + start: 0, + })) + + const containerRef = useRef(null) + const rafRef = useRef(0) + const frameRef = useRef(0) + + useEffect(() => { + if (!image) return + console.log(image) + pdb.getClip(image.id).then((data) => { + state.data = data + }) + + const animate = (time: DOMHighResTimeStamp) => { + if (!containerRef.current || containerRef.current.children.length !== state.data.length) { + rafRef.current = requestAnimationFrame(animate) + return + } + const t = ((time - state.start) / 1000) * 24 + const frame = Math.floor(t) % state.data.length + if (frame !== frameRef.current) { + containerRef.current.children[frameRef.current]?.setAttribute( + "style", + "display: none", + ) + frameRef.current = frame + containerRef.current.children[frameRef.current]?.setAttribute( + "style", + "display: block", + ) + } + rafRef.current = requestAnimationFrame(animate) + } + + rafRef.current = requestAnimationFrame((time) => { + state.start = time + animate(time) + }) + + return () => cancelAnimationFrame(rafRef.current) + }, [image, state]) + + return ( + + {snap.data && image + ? snap.data.map((d, i) => ( + {d.index_in_a_clip.toString()} + )) + : "Loading"} + + ) +} + +export default VideoFrames diff --git a/src/dtProjects/detailsOverlay/Clip.tsx b/src/dtProjects/detailsOverlay/Clip.tsx new file mode 100644 index 0000000..73fbf44 --- /dev/null +++ b/src/dtProjects/detailsOverlay/Clip.tsx @@ -0,0 +1,110 @@ +import { Box, VStack, Text, Button } from "@chakra-ui/react" +import { UIControllerState } from "../state/uiState" +import { proxy, Snapshot, useSnapshot } from "valtio" +import { useEffect, useRef } from "react" +import { createVideoFromFrames, pdb, TensorHistoryNode } from "@/commands" +import { useMotionValue } from "motion/react" +import urls from "@/commands/urls" +import { useProxyRef } from "@/hooks/valtioHooks" + +interface ClipProps extends ChakraProps { + item: Snapshot + itemDetails: Snapshot +} + +function Clip(props: ClipProps) { + const { item, itemDetails, ...restProps } = props + + const { state, snap } = useProxyRef(() => ({ + data: [] as TensorHistoryNode[], + start: 0, + videoPath: "what", + })) + + const containerRef = useRef(null) + const rafRef = useRef(0) + const frameRef = useRef(0) + + useEffect(() => { + return + if (!item) return + console.log(item) + pdb.getClip(item.id).then((data) => { + state.data = data + }) + + const animate = (time: DOMHighResTimeStamp) => { + if (!containerRef.current || containerRef.current.children.length !== state.data.length) + return + const t = ((time - state.start) / 1000) * 24 + const frame = Math.floor(t) % state.data.length + if (frame !== frameRef.current) { + containerRef.current.children[frameRef.current]?.setAttribute( + "style", + "display: none", + ) + frameRef.current = frame + containerRef.current.children[frameRef.current]?.setAttribute( + "style", + "display: block", + ) + } + rafRef.current = requestAnimationFrame(animate) + } + + rafRef.current = requestAnimationFrame((time) => { + state.start = time + animate(time) + }) + + return () => cancelAnimationFrame(rafRef.current) + }, [item, state]) + + return ( + + + + Video: {snap.videoPath} + {/* {snap.data + ? snap.data.map((d, i) => ( + {d.index_in_a_clip.toString()} + )) + : "Loading"} */} + + ) +} + +export default Clip diff --git a/src/dtProjects/detailsOverlay/DTImageContext.tsx b/src/dtProjects/detailsOverlay/DTImageContext.tsx index 074e10a..ebb1265 100644 --- a/src/dtProjects/detailsOverlay/DTImageContext.tsx +++ b/src/dtProjects/detailsOverlay/DTImageContext.tsx @@ -7,12 +7,14 @@ export const DTImageContext = createContext< model?: Model loras?: (Model | undefined)[] controls?: (Model | undefined)[] + refiner?: Model }> >({ image: undefined, model: undefined, loras: undefined, controls: undefined, + refiner: undefined, }) diff --git a/src/dtProjects/detailsOverlay/DTImageProvider.tsx b/src/dtProjects/detailsOverlay/DTImageProvider.tsx index 2b0faf9..8bdf943 100644 --- a/src/dtProjects/detailsOverlay/DTImageProvider.tsx +++ b/src/dtProjects/detailsOverlay/DTImageProvider.tsx @@ -14,6 +14,7 @@ export function DTImageProvider(props: DTImageProviderProps) { const model = models.getModel("Model", image?.config?.model) const loras = image?.node?.loras?.map((l) => models.getModel("Lora", l.file)) const controls = image?.node?.controls?.map((c) => models.getModel("Cnet", c.file)) + const refiner = models.getModel("Model", image?.groupedConfig?.refiner?.model || undefined) - return {children} + return {children} } diff --git a/src/dtProjects/detailsOverlay/DetailsContent.tsx b/src/dtProjects/detailsOverlay/DetailsContent.tsx index 6c230e4..c0a1c37 100644 --- a/src/dtProjects/detailsOverlay/DetailsContent.tsx +++ b/src/dtProjects/detailsOverlay/DetailsContent.tsx @@ -8,6 +8,7 @@ import DataItem from "@/components/DataItem" import Tabs from "@/metadata/infoPanel/tabs" import { useDTP } from "../state/context" import { useDTImage } from "./DTImageContext" +import Clip from "./Clip" interface DetailsContentProps extends ChakraProps { item?: Snapshot | null @@ -58,6 +59,11 @@ function DetailsContent(props: DetailsContentProps) { Raw + {snap.itemDetails.node.clip_id > 0 && ( + + Clip + + )} + + + {/* + + + - + {(itemDetails?.node.clip_id ?? -1) >= 0 ? ( + + ) : ( + + )} {(showSpinner || subItem?.isLoading) && ( From 8ae726f278f14eaefc0c467e24e380ae58c05fbd Mon Sep 17 00:00:00 2001 From: Kelly Jerrell Date: Thu, 15 Jan 2026 00:21:04 -0700 Subject: [PATCH 02/70] add label to jobs --- src/components/DataItem.tsx | 5 ++++- src/dtProjects/controlPane/ProjectsPanel.tsx | 5 ++++- src/dtProjects/state/scanner.ts | 7 ++++++- src/utils/container/queue.ts | 13 +++++++++---- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/components/DataItem.tsx b/src/components/DataItem.tsx index f062ff0..3abf47e 100644 --- a/src/components/DataItem.tsx +++ b/src/components/DataItem.tsx @@ -389,9 +389,12 @@ const templates = { }, Refiner: (props: DataItemTemplateProps<"refiner">) => { const { value, ...rest } = props + const { refiner } = useDTImage() if (!value?.model) return null + + const name = refiner?.name || value.model const start = value.start !== undefined ? ` (${(value.start * 100).toFixed(1)}%)` : "" - return + return }, Shift: (props: DataItemTemplateProps<"shift">) => { const { value, ...rest } = props diff --git a/src/dtProjects/controlPane/ProjectsPanel.tsx b/src/dtProjects/controlPane/ProjectsPanel.tsx index d9f4985..00084d5 100644 --- a/src/dtProjects/controlPane/ProjectsPanel.tsx +++ b/src/dtProjects/controlPane/ProjectsPanel.tsx @@ -144,7 +144,10 @@ function ProjectListItem(props: ProjectListItemProps) { // }} > - {project.path.split("/").pop()?.slice(0, -8)} + + {project.path.split("/").pop()?.slice(0, -8)} + {project.isMissing && " (missing)"} + {project.isScanning ? ( - ) : ( diff --git a/src/dtProjects/state/scanner.ts b/src/dtProjects/state/scanner.ts index 305b4ce..2c0114f 100644 --- a/src/dtProjects/state/scanner.ts +++ b/src/dtProjects/state/scanner.ts @@ -296,6 +296,7 @@ function syncProjectsJob(callback?: () => void): DTPJob { function syncProjectFolderJob(watchFolder: string, callback?: () => void): DTPJob { return { type: "project-folder-scan", + label: watchFolder, data: watchFolder, callback, execute: async (_, container) => { @@ -319,7 +320,11 @@ function syncProjectFolderJob(watchFolder: string, callback?: () => void): DTPJo const existingProject = p.state.projects.find((pj) => pj.path === path) const stats = await getProjectStats(path) - if (!stats || stats === "dne") continue + if (!stats || stats === "dne") { + if (existingProject) existingProject.isMissing = true + console.log("project is missing") + continue + } if (existingProject) { if ( diff --git a/src/utils/container/queue.ts b/src/utils/container/queue.ts index aa768b4..6e85f58 100644 --- a/src/utils/container/queue.ts +++ b/src/utils/container/queue.ts @@ -15,7 +15,8 @@ export interface JobSpec< K extends keyof JM = keyof JM, > { type: K - tag?: string + subtype?: string + label?: string data: JM[K]["data"] execute: (data: JM[K]["data"], container: C) => Promise> | Promise callback?: JobCallback @@ -80,7 +81,7 @@ export class JobQueue extends Servi let firstIndex: number | null = null const jobsData = [] this.jobs.forEach((j, i) => { - if (j.type === item.type && j.tag === item.tag) { + if (j.type === item.type && j.subtype === item.subtype) { firstIndex = firstIndex === null ? i : Math.min(firstIndex, i) j.status = "canceled" if (Array.isArray(j.data)) jobsData.push(...j.data) @@ -99,7 +100,7 @@ export class JobQueue extends Servi if (addToFront) throw new Error("Cannot add job to front with merge=last") const jobsData = [] this.jobs.forEach((j) => { - if (j.type === item.type && j.tag === item.tag) { + if (j.type === item.type && j.subtype === item.subtype) { j.status = "canceled" if (Array.isArray(j.data)) jobsData.push(...j.data) } @@ -171,5 +172,9 @@ export class JobQueue extends Servi } function formatJob(job: Job) { - return `${job.id}:${String(job.type)}:${job.tag}` + let formatted = `${job.id}:${String(job.type)}` + + if (job.subtype) formatted += `:${job.subtype}` + if (job.label) formatted += `:${job.label}` + return formatted } From 90756126582f65920c63a7f70a53afb42465ed4d Mon Sep 17 00:00:00 2001 From: Kelly Jerrell Date: Sun, 18 Jan 2026 03:36:39 -0700 Subject: [PATCH 03/70] backend and db changes to support video --- src-tauri/entity/src/images.rs | 1 + src-tauri/entity/src/projects.rs | 2 + src-tauri/migration/src/lib.rs | 6 +- ...60115_190743_num_frames_and_fingerprint.rs | 102 ++++++++++++++++++ src-tauri/src/projects_db/commands.rs | 2 +- src-tauri/src/projects_db/dt_project.rs | 20 ++++ src-tauri/src/projects_db/projects_db.rs | 20 ++-- src-tauri/src/projects_db/tensor_history.rs | 5 + src/commands/projects.ts | 4 + src/commands/vid.ts | 12 +++ 10 files changed, 161 insertions(+), 13 deletions(-) create mode 100644 src-tauri/migration/src/m20260115_190743_num_frames_and_fingerprint.rs diff --git a/src-tauri/entity/src/images.rs b/src-tauri/entity/src/images.rs index dcc69e6..f1dbbe3 100644 --- a/src-tauri/entity/src/images.rs +++ b/src-tauri/entity/src/images.rs @@ -16,6 +16,7 @@ pub struct Model { #[sea_orm(column_type = "Blob", nullable)] pub thumbnail_half: Option>, pub clip_id: i64, + pub num_frames: Option, pub wall_clock: DateTimeUtc, pub model_id: Option, pub refiner_id: Option, diff --git a/src-tauri/entity/src/projects.rs b/src-tauri/entity/src/projects.rs index 446e3ac..7b64f1b 100644 --- a/src-tauri/entity/src/projects.rs +++ b/src-tauri/entity/src/projects.rs @@ -9,10 +9,12 @@ use serde::Serialize; pub struct Model { #[sea_orm(primary_key)] pub id: i64, + pub fingerprint: String, #[sea_orm(unique)] pub path: String, pub filesize: Option, pub modified: Option, + pub missing_on: Option, pub excluded: bool, #[sea_orm(has_many)] pub images: HasMany, diff --git a/src-tauri/migration/src/lib.rs b/src-tauri/migration/src/lib.rs index 2c605af..531eba1 100644 --- a/src-tauri/migration/src/lib.rs +++ b/src-tauri/migration/src/lib.rs @@ -1,12 +1,16 @@ pub use sea_orm_migration::prelude::*; mod m20220101_000001_create_table; +mod m20260115_190743_num_frames_and_fingerprint; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![Box::new(m20220101_000001_create_table::Migration)] + vec![ + Box::new(m20220101_000001_create_table::Migration), + Box::new(m20260115_190743_num_frames_and_fingerprint::Migration), + ] } } diff --git a/src-tauri/migration/src/m20260115_190743_num_frames_and_fingerprint.rs b/src-tauri/migration/src/m20260115_190743_num_frames_and_fingerprint.rs new file mode 100644 index 0000000..27a80d0 --- /dev/null +++ b/src-tauri/migration/src/m20260115_190743_num_frames_and_fingerprint.rs @@ -0,0 +1,102 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // Clear data + let db = manager.get_connection(); + db.execute_unprepared("DELETE FROM image_loras").await?; + db.execute_unprepared("DELETE FROM image_controls").await?; + db.execute_unprepared("DELETE FROM images").await?; + db.execute_unprepared("DELETE FROM projects").await?; + println!("Deleted data"); + + // Add columns + manager + .alter_table( + Table::alter() + .table(Images::Table) + .add_column(ColumnDef::new(Images::NumFrames).small_integer().null()) + .to_owned(), + ) + .await?; + println!("Added num_frames column"); + + manager + .alter_table( + Table::alter() + .table(Projects::Table) + .add_column( + ColumnDef::new(Projects::Fingerprint) + .string() + .not_null() + .default(""), + ) + .to_owned(), + ) + .await?; + println!("Added fingerprint column"); + + manager + .alter_table( + Table::alter() + .table(Projects::Table) + .add_column( + ColumnDef::new(Projects::MissingOn) + .big_integer() + .null() + ) + .to_owned(), + ) + .await?; + println!("Added missing_on column"); + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager.alter_table( + Table::alter() + .table(Projects::Table) + .drop_column(Projects::MissingOn) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(Projects::Table) + .drop_column(Projects::Fingerprint) + .to_owned(), + ) + .await?; + + manager + .alter_table( + Table::alter() + .table(Images::Table) + .drop_column(Images::NumFrames) + .to_owned(), + ) + .await?; + + Ok(()) + } +} + +#[derive(Iden)] +enum Projects { + Table, + Fingerprint, + MissingOn +} + +#[derive(Iden)] +enum Images { + Table, + NumFrames, +} diff --git a/src-tauri/src/projects_db/commands.rs b/src-tauri/src/projects_db/commands.rs index 9762d9f..3e20b6e 100644 --- a/src-tauri/src/projects_db/commands.rs +++ b/src-tauri/src/projects_db/commands.rs @@ -53,7 +53,7 @@ pub async fn projects_db_project_add( path: String, ) -> Result { let pdb = ProjectsDb::get_or_init(&app_handle).await?; - let project = pdb.add_project(&path).await.unwrap(); + let project = pdb.add_project(&path).await?; update_tags( &app_handle, "projects", diff --git a/src-tauri/src/projects_db/dt_project.rs b/src-tauri/src/projects_db/dt_project.rs index 911a5bc..26a0359 100644 --- a/src-tauri/src/projects_db/dt_project.rs +++ b/src-tauri/src/projects_db/dt_project.rs @@ -124,6 +124,26 @@ impl DTProject { } } + pub async fn get_fingerprint(&self) -> Result { + self.check_table(&DTProjectTable::Thumbs).await?; + + let row = query( + "SELECT + group_concat(rowid || \"-\" || __pk0, \":\") AS fingerprint + FROM ( + SELECT rowid, __pk0 + FROM thumbnailhistorynode + ORDER BY rowid ASC + LIMIT 5 + )", + ) + .fetch_one(&self.pool) + .await?; + + let fingerprint: String = row.get(0); + Ok(fingerprint.trim_end_matches(':').to_string()) + } + pub async fn get_histories( &self, first_id: i64, diff --git a/src-tauri/src/projects_db/projects_db.rs b/src-tauri/src/projects_db/projects_db.rs index 74dcd26..1a6b1e9 100644 --- a/src-tauri/src/projects_db/projects_db.rs +++ b/src-tauri/src/projects_db/projects_db.rs @@ -60,9 +60,13 @@ impl ProjectsDb { Ok(count as u32) } - pub async fn add_project(&self, path: &str) -> Result { + pub async fn add_project(&self, path: &str) -> Result { + let dt_project = DTProject::get(path).await?; + let fingerprint = dt_project.get_fingerprint().await?; + let project = projects::ActiveModel { path: Set(path.to_string()), + fingerprint: Set(fingerprint), ..Default::default() }; @@ -338,6 +342,7 @@ impl ProjectsDb { preview_id: Set(h.preview_id), thumbnail_half: Set(preview_thumb), clip_id: Set(h.clip_id), + num_frames: Set(h.num_frames.and_then(|n| Some(n as i16))), prompt: Set(h.prompt.trim().to_string()), negative_prompt: Set(h.negative_prompt.trim().to_string()), prompt_search: Set(process_prompt(&h.prompt)), @@ -996,24 +1001,16 @@ pub struct ListImagesOptions { #[derive(Debug, FromQueryResult, Serialize)] pub struct ProjectExtra { pub id: i64, + pub fingerprint: String, pub path: String, pub image_count: i64, pub last_id: Option, pub filesize: Option, pub modified: Option, + pub missing_on: Option, pub excluded: bool, } -// #[derive(Serialize, Clone)] -// pub struct ScanProgress { -// pub projects_scanned: i32, -// pub projects_total: i32, -// pub project_final: i32, -// pub project_path: String, -// pub images_scanned: i32, -// pub images_total: i32, -// } - #[derive(Debug)] pub enum MixedError { SeaOrm(DbErr), @@ -1083,6 +1080,7 @@ pub struct ImageExtra { pub model_file: Option, pub prompt: String, pub negative_prompt: String, + pub num_frames: Option, pub preview_id: i64, pub node_id: i64, pub has_depth: bool, diff --git a/src-tauri/src/projects_db/tensor_history.rs b/src-tauri/src/projects_db/tensor_history.rs index 39ac482..cc856d1 100644 --- a/src-tauri/src/projects_db/tensor_history.rs +++ b/src-tauri/src/projects_db/tensor_history.rs @@ -43,6 +43,7 @@ pub struct TensorHistoryImport { pub negative_prompt: String, pub clip_id: i64, pub index_in_a_clip: i32, + pub num_frames: Option, pub cfg_zero_star: bool, // pub image_id: i64, pub row_id: i64, @@ -185,6 +186,10 @@ impl TensorHistoryImport { wall_clock: wall_clock_to_datetime(node.wall_clock()), cfg_zero_star: node.cfg_zero_star(), clip_id: node.clip_id(), + num_frames: match node.clip_id() >= 0 { + true => Some(node.num_frames()), + false => None + }, guidance_scale: node.guidance_scale(), hires_fix: node.hires_fix(), height: node.start_height(), diff --git a/src/commands/projects.ts b/src/commands/projects.ts index 649a955..dbc3b36 100644 --- a/src/commands/projects.ts +++ b/src/commands/projects.ts @@ -25,6 +25,8 @@ export type ImageExtra = { prompt?: string negative_prompt?: string preview_id: number + clip_id: number + num_frames: number node_id: number has_depth: boolean has_pose: boolean @@ -188,6 +190,8 @@ export type DTImageFull = { project: ProjectState config: DrawThingsConfig groupedConfig: DrawThingsConfigGrouped + clipId: number + numFrames: number node: TensorHistoryNode images?: { tensorId?: string diff --git a/src/commands/vid.ts b/src/commands/vid.ts index 9eb30ca..54f98e5 100644 --- a/src/commands/vid.ts +++ b/src/commands/vid.ts @@ -3,3 +3,15 @@ import { invoke } from '@tauri-apps/api/core' export async function createVideoFromFrames(imageId: number): Promise { return await invoke('create_video_from_frames', { imageId }) } + +export async function ffmpegCheck(): Promise { + return await invoke('ffmpeg_check') +} + +export async function ffmpegDownload(): Promise { + return await invoke('ffmpeg_download') +} + +export async function ffmpegCall(args: string[]): Promise { + return await invoke('ffmpeg_call', { args }) +} From 9f1ace4494d7028bc2ba8d564e05817627b63feb Mon Sep 17 00:00:00 2001 From: Kelly Jerrell Date: Sun, 18 Jan 2026 03:44:36 -0700 Subject: [PATCH 04/70] filter by video/image, ffmpeg setup --- src-tauri/Cargo.lock | 67 ++++ src-tauri/Cargo.toml | 5 +- src-tauri/src/ffmpeg.rs | 99 +++++ src-tauri/src/lib.rs | 21 +- src-tauri/src/projects_db/filters.rs | 49 +++ src/App.tsx | 2 +- src/components/DataItem.tsx | 1 + src/components/IconToggle.tsx | 144 ++++++++ src/components/PanelList.tsx | 5 +- src/components/VideoFrames.tsx | 126 ++++--- src/components/iconButton.tsx | 341 ++++++++++-------- src/components/icons.tsx | 37 +- src/components/virtualizedList/PVGrid2.tsx | 6 +- .../controlPane/filters/TypeValueSelector.tsx | 63 ++++ .../controlPane/filters/collections.tsx | 6 + .../detailsOverlay/DTImageContext.tsx | 28 +- .../detailsOverlay/DetailsContent.tsx | 2 + src/dtProjects/imagesList/ImagesList.tsx | 77 +++- .../imagesList/SearchTextWidget.tsx | 6 +- src/dtProjects/imagesList/StatusBar.tsx | 22 +- src/dtProjects/state/details.ts | 2 + src/dtProjects/state/images.ts | 47 ++- src/dtProjects/state/scanner.ts | 14 +- src/dtProjects/state/search.ts | 4 +- src/dtProjects/state/uiState.ts | 25 ++ src/scratch/Reactive.tsx | 71 ---- src/scratch/Vid.tsx | 73 ++++ src/types.ts | 7 +- 28 files changed, 1028 insertions(+), 322 deletions(-) create mode 100644 src-tauri/src/ffmpeg.rs create mode 100644 src/components/IconToggle.tsx create mode 100644 src/dtProjects/controlPane/filters/TypeValueSelector.tsx delete mode 100644 src/scratch/Reactive.tsx create mode 100644 src/scratch/Vid.tsx diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 608a407..cf197ab 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -515,6 +515,21 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "bit-set" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" + [[package]] name = "bitflags" version = "1.3.2" @@ -1619,6 +1634,7 @@ dependencies = [ "flate2", "fpzip-sys", "futures", + "futures-util", "half", "image", "little_exif", @@ -1637,6 +1653,8 @@ dependencies = [ "sea-query", "serde", "serde_json", + "sevenz-rust", + "sha2", "sqlx", "tauri", "tauri-build", @@ -1927,6 +1945,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "filetime_creation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c25b5d475550e559de5b0c0084761c65325444e3b6c9e298af9cefe7a9ef3a5f" +dependencies = [ + "cfg-if", + "filetime", + "windows-sys 0.52.0", +] + [[package]] name = "find-msvc-tools" version = "0.1.7" @@ -3404,6 +3433,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lzma-rust" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baab2bbbd7d75a144d671e9ff79270e903957d92fb7386fd39034c709bd2661" +dependencies = [ + "byteorder", +] + [[package]] name = "mac" version = "0.1.1" @@ -3726,6 +3764,16 @@ dependencies = [ "serde", ] +[[package]] +name = "nt-time" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2de419e64947cd8830e66beb584acc3fb42ed411d103e3c794dda355d1b374b5" +dependencies = [ + "chrono", + "time", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -5894,6 +5942,23 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sevenz-rust" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26482cf1ecce4540dc782fc70019eba89ffc4d87b3717eb5ec524b5db6fdefef" +dependencies = [ + "bit-set", + "byteorder", + "crc", + "filetime_creation", + "js-sys", + "lzma-rust", + "nt-time", + "sha2", + "wasm-bindgen", +] + [[package]] name = "sha1" version = "0.10.6" @@ -7219,7 +7284,9 @@ dependencies = [ "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 01f5525..ebfc3cc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -53,7 +53,7 @@ sea-orm = { version = "2.0.0-rc", features = [ # "debug-print" ] } futures = "0.3.28" -tokio = "1.48.0" +tokio = { version = "1.48.0", features = ["full"] } mime = "0.3.17" once_cell = "1.21.3" moka = { version = "0.12.11", features = ["future"] } @@ -73,6 +73,9 @@ sea-query = "1.0.0-rc.20" unicode-normalization = "0.1.25" log = "0.4" tauri-plugin-log = "2" +sevenz-rust = "0.6.1" +sha2 = "0.10.9" +futures-util = "0.3.31" # macOS-only [target."cfg(target_os = \"macos\")".dependencies] diff --git a/src-tauri/src/ffmpeg.rs b/src-tauri/src/ffmpeg.rs new file mode 100644 index 0000000..7374ab4 --- /dev/null +++ b/src-tauri/src/ffmpeg.rs @@ -0,0 +1,99 @@ +use std::path::PathBuf; +use tauri::{AppHandle, Manager, Emitter}; +use tauri_plugin_http::reqwest; +use futures_util::StreamExt; +use std::io::Write; +use tokio::fs; +use tokio::process::Command; + +#[derive(Clone, serde::Serialize)] +struct DownloadProgress { + progress: f64, + total: Option, + received: u64, +} + +pub async fn get_ffmpeg_path(app: &AppHandle) -> Result { + Ok(app.path().app_data_dir().map_err(|e| e.to_string())?.join("bin").join("ffmpeg")) +} + +pub async fn check_ffmpeg(app: &AppHandle) -> Result { + let path = get_ffmpeg_path(app).await?; + Ok(path.exists()) +} + +pub async fn download_ffmpeg(app: AppHandle) -> Result<(), String> { + let app_data_dir = app.path().app_data_dir().map_err(|e| e.to_string())?; + let temp_dir = app_data_dir.join("temp"); + fs::create_dir_all(&temp_dir).await.map_err(|e| e.to_string())?; + + let bin_dir = app_data_dir.join("bin"); + fs::create_dir_all(&bin_dir).await.map_err(|e| e.to_string())?; + + let ffmpeg_7z = temp_dir.join("ffmpeg.7z"); + + // Download FFmpeg + // The redirect to the latest .7z works through reqwest + let url = "https://evermeet.cx/ffmpeg/get"; + let client = reqwest::Client::new(); + let res = client.get(url).send().await.map_err(|e| e.to_string())?; + + let total_size = res.content_length(); + let mut downloaded: u64 = 0; + let mut stream = res.bytes_stream(); + + let mut file = std::fs::File::create(&ffmpeg_7z).map_err(|e| e.to_string())?; + + while let Some(item) = stream.next().await { + let chunk = item.map_err(|e| e.to_string())?; + file.write_all(&chunk).map_err(|e| e.to_string())?; + downloaded += chunk.len() as u64; + + let _ = app.emit("ffmpeg_download_progress", DownloadProgress { + progress: total_size.map(|s| downloaded as f64 / s as f64).unwrap_or(0.0), + total: total_size, + received: downloaded, + }); + } + + // Extract + // sevenz-rust can extract the .7z file directly + sevenz_rust::decompress_file(&ffmpeg_7z, &bin_dir).map_err(|e| e.to_string())?; + + // Cleanup + let _ = fs::remove_file(&ffmpeg_7z).await; + + // Set executable permission on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let ffmpeg_path = bin_dir.join("ffmpeg"); + if ffmpeg_path.exists() { + let mut perms = fs::metadata(&ffmpeg_path).await.map_err(|e| e.to_string())?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&ffmpeg_path, perms).await.map_err(|e| e.to_string())?; + } + } + + Ok(()) +} + +pub async fn call_ffmpeg(app: &AppHandle, args: Vec) -> Result { + let ffmpeg_path = get_ffmpeg_path(app).await?; + + if !ffmpeg_path.exists() { + return Err("FFmpeg not found. Please download it first.".to_string()); + } + + let output = Command::new(ffmpeg_path) + .args(args) + .output() + .await + .map_err(|e| e.to_string())?; + + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(String::from_utf8_lossy(&output.stderr).to_string()) + } +} \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 933cd1d..d2cb7b8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,6 +10,7 @@ mod clipboard; mod projects_db; mod vid; +mod ffmpeg; use once_cell::sync::Lazy; use tokio::runtime::Runtime; @@ -46,6 +47,21 @@ fn write_clipboard_binary(ty: String, data: Vec) -> Result<(), String> { clipboard::write_clipboard_binary(ty, data) } +#[tauri::command] +async fn ffmpeg_check(app: tauri::AppHandle) -> Result { + ffmpeg::check_ffmpeg(&app).await +} + +#[tauri::command] +async fn ffmpeg_download(app: tauri::AppHandle) -> Result<(), String> { + ffmpeg::download_ffmpeg(app).await +} + +#[tauri::command] +async fn ffmpeg_call(app: tauri::AppHandle, args: Vec) -> Result { + ffmpeg::call_ffmpeg(&app, args).await +} + #[tauri::command] async fn fetch_image_file(url: String) -> Result, String> { let resp = reqwest::get(&url).await.map_err(|e| e.to_string())?; @@ -159,7 +175,10 @@ pub fn run() { dt_project_get_tensor_raw, // #unused dt_project_get_tensor_size, dt_project_decode_tensor, - vid::create_video_from_frames + vid::create_video_from_frames, + ffmpeg_check, + ffmpeg_download, + ffmpeg_call ]) .register_asynchronous_uri_scheme_protocol("dtm", |_ctx, request, responder| { std::thread::spawn(move || { diff --git a/src-tauri/src/projects_db/filters.rs b/src-tauri/src/projects_db/filters.rs index 1547921..1d34b0d 100644 --- a/src-tauri/src/projects_db/filters.rs +++ b/src-tauri/src/projects_db/filters.rs @@ -10,6 +10,7 @@ impl ListImagesFilterTarget { q: sea_orm::Select, ) -> sea_orm::Select { match self { + ListImagesFilterTarget::Type => apply_type_filter(op, value, q), ListImagesFilterTarget::Model => apply_model_filter(op, value, q), ListImagesFilterTarget::Sampler => apply_sampler_filter(op, value, q), ListImagesFilterTarget::Content => apply_content_filter(op, value, q), @@ -29,6 +30,53 @@ impl ListImagesFilterTarget { } } +fn apply_type_filter( + op: ListImagesFilterOperator, + value: &ListImagesFilterValue, + q: sea_orm::Select, +) -> sea_orm::Select { + use sea_orm::QueryFilter; + use ListImagesFilterOperator::*; + + let types = match value { + ListImagesFilterValue::String(v) => v, + _ => return q, + }; + println!("types: {:?}", types); + // types will have "Image" or "Video" or both + // op image video + // is true false isnot false true images only + // is false true isnot true false videos only + // is true true isnot false false both + // is false false isnot true true none + // so just boil it down to has images and has videos + let op_is_is = matches!(op, Is); + let mut has_images = !op_is_is; + let mut has_videos = !op_is_is; + + for t in types { + match t.as_str() { + "image" => has_images = op_is_is, + "video" => has_videos = op_is_is, + _ => {} + } + } + println!("has_images: {}, has_videos: {}, is_is: {}", has_images, has_videos, op_is_is); + + if has_images && has_videos { + return q; + } + if has_images { + return q.filter(images::Column::NumFrames.is_null()); + } + if has_videos { + return q.filter(images::Column::NumFrames.is_not_null()); + } + + // this is pointless but accurate + q.filter(sea_query::Expr::val(false)) +} + fn apply_model_filter( op: ListImagesFilterOperator, value: &ListImagesFilterValue, @@ -197,6 +245,7 @@ pub enum ListImagesFilterTarget { Height, TextGuidance, Shift, + Type, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src/App.tsx b/src/App.tsx index 01eddc3..0b6f436 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -179,7 +179,7 @@ const views = { vid: lazy(() => import("./vid/Vid")), library: lazy(() => import("./library/Library")), projects: lazy(() => import("./dtProjects/DTProjects")), - scratch: lazy(() => import("./scratch/Reactive")), + scratch: lazy(() => import("./scratch/Vid")), } function getView(view: string) { diff --git a/src/components/DataItem.tsx b/src/components/DataItem.tsx index 3abf47e..14257e8 100644 --- a/src/components/DataItem.tsx +++ b/src/components/DataItem.tsx @@ -346,6 +346,7 @@ const templates = { }, NumFrames: (props: DataItemTemplateProps<"numFrames">) => { const { value, ...rest } = props + if (!value) return null return }, diff --git a/src/components/IconToggle.tsx b/src/components/IconToggle.tsx new file mode 100644 index 0000000..4039675 --- /dev/null +++ b/src/components/IconToggle.tsx @@ -0,0 +1,144 @@ +import { type Button, chakra, HStack } from "@chakra-ui/react" +import { + type ComponentProps, + createContext, + type ReactNode, + use, + useCallback, + useEffect, +} from "react" +import { proxy, subscribe, useSnapshot } from "valtio" +import { useProxyRef } from "@/hooks/valtioHooks" +import { IconButton, Tooltip } from "." + +const IconToggleContext = createContext({ + value: {} as Record, + onChange: (_value: Record) => {}, +}) + +interface IconToggleProps extends Omit { + children: ReactNode + value: Record + onChange: (value: Record) => void + requireOne?: boolean +} + +function IconToggle(props: IconToggleProps) { + const { children, value, requireOne, onChange, ...restProps } = props + + // const { snap, state } = useProxyRef(() => ({ entries: {} as Record })) + + // useEffect(() => { + // const unsubscribe = subscribe(state, () => { + // onChange(state.entries) + // }) + // return unsubscribe + // }, [onChange, state]) + + const onClick = useCallback( + (option: string) => { + const entries = { ...value } + const totalOptions = Object.keys(entries).length + const totalSelected = Object.values(entries).filter((v) => v).length + + if (requireOne) { + // if clicking the selected option, and can switch if there are two + if (entries[option] && totalOptions === 2 && totalSelected === 1) { + Object.keys(entries).forEach((key) => { + entries[key] = !entries[key] + }) + } + // otherwise, select the clicked option and disable the others + else { + Object.keys(entries).forEach((key) => { + entries[key] = false + }) + entries[option] = true + } + } + // otherwise just toggle + else entries[option] = !entries[option] + onChange(entries) + }, + [value, onChange, requireOne], + ) + + const cv = { value, onChange, onClick } + + return ( + + + {children} + + + ) +} + +interface TriggerProps extends ComponentProps { + option: string + // initialValue?: boolean + tip?: ReactNode + tipText?: string + tipTitle?: string +} + +function Trigger(props: TriggerProps) { + const { children, option, tip, tipText, tipTitle, ...restProps } = props + + const { value, onChange, onClick } = use(IconToggleContext) + + // useEffect(() => { + // if (!(option in context.entries)) { + // context.entries[option] = initialValue ?? false + // } + // return () => { + // delete context.entries[option] + // } + // }, [option, initialValue, context]) + + return ( + + { + onClick(option) + }} + {...restProps} + > + {children} + + + ) +} + +const TButton = chakra("button", { + base: { + display: "inline-flex", + fontSize: "1.2rem", + paddingInline: 2, + paddingBlock: 1, + }, + variants: { + selected: { + true: { + bgColor: "bg.3", + }, + false: { + bgColor: "bg.deep", + }, + }, + }, +}) + +IconToggle.Trigger = Trigger + +export default IconToggle diff --git a/src/components/PanelList.tsx b/src/components/PanelList.tsx index 534a09d..9a5d1bb 100644 --- a/src/components/PanelList.tsx +++ b/src/components/PanelList.tsx @@ -1,12 +1,11 @@ import { HStack, Spacer } from "@chakra-ui/react" -import { motion, useSpring } from "motion/react" import { type ComponentType, useEffect, useMemo, useRef } from "react" +import { proxy, type Snapshot, useSnapshot } from "valtio" import type { IconType } from "@/components/icons" import { PiInfo } from "@/components/icons" -import { proxy, type Snapshot, useSnapshot } from "valtio" import { type Selectable, useSelectableGroup } from "@/hooks/useSelectableV" import { IconButton, PaneListContainer, PanelListItem, PanelSectionHeader, Tooltip } from "." -import { PanelListScrollContent, PanelSection, PaneListScrollContainer } from "./common" +import { PaneListScrollContainer, PanelListScrollContent, PanelSection } from "./common" interface PanelListComponentProps extends ChakraProps { emptyListText?: string | boolean diff --git a/src/components/VideoFrames.tsx b/src/components/VideoFrames.tsx index ca8d004..2593b30 100644 --- a/src/components/VideoFrames.tsx +++ b/src/components/VideoFrames.tsx @@ -1,82 +1,108 @@ -import { pdb, TensorHistoryNode } from "@/commands" +import { Box } from "@chakra-ui/react" +import { type CSSProperties, useEffect, useRef } from "react" +import type { Snapshot } from "valtio" +import { pdb } from "@/commands" import urls from "@/commands/urls" -import { UIControllerState } from "@/dtProjects/state/uiState" +import type { UIControllerState } from "@/dtProjects/state/uiState" import { useProxyRef } from "@/hooks/valtioHooks" -import { Box } from "@chakra-ui/react" -import { useEffect, useRef } from "react" -import { Snapshot } from "valtio" interface VideoFramesProps extends ChakraProps { image: Snapshot + half?: boolean + objectFit?: CSSProperties["objectFit"] } function VideoFrames(props: VideoFramesProps) { - const { image, ...restProps } = props + const { image, half, objectFit, ...restProps } = props const { state, snap } = useProxyRef(() => ({ - data: [] as TensorHistoryNode[], + data: [] as string[], start: 0, })) + const getUrl = half ? urls.thumbHalf : urls.thumb + const containerRef = useRef(null) + const imgRef = useRef(null) const rafRef = useRef(0) const frameRef = useRef(0) useEffect(() => { if (!image) return - console.log(image) - pdb.getClip(image.id).then((data) => { - state.data = data - }) - - const animate = (time: DOMHighResTimeStamp) => { - if (!containerRef.current || containerRef.current.children.length !== state.data.length) { + pdb.getClip(image.id).then(async (data) => { + if (!image) return + if (!imgRef.current) return + state.data = data.map((d) => getUrl(image.project_id, d.preview_id)) + await preloadImages(state.data) + console.log("preloaded") + const animate = (time: DOMHighResTimeStamp) => { + if (!imgRef.current) { + rafRef.current = requestAnimationFrame(animate) + return + } + const t = ((time - state.start) / 1000) * 24 + const frame = Math.floor(t) % state.data.length + if (frame !== frameRef.current) { + frameRef.current = frame + imgRef.current.src = state.data[frame] + // containerRef.current.children[frameRef.current]?.setAttribute( + // "style", + // "display: none", + // ) + // containerRef.current.children[frameRef.current]?.setAttribute( + // "style", + // "display: block", + // ) + } rafRef.current = requestAnimationFrame(animate) - return - } - const t = ((time - state.start) / 1000) * 24 - const frame = Math.floor(t) % state.data.length - if (frame !== frameRef.current) { - containerRef.current.children[frameRef.current]?.setAttribute( - "style", - "display: none", - ) - frameRef.current = frame - containerRef.current.children[frameRef.current]?.setAttribute( - "style", - "display: block", - ) } - rafRef.current = requestAnimationFrame(animate) - } - rafRef.current = requestAnimationFrame((time) => { - state.start = time - animate(time) + cancelAnimationFrame(rafRef.current) + + rafRef.current = requestAnimationFrame((time) => { + state.start = time + animate(time) + }) }) return () => cancelAnimationFrame(rafRef.current) - }, [image, state]) + }, [image, state, getUrl]) + + if (!image) return null return ( - - {snap.data && image - ? snap.data.map((d, i) => ( - {d.index_in_a_clip.toString()} - )) - : "Loading"} + + {/* {snap.data && image */} + {/* ? snap.data.map((d, i) => ( */} + {"clip"} + {/* )) */} + {/* : "Loading"} */} ) } export default VideoFrames + +async function preloadImages(urls: string[]) { + const promises = urls.map((url) => { + return new Promise((resolve, reject) => { + const img = new Image() + img.src = url + img.onload = resolve + img.onerror = reject + }) + }) + await Promise.all(promises) +} diff --git a/src/components/iconButton.tsx b/src/components/iconButton.tsx index 4218211..af2b233 100644 --- a/src/components/iconButton.tsx +++ b/src/components/iconButton.tsx @@ -1,168 +1,211 @@ -import { chakra, type HTMLChakraProps, type RecipeProps } from "@chakra-ui/react" -import type { ReactNode } from "react" +import { chakra } from "@chakra-ui/react" +import type { ComponentProps, ReactNode } from "react" import { Tooltip } from "." // from the chakra ui button recipe // https://github.com/chakra-ui/chakra-ui/blob/main/packages/react/src/theme/recipes/button.ts const Base = chakra("button", { - base: { - color: "fg.3", - backgroundColor: "transparent", - aspectRatio: "1", - bgColor: "transparent", - display: "inline-flex", - appearance: "none", - alignItems: "center", - justifyContent: "center", - userSelect: "none", - position: "relative", - borderRadius: "l2", - whiteSpace: "nowrap", - verticalAlign: "middle", - borderWidth: "1px", - borderColor: "transparent", - cursor: "button", - flexShrink: "0", - outline: "0", - lineHeight: "1.2", - isolation: "isolate", - fontWeight: "medium", - transitionProperty: "common", - transitionDuration: "moderate", - focusVisibleRing: "outside", - _hover: { - scale: "1.2", - color: "fg.1", - }, - _disabled: { - layerStyle: "disabled", - cursor: "default", - }, - _icon: { - flexShrink: "0", - }, - }, + base: { + color: "fg.3", + aspectRatio: "1", + bgColor: "transparent", + display: "inline-flex", + appearance: "none", + alignItems: "center", + justifyContent: "center", + userSelect: "none", + position: "relative", + borderRadius: "l2", + whiteSpace: "nowrap", + verticalAlign: "middle", + borderWidth: "1px", + borderColor: "transparent", + cursor: "button", + flexShrink: "0", + outline: "0", + lineHeight: "1.2", + isolation: "isolate", + fontWeight: "medium", + transitionProperty: "common", + transitionDuration: "moderate", + focusVisibleRing: "outside", + _hover: { + scale: "1.2", + color: "fg.1", + }, + _disabled: { + layerStyle: "disabled", + cursor: "default", + }, + _icon: { + flexShrink: "0", + }, + }, - variants: { - size: { - min: { - h: "min-content", - minH: 0, - w: "min-content", - minW: 0, - textStyle: "xs", - _icon: { - width: "5", - height: "5", - }, - }, - "2xs": { - h: "6", - minW: "6", - textStyle: "xs", - px: "2", - gap: "1", - _icon: { - width: "3.5", - height: "3.5", - gap: "1.5", - }, - }, - xs: { - h: "8", - minW: "8", - textStyle: "xs", - px: "2.5", - gap: "1", - _icon: { - width: "4", - height: "4", - }, - }, - sm: { - h: "8", - minW: "8", - px: "0", - textStyle: "sm", - gap: "2", - _icon: { - width: "5", - height: "5", - }, - }, - md: { - h: "10", - minW: "10", - textStyle: "sm", - px: "4", - gap: "2", - _icon: { - width: "6", - height: "6", - }, - }, - lg: { - h: "11", - minW: "11", - textStyle: "md", - px: "5", - gap: "3", - _icon: { - width: "5", - height: "5", - }, - }, - xl: { - h: "12", - minW: "12", - textStyle: "md", - px: "5", - gap: "2.5", - _icon: { - width: "5", - height: "5", - }, - }, - "2xl": { - h: "16", - minW: "16", - textStyle: "lg", - px: "7", - gap: "3", - _icon: { - width: "6", - height: "6", - }, - }, - }, - }, + variants: { + variant: { + toggle: { + paddingBlock: 0.5, + height: "unset", + paddingInline: 1, + border: "1px solid", + borderColor: "transparent", + borderRadius: 0, + margin: 0, + marginInline: "-0.5px", + _hover: { + scale: "1", + color: "fg.1", + "& *": { + scale: 1.05, + }, + }, + "& *": { + transformOrigin: "center center", + }, + "&:first-child": { + borderTopLeftRadius: "md", + borderBottomLeftRadius: "md", + marginLeft: 0, + }, + "&:last-child": { + borderTopRightRadius: "md", + borderBottomRightRadius: "md", + marginRight: 0, + }, + }, + }, + toggled: { + true: { + bgColor: "bg.3", + border: "1px solid {gray/30}", + color: "fg.1", + }, + false: { + color: "fg.3", + bgColor: "bg.deep", + border: "1px solid {gray/30}", + }, + }, + size: { + min: { + h: "min-content", + minH: 0, + w: "min-content", + minW: 0, + textStyle: "xs", + _icon: { + width: "5", + height: "5", + }, + }, + "2xs": { + h: "6", + minW: "6", + textStyle: "xs", + px: "2", + gap: "1", + _icon: { + width: "3.5", + height: "3.5", + gap: "1.5", + }, + }, + xs: { + h: "8", + minW: "8", + textStyle: "xs", + px: "2.5", + gap: "1", + _icon: { + width: "4", + height: "4", + }, + }, + sm: { + h: "8", + minW: "8", + px: "0", + textStyle: "sm", + gap: "2", + _icon: { + width: "5", + height: "5", + }, + }, + md: { + h: "10", + minW: "10", + textStyle: "sm", + px: "4", + gap: "2", + _icon: { + width: "6", + height: "6", + }, + }, + lg: { + h: "11", + minW: "11", + textStyle: "md", + px: "5", + gap: "3", + _icon: { + width: "5", + height: "5", + }, + }, + xl: { + h: "12", + minW: "12", + textStyle: "md", + px: "5", + gap: "2.5", + _icon: { + width: "5", + height: "5", + }, + }, + "2xl": { + h: "16", + minW: "16", + textStyle: "lg", + px: "7", + gap: "3", + _icon: { + width: "6", + height: "6", + }, + }, + }, + }, - defaultVariants: { - size: "sm", - }, + defaultVariants: { + size: "sm", + }, }) -export interface IconButtonProps extends HTMLChakraProps<"button", RecipeProps<"button">> { - tip?: ReactNode - tipTitle?: string - tipText?: string +export interface IconButtonProps extends ComponentProps { + tip?: ReactNode + tipTitle?: string + tipText?: string } const IconButton = (props: IconButtonProps) => { - const { tip, tipTitle, tipText, children, ...rest } = props + const { tip, tipTitle, tipText, children, ...rest } = props - const button = {children} + const button = {children} - if (tip || tipTitle || tipText) { - return ( - - {button} - - ) - } + if (tip || tipTitle || tipText) { + return ( + + {button} + + ) + } - return button + return button } export default IconButton diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 9e28d48..9533784 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -1,10 +1,27 @@ -export type { IconType } from "react-icons/lib"; - -export { BiDetail } from "react-icons/bi"; -export { FaMinus, FaMoon, FaPlus } from "react-icons/fa6"; -export { FiClipboard, FiCopy, FiEye, FiEyeOff, FiFolder, FiList, FiRefreshCw, FiSave, FiX, FiXCircle } from "react-icons/fi"; -export { GoGear } from "react-icons/go"; -export { LuFolderTree, LuMoon, LuSun, LuX } from "react-icons/lu"; -export { MdBlock, MdImageSearch } from "react-icons/md"; -export { PiCoffee, PiInfo, PiListMagnifyingGlassBold } from "react-icons/pi"; -export { TbBrowser, TbSortAscending, TbSortAscending2, TbSortAscendingLetters, TbSortDescending2, TbSortDescendingNumbers } from "react-icons/tb"; +export { BiDetail } from "react-icons/bi" +export { FaMinus, FaMoon, FaPlus } from "react-icons/fa6" +export { + FiClipboard, + FiCopy, + FiEye, + FiEyeOff, + FiFolder, + FiList, + FiRefreshCw, + FiSave, + FiX, + FiXCircle, +} from "react-icons/fi" +export { GoGear } from "react-icons/go" +export type { IconType } from "react-icons/lib" +export { LuFolderTree, LuMoon, LuSun, LuX } from "react-icons/lu" +export { MdBlock, MdImageSearch } from "react-icons/md" +export { PiCoffee, PiFilmStrip, PiImage, PiInfo, PiListMagnifyingGlassBold } from "react-icons/pi" +export { + TbBrowser, + TbSortAscending, + TbSortAscending2, + TbSortAscendingLetters, + TbSortDescending2, + TbSortDescendingNumbers, +} from "react-icons/tb" diff --git a/src/components/virtualizedList/PVGrid2.tsx b/src/components/virtualizedList/PVGrid2.tsx index da860d3..80fe075 100644 --- a/src/components/virtualizedList/PVGrid2.tsx +++ b/src/components/virtualizedList/PVGrid2.tsx @@ -83,6 +83,7 @@ function PVGrid(props: PVGridProps) { itemProps, maxItemSize, onImagesChanged, + onScroll, ...restProps } = props const Item = itemComponent @@ -187,7 +188,10 @@ function PVGrid(props: PVGridProps) { handleScroll(e)} + onScroll={(e) => { + handleScroll(e) + onScroll?.(e) + }} {...restProps} > ) { + const { value, onValueChange, ...boxProps } = props + + return ( + { + onValueChange?.(value.value as string[]) + }} + multiple={true} + {...boxProps} + > + + + + {value?.map((value) => ( + + {typeValues[value as keyof typeof typeValues] ?? "unknown"} + + ))} + {value?.length === 0 && Select type} + + + + + {typeValuesCollection.items.map((item) => ( + + {item.label} + + + ))} + + + ) +} + +const TypeValueSelector = TypeValueSelectorComponent as FilterValueSelector + +TypeValueSelector.getValueLabel = (values) => { + if (!Array.isArray(values)) return [] + return values.map((v) => + v in typeValues ? typeValues[v as keyof typeof typeValues] : "unknown", + ) +} + +export default TypeValueSelector diff --git a/src/dtProjects/controlPane/filters/collections.tsx b/src/dtProjects/controlPane/filters/collections.tsx index d72ee17..f7c7d69 100644 --- a/src/dtProjects/controlPane/filters/collections.tsx +++ b/src/dtProjects/controlPane/filters/collections.tsx @@ -8,6 +8,7 @@ import FloatValueInput from "./FloatValueInput" import IntValueInput from "./IntValueInput" import { ControlValueSelector, LoraValueSelector, ModelValueSelector } from "./ModelValueSelector" import SamplerValueSelector from "./SamplerValueSelector" +import TypeValueSelector from "./TypeValueSelector" export function createValueLabelCollection(values: Record) { return createListCollection({ @@ -74,6 +75,11 @@ const prepareModelFilterValue = (value: (Model | VersionModel)[]) => { const prepareSizeFilterValue = (value: number) => Math.round(value / 64) export const filterTargets = { + type: { + collection: isIsNotOpsCollection, + ValueComponent: TypeValueSelector, + initialValue: [], + }, model: { collection: isIsNotOpsCollection, ValueComponent: ModelValueSelector, diff --git a/src/dtProjects/detailsOverlay/DTImageContext.tsx b/src/dtProjects/detailsOverlay/DTImageContext.tsx index ebb1265..c4d655a 100644 --- a/src/dtProjects/detailsOverlay/DTImageContext.tsx +++ b/src/dtProjects/detailsOverlay/DTImageContext.tsx @@ -2,23 +2,21 @@ import { createContext, useContext } from "react" import type { DTImageFull, Model } from "@/commands" export const DTImageContext = createContext< - MaybeReadonly<{ - image?: DTImageFull - model?: Model - loras?: (Model | undefined)[] - controls?: (Model | undefined)[] - refiner?: Model - }> + MaybeReadonly<{ + image?: DTImageFull + model?: Model + loras?: (Model | undefined)[] + controls?: (Model | undefined)[] + refiner?: Model + }> >({ - image: undefined, - model: undefined, - loras: undefined, - controls: undefined, - refiner: undefined, + image: undefined, + model: undefined, + loras: undefined, + controls: undefined, + refiner: undefined, }) - - export function useDTImage() { - return useContext(DTImageContext) + return useContext(DTImageContext) } diff --git a/src/dtProjects/detailsOverlay/DetailsContent.tsx b/src/dtProjects/detailsOverlay/DetailsContent.tsx index c0a1c37..99e4d1a 100644 --- a/src/dtProjects/detailsOverlay/DetailsContent.tsx +++ b/src/dtProjects/detailsOverlay/DetailsContent.tsx @@ -178,9 +178,11 @@ function DetailsContent(props: DetailsContentProps) { {/* */} + + diff --git a/src/dtProjects/imagesList/ImagesList.tsx b/src/dtProjects/imagesList/ImagesList.tsx index 5027039..0b32719 100644 --- a/src/dtProjects/imagesList/ImagesList.tsx +++ b/src/dtProjects/imagesList/ImagesList.tsx @@ -1,6 +1,8 @@ import { Box } from "@chakra-ui/react" -import { useCallback } from "react" +import { useCallback, useState } from "react" import type { ImageExtra } from "@/commands" +import { PiFilmStrip } from "@/components/icons" +import VideoFrames from "@/components/VideoFrames" import PVGrid, { type PVGridItemComponent, type PVGridItemProps, @@ -13,6 +15,7 @@ function ImagesList(props: ChakraProps) { const { images, uiState } = useDTP() const uiSnap = uiState.useSnap() const imagesSnap = images.useSnap() + const [hoveredIndex, setHoveredIndex] = useState(null) const itemSource = images.useItemSource() @@ -23,8 +26,19 @@ function ImagesList(props: ChakraProps) { [images], ) + const onPointerEnter = useCallback((index: number) => { + setHoveredIndex((value) => { + return index + }) + }, []) + + const onPointerLeave = useCallback((index: number) => { + setHoveredIndex(null) + }, []) + return ( + onScroll={() => setHoveredIndex(null)} freeze={!!uiSnap.detailsView?.item} inert={uiSnap.isGridInert} bgColor={"transparent"} @@ -33,29 +47,69 @@ function ImagesList(props: ChakraProps) { itemSource={itemSource} maxItemSize={imagesSnap.imageSize ?? 5} onImagesChanged={images.onImagesChanged} - itemProps={{ showDetailsOverlay }} + itemProps={{ showDetailsOverlay, onPointerEnter, onPointerLeave, hoveredIndex }} keyFn={keyFn} {...props} /> ) } +function GridItemWrapper( + props: PVGridItemProps void }>, +) { + const { value: item } = props + if (!item) return null + + if ( + item.clip_id !== null && + item.clip_id !== undefined && + item.clip_id >= 0 && + item.num_frames + ) { + return + } + return +} + function GridItemAnim( props: PVGridItemProps< ImageExtra, { showDetailsOverlay: (index: number) => void + onPointerEnter?: (index: number) => void + onPointerLeave?: (index: number) => void + hoveredIndex?: number } >, ) { - const { value: item, showDetailsOverlay, index } = props + const { + value: item, + showDetailsOverlay, + index, + hoveredIndex, + onPointerEnter, + onPointerLeave, + } = props + + if (!item) return const previewId = `${item?.project_id}/${item?.preview_id}` const url = `dtm://dtproject/thumbhalf/${previewId}` + const isVideo = item.num_frames > 0 + const showVideo = isVideo && hoveredIndex === index + if (showVideo) console.log("show vidoeo", index) return ( - showDetailsOverlay(index)}> - {item && ( + onPointerEnter?.(index)} + onPointerLeave={() => onPointerLeave?.(index)} + onClick={() => showDetailsOverlay(index)} + > + {showVideo ? ( + + ) : (
)} + {isVideo && ( + + )}
) } diff --git a/src/dtProjects/imagesList/SearchTextWidget.tsx b/src/dtProjects/imagesList/SearchTextWidget.tsx index 0f3b218..67d77ec 100644 --- a/src/dtProjects/imagesList/SearchTextWidget.tsx +++ b/src/dtProjects/imagesList/SearchTextWidget.tsx @@ -1,8 +1,8 @@ import { Box, HStack } from "@chakra-ui/react" -import { FiX } from "@/components/icons" import { IconButton } from "@/components" -import { useDTP } from "../state/context" +import { FiX } from "@/components/icons" import { plural } from "@/utils/helpers" +import { useDTP } from "../state/context" interface SearchTextWidgetProps extends ChakraProps {} @@ -12,7 +12,7 @@ function SearchTextWidget(props: SearchTextWidgetProps) { const snap = images.useSnap() const search = snap.imageSource.search - const filters = snap.imageSource.filters?.length + const filters = snap.imageSource.filters?.filter((f) => f.target !== "type").length if (!search && !filters) return null diff --git a/src/dtProjects/imagesList/StatusBar.tsx b/src/dtProjects/imagesList/StatusBar.tsx index 131b1db..c229bd6 100644 --- a/src/dtProjects/imagesList/StatusBar.tsx +++ b/src/dtProjects/imagesList/StatusBar.tsx @@ -1,4 +1,7 @@ import { Box, Grid, HStack } from "@chakra-ui/react" +import IconToggle from "@/components/IconToggle" +import { PiFilmStrip, PiImage } from "@/components/icons" +import { useDTP } from "../state/context" import ProjectsWidget from "./ProjectsWidget" import SearchTextWidget from "./SearchTextWidget" @@ -7,17 +10,34 @@ interface StatusBarProps extends ChakraProps {} function StatusBar(props: StatusBarProps) { const { ...restProps } = props + const { uiState } = useDTP() + const snap = uiState.useSnap() + return ( + { + uiState.setShowImages(value.image ?? false) + uiState.setShowVideos(value.video ?? false) + }} + > + + + + + + + { searchId: 0, }) + showImages = true + showVideos = true + itemSource: IItemSource = new EmptyItemSource() eventTimer: NodeJS.Timeout | undefined @@ -50,10 +53,12 @@ class ImagesController extends DTPStateController { const p = get(projectsService.state.selectedProjects) this.setSelectedProjects(p) }) - this.watchProxy((get) => { + this.watchProxy(async (get) => { const p = get(projectsService.state.projects) const changed = updateProjectsCache(p, this.projectsCache) + if (changed.length > 0) { + await this.container.services.uiState.importLockPromise if (this.eventTimer) return clearTimeout(this.eventTimer) this.eventTimer = setTimeout(async () => { @@ -65,6 +70,44 @@ class ImagesController extends DTPStateController { }) }) + this.container.getFutureService("uiState").then((uiState) => { + const unsub = subscribe(uiState.state, () => { + const { showVideos, showImages } = uiState.state + if (this.showImages === showImages && this.showVideos === showVideos) return + + const imageSource = this.state.imageSource + + const isFilterNeeded = !(showImages === showVideos) + const filterIndex = + this.state.imageSource.filters?.findIndex((f) => f.target === "type") ?? -1 + let filter = filterIndex >= 0 ? imageSource.filters?.[filterIndex] : undefined + + if (!isFilterNeeded) { + if (filter) imageSource.filters?.splice(filterIndex, 1) + return + } + + if (!imageSource.filters) imageSource.filters = [] + + if (!filter) { + filter = { + target: "type", + operator: "is", + value: [] as string[], + } + imageSource.filters.push(filter) + } + filter.value = [] as string[] + + if (showImages) filter.value.push("image") + if (showVideos) filter.value.push("video") + + this.showImages = showImages + this.showVideos = showVideos + }) + this.unwatchFns.push(unsub) + }) + this.watchProxy((get) => { const source = get(this.state.imageSource) diff --git a/src/dtProjects/state/scanner.ts b/src/dtProjects/state/scanner.ts index 2c0114f..4163343 100644 --- a/src/dtProjects/state/scanner.ts +++ b/src/dtProjects/state/scanner.ts @@ -1,6 +1,7 @@ import { exists, stat } from "@tauri-apps/plugin-fs" -import { pdb } from "@/commands" +import { type ProjectExtra, pdb } from "@/commands" import type { JobCallback } from "@/utils/container/queue" +import { getRefreshModelsJob } from "./models" import { type DTPJob, type DTPJobSpec, @@ -9,7 +10,6 @@ import { type WatchFoldersChangedPayload, } from "./types" import type { WatchFolderState } from "./watchFolders" -import { getRefreshModelsJob } from "./models" class ScannerService extends DTPStateService { constructor() { @@ -380,18 +380,20 @@ function getProjectJob( callback, execute: async (data: string[], container) => { container.services.uiState.setImportLock(true) + const projects = [] as ProjectExtra[] for (const p of data) { try { - await pdb.addProject(p) + const project = await pdb.addProject(p) + projects.push(project) } catch (e) { console.error(e) } } - for (const p of data) { + for (const p of projects) { try { - const stats = await getProjectStats(p) + const stats = await getProjectStats(p.path) if (!stats || stats === "dne") continue - await pdb.scanProject(p, false, stats.size, stats.mtime) + await pdb.scanProject(p.path, false, stats.size, stats.mtime) } catch (e) { console.error(e) } diff --git a/src/dtProjects/state/search.ts b/src/dtProjects/state/search.ts index 09a0d3d..96e9ccc 100644 --- a/src/dtProjects/state/search.ts +++ b/src/dtProjects/state/search.ts @@ -1,6 +1,7 @@ import { useMemo } from "react" import { proxy, useSnapshot } from "valtio" import type { Model } from "@/commands" +import type { MediaType, SamplerType } from '@/types' import { arrayIfOnly } from "@/utils/helpers" import { type FilterValueSelector, @@ -8,7 +9,6 @@ import { targetCollection, } from "../controlPane/filters/collections" import { DTPStateController } from "./types" -import { SamplerType } from '@/types' export type SearchControllerState = { searchInput: string @@ -42,7 +42,7 @@ export type BackendFilter = { } export type ContentType = "depth" | "pose" | "color" | "custom" | "scribble" | "shuffle" -export type FilterValue = number | ContentType[] | Model | SamplerType +export type FilterValue = number | ContentType[] | Model | SamplerType | MediaType /** * Handles state for building search queries. diff --git a/src/dtProjects/state/uiState.ts b/src/dtProjects/state/uiState.ts index ec1b5f5..be176fa 100644 --- a/src/dtProjects/state/uiState.ts +++ b/src/dtProjects/state/uiState.ts @@ -36,6 +36,8 @@ export type UIControllerState = { isSettingsOpen: boolean isGridInert: boolean importLock: boolean + showVideos: boolean + showImages: boolean } type Handler = (payload: T) => void @@ -59,6 +61,8 @@ export class UIController extends DTPStateController { isSettingsOpen: false, isGridInert: false, importLock: false, + showVideos: false, + showImages: false, }) constructor() { @@ -79,6 +83,14 @@ export class UIController extends DTPStateController { } } + setShowVideos(show: boolean) { + this.state.showVideos = show + } + + setShowImages(show: boolean) { + this.state.showImages = show + } + setSelectedTab(tab: "projects" | "search", focusElement?: string) { this.state.selectedTab = tab this.state.shouldFocus = focusElement @@ -94,8 +106,21 @@ export class UIController extends DTPStateController { this.state.isGridInert = inert ?? !this.state.isGridInert } + _importLockPromise = Promise.resolve() + _importLockResolver: (() => void) | null = null + get importLockPromise() { + return this._importLockPromise + } + /** show/hide the import lock */ setImportLock(lock: boolean) { this.state.importLock = lock + if (lock) { + this._importLockPromise = new Promise((resolve) => { + this._importLockResolver = resolve + }) + } else { + this._importLockResolver?.() + } } async showDetailsOverlay(item: ImageExtra) { diff --git a/src/scratch/Reactive.tsx b/src/scratch/Reactive.tsx deleted file mode 100644 index 3cd695e..0000000 --- a/src/scratch/Reactive.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { Box, Button, Grid } from "@chakra-ui/react" -import { proxy, useSnapshot } from "valtio" -import { CheckRoot, Panel } from "@/components" -import { computed } from "valtio-reactive" - -const store = proxy({ - a: 0, - b: 0, - c: 0, -}) - -function log(msg: string) { - console.log(msg) -} - -const preSum = computed({ - a: () => { - const { a, b, c } = store - return { a, b, c } - }, -}) - -const preDoubleA = computed({ - a: () => store.a, -}) - -const something = computed({ - sum: () => { - log("sum computed") - return preSum.a + preSum.b + preSum.c - }, - doubleA: () => { - log("doubleA computed") - return preDoubleA.a * 2 - }, -}) - -function Empty() { - const snap = useSnapshot(store) - const snap2 = useSnapshot(something) - // const snaplog = useSnapshot(logproxy) - console.log(snap2) - return ( - - - - - {snap.a} - {snap2.sum} - - {snap.b} - - - {snap.c} - - - - {/* {snaplog.log.join("\n")} */} - - - ) -} - -export default Empty diff --git a/src/scratch/Vid.tsx b/src/scratch/Vid.tsx new file mode 100644 index 0000000..b6d89f1 --- /dev/null +++ b/src/scratch/Vid.tsx @@ -0,0 +1,73 @@ +import { Box, Button, Grid } from "@chakra-ui/react" +import { listen } from "@tauri-apps/api/event" +import { useEffect } from "react" +import { proxy, useSnapshot } from "valtio" +import * as vid from "@/commands/vid" +import { CheckRoot, Panel } from "@/components" + +const store = proxy({ + a: undefined as boolean | undefined, + progress: 0, + total: 0, + received: 0, + downloadResult: undefined, + callResult: "", +}) + +function Empty() { + const snap = useSnapshot(store) + + useEffect(() => { + listen("ffmpeg_download_progress", (e) => { + store.progress = e.payload.progress + store.total = e.payload.total + store.received = e.payload.received + }) + }, []) + + return ( + + + + + {`Check: ${snap.a}`} + + + {`Progress: ${snap.progress}, Total: ${snap.total}, Received: ${snap.received}`} + {`Result: ${snap.downloadResult}`} + + {snap.callResult} + + + + + ) +} + +export default Empty diff --git a/src/types.ts b/src/types.ts index e7b84f8..63956f5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -232,4 +232,9 @@ export const SeedModeLabels = [ 'Torch Cpu Compatible', 'Scale Alike', 'Nvidia Gpu Compatible', -] \ No newline at end of file +] + +export enum MediaType { + Image = 0, + Video = 1, +} \ No newline at end of file From ed7041d9e28fefc56d4a337868a256353e6b8946 Mon Sep 17 00:00:00 2001 From: Kelly Jerrell Date: Tue, 20 Jan 2026 20:21:55 -0700 Subject: [PATCH 05/70] big checkin --- src-tauri/src/projects_db/commands.rs | 43 +- src-tauri/src/projects_db/dt_project.rs | 81 +- src-tauri/src/projects_db/dtm_dtproject.rs | 2 +- src-tauri/src/projects_db/dtos/image.rs | 57 + src-tauri/src/projects_db/dtos/mod.rs | 6 + src-tauri/src/projects_db/dtos/model.rs | 12 + src-tauri/src/projects_db/dtos/project.rs | 39 + src-tauri/src/projects_db/dtos/tensor.rs | 210 ++ src-tauri/src/projects_db/dtos/text.rs | 30 + .../src/projects_db/dtos/watch_folder.rs | 24 + src-tauri/src/projects_db/filters.rs | 49 - src-tauri/src/projects_db/metadata.rs | 2 +- src-tauri/src/projects_db/mod.rs | 6 +- src-tauri/src/projects_db/projects_db.rs | 210 +- src-tauri/src/projects_db/tensor_history.rs | 252 +- src-tauri/src/projects_db/tensors.rs | 4 +- src-tauri/src/projects_db/text_history.rs | 28 +- .../src/projects_db/text_history_sample.json | 2836 +++++++++++++++++ src-tauri/src/vid.rs | 2 +- src/App.tsx | 2 +- src/commands/projects.ts | 711 ++--- src/components/FrameCountIndicator.tsx | 86 + src/components/IconToggle.tsx | 47 +- src/components/VideoFrames.tsx | 108 - src/components/video/Seekbar.tsx | 109 + src/components/video/Video.tsx | 22 + src/components/video/VideoImage.tsx | 41 + src/components/video/context.ts | 113 + src/components/video/hooks.ts | 62 + src/dtProjects/controlPane/SearchPanel.tsx | 23 +- .../controlPane/filters/TypeValueSelector.tsx | 63 - .../controlPane/filters/collections.tsx | 6 - .../detailsOverlay/DetailsImages.tsx | 27 +- .../detailsOverlay/DetailsOverlay.tsx | 22 +- src/dtProjects/imagesList/ImagesList.tsx | 65 +- src/dtProjects/imagesList/StatusBar.tsx | 13 +- src/dtProjects/state/context.tsx | 2 +- src/dtProjects/state/details.ts | 7 +- src/dtProjects/state/images.ts | 92 +- src/dtProjects/state/overview.md | 53 + src/dtProjects/state/projects.ts | 6 +- src/dtProjects/state/uiState.ts | 15 +- src/dtProjects/types.ts | 78 +- src/generated/commands.ts | 142 + src/generated/types.ts | 339 ++ src/hooks/useGetContext.tsx | 21 + src/scratch/EmptyDesign.tsx | 14 + src/scratch/{Empty.tsx => EmptyValtio.tsx} | 0 src/scratch/FilmStrip.tsx | 85 + src/theme/theme.ts | 416 +-- src/utils/helpers.ts | 4 + tauri-codegen.toml | 19 + 52 files changed, 5263 insertions(+), 1443 deletions(-) create mode 100644 src-tauri/src/projects_db/dtos/image.rs create mode 100644 src-tauri/src/projects_db/dtos/mod.rs create mode 100644 src-tauri/src/projects_db/dtos/model.rs create mode 100644 src-tauri/src/projects_db/dtos/project.rs create mode 100644 src-tauri/src/projects_db/dtos/tensor.rs create mode 100644 src-tauri/src/projects_db/dtos/text.rs create mode 100644 src-tauri/src/projects_db/dtos/watch_folder.rs create mode 100644 src-tauri/src/projects_db/text_history_sample.json create mode 100644 src/components/FrameCountIndicator.tsx delete mode 100644 src/components/VideoFrames.tsx create mode 100644 src/components/video/Seekbar.tsx create mode 100644 src/components/video/Video.tsx create mode 100644 src/components/video/VideoImage.tsx create mode 100644 src/components/video/context.ts create mode 100644 src/components/video/hooks.ts delete mode 100644 src/dtProjects/controlPane/filters/TypeValueSelector.tsx create mode 100644 src/dtProjects/state/overview.md create mode 100644 src/generated/commands.ts create mode 100644 src/generated/types.ts create mode 100644 src/hooks/useGetContext.tsx create mode 100644 src/scratch/EmptyDesign.tsx rename src/scratch/{Empty.tsx => EmptyValtio.tsx} (100%) create mode 100644 src/scratch/FilmStrip.tsx create mode 100644 tauri-codegen.toml diff --git a/src-tauri/src/projects_db/commands.rs b/src-tauri/src/projects_db/commands.rs index 3e20b6e..a13ac50 100644 --- a/src-tauri/src/projects_db/commands.rs +++ b/src-tauri/src/projects_db/commands.rs @@ -2,11 +2,18 @@ use serde_json::Value; use tauri::Emitter; use crate::projects_db::{ - dt_project::{ProjectRef, TensorHistoryExtra, TensorRaw}, + DTProject, ProjectsDb, + dt_project::ProjectRef, + dtos::{ + image::{ListImagesResult, ListImagesOptions}, + project::ProjectExtra, + model::ModelExtra, + watch_folder::WatchFolderDTO, + tensor::{TensorHistoryClip, TensorHistoryExtra, TensorRaw, TensorSize, TensorHistoryImport}, + text::TextHistoryNode as TextHistoryNodeDTO, + }, filters::ListImagesFilter, - projects_db::{ListImagesResult, ModelExtra, ProjectExtra}, - tensors::decode_tensor, - DTProject, ProjectsDb, TensorHistoryImport, + tensors::decode_tensor }; #[derive(serde::Serialize, Clone)] @@ -90,7 +97,7 @@ pub async fn projects_db_project_remove( #[tauri::command] pub async fn projects_db_project_list( app_handle: tauri::AppHandle, -) -> Result, String> { +) -> Result, String> { let pdb = ProjectsDb::get_or_init(&app_handle).await?; let projects = pdb.list_projects().await.unwrap(); Ok(projects) @@ -116,8 +123,8 @@ pub async fn projects_db_project_scan( app: tauri::AppHandle, path: String, full_scan: Option, - filesize: Option, - modified: Option, + _filesize: Option, + _modified: Option, ) -> Result { let pdb = ProjectsDb::get_or_init(&app).await?; // let update = |images_scanned: i32, images_total: i32| { @@ -142,7 +149,7 @@ pub async fn projects_db_project_scan( match result { Ok((_id, total)) => { let project = pdb - .update_project(&path, filesize, modified) + .update_project(&path, _filesize, _modified) .await .map_err(|e| e.to_string())?; @@ -190,9 +197,11 @@ pub async fn projects_db_image_list( take: Option, skip: Option, count: Option, + show_video: Option, + show_image: Option, ) -> Result { let projects_db = ProjectsDb::get_or_init(&app).await?; - let opts = super::projects_db::ListImagesOptions { + let opts = ListImagesOptions { project_ids, search, filters, @@ -201,6 +210,8 @@ pub async fn projects_db_image_list( take, skip, count, + show_video, + show_image, }; Ok(projects_db.list_images(opts).await.unwrap()) @@ -210,7 +221,7 @@ pub async fn projects_db_image_list( pub async fn projects_db_get_clip( app_handle: tauri::AppHandle, image_id: i64, -) -> Result, String> { +) -> Result, String> { let projects_db = ProjectsDb::get_or_init(&app_handle).await?; projects_db.get_clip(image_id).await } @@ -225,7 +236,7 @@ pub async fn projects_db_image_rebuild_fts(app: tauri::AppHandle) -> Result<(), #[tauri::command] pub async fn projects_db_watch_folder_list( app: tauri::AppHandle, -) -> Result, String> { +) -> Result, String> { let projects_db = ProjectsDb::get_or_init(&app).await?; Ok(projects_db.list_watch_folders().await.unwrap()) } @@ -236,7 +247,7 @@ pub async fn projects_db_watch_folder_add( path: String, item_type: entity::enums::ItemType, recursive: bool, -) -> Result { +) -> Result { let projects_db = ProjectsDb::get_or_init(&app).await?; let result = projects_db .add_watch_folder(&path, item_type, recursive) @@ -262,10 +273,10 @@ pub async fn projects_db_watch_folder_remove( #[tauri::command] pub async fn projects_db_watch_folder_update( app: tauri::AppHandle, - id: i32, + id: i64, recursive: Option, last_updated: Option, -) -> Result { +) -> Result { let projects_db = ProjectsDb::get_or_init(&app).await?; let result = projects_db .update_watch_folder(id, recursive, last_updated) @@ -322,7 +333,7 @@ pub async fn dt_project_get_tensor_history( #[tauri::command] pub async fn dt_project_get_text_history( project_file: String, -) -> Result, String> { +) -> Result, String> { let project = DTProject::get(&project_file).await.unwrap(); Ok(project.get_text_history().await.unwrap()) } @@ -364,7 +375,7 @@ pub async fn dt_project_get_tensor_size( project_id: Option, project_path: Option, tensor_id: String, -) -> Result { +) -> Result { let project = get_project(app, project_path, project_id).await.unwrap(); let tensor = project.get_tensor_size(&tensor_id).await.unwrap(); Ok(tensor) diff --git a/src-tauri/src/projects_db/dt_project.rs b/src-tauri/src/projects_db/dt_project.rs index 26a0359..cdf1234 100644 --- a/src-tauri/src/projects_db/dt_project.rs +++ b/src-tauri/src/projects_db/dt_project.rs @@ -1,5 +1,10 @@ use crate::projects_db::{ - tensor_history::TensorHistoryNode, TensorHistoryImport, TextHistory, TextHistoryNode, + TextHistory, + dtos::{ + tensor::{TensorHistoryClip, TensorHistoryNode, TensorHistoryExtra, TensorRaw, TensorSize, TensorHistoryImport}, + project::DTProjectInfo, + text::TextHistoryNode, + }, }; use moka::future::Cache; use once_cell::sync::Lazy; @@ -374,7 +379,7 @@ impl DTProject { pub async fn get_histories_from_clip( &self, node_id: i64, - ) -> Result, Error> { + ) -> Result, Error> { self.check_table(&DTProjectTable::TensorHistory).await?; // get_history_full @@ -384,7 +389,20 @@ impl DTProject { let num_frames = history.history.num_frames; println!("num_frames: {}, {}", num_frames, self.path); // and return get_histories(node_id, num_frames) - self.get_histories(node_id, num_frames as i64).await + // self.get_histories(node_id, num_frames as i64).await + + let items: Vec = query(CLIP_QUERY) + .bind(node_id) + .bind(node_id + num_frames as i64) + .map(|row: SqliteRow| self.map_clip(row)) + .fetch_all(&self.pool) + .await?; + + Ok(items) + } + + fn map_clip(self: &DTProject, row: SqliteRow) -> TensorHistoryClip { + TensorHistoryClip::new(row.get(0), row.get(1), row.get(2)).unwrap() } pub async fn get_history_full(&self, row_id: i64) -> Result { @@ -644,48 +662,27 @@ fn import_query(has_moodboard: bool) -> String { ) } -pub struct DTProjectInfo { - pub _path: String, - pub _history_count: i64, - pub history_max_id: i64, -} +const CLIP_QUERY: &str = " + SELECT + thn.rowid, + thn.p AS data_blob, + 'tensor_history_' || td_f20.f20 AS tensor_id -#[derive(Debug, Serialize, Clone)] -pub struct TensorHistoryExtra { - pub row_id: i64, - pub lineage: i64, - pub logical_time: i64, - pub tensor_id: Option, - pub mask_id: Option, - pub depth_map_id: Option, - pub scribble_id: Option, - pub pose_id: Option, - pub color_palette_id: Option, - pub custom_id: Option, - pub moodboard_ids: Vec, - pub history: TensorHistoryNode, - pub project_path: String, -} + FROM tensorhistorynode AS thn -#[derive(Debug, Serialize, Clone)] -pub struct TensorRaw { - pub name: String, - pub tensor_type: i64, - pub data_type: i32, - pub format: i32, - pub width: i32, - pub height: i32, - pub channels: i32, - pub dim: Vec, - pub data: Vec, -} + LEFT JOIN tensordata AS td + ON thn.__pk0 = td.__pk0 + AND thn.__pk1 = td.__pk1 + + LEFT JOIN tensordata__f20 as td_f20 + ON td.rowid = td_f20.rowid -#[derive(Debug, Serialize, Clone)] -pub struct TensorSize { - pub width: i32, - pub height: i32, - pub channels: i32, -} + WHERE thn.rowid >= ?1 + AND thn.rowid < ?2 + + GROUP BY thn.rowid, thn.__pk0, thn.__pk1 + ORDER BY thn.rowid; + "; fn full_query_where(where_expr: &str) -> String { format!( diff --git a/src-tauri/src/projects_db/dtm_dtproject.rs b/src-tauri/src/projects_db/dtm_dtproject.rs index fe7faf1..eb2d1b6 100644 --- a/src-tauri/src/projects_db/dtm_dtproject.rs +++ b/src-tauri/src/projects_db/dtm_dtproject.rs @@ -140,7 +140,7 @@ async fn tensor( node: Option, scale: Option, invert: Option, - mask: Option, + _mask: Option, ) -> Result>, String> { let dtp = DTProject::get(project_file) .await diff --git a/src-tauri/src/projects_db/dtos/image.rs b/src-tauri/src/projects_db/dtos/image.rs new file mode 100644 index 0000000..2c32926 --- /dev/null +++ b/src-tauri/src/projects_db/dtos/image.rs @@ -0,0 +1,57 @@ +use crate::projects_db::filters::ListImagesFilter; +use sea_orm::FromQueryResult; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct ListImagesOptions { + pub project_ids: Option>, + pub search: Option, + pub filters: Option>, + pub sort: Option, + pub direction: Option, + pub take: Option, + pub skip: Option, + pub count: Option, + pub show_video: Option, + pub show_image: Option, +} + +#[derive(Debug, Serialize)] +pub struct ListImagesResult { + pub counts: Option>, + pub images: Option>, + pub total: u64, +} + +#[derive(Debug, FromQueryResult, Serialize)] +pub struct ImageCount { + pub project_id: i64, + pub count: i64, +} + +#[derive(Debug, FromQueryResult, Serialize)] +pub struct ImageExtra { + pub id: i64, + pub project_id: i64, + pub model_id: Option, + pub model_file: Option, + pub prompt: String, + pub negative_prompt: String, + pub num_frames: Option, + pub preview_id: i64, + pub node_id: i64, + pub has_depth: bool, + pub has_pose: bool, + pub has_color: bool, + pub has_custom: bool, + pub has_scribble: bool, + pub has_shuffle: bool, + pub start_width: i32, + pub start_height: i32, +} + +#[derive(Debug, Serialize)] +pub struct Paged { + pub items: Vec, + pub total: u64, +} \ No newline at end of file diff --git a/src-tauri/src/projects_db/dtos/mod.rs b/src-tauri/src/projects_db/dtos/mod.rs new file mode 100644 index 0000000..0ff5cf0 --- /dev/null +++ b/src-tauri/src/projects_db/dtos/mod.rs @@ -0,0 +1,6 @@ +pub mod project; +pub mod image; +pub mod watch_folder; +pub mod model; +pub mod tensor; +pub mod text; \ No newline at end of file diff --git a/src-tauri/src/projects_db/dtos/model.rs b/src-tauri/src/projects_db/dtos/model.rs new file mode 100644 index 0000000..e796776 --- /dev/null +++ b/src-tauri/src/projects_db/dtos/model.rs @@ -0,0 +1,12 @@ +pub use entity::enums::ModelType; +use serde::Serialize; + +#[derive(Debug, Serialize, Clone)] +pub struct ModelExtra { + pub id: i64, + pub model_type: ModelType, + pub filename: String, + pub name: Option, + pub version: Option, + pub count: i64, +} diff --git a/src-tauri/src/projects_db/dtos/project.rs b/src-tauri/src/projects_db/dtos/project.rs new file mode 100644 index 0000000..d71f790 --- /dev/null +++ b/src-tauri/src/projects_db/dtos/project.rs @@ -0,0 +1,39 @@ +use entity::projects; +use sea_orm::FromQueryResult; +use serde::Serialize; + +#[derive(Debug, FromQueryResult, Serialize)] +pub struct ProjectExtra { + pub id: i64, + pub fingerprint: String, + pub path: String, + pub image_count: Option, + pub last_id: Option, + pub filesize: Option, + pub modified: Option, + pub missing_on: Option, + pub excluded: bool, +} + +#[derive(Debug, Serialize, Clone)] +pub struct DTProjectInfo { + pub _path: String, + pub _history_count: i64, + pub history_max_id: i64, +} + +impl From for ProjectExtra { + fn from(m: projects::Model) -> Self { + Self { + id: m.id, + fingerprint: m.fingerprint, + path: m.path, + image_count: None, + last_id: None, + filesize: m.filesize, + modified: m.modified, + missing_on: m.missing_on, + excluded: m.excluded, + } + } +} diff --git a/src-tauri/src/projects_db/dtos/tensor.rs b/src-tauri/src/projects_db/dtos/tensor.rs new file mode 100644 index 0000000..05bfc9c --- /dev/null +++ b/src-tauri/src/projects_db/dtos/tensor.rs @@ -0,0 +1,210 @@ + +use crate::projects_db::tensor_history_mod::{Control, LoRA}; +use chrono::NaiveDateTime; + +#[derive(serde::Serialize, Debug, Clone)] +pub struct ModelAndWeight { + pub model: String, + pub weight: f32, +} + +#[derive(serde::Serialize, Debug)] +pub struct TensorHistoryImport { + pub lineage: i64, + pub logical_time: i64, + pub tensor_id: String, + pub width: u16, + pub height: u16, + pub seed: u32, + pub steps: u32, + pub guidance_scale: f32, + pub strength: f32, + pub model: String, + pub wall_clock: Option, + pub sampler: i8, + pub hires_fix: bool, + pub upscaler: Option, + pub generated: bool, + pub controls: Vec, + pub loras: Vec, + pub preview_id: i64, + pub refiner_model: Option, + pub refiner_start: f32, + pub shift: f32, + pub tiled_decoding: bool, + pub tiled_diffusion: bool, + pub resolution_dependent_shift: bool, + pub tea_cache: bool, + pub prompt: String, + pub negative_prompt: String, + pub clip_id: i64, + pub index_in_a_clip: i32, + pub num_frames: Option, + pub cfg_zero_star: bool, + pub row_id: i64, + pub has_depth: bool, + pub has_pose: bool, + pub has_color: bool, + pub has_custom: bool, + pub has_scribble: bool, + pub has_shuffle: bool, + pub has_mask: bool, + pub text_edits: i64, + pub text_lineage: i64, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct TensorHistoryNode { + pub lineage: i64, + pub logical_time: i64, + pub start_width: u16, + pub start_height: u16, + pub seed: u32, + pub steps: u32, + pub guidance_scale: f32, + pub strength: f32, + pub model: Option, + pub tensor_id: i64, + pub mask_id: i64, + pub wall_clock: Option, + pub text_edits: i64, + pub text_lineage: i64, + pub batch_size: u32, + pub sampler: i8, + pub hires_fix: bool, + pub hires_fix_start_width: u16, + pub hires_fix_start_height: u16, + pub hires_fix_strength: f32, + pub upscaler: Option, + pub scale_factor: u16, + pub depth_map_id: i64, + pub generated: bool, + pub image_guidance_scale: f32, + pub seed_mode: i8, + pub clip_skip: u32, + pub controls: Option>, + pub scribble_id: i64, + pub pose_id: i64, + pub loras: Option>, + pub color_palette_id: i64, + pub mask_blur: f32, + pub custom_id: i64, + pub face_restoration: Option, + pub clip_weight: f32, + pub negative_prompt_for_image_prior: bool, + pub image_prior_steps: u32, + pub data_stored: i32, + pub preview_id: i64, + pub content_offset_x: i32, + pub content_offset_y: i32, + pub scale_factor_by_120: i32, + pub refiner_model: Option, + pub original_image_height: u32, + pub original_image_width: u32, + pub crop_top: i32, + pub crop_left: i32, + pub target_image_height: u32, + pub target_image_width: u32, + pub aesthetic_score: f32, + pub negative_aesthetic_score: f32, + pub zero_negative_prompt: bool, + pub refiner_start: f32, + pub negative_original_image_height: u32, + pub negative_original_image_width: u32, + pub shuffle_data_stored: i32, + pub fps_id: u32, + pub motion_bucket_id: u32, + pub cond_aug: f32, + pub start_frame_cfg: f32, + pub num_frames: u32, + pub mask_blur_outset: i32, + pub sharpness: f32, + pub shift: f32, + pub stage_2_steps: u32, + pub stage_2_cfg: f32, + pub stage_2_shift: f32, + pub tiled_decoding: bool, + pub decoding_tile_width: u16, + pub decoding_tile_height: u16, + pub decoding_tile_overlap: u16, + pub stochastic_sampling_gamma: f32, + pub preserve_original_after_inpaint: bool, + pub tiled_diffusion: bool, + pub diffusion_tile_width: u16, + pub diffusion_tile_height: u16, + pub diffusion_tile_overlap: u16, + pub upscaler_scale_factor: u8, + pub script_session_id: u64, + pub t5_text_encoder: bool, + pub separate_clip_l: bool, + pub clip_l_text: Option, + pub separate_open_clip_g: bool, + pub open_clip_g_text: Option, + pub speed_up_with_guidance_embed: bool, + pub guidance_embed: f32, + pub resolution_dependent_shift: bool, + pub tea_cache_start: i32, + pub tea_cache_end: i32, + pub tea_cache_threshold: f32, + pub tea_cache: bool, + pub separate_t5: bool, + pub t5_text: Option, + pub tea_cache_max_skip_steps: i32, + pub text_prompt: Option, + pub negative_text_prompt: Option, + pub clip_id: i64, + pub index_in_a_clip: i32, + pub causal_inference_enabled: bool, + pub causal_inference: i32, + pub causal_inference_pad: i32, + pub cfg_zero_star: bool, + pub cfg_zero_init_steps: i32, + pub generation_time: f64, + pub reason: i32, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct TensorHistoryExtra { + pub row_id: i64, + pub lineage: i64, + pub logical_time: i64, + pub tensor_id: Option, + pub mask_id: Option, + pub depth_map_id: Option, + pub scribble_id: Option, + pub pose_id: Option, + pub color_palette_id: Option, + pub custom_id: Option, + pub moodboard_ids: Vec, + pub history: TensorHistoryNode, + pub project_path: String, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct TensorRaw { + pub name: String, + pub tensor_type: i64, + pub data_type: i32, + pub format: i32, + pub width: i32, + pub height: i32, + pub channels: i32, + pub dim: Vec, + pub data: Vec, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct TensorSize { + pub width: i32, + pub height: i32, + pub channels: i32, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct TensorHistoryClip { + pub tensor_id: String, + pub preview_id: i64, + pub clip_id: i64, + pub index_in_a_clip: i32, + pub row_id: i64, +} diff --git a/src-tauri/src/projects_db/dtos/text.rs b/src-tauri/src/projects_db/dtos/text.rs new file mode 100644 index 0000000..1267622 --- /dev/null +++ b/src-tauri/src/projects_db/dtos/text.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +pub enum TextType { + PositiveText, + NegativeText, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Default)] +pub struct TextRange { + pub location: i32, + pub length: i32, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TextModification { + pub modification_type: TextType, + pub range: TextRange, + pub text: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TextHistoryNode { + pub lineage: i64, + pub logical_time: i64, + pub start_edits: i64, + pub start_positive_text: String, + pub start_negative_text: String, + pub modifications: Vec, +} diff --git a/src-tauri/src/projects_db/dtos/watch_folder.rs b/src-tauri/src/projects_db/dtos/watch_folder.rs new file mode 100644 index 0000000..28b406c --- /dev/null +++ b/src-tauri/src/projects_db/dtos/watch_folder.rs @@ -0,0 +1,24 @@ +use entity::watch_folders; +pub use entity::enums::ItemType; +use serde::Serialize; + +#[derive(Debug, Serialize, Clone)] +pub struct WatchFolderDTO { + pub id: i64, + pub path: String, + pub recursive: Option, + pub item_type: ItemType, + pub last_updated: Option, +} + +impl From for WatchFolderDTO { + fn from(m: watch_folders::Model) -> Self { + Self { + id: m.id, + path: m.path, + recursive: m.recursive, + item_type: m.item_type, + last_updated: m.last_updated, + } + } +} diff --git a/src-tauri/src/projects_db/filters.rs b/src-tauri/src/projects_db/filters.rs index 1d34b0d..1547921 100644 --- a/src-tauri/src/projects_db/filters.rs +++ b/src-tauri/src/projects_db/filters.rs @@ -10,7 +10,6 @@ impl ListImagesFilterTarget { q: sea_orm::Select, ) -> sea_orm::Select { match self { - ListImagesFilterTarget::Type => apply_type_filter(op, value, q), ListImagesFilterTarget::Model => apply_model_filter(op, value, q), ListImagesFilterTarget::Sampler => apply_sampler_filter(op, value, q), ListImagesFilterTarget::Content => apply_content_filter(op, value, q), @@ -30,53 +29,6 @@ impl ListImagesFilterTarget { } } -fn apply_type_filter( - op: ListImagesFilterOperator, - value: &ListImagesFilterValue, - q: sea_orm::Select, -) -> sea_orm::Select { - use sea_orm::QueryFilter; - use ListImagesFilterOperator::*; - - let types = match value { - ListImagesFilterValue::String(v) => v, - _ => return q, - }; - println!("types: {:?}", types); - // types will have "Image" or "Video" or both - // op image video - // is true false isnot false true images only - // is false true isnot true false videos only - // is true true isnot false false both - // is false false isnot true true none - // so just boil it down to has images and has videos - let op_is_is = matches!(op, Is); - let mut has_images = !op_is_is; - let mut has_videos = !op_is_is; - - for t in types { - match t.as_str() { - "image" => has_images = op_is_is, - "video" => has_videos = op_is_is, - _ => {} - } - } - println!("has_images: {}, has_videos: {}, is_is: {}", has_images, has_videos, op_is_is); - - if has_images && has_videos { - return q; - } - if has_images { - return q.filter(images::Column::NumFrames.is_null()); - } - if has_videos { - return q.filter(images::Column::NumFrames.is_not_null()); - } - - // this is pointless but accurate - q.filter(sea_query::Expr::val(false)) -} - fn apply_model_filter( op: ListImagesFilterOperator, value: &ListImagesFilterValue, @@ -245,7 +197,6 @@ pub enum ListImagesFilterTarget { Height, TextGuidance, Shift, - Type, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/src-tauri/src/projects_db/metadata.rs b/src-tauri/src/projects_db/metadata.rs index 9926d43..6453fb5 100644 --- a/src-tauri/src/projects_db/metadata.rs +++ b/src-tauri/src/projects_db/metadata.rs @@ -2,7 +2,7 @@ use num_enum::TryFromPrimitive; use serde::{Deserialize, Serialize, Serializer}; use serde_json::{json, Value}; -use crate::projects_db::tensor_history::TensorHistoryNode; +use crate::projects_db::dtos::tensor::TensorHistoryNode; /// represents the draw things metadata as it is stored in image metadata. /// contains mostly data from TensorHistoryNode #[derive(Debug, Serialize, Deserialize)] diff --git a/src-tauri/src/projects_db/mod.rs b/src-tauri/src/projects_db/mod.rs index 78da073..eb3b8f0 100644 --- a/src-tauri/src/projects_db/mod.rs +++ b/src-tauri/src/projects_db/mod.rs @@ -4,8 +4,6 @@ mod projects_db; pub use projects_db::ProjectsDb; mod tensor_history; -pub use tensor_history::TensorHistoryImport; - pub mod tensor_history_generated; pub mod commands; @@ -20,9 +18,11 @@ mod tensors; mod metadata; mod text_history; -pub use text_history::{TextHistory, TextHistoryNode, TextModification, TextRange, TextType}; +pub use text_history::TextHistory; pub mod fbs; mod filters; mod search; + +pub mod dtos; \ No newline at end of file diff --git a/src-tauri/src/projects_db/projects_db.rs b/src-tauri/src/projects_db/projects_db.rs index 1a6b1e9..cf2011c 100644 --- a/src-tauri/src/projects_db/projects_db.rs +++ b/src-tauri/src/projects_db/projects_db.rs @@ -7,19 +7,25 @@ use migration::{Migrator, MigratorTrait}; use sea_orm::{ sea_query::{Expr, OnConflict}, ActiveModelTrait, ColumnTrait, ConnectionTrait, Database, DatabaseConnection, DbErr, - EntityTrait, ExprTrait, FromQueryResult, JoinType, Order, PaginatorTrait, QueryFilter, - QueryOrder, QuerySelect, QueryTrait, RelationTrait, Set, + EntityTrait, ExprTrait, JoinType, Order, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, + QueryTrait, RelationTrait, Set, }; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use std::collections::{HashMap, HashSet}; use tauri::Manager; use tokio::sync::OnceCell; use crate::projects_db::{ dt_project::{self, ProjectRef}, - filters::ListImagesFilter, + dtos::{ + image::{ImageCount, ImageExtra, ListImagesOptions, ListImagesResult, Paged}, + model::ModelExtra, + project::ProjectExtra, + tensor::{TensorHistoryClip, TensorHistoryImport}, + watch_folder::WatchFolderDTO, + }, search::{self, process_prompt}, - DTProject, TensorHistoryImport, + DTProject, }; static CELL: OnceCell = OnceCell::const_new(); @@ -169,7 +175,7 @@ impl ProjectsDb { path: &str, filesize: Option, modified: Option, - ) -> Result { + ) -> Result { // Fetch existing project let mut project: projects::ActiveModel = projects::Entity::find() .filter(projects::Column::Path.eq(path)) @@ -188,7 +194,7 @@ impl ProjectsDb { } // Save changes - let updated: projects::Model = project.update(&self.db).await?; + let updated: ProjectExtra = project.update(&self.db).await?.into(); Ok(updated) } @@ -276,9 +282,9 @@ impl ProjectsDb { self.rebuild_images_fts().await?; - match total { - ListImagesResult::Counts(_) => panic!("Unexpected result"), - ListImagesResult::Images(images) => Ok((project.id, images.total)), + match total.images { + Some(_) => Ok((project.id, total.total)), + None => panic!("Unexpected result"), } } @@ -561,6 +567,25 @@ impl ProjectsDb { } } + // Apply show_image / show_video filters + let show_image = opts.show_image.unwrap_or(true); + let show_video = opts.show_video.unwrap_or(true); + + if !show_image && !show_video { + return Ok(ListImagesResult { + counts: None, + images: Some(vec![]), + total: 0, + }); + } + + if show_image && !show_video { + query = query.filter(images::Column::NumFrames.is_null()); + } + else if !show_image && show_video { + query = query.filter(images::Column::NumFrames.is_not_null()); + } + if Some(true) == opts.count { let project_counts = query .select_only() @@ -571,15 +596,22 @@ impl ProjectsDb { .all(&self.db) .await?; - return Ok(ListImagesResult::Counts( - project_counts - .into_iter() - .map(|p| ImageCount { - project_id: p.project_id, - count: p.count, - }) - .collect(), - )); + let mut total: u64 = 0; + let counts = project_counts + .into_iter() + .map(|p| { + total += p.count as u64; + ImageCount { + project_id: p.project_id, + count: p.count, + }}) + .collect(); + + return Ok(ListImagesResult { + counts: Some(counts), + images: None, + total, + }); } if let Some(skip) = opts.skip { @@ -596,19 +628,20 @@ impl ProjectsDb { let count = query.clone().count(&self.db).await?; let result = query.into_model::().all(&self.db).await?; - Ok(ListImagesResult::Images(Paged { - items: result, + Ok(ListImagesResult { + images: Some(result), total: count, - })) + counts: None, + }) } - pub async fn list_watch_folders(&self) -> Result, DbErr> { - let folder = entity::watch_folders::Entity::find() - .into_model() + pub async fn list_watch_folders(&self) -> Result, DbErr> { + let folders = entity::watch_folders::Entity::find() + .order_by_asc(entity::watch_folders::Column::Path) .all(&self.db) .await?; - Ok(folder) + Ok(folders.into_iter().map(|f| f.into()).collect()) } // pub async fn get_project_folder( @@ -631,26 +664,17 @@ impl ProjectsDb { path: &str, item_type: entity::enums::ItemType, recursive: bool, - ) -> Result { - let folder = entity::watch_folders::ActiveModel { + ) -> Result { + let model = entity::watch_folders::ActiveModel { path: Set(path.to_string()), item_type: Set(item_type), recursive: Set(Some(recursive)), ..Default::default() - }; - let folder = entity::watch_folders::Entity::insert(folder) - .on_conflict( - OnConflict::columns([ - entity::watch_folders::Column::Path, - entity::watch_folders::Column::ItemType, - ]) - .value(entity::watch_folders::Column::Path, path) - .to_owned(), - ) - .exec_with_returning(&self.db) - .await?; + } + .insert(&self.db) + .await?; - Ok(folder) + Ok(model.into()) } pub async fn remove_watch_folders(&self, ids: Vec) -> Result<(), DbErr> { @@ -668,23 +692,27 @@ impl ProjectsDb { pub async fn update_watch_folder( &self, - id: i32, + id: i64, recursive: Option, last_updated: Option, - ) -> Result { - let folder = entity::watch_folders::Entity::find_by_id(id) - .one(&self.db) - .await?; - let mut folder: entity::watch_folders::ActiveModel = folder.unwrap().into(); - if let Some(recursive) = recursive { - folder.recursive = Set(Some(recursive)); + ) -> Result { + let mut model: entity::watch_folders::ActiveModel = + entity::watch_folders::Entity::find_by_id(id as i64) + .one(&self.db) + .await? + .unwrap() + .into(); + + if let Some(r) = recursive { + model.recursive = Set(Some(r)); } - if let Some(last_updated) = last_updated { - folder.last_updated = Set(Some(last_updated)); + + if let Some(lu) = last_updated { + model.last_updated = Set(Some(lu)); } - let folder: entity::watch_folders::Model = folder.update(&self.db).await?; - Ok(folder) + let model = model.update(&self.db).await?; + Ok(model.into()) } pub async fn update_exclude(&self, project_id: i32, exclude: bool) -> Result<(), DbErr> { @@ -741,7 +769,7 @@ impl ProjectsDb { Ok(dt_project::DTProject::get(&project_path).await.unwrap()) } - pub async fn get_clip(&self, image_id: i64) -> Result, String> { + pub async fn get_clip(&self, image_id: i64) -> Result, String> { let result: Option<(String, i64)> = images::Entity::find_by_id(image_id) .join(JoinType::InnerJoin, images::Relation::Projects.def()) .select_only() @@ -964,53 +992,6 @@ impl ProjectsDb { } } -#[derive(Debug, FromQueryResult, Serialize)] -pub struct ImageCount { - pub project_id: i64, - pub count: i64, -} - -#[derive(Debug, Serialize)] -pub enum ListImagesResult { - Counts(Vec), - Images(Paged), -} - -#[derive(Debug, Serialize, Clone)] -pub struct ModelExtra { - pub id: i64, - pub model_type: ModelType, - pub filename: String, - pub name: Option, - pub version: Option, - pub count: i64, -} - -#[derive(Debug, Serialize, Clone, Default)] -pub struct ListImagesOptions { - pub project_ids: Option>, - pub search: Option, - pub filters: Option>, - pub sort: Option, - pub direction: Option, - pub take: Option, - pub skip: Option, - pub count: Option, -} - -#[derive(Debug, FromQueryResult, Serialize)] -pub struct ProjectExtra { - pub id: i64, - pub fingerprint: String, - pub path: String, - pub image_count: i64, - pub last_id: Option, - pub filesize: Option, - pub modified: Option, - pub missing_on: Option, - pub excluded: bool, -} - #[derive(Debug)] pub enum MixedError { SeaOrm(DbErr), @@ -1072,33 +1053,6 @@ impl From for String { } } -#[derive(Debug, FromQueryResult, Serialize)] -pub struct ImageExtra { - pub id: i64, - pub project_id: i64, - pub model_id: Option, - pub model_file: Option, - pub prompt: String, - pub negative_prompt: String, - pub num_frames: Option, - pub preview_id: i64, - pub node_id: i64, - pub has_depth: bool, - pub has_pose: bool, - pub has_color: bool, - pub has_custom: bool, - pub has_scribble: bool, - pub has_shuffle: bool, - pub start_width: i32, - pub start_height: i32, -} - -#[derive(Debug, Serialize)] -pub struct Paged { - pub items: Vec, - pub total: u64, -} - type ModelTypeAndFile = (String, ModelType); struct NodeModelWeight { pub node_id: i64, diff --git a/src-tauri/src/projects_db/tensor_history.rs b/src-tauri/src/projects_db/tensor_history.rs index cc856d1..394e2cf 100644 --- a/src-tauri/src/projects_db/tensor_history.rs +++ b/src-tauri/src/projects_db/tensor_history.rs @@ -1,133 +1,10 @@ use chrono::{DateTime, NaiveDateTime}; // use entity::enums::Sampler; // Unused import -// use tauri::Emitter; // Unused import use super::tensor_history_mod::{Control, LoRA}; -// use crate::projects_db::{ListImagesResult, ModelExtra, ProjectExtra}; // Unused import +use crate::projects_db::dtos::tensor::{ModelAndWeight, TensorHistoryImport, TensorHistoryClip, TensorHistoryNode}; use crate::projects_db::tensor_history_generated::root_as_tensor_history_node; -#[derive(serde::Serialize, Debug, Clone)] -pub struct ModelAndWeight { - pub model: String, - pub weight: f32, -} - -#[derive(serde::Serialize, Debug)] -pub struct TensorHistoryImport { - pub lineage: i64, - pub logical_time: i64, - pub tensor_id: String, - pub width: u16, - pub height: u16, - pub seed: u32, - pub steps: u32, - pub guidance_scale: f32, - pub strength: f32, - pub model: String, - pub wall_clock: Option, - pub sampler: i8, - pub hires_fix: bool, - pub upscaler: Option, - pub generated: bool, - pub controls: Vec, - pub loras: Vec, - pub preview_id: i64, - pub refiner_model: Option, - pub refiner_start: f32, - pub shift: f32, - pub tiled_decoding: bool, - pub tiled_diffusion: bool, - pub resolution_dependent_shift: bool, - pub tea_cache: bool, - pub prompt: String, - pub negative_prompt: String, - pub clip_id: i64, - pub index_in_a_clip: i32, - pub num_frames: Option, - pub cfg_zero_star: bool, - // pub image_id: i64, - pub row_id: i64, - pub has_depth: bool, - pub has_pose: bool, - pub has_color: bool, - pub has_custom: bool, - pub has_scribble: bool, - pub has_shuffle: bool, - pub has_mask: bool, - pub text_edits: i64, - pub text_lineage: i64, - // pub batch_size: u32, - // pub hires_fix_start_width: u16, - // pub hires_fix_start_height: u16, - // pub hires_fix_strength: f32, - // pub scale_factor: u16, - // pub image_guidance_scale: f32, - // pub seed_mode: String, - // pub clip_skip: u32, - // pub mask_blur: f32, - // pub face_restoration: Option, - // pub decode_with_attention: bool, - // pub hires_fix_decode_with_attention: bool, - // pub clip_weight: f32, - // pub negative_prompt_for_image_prior: bool, - // pub image_prior_steps: u32, - // pub data_stored: i32, - // pub content_offset_x: i32, - // pub content_offset_y: i32, - // pub scale_factor_by_120: i32, - // pub original_image_height: u32, - // pub original_image_width: u32, - // pub crop_top: i32, - // pub crop_left: i32, - // pub target_image_height: u32, - // pub target_image_width: u32, - // pub aesthetic_score: f32, - // pub negative_aesthetic_score: f32, - // pub zero_negative_prompt: bool, - // pub negative_original_image_height: u32, - // pub negative_original_image_width: u32, - // pub shuffle_data_stored: i32, - // pub fps_id: u32, - // pub motion_bucket_id: u32, - // pub cond_aug: f32, - // pub start_frame_cfg: f32, - // pub num_frames: u32, - // pub mask_blur_outset: i32, - // pub sharpness: f32, - // pub stage_2_steps: u32, - // pub stage_2_cfg: f32, - // pub stage_2_shift: f32, - // pub decoding_tile_width: u16, - // pub decoding_tile_height: u16, - // pub decoding_tile_overlap: u16, - // pub stochastic_sampling_gamma: f32, - // pub preserve_original_after_inpaint: bool, - // pub diffusion_tile_width: u16, - // pub diffusion_tile_height: u16, - // pub diffusion_tile_overlap: u16, - // pub upscaler_scale_factor: u8, - // pub script_session_id: u64, - // pub t5_text_encoder: bool, - // pub separate_clip_l: bool, - // pub clip_l_text: Option, - // pub separate_open_clip_g: bool, - // pub open_clip_g_text: Option, - // pub speed_up_with_guidance_embed: bool, - // pub guidance_embed: f32, - // pub profile_data: Vec, - // pub tea_cache_start: i32, - // pub tea_cache_end: i32, - // pub tea_cache_threshold: f32, - // pub separate_t5: bool, - // pub t5_text: Option, - // pub tea_cache_max_skip_steps: i32, - // pub causal_inference_enabled: bool, - // pub causal_inference: i32, - // pub causal_inference_pad: i32, - // pub cfg_zero_init_steps: i32, - // pub generation_time: f64, - // pub reason: i32, -} impl TensorHistoryImport { pub fn new( @@ -188,7 +65,7 @@ impl TensorHistoryImport { clip_id: node.clip_id(), num_frames: match node.clip_id() >= 0 { true => Some(node.num_frames()), - false => None + false => None, }, guidance_scale: node.guidance_scale(), hires_fix: node.hires_fix(), @@ -226,116 +103,6 @@ impl TensorHistoryImport { * values are exactly as they are when stored in the project files, * with the exception of profile data which is not implemented */ -#[derive(serde::Serialize, Debug, Clone)] -pub struct TensorHistoryNode { - pub lineage: i64, - pub logical_time: i64, - pub start_width: u16, // - pub start_height: u16, // - pub seed: u32, // - pub steps: u32, // - pub guidance_scale: f32, // - pub strength: f32, // - pub model: Option, - pub tensor_id: i64, - pub mask_id: i64, - pub wall_clock: Option, - pub text_edits: i64, - pub text_lineage: i64, - pub batch_size: u32, - pub sampler: i8, // - pub hires_fix: bool, // - pub hires_fix_start_width: u16, // - pub hires_fix_start_height: u16, // - pub hires_fix_strength: f32, // - pub upscaler: Option, // - pub scale_factor: u16, - pub depth_map_id: i64, - pub generated: bool, - pub image_guidance_scale: f32, // - pub seed_mode: i8, - pub clip_skip: u32, - pub controls: Option>, - pub scribble_id: i64, - pub pose_id: i64, - pub loras: Option>, - pub color_palette_id: i64, - pub mask_blur: f32, - pub custom_id: i64, - pub face_restoration: Option, - pub clip_weight: f32, - pub negative_prompt_for_image_prior: bool, - pub image_prior_steps: u32, - pub data_stored: i32, - pub preview_id: i64, - pub content_offset_x: i32, - pub content_offset_y: i32, - pub scale_factor_by_120: i32, - pub refiner_model: Option, - pub original_image_height: u32, - pub original_image_width: u32, - pub crop_top: i32, - pub crop_left: i32, - pub target_image_height: u32, - pub target_image_width: u32, - pub aesthetic_score: f32, - pub negative_aesthetic_score: f32, - pub zero_negative_prompt: bool, - pub refiner_start: f32, - pub negative_original_image_height: u32, - pub negative_original_image_width: u32, - pub shuffle_data_stored: i32, - pub fps_id: u32, - pub motion_bucket_id: u32, - pub cond_aug: f32, - pub start_frame_cfg: f32, - pub num_frames: u32, - pub mask_blur_outset: i32, - pub sharpness: f32, - pub shift: f32, // - pub stage_2_steps: u32, - pub stage_2_cfg: f32, - pub stage_2_shift: f32, - pub tiled_decoding: bool, // - pub decoding_tile_width: u16, - pub decoding_tile_height: u16, - pub decoding_tile_overlap: u16, - pub stochastic_sampling_gamma: f32, - pub preserve_original_after_inpaint: bool, - pub tiled_diffusion: bool, // - pub diffusion_tile_width: u16, - pub diffusion_tile_height: u16, - pub diffusion_tile_overlap: u16, - pub upscaler_scale_factor: u8, - pub script_session_id: u64, - pub t5_text_encoder: bool, - pub separate_clip_l: bool, - pub clip_l_text: Option, - pub separate_open_clip_g: bool, - pub open_clip_g_text: Option, - pub speed_up_with_guidance_embed: bool, - pub guidance_embed: f32, // - pub resolution_dependent_shift: bool, // - // pub profile_data: Option>>, - pub tea_cache_start: i32, - pub tea_cache_end: i32, - pub tea_cache_threshold: f32, - pub tea_cache: bool, // - pub separate_t5: bool, - pub t5_text: Option, - pub tea_cache_max_skip_steps: i32, - pub text_prompt: Option, - pub negative_text_prompt: Option, - pub clip_id: i64, - pub index_in_a_clip: i32, - pub causal_inference_enabled: bool, - pub causal_inference: i32, - pub causal_inference_pad: i32, - pub cfg_zero_star: bool, - pub cfg_zero_init_steps: i32, - pub generation_time: f64, - pub reason: i32, -} impl TryFrom<&[u8]> for TensorHistoryNode { type Error = flatbuffers::InvalidFlatbuffer; @@ -478,3 +245,18 @@ fn wall_clock_to_datetime(value: i64) -> Option { None } } + + +impl TensorHistoryClip { + pub fn new(row_id: i64, blob: &[u8], tensor_id: String) -> Result { + let node = root_as_tensor_history_node(blob) + .map_err(|e| format!("flatbuffers parse error: {:?}", e))?; + Ok(Self { + tensor_id, + preview_id: node.preview_id(), + clip_id: node.clip_id(), + index_in_a_clip: node.index_in_a_clip(), + row_id, + }) + } +} diff --git a/src-tauri/src/projects_db/tensors.rs b/src-tauri/src/projects_db/tensors.rs index 2e34cf9..d799b14 100644 --- a/src-tauri/src/projects_db/tensors.rs +++ b/src-tauri/src/projects_db/tensors.rs @@ -5,13 +5,13 @@ use fpzip_sys::*; use image::GrayImage; use png::{BitDepth, ColorType, Encoder}; + use std::ffi::c_void; use std::io::Cursor; use std::io::Read; -use crate::projects_db::dt_project::TensorRaw; +use crate::projects_db::dtos::tensor::{TensorRaw, TensorHistoryNode}; use crate::projects_db::metadata::DrawThingsMetadata; -use crate::projects_db::tensor_history::TensorHistoryNode; // const HEADER_SIZE: usize = 68; // const FPZIP_MAGIC: u32 = 1012247; diff --git a/src-tauri/src/projects_db/text_history.rs b/src-tauri/src/projects_db/text_history.rs index 26a4997..5ac268b 100644 --- a/src-tauri/src/projects_db/text_history.rs +++ b/src-tauri/src/projects_db/text_history.rs @@ -1,12 +1,8 @@ use super::fbs; -use serde::{Deserialize, Serialize}; +use crate::projects_db::dtos::text::{TextType, TextRange, TextModification, TextHistoryNode}; +use serde::Serialize; use std::sync::Mutex; -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] -pub enum TextType { - PositiveText, - NegativeText, -} impl From for TextType { fn from(fb: fbs::TextType) -> Self { @@ -18,11 +14,6 @@ impl From for TextType { } } -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Default)] -pub struct TextRange { - pub location: i32, - pub length: i32, -} impl From<&fbs::TextRange> for TextRange { fn from(fb: &fbs::TextRange) -> Self { @@ -33,12 +24,6 @@ impl From<&fbs::TextRange> for TextRange { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct TextModification { - pub modification_type: TextType, - pub range: TextRange, - pub text: String, -} impl TryFrom> for TextModification { type Error = flatbuffers::InvalidFlatbuffer; @@ -52,15 +37,6 @@ impl TryFrom> for TextModification { } } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct TextHistoryNode { - pub lineage: i64, - pub logical_time: i64, - pub start_edits: i64, - pub start_positive_text: String, - pub start_negative_text: String, - pub modifications: Vec, -} impl TryFrom<&[u8]> for TextHistoryNode { type Error = flatbuffers::InvalidFlatbuffer; diff --git a/src-tauri/src/projects_db/text_history_sample.json b/src-tauri/src/projects_db/text_history_sample.json new file mode 100644 index 0000000..dab13c3 --- /dev/null +++ b/src-tauri/src/projects_db/text_history_sample.json @@ -0,0 +1,2836 @@ +[ + { + "lineage": 2, + "logical_time": 0, + "start_edits": 0, + "start_positive_text": "", + "start_negative_text": "", + "modifications": [ + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 0 + }, + "text": "war in space, lasers, spacestation in space, people, futuristic, planet outside window, unreal engine, octane render, bokeh, vray, houdini render, quixel megascans, arnold render, 8k uhd, raytracing, cgi, lumen reflections, cgsociety, ultra realistic, 100mm, film photography, dslr, cinema4d, studio quality, film grain, trending on artstation, trending on cgsociety" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 366 + }, + "text": "3d fluffy llama, closeup cute and adorable, cute big circular reflective eyes, long fuzzy fur, pixar render, unreal engine cinematic smooth, intricate detail, cinematic" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 168 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 0 + }, + "text": "O" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "l" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "m" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 14 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 0 + }, + "text": "T" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "f" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "d" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": "o" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "s" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 19 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "l" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "t" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "k" + } + ] + }, + { + "lineage": 2, + "logical_time": 1, + "start_edits": 50, + "start_positive_text": "an ugly turke", + "start_negative_text": "", + "modifications": [ + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "(" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "#" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "1" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": ")" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "2" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 13 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 0 + }, + "text": "s" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": "w" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "m" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "p" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "c" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "t" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "3" + } + ] + }, + { + "lineage": 5, + "logical_time": 1, + "start_edits": 50, + "start_positive_text": "an ugly turke", + "start_negative_text": "", + "modifications": [ + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "(" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "#" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "1" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": ")" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "2" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 12 + }, + "text": "b" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "t" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "f" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "l" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "l" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 21, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 21, + "length": 0 + }, + "text": "3" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": " " + } + ] + }, + { + "lineage": 3, + "logical_time": 1, + "start_edits": 50, + "start_positive_text": "an ugly turke", + "start_negative_text": "", + "modifications": [ + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "(" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "#" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "1" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": ")" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "2" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 13 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 0 + }, + "text": "s" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": "w" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "m" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "p" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "c" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "t" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "3" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "4" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 15 + }, + "text": "h" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": "p" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "p" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "d" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "o" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "s" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "u" + } + ] + }, + { + "lineage": 3, + "logical_time": 2, + "start_edits": 100, + "start_positive_text": "happy dinosaur(#4)", + "start_negative_text": "", + "modifications": [ + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": " " + } + ] + }, + { + "lineage": 4, + "logical_time": 1, + "start_edits": 50, + "start_positive_text": "an ugly turke", + "start_negative_text": "", + "modifications": [ + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "(" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "#" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "1" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": ")" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 19 + }, + "text": "m" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "t" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "m" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "o" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "s" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "t" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "(" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": "#" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "2" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": ")" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": "#" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "7" + } + ] + }, + { + "lineage": 6, + "logical_time": 1, + "start_edits": 50, + "start_positive_text": "an ugly turke", + "start_negative_text": "", + "modifications": [ + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "(" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "#" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "1" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": ")" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "2" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 12 + }, + "text": "b" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "t" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "f" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "l" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "l" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 21, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 21, + "length": 0 + }, + "text": "3" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 9 + }, + "text": "s" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "c" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "l" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 2 + }, + "text": "f" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "s" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "h" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 3 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "5" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "d" + } + ] + }, + { + "lineage": 7, + "logical_time": 1, + "start_edits": 50, + "start_positive_text": "an ugly turke", + "start_negative_text": "", + "modifications": [ + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "(" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "#" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "1" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": ")" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "2" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "3" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 15 + }, + "text": "b" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "t" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "o" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": " " + } + ] + }, + { + "lineage": 8, + "logical_time": 0, + "start_edits": 0, + "start_positive_text": "", + "start_negative_text": "", + "modifications": [ + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 0 + }, + "text": "war in space, lasers, spacestation in space, people, futuristic, planet outside window, unreal engine, octane render, bokeh, vray, houdini render, quixel megascans, arnold render, 8k uhd, raytracing, cgi, lumen reflections, cgsociety, ultra realistic, 100mm, film photography, dslr, cinema4d, studio quality, film grain, trending on artstation, trending on cgsociety" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 366 + }, + "text": "3d fluffy llama, closeup cute and adorable, cute big circular reflective eyes, long fuzzy fur, pixar render, unreal engine cinematic smooth, intricate detail, cinematic" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 168 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 0 + }, + "text": "O" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "l" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "m" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 14 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 0 + }, + "text": "T" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 4, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "f" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "g" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "d" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": "o" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "s" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": "r" + }, + { + "modification_type": "NegativeText", + "range": { + "location": 0, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 1 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 13 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 0, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 1, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 2, + "length": 0 + }, + "text": "b" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 3, + "length": 0 + }, + "text": "r" + } + ] + }, + { + "lineage": 8, + "logical_time": 1, + "start_edits": 50, + "start_positive_text": "a bro", + "start_negative_text": " ", + "modifications": [ + { + "modification_type": "PositiveText", + "range": { + "location": 5, + "length": 0 + }, + "text": "w" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 6, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 7, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 8, + "length": 0 + }, + "text": "f" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 9, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 10, + "length": 0 + }, + "text": "z" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 11, + "length": 0 + }, + "text": "z" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 12, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 13, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 14, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 15, + "length": 0 + }, + "text": "l" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 16, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 17, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 18, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 19, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 20, + "length": 0 + }, + "text": "w" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 21, + "length": 0 + }, + "text": "i" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 22, + "length": 0 + }, + "text": "t" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 23, + "length": 0 + }, + "text": "h" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 24, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 25, + "length": 0 + }, + "text": "o" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 26, + "length": 0 + }, + "text": "n" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 27, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 28, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 29, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 30, + "length": 0 + }, + "text": "y" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 31, + "length": 0 + }, + "text": "e" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 24, + "length": 8 + }, + "text": "" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 24, + "length": 0 + }, + "text": "o" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 25, + "length": 0 + }, + "text": "u" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 26, + "length": 0 + }, + "text": "t" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 27, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 28, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 29, + "length": 0 + }, + "text": " " + }, + { + "modification_type": "PositiveText", + "range": { + "location": 30, + "length": 0 + }, + "text": "f" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 31, + "length": 0 + }, + "text": "a" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 32, + "length": 0 + }, + "text": "c" + }, + { + "modification_type": "PositiveText", + "range": { + "location": 33, + "length": 0 + }, + "text": "e" + } + ] + } +] \ No newline at end of file diff --git a/src-tauri/src/vid.rs b/src-tauri/src/vid.rs index db50bc9..431223d 100644 --- a/src-tauri/src/vid.rs +++ b/src-tauri/src/vid.rs @@ -1,5 +1,5 @@ use sea_orm::{ - ColumnTrait, EntityTrait, JoinType, QuerySelect, RelationTrait, + EntityTrait, JoinType, QuerySelect, RelationTrait, }; use std::fs; use std::process::Command; diff --git a/src/App.tsx b/src/App.tsx index 0b6f436..645af22 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -179,7 +179,7 @@ const views = { vid: lazy(() => import("./vid/Vid")), library: lazy(() => import("./library/Library")), projects: lazy(() => import("./dtProjects/DTProjects")), - scratch: lazy(() => import("./scratch/Vid")), + scratch: lazy(() => import("./scratch/FilmStrip")), } function getView(view: string) { diff --git a/src/commands/projects.ts b/src/commands/projects.ts index dbc3b36..066ddb3 100644 --- a/src/commands/projects.ts +++ b/src/commands/projects.ts @@ -1,421 +1,396 @@ import { invoke } from "@tauri-apps/api/core" import type { ProjectState } from "@/dtProjects/state/projects" +import type { + ImageExtra, + ListImagesResult, + ListImagesResultCounts, + ListImagesResultImages, + ProjectExtra, +} from "@/generated/types" import type { DrawThingsConfig, DrawThingsConfigGrouped } from "@/types" import type { ImagesSource as ListImagesOpts } from "../dtProjects/types" -// -------------------- -// Type definitions -// -------------------- - -export type ProjectExtra = { - id: number - path: string - image_count: number - last_id?: number - filesize: number - modified: number - excluded: boolean +export type { + ImageExtra, + ProjectExtra, + ListImagesResult, + ListImagesResultCounts, + ListImagesResultImages, } -export type ImageExtra = { - id: number - project_id: number - model_id?: number - model_file?: string - prompt?: string - negative_prompt?: string - preview_id: number - clip_id: number - num_frames: number - node_id: number - has_depth: boolean - has_pose: boolean - has_color: boolean - has_custom: boolean - has_scribble: boolean - has_shuffle: boolean - start_width: number - start_height: number - } - export type Control = { - file?: string - weight: number - guidance_start: number - guidance_end: number - no_prompt: boolean - global_average_pooling: boolean - down_sampling_rate: number - control_mode: string - target_blocks?: string[] - input_override: string + file?: string + weight: number + guidance_start: number + guidance_end: number + no_prompt: boolean + global_average_pooling: boolean + down_sampling_rate: number + control_mode: string + target_blocks?: string[] + input_override: string } export type LoRA = { - file?: string - weight: number - mode: string + file?: string + weight: number + mode: string } export type TensorHistoryNode = { - lineage: number - logical_time: number - start_width: number - start_height: number - seed: number - steps: number - guidance_scale: number - strength: number - model?: string - tensor_id: number - mask_id: number - wall_clock?: string - text_edits: number - text_lineage: number - batch_size: number - sampler: number - hires_fix: boolean - hires_fix_start_width: number - hires_fix_start_height: number - hires_fix_strength: number - upscaler?: string - scale_factor: number - depth_map_id: number - generated: boolean - image_guidance_scale: number - seed_mode: number - clip_skip: number - controls?: Control[] - scribble_id: number - pose_id: number - loras?: LoRA[] - color_palette_id: number - mask_blur: number - custom_id: number - face_restoration?: string - clip_weight: number - negative_prompt_for_image_prior: boolean - image_prior_steps: number - data_stored: number - preview_id: number - content_offset_x: number - content_offset_y: number - scale_factor_by_120: number - refiner_model?: string - original_image_height: number - original_image_width: number - crop_top: number - crop_left: number - target_image_height: number - target_image_width: number - aesthetic_score: number - negative_aesthetic_score: number - zero_negative_prompt: boolean - refiner_start: number - negative_original_image_height: number - negative_original_image_width: number - shuffle_data_stored: number - fps_id: number - motion_bucket_id: number - cond_aug: number - start_frame_cfg: number - num_frames: number - mask_blur_outset: number - sharpness: number - shift: number - stage_2_steps: number - stage_2_cfg: number - stage_2_shift: number - tiled_decoding: boolean - decoding_tile_width: number - decoding_tile_height: number - decoding_tile_overlap: number - stochastic_sampling_gamma: number - preserve_original_after_inpaint: boolean - tiled_diffusion: boolean - diffusion_tile_width: number - diffusion_tile_height: number - diffusion_tile_overlap: number - upscaler_scale_factor: number - script_session_id: number - t5_text_encoder: boolean - separate_clip_l: boolean - clip_l_text?: string - separate_open_clip_g: boolean - open_clip_g_text?: string - speed_up_with_guidance_embed: boolean - guidance_embed: number - resolution_dependent_shift: boolean - tea_cache_start: number - tea_cache_end: number - tea_cache_threshold: number - tea_cache: boolean - separate_t5: boolean - t5_text?: string - tea_cache_max_skip_steps: number - text_prompt?: string - negative_text_prompt?: string - clip_id: number - index_in_a_clip: number - causal_inference_enabled: boolean - causal_inference: number - causal_inference_pad: number - cfg_zero_star: boolean - cfg_zero_init_steps: number - generation_time: number - reason: number + lineage: number + logical_time: number + start_width: number + start_height: number + seed: number + steps: number + guidance_scale: number + strength: number + model?: string + tensor_id: number + mask_id: number + wall_clock?: string + text_edits: number + text_lineage: number + batch_size: number + sampler: number + hires_fix: boolean + hires_fix_start_width: number + hires_fix_start_height: number + hires_fix_strength: number + upscaler?: string + scale_factor: number + depth_map_id: number + generated: boolean + image_guidance_scale: number + seed_mode: number + clip_skip: number + controls?: Control[] + scribble_id: number + pose_id: number + loras?: LoRA[] + color_palette_id: number + mask_blur: number + custom_id: number + face_restoration?: string + clip_weight: number + negative_prompt_for_image_prior: boolean + image_prior_steps: number + data_stored: number + preview_id: number + content_offset_x: number + content_offset_y: number + scale_factor_by_120: number + refiner_model?: string + original_image_height: number + original_image_width: number + crop_top: number + crop_left: number + target_image_height: number + target_image_width: number + aesthetic_score: number + negative_aesthetic_score: number + zero_negative_prompt: boolean + refiner_start: number + negative_original_image_height: number + negative_original_image_width: number + shuffle_data_stored: number + fps_id: number + motion_bucket_id: number + cond_aug: number + start_frame_cfg: number + num_frames: number + mask_blur_outset: number + sharpness: number + shift: number + stage_2_steps: number + stage_2_cfg: number + stage_2_shift: number + tiled_decoding: boolean + decoding_tile_width: number + decoding_tile_height: number + decoding_tile_overlap: number + stochastic_sampling_gamma: number + preserve_original_after_inpaint: boolean + tiled_diffusion: boolean + diffusion_tile_width: number + diffusion_tile_height: number + diffusion_tile_overlap: number + upscaler_scale_factor: number + script_session_id: number + t5_text_encoder: boolean + separate_clip_l: boolean + clip_l_text?: string + separate_open_clip_g: boolean + open_clip_g_text?: string + speed_up_with_guidance_embed: boolean + guidance_embed: number + resolution_dependent_shift: boolean + tea_cache_start: number + tea_cache_end: number + tea_cache_threshold: number + tea_cache: boolean + separate_t5: boolean + t5_text?: string + tea_cache_max_skip_steps: number + text_prompt?: string + negative_text_prompt?: string + clip_id: number + index_in_a_clip: number + causal_inference_enabled: boolean + causal_inference: number + causal_inference_pad: number + cfg_zero_star: boolean + cfg_zero_init_steps: number + generation_time: number + reason: number } export type TensorHistoryExtra = { - row_id: number - lineage: number - logical_time: number - tensor_id?: string - mask_id?: string - depth_map_id?: string - scribble_id?: string - pose_id?: string - color_palette_id?: string - custom_id?: string - moodboard_ids: string[] - history: TensorHistoryNode - project_path: string + row_id: number + lineage: number + logical_time: number + tensor_id?: string + mask_id?: string + depth_map_id?: string + scribble_id?: string + pose_id?: string + color_palette_id?: string + custom_id?: string + moodboard_ids: string[] + history: TensorHistoryNode + project_path: string } export type DTImageFull = { - id: number - prompt?: string - negativePrompt?: string - model?: Model - project: ProjectState - config: DrawThingsConfig - groupedConfig: DrawThingsConfigGrouped - clipId: number - numFrames: number - node: TensorHistoryNode - images?: { - tensorId?: string - previewId?: number - maskId?: string - depthMapId?: string - scribbleId?: string - poseId?: string - colorPaletteId?: string - customId?: string - moodboardIds?: string[] - } + id: number + prompt?: string + negativePrompt?: string + model?: Model + project: ProjectState + config: DrawThingsConfig + groupedConfig: DrawThingsConfigGrouped + clipId: number + numFrames: number + node: TensorHistoryNode + images?: { + tensorId?: string + previewId?: number + maskId?: string + depthMapId?: string + scribbleId?: string + poseId?: string + colorPaletteId?: string + customId?: string + moodboardIds?: string[] + } } export type ScanProgress = { - projects_scanned: number - projects_total: number - project_path: string - images_scanned: number - images_total: number + projects_scanned: number + projects_total: number + project_path: string + images_scanned: number + images_total: number } export type TensorRaw = { - tensor_type: number - data_type: number - format: number - width: number - height: number - channels: number - dim: ArrayBuffer - data: ArrayBuffer + tensor_type: number + data_type: number + format: number + width: number + height: number + channels: number + dim: ArrayBuffer + data: ArrayBuffer } export type ListImagesOptions = { - projectIds?: number[] - nodeId?: number - sort?: string - direction?: string - model?: number[] - control?: number[] - lora?: number[] - search?: string - take?: number - skip?: number + projectIds?: number[] + nodeId?: number + sort?: string + direction?: string + model?: number[] + control?: number[] + lora?: number[] + search?: string + take?: number + skip?: number } export type WatchFolder = { - id: number - path: string - recursive: boolean - item_type: "Projects" | "ModelInfo" - last_updated?: number | null - } + id: number + path: string + recursive: boolean + item_type: "Projects" | "ModelInfo" + last_updated?: number | null +} // -------------------- // Command wrappers // -------------------- export const pdb = { - // #unused - getImageCount: async (): Promise => invoke("projects_db_image_count"), - - addProject: async (path: string): Promise => - invoke("projects_db_project_add", { path }), - - removeProject: async (path: string): Promise => - invoke("projects_db_project_remove", { path }), - - listProjects: async (): Promise => invoke("projects_db_project_list"), - - scanProject: async ( - path: string, - fullScan = false, - filesize?: number, - modified?: number, - ): Promise => - invoke("projects_db_project_scan", { path, fullScan, filesize, modified }), - - updateExclude: async (id: number, exclude: boolean): Promise => - invoke("projects_db_project_update_exclude", { id, exclude }), - - listImages: async ( - source: MaybeReadonly, - skip: number, - take: number, - ): Promise<{ items: ImageExtra[]; total: number }> => { - const result: Record = await invoke("projects_db_image_list", { - ...source, - skip, - take, - }) - if ("Images" in result) return result.Images as { items: ImageExtra[]; total: number } - else return { items: [], total: 0 } - }, - - getClip: async (imageId: number): Promise => - invoke("projects_db_get_clip", { imageId }), - - /** - * ignores projectIds, returns count of image matches in each project. - */ - listImagesCount: async (source: MaybeReadonly) => { - const opts = { ...source, projectIds: undefined, count: true } - const result: Record = await invoke("projects_db_image_list", opts) - if ("Counts" in result) { - const counts = result.Counts as { project_id: number; count: number }[] - const total = counts.reduce((acc, item) => acc + item.count, 0) - return { counts, total } - } else return { counts: [], total: 0 } - }, - - rebuildIndex: async (): Promise => invoke("projects_db_image_rebuild_fts"), - - watchFolders: { - listAll: async (): Promise => invoke("projects_db_watch_folder_list"), - - add: async ( - path: string, - itemType: "Projects" | "ModelInfo", - recursive: boolean, - ): Promise => - invoke("projects_db_watch_folder_add", { path, itemType, recursive }), - - remove: async (ids: number[] | number): Promise => - invoke("projects_db_watch_folder_remove", { ids: Array.isArray(ids) ? ids : [ids] }), - - update: async ( - id: number, - recursive?: boolean, - lastUpdated?: number, - ): Promise => - invoke("projects_db_watch_folder_update", { id, recursive, lastUpdated }), - }, - - scanModelInfo: async (filePath: string, modelType: ModelType): Promise => - invoke("projects_db_scan_model_info", { filePath, modelType }), - - listModels: async (modelType?: ModelType): Promise => - invoke("projects_db_list_models", { modelType }), + // #unused + getImageCount: async (): Promise => invoke("projects_db_image_count"), + + addProject: async (path: string): Promise => + invoke("projects_db_project_add", { path }), + + removeProject: async (path: string): Promise => + invoke("projects_db_project_remove", { path }), + + listProjects: async (): Promise => invoke("projects_db_project_list"), + + scanProject: async ( + path: string, + fullScan = false, + filesize?: number, + modified?: number, + ): Promise => + invoke("projects_db_project_scan", { path, fullScan, filesize, modified }), + + updateExclude: async (id: number, exclude: boolean): Promise => + invoke("projects_db_project_update_exclude", { id, exclude }), + + listImages: async ( + source: MaybeReadonly, + skip: number, + take: number, + ): Promise => { + const result: ListImagesResultImages = await invoke("projects_db_image_list", { + ...source, + skip, + take, + }) + return result + }, + + getClip: async (imageId: number): Promise => + invoke("projects_db_get_clip", { imageId }), + + /** + * ignores projectIds, returns count of image matches in each project. + */ + listImagesCount: async (source: MaybeReadonly) => { + const opts = { ...source, projectIds: undefined, count: true } + const result: ListImagesResultCounts = await invoke("projects_db_image_list", opts) + return result + }, + + rebuildIndex: async (): Promise => invoke("projects_db_image_rebuild_fts"), + + watchFolders: { + listAll: async (): Promise => invoke("projects_db_watch_folder_list"), + + add: async ( + path: string, + itemType: "Projects" | "ModelInfo", + recursive: boolean, + ): Promise => + invoke("projects_db_watch_folder_add", { path, itemType, recursive }), + + remove: async (ids: number[] | number): Promise => + invoke("projects_db_watch_folder_remove", { ids: Array.isArray(ids) ? ids : [ids] }), + + update: async ( + id: number, + recursive?: boolean, + lastUpdated?: number, + ): Promise => + invoke("projects_db_watch_folder_update", { id, recursive, lastUpdated }), + }, + + scanModelInfo: async (filePath: string, modelType: ModelType): Promise => + invoke("projects_db_scan_model_info", { filePath, modelType }), + + listModels: async (modelType?: ModelType): Promise => + invoke("projects_db_list_models", { modelType }), } export type ModelType = "Model" | "Lora" | "Cnet" | "Upscaler" export type Model = { - id: number - model_type: ModelType - filename: string - name?: string - version?: string - count?: number + id: number + model_type: ModelType + filename: string + name?: string + version?: string + count?: number } export type ModelInfo = { - file: string - name: string - version: string - model_type: ModelType + file: string + name: string + version: string + model_type: ModelType } export type TensorSize = { - width: number - height: number - channels: number + width: number + height: number + channels: number } export const dtProject = { - // #unused - getTensorHistory: async ( - project_file: string, - index: number, - count: number, - ): Promise[]> => - invoke("dt_project_get_tensor_history", { project_file, index, count }), - - // #unused - getThumbHalf: async (project_file: string, thumb_id: number): Promise => - invoke("dt_project_get_thumb_half", { project_file, thumb_id }), - - getHistoryFull: async (projectFile: string, rowId: number): Promise => - invoke("dt_project_get_history_full", { projectFile, rowId }), - - // #unused - getTensorRaw: async ( - projectFile: string, - projectId: number, - tensorId: string, - ): Promise => - invoke("dt_project_get_tensor_raw", { projectFile, projectId, tensorId }), - - getTensorSize: async (project: string | number, tensorId: string): Promise => { - const opts = { - tensorId, - projectId: typeof project === "string" ? undefined : project, - projectFile: typeof project === "string" ? project : undefined, - } - return invoke("dt_project_get_tensor_size", opts) - }, - - decodeTensor: async ( - project: string | number, - tensorId: string, - asPng: boolean, - nodeId?: number, - ): Promise> => { - const opts = { - tensorId, - projectId: typeof project === "string" ? undefined : project, - projectFile: typeof project === "string" ? project : undefined, - asPng, - nodeId, - } - return new Uint8Array(await invoke("dt_project_decode_tensor", opts)) - }, - - getPredecessorCandidates: async ( - projectFile: string, - rowId: number, - lineage: number, - logicalTime: number, - ): Promise => - invoke("dt_project_find_predecessor_candidates", { - projectFile, - rowId, - lineage, - logicalTime, - }), + // #unused + getTensorHistory: async ( + project_file: string, + index: number, + count: number, + ): Promise[]> => + invoke("dt_project_get_tensor_history", { project_file, index, count }), + + // #unused + getThumbHalf: async (project_file: string, thumb_id: number): Promise => + invoke("dt_project_get_thumb_half", { project_file, thumb_id }), + + getHistoryFull: async (projectFile: string, rowId: number): Promise => + invoke("dt_project_get_history_full", { projectFile, rowId }), + + // #unused + getTensorRaw: async ( + projectFile: string, + projectId: number, + tensorId: string, + ): Promise => + invoke("dt_project_get_tensor_raw", { projectFile, projectId, tensorId }), + + getTensorSize: async (project: string | number, tensorId: string): Promise => { + const opts = { + tensorId, + projectId: typeof project === "string" ? undefined : project, + projectFile: typeof project === "string" ? project : undefined, + } + return invoke("dt_project_get_tensor_size", opts) + }, + + decodeTensor: async ( + project: string | number, + tensorId: string, + asPng: boolean, + nodeId?: number, + ): Promise> => { + const opts = { + tensorId, + projectId: typeof project === "string" ? undefined : project, + projectFile: typeof project === "string" ? project : undefined, + asPng, + nodeId, + } + return new Uint8Array(await invoke("dt_project_decode_tensor", opts)) + }, + + getPredecessorCandidates: async ( + projectFile: string, + rowId: number, + lineage: number, + logicalTime: number, + ): Promise => + invoke("dt_project_find_predecessor_candidates", { + projectFile, + rowId, + lineage, + logicalTime, + }), } diff --git a/src/components/FrameCountIndicator.tsx b/src/components/FrameCountIndicator.tsx new file mode 100644 index 0000000..b6b13cc --- /dev/null +++ b/src/components/FrameCountIndicator.tsx @@ -0,0 +1,86 @@ +import { Box, chakra } from "@chakra-ui/react" +import { Fragment } from "react/jsx-runtime" + +const topEdge = 35 +const bottomEdge = 165 +const width = 220 +const height = 200 +const thickness = 15 +const margin = thickness / 2 + 5 + +interface FrameCountIndicatorProps extends ChakraProps { + count?: number + bgColor?: string +} + +function FrameCountIndicator(props: FrameCountIndicatorProps) { + const { count, bgColor, ...restProps } = props + + return ( + + + + + + + {Array.from({ length: 4 }).map((_, i) => ( + + + + + ))} + + {count} + + + + ) +} + +export default FrameCountIndicator diff --git a/src/components/IconToggle.tsx b/src/components/IconToggle.tsx index 4039675..152508a 100644 --- a/src/components/IconToggle.tsx +++ b/src/components/IconToggle.tsx @@ -1,19 +1,10 @@ -import { type Button, chakra, HStack } from "@chakra-ui/react" -import { - type ComponentProps, - createContext, - type ReactNode, - use, - useCallback, - useEffect, -} from "react" -import { proxy, subscribe, useSnapshot } from "valtio" -import { useProxyRef } from "@/hooks/valtioHooks" +import { HStack } from "@chakra-ui/react" +import { type ComponentProps, createContext, type ReactNode, use, useCallback } from "react" import { IconButton, Tooltip } from "." const IconToggleContext = createContext({ value: {} as Record, - onChange: (_value: Record) => {}, + onClick: (_option: string) => {}, }) interface IconToggleProps extends Omit { @@ -26,15 +17,6 @@ interface IconToggleProps extends Omit { function IconToggle(props: IconToggleProps) { const { children, value, requireOne, onChange, ...restProps } = props - // const { snap, state } = useProxyRef(() => ({ entries: {} as Record })) - - // useEffect(() => { - // const unsubscribe = subscribe(state, () => { - // onChange(state.entries) - // }) - // return unsubscribe - // }, [onChange, state]) - const onClick = useCallback( (option: string) => { const entries = { ...value } @@ -63,7 +45,7 @@ function IconToggle(props: IconToggleProps) { [value, onChange, requireOne], ) - const cv = { value, onChange, onClick } + const cv = { value, onClick } return ( @@ -93,7 +75,7 @@ interface TriggerProps extends ComponentProps { function Trigger(props: TriggerProps) { const { children, option, tip, tipText, tipTitle, ...restProps } = props - const { value, onChange, onClick } = use(IconToggleContext) + const { value, onClick } = use(IconToggleContext) // useEffect(() => { // if (!(option in context.entries)) { @@ -120,25 +102,6 @@ function Trigger(props: TriggerProps) { ) } -const TButton = chakra("button", { - base: { - display: "inline-flex", - fontSize: "1.2rem", - paddingInline: 2, - paddingBlock: 1, - }, - variants: { - selected: { - true: { - bgColor: "bg.3", - }, - false: { - bgColor: "bg.deep", - }, - }, - }, -}) - IconToggle.Trigger = Trigger export default IconToggle diff --git a/src/components/VideoFrames.tsx b/src/components/VideoFrames.tsx deleted file mode 100644 index 2593b30..0000000 --- a/src/components/VideoFrames.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { Box } from "@chakra-ui/react" -import { type CSSProperties, useEffect, useRef } from "react" -import type { Snapshot } from "valtio" -import { pdb } from "@/commands" -import urls from "@/commands/urls" -import type { UIControllerState } from "@/dtProjects/state/uiState" -import { useProxyRef } from "@/hooks/valtioHooks" - -interface VideoFramesProps extends ChakraProps { - image: Snapshot - half?: boolean - objectFit?: CSSProperties["objectFit"] -} - -function VideoFrames(props: VideoFramesProps) { - const { image, half, objectFit, ...restProps } = props - - const { state, snap } = useProxyRef(() => ({ - data: [] as string[], - start: 0, - })) - - const getUrl = half ? urls.thumbHalf : urls.thumb - - const containerRef = useRef(null) - const imgRef = useRef(null) - const rafRef = useRef(0) - const frameRef = useRef(0) - - useEffect(() => { - if (!image) return - pdb.getClip(image.id).then(async (data) => { - if (!image) return - if (!imgRef.current) return - state.data = data.map((d) => getUrl(image.project_id, d.preview_id)) - await preloadImages(state.data) - console.log("preloaded") - const animate = (time: DOMHighResTimeStamp) => { - if (!imgRef.current) { - rafRef.current = requestAnimationFrame(animate) - return - } - const t = ((time - state.start) / 1000) * 24 - const frame = Math.floor(t) % state.data.length - if (frame !== frameRef.current) { - frameRef.current = frame - imgRef.current.src = state.data[frame] - // containerRef.current.children[frameRef.current]?.setAttribute( - // "style", - // "display: none", - // ) - // containerRef.current.children[frameRef.current]?.setAttribute( - // "style", - // "display: block", - // ) - } - rafRef.current = requestAnimationFrame(animate) - } - - cancelAnimationFrame(rafRef.current) - - rafRef.current = requestAnimationFrame((time) => { - state.start = time - animate(time) - }) - }) - - return () => cancelAnimationFrame(rafRef.current) - }, [image, state, getUrl]) - - if (!image) return null - - return ( - - {/* {snap.data && image */} - {/* ? snap.data.map((d, i) => ( */} - {"clip"} - {/* )) */} - {/* : "Loading"} */} - - ) -} - -export default VideoFrames - -async function preloadImages(urls: string[]) { - const promises = urls.map((url) => { - return new Promise((resolve, reject) => { - const img = new Image() - img.src = url - img.onload = resolve - img.onerror = reject - }) - }) - await Promise.all(promises) -} diff --git a/src/components/video/Seekbar.tsx b/src/components/video/Seekbar.tsx new file mode 100644 index 0000000..6b61e05 --- /dev/null +++ b/src/components/video/Seekbar.tsx @@ -0,0 +1,109 @@ +import { Box } from "@chakra-ui/react" +import { motion, useMotionValue } from "motion/react" +import { useRef } from "react" +import { useVideoContext } from "./context" + +interface SeekbarProps extends ChakraProps {} + +function Seekbar(props: SeekbarProps) { + const { ...restProps } = props + + const trackRef = useRef(null) + + const { controls, nFrames } = useVideoContext() + + const pointerDownRef = useRef(false) + + // const thumbX = useTransform(controls.frameMv, (frame) => frame * (trackRef.current?.offsetWidth ?? 0)) + + const hoverPos = useMotionValue(0) + + return ( + { + const rect = trackRef.current?.getBoundingClientRect() + if (!rect) return + hoverPos.set(e.clientX - rect.left) + }} + onPointerDown={(e) => { + e.stopPropagation() + pointerDownRef.current = true + e.currentTarget.setPointerCapture(e.pointerId) + const rect = trackRef.current?.getBoundingClientRect() + if (!rect) return + const x = e.clientX - rect.left + const pos = x / (rect.width) + controls.pause() + controls.seek(pos) + }} + onPointerUp={(e) => { + e.stopPropagation() + pointerDownRef.current = false + }} + onPointerMove={(e) => { + e.stopPropagation() + if (!pointerDownRef.current) return + const rect = trackRef.current?.getBoundingClientRect() + if (!rect) return + const x = e.clientX - rect.left + const pos = x / (rect.width) + controls.seek(pos) + }} + {...restProps} + > + + + {/* + + Hello + + */} + + ) +} + +export default Seekbar diff --git a/src/components/video/Video.tsx b/src/components/video/Video.tsx new file mode 100644 index 0000000..e6510ec --- /dev/null +++ b/src/components/video/Video.tsx @@ -0,0 +1,22 @@ +import type { ImageExtra } from "@/generated/types" +import { useCreateVideoContext, VideoContext } from "./context" + +interface VideoFramesProps extends ChakraProps { + image: ImageExtra + half?: boolean + halfFps?: boolean + fps?: number + autoStart?: boolean +} + +function Video(props: VideoFramesProps) { + const { image, half, fps, halfFps, autoStart, children, ...restProps } = props + + const cv = useCreateVideoContext({ image, half, halfFps, fps, autoStart }) + + if (!image) return null + + return {children} +} + +export default Video diff --git a/src/components/video/VideoImage.tsx b/src/components/video/VideoImage.tsx new file mode 100644 index 0000000..5bb8033 --- /dev/null +++ b/src/components/video/VideoImage.tsx @@ -0,0 +1,41 @@ +import { Box } from "@chakra-ui/react" +import type { CSSProperties } from "react" +import { useVideoContext } from "./context" + +interface VideoImageProps extends ChakraProps { + objectFit?: CSSProperties["objectFit"] + clickToPause?: boolean +} + +export function VideoImage(props: VideoImageProps) { + const { objectFit, clickToPause, ...restProps } = props + + const { imgSrc, imgRef, controls } = useVideoContext() + + const handlers = clickToPause + ? { + onPointerDown: (e) => { + e.stopPropagation() + controls.togglePlayPause() + console.log("I got clicked") + }, + onClick: (e) => e.stopPropagation(), + } + : {} + + return ( + + {"clip"} + + ) +} diff --git a/src/components/video/context.ts b/src/components/video/context.ts new file mode 100644 index 0000000..ec15c28 --- /dev/null +++ b/src/components/video/context.ts @@ -0,0 +1,113 @@ +import { createContext, useContext, useEffect, useRef } from "react" +import { pdb } from "@/commands" +import urls from "@/commands/urls" +import type { ImageExtra } from "@/generated/types" +import { useProxyRef } from "@/hooks/valtioHooks" +import { everyNth } from "@/utils/helpers" +import { useFrameAnimation } from "./hooks" + +export type VideoContextType = { + imgRef: React.RefObject + imgSrc: string + callbacksRef: React.RefObject + controls: ReturnType + fps: number + frames: number +} + +export const VideoContext = createContext(null) + +type OnFrameChanged = (frame: number, fps: number, nFrames: number) => void + +export type UseVideoContextOpts = { + image: ImageExtra + half?: boolean + halfFps?: boolean + fps?: number + onFrameChanged?: OnFrameChanged + autoStart?: boolean +} + +export function useCreateVideoContext(opts: UseVideoContextOpts) { + const { image, half, halfFps, fps: fpsProp = 20, onFrameChanged, autoStart } = opts + + const fps = halfFps ? fpsProp / 2 : fpsProp + + const { state, snap } = useProxyRef(() => ({ + data: [] as string[], + start: 0, + })) + + const callbacksRef = useRef([]) + + useEffect(() => { + if (onFrameChanged) callbacksRef.current.push(onFrameChanged) + return () => { + callbacksRef.current = callbacksRef.current.filter((cb) => cb !== onFrameChanged) + } + }, [onFrameChanged]) + + const getUrl = half ? urls.thumbHalf : urls.thumb + const imgSrc = getUrl(image.project_id, image.preview_id) + + const imgRef = useRef(null) + + const controls = useFrameAnimation({ + fps, + nFrames: snap.data.length, + autoStart, + onChange: (frame) => { + if (imgRef.current) imgRef.current.src = state.data[frame] + }, + }) + + useEffect(() => { + if (!image) return + pdb.getClip(image.id).then(async (data) => { + if (!image) return + if (!imgRef.current) return + + const frameUrls = data.map((d) => getUrl(image.project_id, d.preview_id)) + if (halfFps) state.data = everyNth(frameUrls, 2) + else state.data = frameUrls + await preloadImages(state.data) + }) + }, [image, state, getUrl, halfFps]) + + return { + imgRef, + imgSrc, + callbacksRef, + fps, + frames: state.data.length, + controls + } +} + +export function useVideoContext(onFrameChanged?: OnFrameChanged) { + const ctx = useContext(VideoContext) + if (!ctx) throw new Error("useVideoContext must be used within VideoFramesProvider") + + useEffect(() => { + if (onFrameChanged) ctx.callbacksRef.current.push(onFrameChanged) + return () => { + ctx.callbacksRef.current = ctx.callbacksRef.current.filter( + (cb) => cb !== onFrameChanged, + ) + } + }, [onFrameChanged, ctx]) + + return ctx +} + +async function preloadImages(urls: string[]) { + const promises = urls.map((url) => { + return new Promise((resolve, reject) => { + const img = new Image() + img.src = url + img.onload = resolve + img.onerror = reject + }) + }) + await Promise.all(promises) +} diff --git a/src/components/video/hooks.ts b/src/components/video/hooks.ts new file mode 100644 index 0000000..312589c --- /dev/null +++ b/src/components/video/hooks.ts @@ -0,0 +1,62 @@ +import { + type AnimationPlaybackControlsWithThen, + animate, + useMotionValue, + useMotionValueEvent, + useTransform, +} from "motion/react" +import { useEffect, useMemo, useRef } from "react" + +export type UseFrameAnimationOpts = { + nFrames: number + fps: number + onChange?: (frame: number) => void + autoStart?: boolean +} + +export function useFrameAnimation(opts: UseFrameAnimationOpts) { + const { nFrames, fps, onChange, autoStart } = opts + + const posMv = useMotionValue(0) + const frameMv = useTransform(posMv, (frame) => Math.floor(frame * nFrames)) + + useMotionValueEvent(frameMv, "change", (frame) => { + onChange?.(frame) + }) + + const animationRef = useRef(null) + + useEffect(() => { + animationRef.current = animate(posMv, [0, 1], { + duration: nFrames / fps, + repeat: Infinity, + repeatType: "loop", + ease: "linear", + autoplay: autoStart, + }) + }, [autoStart, fps, posMv, nFrames]) + + const controls = useMemo( + () => ({ + pause: () => { + animationRef.current?.pause() + }, + play: () => { + animationRef.current?.play() + }, + togglePlayPause: () => { + console.log(animationRef.current) + if (animationRef.current?.state === "running") animationRef.current?.pause() + else animationRef.current?.play() + }, + seek: (pos: number) => { + if (animationRef.current) animationRef.current.time = (pos * nFrames) / fps + }, + posMv, + frameMv, + }), + [frameMv, posMv, fps, nFrames], + ) + + return controls +} diff --git a/src/dtProjects/controlPane/SearchPanel.tsx b/src/dtProjects/controlPane/SearchPanel.tsx index 81f377b..0cffd1c 100644 --- a/src/dtProjects/controlPane/SearchPanel.tsx +++ b/src/dtProjects/controlPane/SearchPanel.tsx @@ -6,7 +6,6 @@ import TabContent from "@/metadata/infoPanel/TabContent" import { useDTP } from "../state/context" import SearchFilterForm from "./filters/SearchFilterForm" - interface SearchPanelComponentProps extends ChakraProps {} function SearchPanel(props: SearchPanelComponentProps) { @@ -102,6 +101,7 @@ function SearchPanel(props: SearchPanelComponentProps) { flex={"0 0 auto"} onClick={() => { setSearchInput("") + search.state.searchInput = "" search.clearFilters() }} > @@ -123,14 +123,25 @@ function SearchInfo() { return ( Images will match if the prompt contains any of the search terms. - Search terms are stemmed, so shade, shades, shading, and shaded are all seen as the same word. + + Search terms are stemmed, so shade, shades, shading, and{" "} + shaded are all seen as the same word. + Wrap words or phrases in "quotes" to require an exact text match. cow boy - Matches images that have the words cow or boy in the prompt - but not cowboy since that is a different word + + Matches images that have the words cow or boy in the prompt - but + not cowboy since that is a different word + "cow" "boy" - Matches images that have both cow and boy in the prompt - including cowboys + + Matches images that have both cow and boy in the prompt - + including cowboys + "cow boy" - Matches images that have the exact phrase cow boy in the prompt + + Matches images that have the exact phrase cow boy in the prompt + ) } @@ -138,7 +149,7 @@ function SearchInfo() { const B = chakra("span", { base: { fontWeight: "bold", - marginBottom: "0" + marginBottom: "0", }, }) diff --git a/src/dtProjects/controlPane/filters/TypeValueSelector.tsx b/src/dtProjects/controlPane/filters/TypeValueSelector.tsx deleted file mode 100644 index e76c97d..0000000 --- a/src/dtProjects/controlPane/filters/TypeValueSelector.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Select, Text, VStack } from "@chakra-ui/react" -import { - createValueLabelCollection, - type FilterValueSelector, - type ValueSelectorProps, -} from "./collections" -import FilterSelect from "./FilterSelect" - -const typeValues = { - image: "Image", - video: "Video", -} as const - -const typeValuesCollection = createValueLabelCollection(typeValues) - -function TypeValueSelectorComponent(props: ValueSelectorProps) { - const { value, onValueChange, ...boxProps } = props - - return ( - { - onValueChange?.(value.value as string[]) - }} - multiple={true} - {...boxProps} - > - - - - {value?.map((value) => ( - - {typeValues[value as keyof typeof typeValues] ?? "unknown"} - - ))} - {value?.length === 0 && Select type} - - - - - {typeValuesCollection.items.map((item) => ( - - {item.label} - - - ))} - - - ) -} - -const TypeValueSelector = TypeValueSelectorComponent as FilterValueSelector - -TypeValueSelector.getValueLabel = (values) => { - if (!Array.isArray(values)) return [] - return values.map((v) => - v in typeValues ? typeValues[v as keyof typeof typeValues] : "unknown", - ) -} - -export default TypeValueSelector diff --git a/src/dtProjects/controlPane/filters/collections.tsx b/src/dtProjects/controlPane/filters/collections.tsx index f7c7d69..d72ee17 100644 --- a/src/dtProjects/controlPane/filters/collections.tsx +++ b/src/dtProjects/controlPane/filters/collections.tsx @@ -8,7 +8,6 @@ import FloatValueInput from "./FloatValueInput" import IntValueInput from "./IntValueInput" import { ControlValueSelector, LoraValueSelector, ModelValueSelector } from "./ModelValueSelector" import SamplerValueSelector from "./SamplerValueSelector" -import TypeValueSelector from "./TypeValueSelector" export function createValueLabelCollection(values: Record) { return createListCollection({ @@ -75,11 +74,6 @@ const prepareModelFilterValue = (value: (Model | VersionModel)[]) => { const prepareSizeFilterValue = (value: number) => Math.round(value / 64) export const filterTargets = { - type: { - collection: isIsNotOpsCollection, - ValueComponent: TypeValueSelector, - initialValue: [], - }, model: { collection: isIsNotOpsCollection, ValueComponent: ModelValueSelector, diff --git a/src/dtProjects/detailsOverlay/DetailsImages.tsx b/src/dtProjects/detailsOverlay/DetailsImages.tsx index 3c06e4c..910f264 100644 --- a/src/dtProjects/detailsOverlay/DetailsImages.tsx +++ b/src/dtProjects/detailsOverlay/DetailsImages.tsx @@ -1,21 +1,30 @@ -import { Spinner } from "@chakra-ui/react" +import { Spinner, VStack } from "@chakra-ui/react" import type { Snapshot } from "valtio" -import type { DTImageFull, ImageExtra } from "@/commands" +import type { DTImageFull } from "@/commands" import urls from "@/commands/urls" +import { VideoContext, type VideoContextType } from "@/components/video/context" +import Seekbar from "@/components/video/Seekbar" +import Video from "@/components/video/Video" +import { VideoImage } from "@/components/video/VideoImage" +import type { ImageExtra } from "@/generated/types" +import { useGetContext } from "@/hooks/useGetContext" import type { UIControllerState } from "../state/uiState" -import DetailsImage from "./DetailsImage" import { DetailsSpinnerRoot } from "./common" -import VideoFrames from '@/components/VideoFrames' +import DetailsImage from "./DetailsImage" interface DetailsImagesProps { item: ImageExtra itemDetails?: Snapshot subItem?: Snapshot showSpinner: boolean + videoRef?: React.RefObject } function DetailsImages(props: DetailsImagesProps) { - const { item, itemDetails, subItem, showSpinner } = props + const { item, itemDetails, subItem, showSpinner, videoRef } = props + + const { Extractor } = useGetContext(VideoContext, videoRef) + if (!item) return null const srcHalf = urls.thumbHalf(item) @@ -27,7 +36,13 @@ function DetailsImages(props: DetailsImagesProps) { return ( <> {(itemDetails?.node.clip_id ?? -1) >= 0 ? ( - + e.stopPropagation()}> + + ) : ( (null) + const isVideo = !!snap.item?.num_frames + const { item, itemDetails } = snap const isVisible = !!item @@ -100,6 +104,7 @@ function DetailsOverlay(props: DetailsOverlayProps) { project?: ProjectState + videoRef?: React.RefObject + isVideo?: boolean } function DetailsButtonBar(props: DetailsButtonBarProps) { - const { item, tensorId, show, addMetadata, subItem, project, ...restProps } = props + const { item, tensorId, show, addMetadata, subItem, project, videoRef, isVideo, ...restProps } = props const { uiState } = useDTP() const [lockButtons, setLockButtons] = useState(false) @@ -286,6 +295,15 @@ function DetailsButtonBar(props: DetailsButtonBarProps) { > + { + console.log(videoRef?.current?.controls?.frameMv?.get()) + }} + tip="Get frame" + > + + ) diff --git a/src/dtProjects/imagesList/ImagesList.tsx b/src/dtProjects/imagesList/ImagesList.tsx index 0b32719..5ff102d 100644 --- a/src/dtProjects/imagesList/ImagesList.tsx +++ b/src/dtProjects/imagesList/ImagesList.tsx @@ -1,8 +1,9 @@ import { Box } from "@chakra-ui/react" import { useCallback, useState } from "react" import type { ImageExtra } from "@/commands" -import { PiFilmStrip } from "@/components/icons" -import VideoFrames from "@/components/VideoFrames" +import FrameCountIndicator from "@/components/FrameCountIndicator" +import Video from "@/components/video/Video" +import { VideoImage } from "@/components/video/VideoImage" import PVGrid, { type PVGridItemComponent, type PVGridItemProps, @@ -55,18 +56,21 @@ function ImagesList(props: ChakraProps) { } function GridItemWrapper( - props: PVGridItemProps void }>, + props: PVGridItemProps< + ImageExtra, + { + showDetailsOverlay: (index: number) => void + onPointerEnter?: (index: number) => void + onPointerLeave?: (index: number) => void + hoveredIndex?: number + } + >, ) { const { value: item } = props if (!item) return null - if ( - item.clip_id !== null && - item.clip_id !== undefined && - item.clip_id >= 0 && - item.num_frames - ) { - return + if ((item.num_frames ?? 0) > 0) { + return