Compare commits

...
6 changed files with 134 additions and 331 deletions
@@ -36,9 +36,14 @@ interface Props {
const appTabs: TabItem[] = [
{ id: "2D", label: "2D" },
{ id: "3D", label: "3D" },
// { id: "VIDEO", label: "Video" },
{ id: "VIDEO", label: "Video" },
];
import { signal } from "@preact/signals-react";
export const topNavMediaId = signal<string>("");
export const topNavMediaUrl = signal<string>("");
export const TopBar = ({
pageName,
appTabIdSignal,
@@ -55,6 +60,9 @@ export const TopBar = ({
const [activeLibraryTab, setActiveLibraryTab] = useState("my-media");
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const [url, setUrl] = useState<string>("");
const [mediaId, setMediaId] = useState<string>("");
const engine3D = useContext(EngineContext);
const handleTabChange = (tabId: string) => {
if (appTabIdSignal.peek() == "3D") {
@@ -75,35 +83,28 @@ export const TopBar = ({
url: string,
media_id: string | undefined,
) => {
console.log("add to scene", url);
console.log("Items to add to scene", currentAppTabId);
console.log("url", url);
console.log("media_id", media_id);
setUrl(url);
setMediaId(media_id ?? "");
if (currentAppTabId === "2D") {
console.log("Adding to 2D scene");
// from the uploaded image url.
} else if (currentAppTabId === "3D") {
console.log("Adding to 3D scene");
// from the uploaded image url.
// engine3D?.activeScene.loadObjectFromUrl(
// "https://cdn-2.fakeyou.com/media/r/h/0/5/3/rh053gbx8yvjwk1mdvag4gkr5c0h9wbh/image_rh053gbx8yvjwk1mdvag4gkr5c0h9wbh.png",
// );
// from the gallery modal.
// engine3D?.activeScene.loadObjectFromUrl(
// "https://cdn-2.fakeyou.com/media/1/t/b/0/e/1tb0e53691mdgqmtd4fmzy58wp96vwxz/image_1tb0e53691mdgqmtd4fmzy58wp96vwxz.png",
// );
// // working media id m_ehq8051fx4hddhja54a7zmz897cgrk
// engine3D?.activeScene.loadObject(
// "m_ehq8051fx4hddhja54a7zmz897cgrk",
// "image",
// true,
// );
// media id from the image selected from gallery.
engine3D?.activeScene.loadObject(media_id, "image", true);
if (media_id) {
engine3D?.activeScene.loadObject(media_id, "image", true);
} else {
console.warn("No media id provided");
}
} else if (currentAppTabId === "VIDEO") {
console.log("Adding to Video scene");
topNavMediaId.value = media_id ?? "";
topNavMediaUrl.value = url;
} else {
console.warn(`Unknown tab type: ${currentAppTabId}`);
}
@@ -50,6 +50,8 @@ import { LoginModal } from "@storyteller/ui-login-modal";
import { DomLevels } from "./signals/hotkeys";
import { AUTH_STATUS } from "../../enums/authentication";
import { authentication } from "../../signals/authentication/authentication";
import { topNavMediaId } from "~/components/signaled/TopBar/TopBar";
import { topNavMediaUrl } from "~/components/signaled/TopBar/TopBar";
export const PageEditor = () => {
useSignals();
@@ -345,7 +347,11 @@ export const PageEditor = () => {
</KonvaCanvasContainer>
<div style={{ display: displayVideo }}>
<ImageToVideo className="h-full w-full" />
<ImageToVideo
className="h-full w-full"
imageMediaId={topNavMediaId.value}
imageUrl={topNavMediaUrl.value}
/>
</div>
<LoginModal
@@ -5,25 +5,24 @@ import BackgroundGallery from "./BackgroundGallery";
import { PopoverMenu, type PopoverItem } from "@storyteller/ui-popover";
import { faFilm, faClock } from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useSignals } from "@preact/signals-react/runtime";
const ImageToVideo = () => {
interface ImageToVideoProps {
imageMediaId?: string;
imageUrl?: string;
}
const ImageToVideo = ({ imageMediaId, imageUrl }: ImageToVideoProps) => {
const containerRef = useRef<HTMLDivElement>(null);
const [selectedModel, setSelectedModel] = useState<string>("Kling 2.0");
const [useSystemPrompt, setUseSystemPrompt] = useState(false);
const [useSystemPrompt, setUseSystemPrompt] = useState(false);
const [isImageBeingProcessed, setIsImageBeingProcessed] = useState(false);
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const [imagePreviewUrl, setImagePreviewUrl] = useState<string>("");
const [isDialogOpen, setIsDialogOpen] = useState(false);
const handleClose = () => setIsDialogOpen(false);
const handleAddImage = (image: File) => {
setSelectedImage(image);
setIsImageBeingProcessed(true);
setImagePreviewUrl(URL.createObjectURL(image));
setTimeout(() => setIsImageBeingProcessed(false), 3000);
};
const jobContext: JobContextType = {
jobTokens: [],
addJobToken: () => {},
@@ -72,8 +71,10 @@ const ImageToVideo = () => {
console.log("Using job context");
return jobContext;
}}
model={selectedModel}
imageMediaId={imageMediaId}
url={imageUrl ?? imagePreviewUrl ?? undefined}
onEnqueuePressed={async () => {
// TODO: Implement enqueue logic
console.log("Enqueue pressed");
}}
/>
@@ -1,7 +1,7 @@
import { signal } from "@preact/signals-react";
// change starting tab
export const appTabId = signal("3D");
export const appTabId = signal("VIDEO");
export const setAppTabId = (newId: string) => {
if (newId != "2D" && newId != "3D" && newId != "VIDEO") {
@@ -26,7 +26,6 @@ interface LightboxModalProps {
url: string,
media_id: string | undefined
) => Promise<void>;
activeTab?: string;
}
export function LightboxModal({
@@ -43,7 +42,6 @@ export function LightboxModal({
mediaId, // media id of the image
onDownloadClicked,
onAddToSceneClicked,
activeTab,
}: LightboxModalProps) {
return createPortal(
<Transition appear show={isOpen}>
@@ -94,11 +92,6 @@ export function LightboxModal({
<div className="flex h-full w-full items-center justify-center bg-gray-800">
<span className="text-white/60">Image not available</span>
</div>
) : activeTab === "videos" ? (
<video controls className="h-full w-full object-contain">
<source src={imageUrl} type="video/mp4" />
Your browser does not support the video tag.
</video>
) : (
<img
src={imageUrl}
@@ -113,7 +106,7 @@ export function LightboxModal({
<div className="flex h-full flex-col py-5 pe-5">
<div className="flex-1 space-y-4">
<div className="text-xl font-medium">
{title || (activeTab === "videos" ? "Video" : "Image")}
{title || "Image Generation"}
</div>
{createdAt && (
<div className="text-sm text-white/60">
@@ -127,52 +120,34 @@ export function LightboxModal({
{/* buttons with spacing */}
{(onAddToSceneClicked && downloadUrl) || downloadUrl ? (
<div className="mt-15 mb-15 flex justify-end gap-2">
{activeTab !== "videos" && (
<>
<Button
onClick={async (e) => {
let _result = await FalKlingImageToVideo({
image_media_token: mediaId,
//base64_image: downloadUrl,
});
//e.stopPropagation();
//await onAddToSceneClicked(downloadUrl, mediaId);
//onClose(); // close the lightbox
//onCloseGallery(); // close the gallery
}}
>
Video
</Button>
<Button
onClick={async (e) => {
let _result = await FalHunyuanImageTo3d({
image_media_token: mediaId,
//base64_image: downloadUrl,
});
//e.stopPropagation();
//await onAddToSceneClicked(downloadUrl, mediaId);
//onClose(); // close the lightbox
//onCloseGallery(); // close the gallery
}}
>
3D
</Button>
<Button
onClick={async (e) => {
let _result = await FalHunyuanImageTo3d({
image_media_token: mediaId,
//base64_image: downloadUrl,
});
//e.stopPropagation();
//await onAddToSceneClicked(downloadUrl, mediaId);
//onClose(); // close the lightbox
//onCloseGallery(); // close the gallery
}}
>
3D
</Button>
{onAddToSceneClicked && downloadUrl && (
<Button
onClick={async (e) => {
e.stopPropagation();
await onAddToSceneClicked(downloadUrl, mediaId);
onClose(); // close the lightbox
onCloseGallery(); // close the gallery
}}
>
Add to Current Scene
</Button>
)}
</>
{onAddToSceneClicked && downloadUrl && (
<Button
onClick={async (e) => {
e.stopPropagation();
await onAddToSceneClicked(downloadUrl, mediaId);
onClose(); // close the lightbox
onCloseGallery(); // close the gallery
}}
>
Add to Current Scene
</Button>
)}
{downloadUrl &&
(onDownloadClicked ? (
<Button
@@ -1,17 +1,13 @@
import { useState, useRef, useEffect } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { toast } from "@storyteller/ui-toaster";
import {
JobContextType,
MaybeCanvasRenderBitmapType,
UploaderState,
UploaderStates,
} from "@storyteller/common";
import { JobContextType } from "@storyteller/common";
import { downloadFileFromUrl } from "@storyteller/api";
import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover";
import { Tooltip } from "@storyteller/ui-tooltip";
import { Button, ToggleButton } from "@storyteller/ui-button";
import { Modal } from "@storyteller/ui-modal";
import { FalKlingImageToVideo } from "@storyteller/tauri-api";
import {
faMessageXmark,
faMessageCheck,
@@ -30,12 +26,9 @@ import {
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
import { PromptsApi } from "@storyteller/api";
import {
CommandSuccessStatus,
GetAppPreferences,
SoraImageRemix,
SoraImageRemixErrorType,
} from "@storyteller/tauri-api";
import { SoundRegistry } from "@storyteller/soundboard";
@@ -47,30 +40,35 @@ interface ReferenceImage {
}
interface PromptBoxVideoProps {
uploadImage: ({
title,
assetFile,
progressCallback,
}: {
title: string;
assetFile: File;
progressCallback: (newState: UploaderState) => void;
}) => Promise<void>;
getCanvasRenderBitmap: () => MaybeCanvasRenderBitmapType;
EncodeImageBitmapToBase64: (imageBitmap: ImageBitmap) => Promise<string>;
useJobContext: () => JobContextType;
onEnqueuePressed?: () => void | Promise<void>;
model: string;
imageMediaId?: string;
url?: string;
}
export const PromptBoxVideo = ({
uploadImage,
getCanvasRenderBitmap,
EncodeImageBitmapToBase64,
useJobContext,
onEnqueuePressed,
model,
imageMediaId,
url,
}: PromptBoxVideoProps) => {
useSignals();
// for the image media id and url, we need to set the reference image gallery panel.
useEffect(() => {
if (imageMediaId && url) {
const referenceImage: ReferenceImage = {
id: Math.random().toString(36).substring(7),
url: url,
file: new File([], "library-image"),
mediaToken: imageMediaId,
};
setReferenceImages([referenceImage]);
}
}, [imageMediaId, url]);
console.log("Is this a desktop app?", IsDesktopApp());
const [isModalOpen, setIsModalOpen] = useState(false);
const [content, setContent] = useState("");
@@ -112,6 +110,18 @@ export const PromptBoxVideo = ({
}
});
useEffect(() => {
if (imageMediaId && url) {
const referenceImage: ReferenceImage = {
id: Math.random().toString(36).substring(7),
url: url,
file: new File([], "library-image"),
mediaToken: imageMediaId,
};
setReferenceImages([referenceImage]);
}
}, [imageMediaId, url]);
const handleResolutionSelect = (selectedItem: PopoverItem) => {
setResolutionList((prev) =>
prev.map((item) => ({
@@ -121,53 +131,6 @@ export const PromptBoxVideo = ({
);
};
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files) {
Array.from(files).forEach((file) => {
const uploadId = Math.random().toString(36).substring(7);
setUploadingImages((prev) => [...prev, { id: uploadId, file }]);
const reader = new FileReader();
reader.onloadend = () => {
uploadImage({
title: `reference-image-${Math.random()
.toString(36)
.substring(2, 15)}`,
assetFile: file,
progressCallback: (newState) => {
console.debug("Upload progress:", newState.data);
if (newState.status === UploaderStates.success && newState.data) {
const referenceImage: ReferenceImage = {
id: Math.random().toString(36).substring(7),
url: reader.result as string,
file,
mediaToken: newState.data || "",
};
setReferenceImages((prev) => [...prev, referenceImage]);
setUploadingImages((prev) =>
prev.filter((img) => img.id !== uploadId)
);
toast.success("Reference image added.");
} else if (
newState.status === UploaderStates.assetError ||
newState.status === UploaderStates.imageCreateError
) {
setUploadingImages((prev) =>
prev.filter((img) => img.id !== uploadId)
);
toast.error("Upload failed. Please try again.");
}
},
});
};
reader.readAsDataURL(file);
});
}
};
const handleRemoveReference = (id: string) => {
setReferenceImages((prev) => prev.filter((img) => img.id !== id));
if (fileInputRef.current) {
@@ -175,10 +138,6 @@ export const PromptBoxVideo = ({
}
};
const handleUploadClick = () => {
fileInputRef.current?.click();
};
const handleGallerySelect = () => {
setIsGalleryModalOpen(true);
};
@@ -236,49 +195,14 @@ export const PromptBoxVideo = ({
setPrompt(e.target.value);
};
const handleTauriEnqueue = async () => {
const api = new PromptsApi();
let image = getCanvasRenderBitmap();
if (image === undefined) {
console.log("image is undefined");
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const byteString = atob(base64Bitmap);
const mimeString = "image/png";
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const uuid = crypto.randomUUID(); // Generate a new UUID
const file = new File([ab], `${uuid}.png`, { type: mimeString });
const snapshotMediaToken = await api.uploadSceneSnapshot({
screenshot: file,
});
if (snapshotMediaToken.data === undefined) {
toast.error("Error: Unable to upload scene snapshot Please try again.");
return;
}
console.log("useSystemPrompt", useSystemPrompt);
console.log("Snapshot media token:", snapshotMediaToken.data);
const generateResponse = await SoraImageRemix({
snapshot_media_token: snapshotMediaToken.data,
disable_system_prompt: !useSystemPrompt,
prompt: prompt,
maybe_additional_images: referenceImages.map((image) => image.mediaToken),
maybe_number_of_samples: 1,
const handleEnqueue = async () => {
setIsEnqueueing(true);
const generateResponse = await FalKlingImageToVideo({
image_media_token: referenceImages[0].mediaToken,
});
console.log("generateResponse", generateResponse);
onEnqueuePressed?.();
if (generateResponse.status === CommandSuccessStatus.Success) {
const prefs = await GetAppPreferences();
const soundName = prefs.preferences?.enqueue_success_sound;
@@ -294,129 +218,34 @@ export const PromptBoxVideo = ({
const registry = SoundRegistry.getInstance();
registry.playSound(soundName);
}
let errorMessage = "Failed to enqueue image";
if ("error_type" in generateResponse) {
switch (generateResponse.error_type) {
case SoraImageRemixErrorType.SoraLoginRequired:
errorMessage =
"You need to log into Sora to continue. See the settings menu.";
break;
case SoraImageRemixErrorType.ServerError:
errorMessage = "Server error. Failed to enqueue image.";
break;
case SoraImageRemixErrorType.TooManyConcurrentTasks:
errorMessage =
"You have too many Sora image generation tasks running. Please wait a moment.";
break;
case SoraImageRemixErrorType.SoraUsernameNotYetCreated:
errorMessage =
"Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
break;
case SoraImageRemixErrorType.SoraIsHavingProblems:
errorMessage = "Sora is having problems. Please try again later.";
break;
}
}
toast.error(errorMessage);
}
};
const handleWebEnqueue = async () => {
const api = new PromptsApi();
// to take the snapshot of the canvas
// Call onEnqueuePressed if it exists
let image = getCanvasRenderBitmap();
if (image === undefined) {
toast.error(
"Error: Unable to generate image. Please check the input and try again."
);
return;
}
const base64Bitmap = await EncodeImageBitmapToBase64(image);
const byteString = atob(base64Bitmap);
const mimeString = "image/png";
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const uuid = crypto.randomUUID(); // Generate a new UUID
const file = new File([ab], `${uuid}.png`, { type: mimeString });
const snapshotMediaToken = await api.uploadSceneSnapshot({
screenshot: file,
});
if (snapshotMediaToken.data === undefined) {
toast.error("Error: Unable to upload scene snapshot Please try again.");
return;
}
console.log("useSystemPrompt", useSystemPrompt);
console.log("Snapshot media token:", snapshotMediaToken.data);
const response = await api.enqueueImageGeneration({
disableSystemPrompt: !useSystemPrompt,
prompt: prompt,
snapshotMediaToken: snapshotMediaToken.data,
additionalImages: referenceImages.map((image) => image.mediaToken),
});
if (response.success === true) {
toast.success("Please wait while we process your image.");
if (response.data) {
addJobToken(response.data);
}
return;
} else {
toast.error("Failed to enqueue image generation. Please try again.");
}
};
const handleEnqueue = async () => {
console.log("Pressing Enqueue");
if (onEnqueuePressed) {
try {
await onEnqueuePressed();
} catch (error) {
console.error("Error in onEnqueuePressed callback:", error);
return;
}
}
if (isEnqueueing) return;
setIsEnqueueing(true);
if (!prompt.trim()) return;
try {
console.log(
"(4) Enqueuing with prompt:",
prompt,
"and reference images:",
referenceImages
);
const isDesktop = IsDesktopApp();
console.log("Is this a desktop app?", isDesktop);
if (isDesktop) {
await handleTauriEnqueue();
} else {
await handleWebEnqueue();
}
} catch (error) {
console.error("Error during image generation:", error);
toast.error(
"An error occurred while generating the image. Please try again."
);
} finally {
await new Promise((resolve) => setTimeout(resolve, 100));
setIsEnqueueing(false);
}
// let errorMessage = "Failed to enqueue image";
// if ("error_type" in generateResponse) {
// switch (generateResponse.error_type) {
// case SoraImageRemixErrorType.SoraLoginRequired:
// errorMessage =
// "You need to log into Sora to continue. See the settings menu.";
// break;
// case SoraImageRemixErrorType.ServerError:
// errorMessage = "Server error. Failed to enqueue image.";
// break;
// case SoraImageRemixErrorType.TooManyConcurrentTasks:
// errorMessage =
// "You have too many Sora image generation tasks running. Please wait a moment.";
// break;
// case SoraImageRemixErrorType.SoraUsernameNotYetCreated:
// errorMessage =
// "Your Sora username is not yet created. Please visit the Sora.com website to create it first.";
// break;
// case SoraImageRemixErrorType.SoraIsHavingProblems:
// errorMessage = "Sora is having problems. Please try again later.";
// break;
// }
// }
// toast.error(errorMessage);
// }
setIsEnqueueing(false);
};
const getCurrentResolutionIcon = () => {
@@ -429,7 +258,6 @@ export const PromptBoxVideo = ({
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleEnqueue();
}
};
@@ -492,14 +320,6 @@ export const PromptBoxVideo = ({
</div>
)}
<div className="glass w-[730px] rounded-xl p-4">
<input
type="file"
ref={fileInputRef}
className="hidden"
accept="image/*"
onChange={handleFileUpload}
multiple
/>
<div className="flex justify-center gap-2">
<PopoverMenu
mode="button"