mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
reprompt/recreate updated to use prompts api (#1208)
This commit is contained in:
@@ -16,7 +16,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { signal } from "@preact/signals-react";
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
import { FilterMediaClasses } from "@storyteller/api";
|
||||
import { getCreatorIcon, ModelCreator, IMAGE_MODELS_BY_ID, VIDEO_MODELS_BY_ID } from "@storyteller/model-list";
|
||||
import { getCreatorIcon, ModelCreator, IMAGE_MODELS_BY_ID, VIDEO_MODELS_BY_ID, CommonAspectRatio, CommonResolution } from "@storyteller/model-list";
|
||||
import { useClassyModelSelectorStore, ModelPage } from "@storyteller/ui-model-selector";
|
||||
import { useCreditsState } from "@storyteller/credits";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
@@ -51,7 +51,8 @@ import {
|
||||
useCostBreakdownModalStore,
|
||||
CreditsModal,
|
||||
} from "@storyteller/ui-pricing-modal";
|
||||
import { RefImage, usePromptImageStore, usePromptVideoStore } from "@storyteller/ui-promptbox";
|
||||
import { RefImage, RefVideo, RefAudio, usePromptImageStore, usePromptVideoStore } from "@storyteller/ui-promptbox";
|
||||
import type { Prompts } from "@storyteller/api";
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
import { SettingsModal } from "@storyteller/ui-settings-modal";
|
||||
import { Tooltip } from "@storyteller/ui-tooltip";
|
||||
@@ -265,47 +266,126 @@ export const TopBar = ({ pageName }: Props) => {
|
||||
};
|
||||
|
||||
const handleRecreateFromGallery = (data: {
|
||||
prompt: string | null;
|
||||
promptData: Prompts;
|
||||
mediaClass: string | undefined;
|
||||
modelType: string | null;
|
||||
contextImages: Array<{
|
||||
media_links: {
|
||||
cdn_url: string;
|
||||
maybe_thumbnail_template: string;
|
||||
};
|
||||
media_token: string;
|
||||
semantic: string;
|
||||
}> | null;
|
||||
}) => {
|
||||
try {
|
||||
const { prompt: recreatePrompt, mediaClass: recreateMediaClass, modelType: recreateModelType, contextImages: recreateContextImages } = data;
|
||||
const { promptData, mediaClass: recreateMediaClass } = data;
|
||||
const contextImages = promptData.maybe_context_images || [];
|
||||
|
||||
// Build reference images from context images
|
||||
const refImages: RefImage[] = (recreateContextImages || []).map((ci) => ({
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: ci.media_links.cdn_url,
|
||||
file: new File([], "recreate-ref"),
|
||||
mediaToken: ci.media_token,
|
||||
}));
|
||||
// Partition context images by semantic type
|
||||
const imgRefs: RefImage[] = [];
|
||||
let endFrameImage: RefImage | undefined;
|
||||
const vidRefs: RefVideo[] = [];
|
||||
const audioRefs: RefAudio[] = [];
|
||||
|
||||
for (const ci of contextImages) {
|
||||
const base = {
|
||||
id: Math.random().toString(36).substring(7),
|
||||
url: ci.media_links.cdn_url,
|
||||
mediaToken: ci.media_token,
|
||||
};
|
||||
|
||||
switch (ci.semantic) {
|
||||
case "vid_end_frame":
|
||||
endFrameImage = { ...base, file: new File([], "recreate-ref") };
|
||||
break;
|
||||
case "vid_ref":
|
||||
vidRefs.push({ ...base, file: new File([], "recreate-ref"), duration: 0 });
|
||||
break;
|
||||
case "audioref":
|
||||
audioRefs.push({ ...base, file: new File([], "recreate-ref"), duration: 0 });
|
||||
break;
|
||||
default:
|
||||
// imgref, imgref_character, imgref_style, imgref_bg, imgsrc, vid_start_frame, imgmask
|
||||
imgRefs.push({ ...base, file: new File([], "recreate-ref") });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine input mode from generation_mode or context image semantics
|
||||
const hasKeyframeSemantics = contextImages.some(
|
||||
(ci) => ci.semantic === "vid_start_frame" || ci.semantic === "vid_end_frame",
|
||||
);
|
||||
const inputMode: "keyframe" | "reference" =
|
||||
promptData.maybe_generation_mode === "keyframe" || hasKeyframeSemantics
|
||||
? "keyframe"
|
||||
: promptData.maybe_generation_mode === "reference"
|
||||
? "reference"
|
||||
: imgRefs.length > 0
|
||||
? "reference"
|
||||
: "keyframe";
|
||||
|
||||
const modelStore = useClassyModelSelectorStore.getState();
|
||||
|
||||
if (recreateMediaClass === "video") {
|
||||
const videoStore = usePromptVideoStore.getState();
|
||||
if (recreatePrompt) videoStore.setPrompt(recreatePrompt);
|
||||
if (refImages.length > 0) videoStore.setReferenceImages(refImages);
|
||||
if (recreateModelType) {
|
||||
const model = VIDEO_MODELS_BY_ID.get(recreateModelType);
|
||||
if (model) modelStore.setSelectedModel(ModelPage.ImageToVideo, model);
|
||||
|
||||
// Set model first so the UI syncs sizeOptions / durationOptions
|
||||
const videoModel = promptData.maybe_model_type
|
||||
? VIDEO_MODELS_BY_ID.get(promptData.maybe_model_type)
|
||||
: undefined;
|
||||
if (videoModel) {
|
||||
modelStore.setSelectedModel(ModelPage.ImageToVideo, videoModel);
|
||||
}
|
||||
|
||||
if (promptData.maybe_positive_prompt) {
|
||||
videoStore.setPrompt(promptData.maybe_positive_prompt);
|
||||
}
|
||||
if (imgRefs.length > 0) videoStore.setReferenceImages(imgRefs);
|
||||
if (endFrameImage) videoStore.setEndFrameImage(endFrameImage);
|
||||
if (vidRefs.length > 0) videoStore.setReferenceVideos(vidRefs);
|
||||
if (audioRefs.length > 0) videoStore.setReferenceAudios(audioRefs);
|
||||
if (promptData.maybe_generate_audio !== null) {
|
||||
videoStore.setGenerateWithSound(promptData.maybe_generate_audio);
|
||||
}
|
||||
if (promptData.maybe_duration_seconds !== null) {
|
||||
videoStore.setDuration(promptData.maybe_duration_seconds);
|
||||
}
|
||||
|
||||
// Map API aspect ratio (tauriValue like "wide_sixteen_by_nine") → textLabel (like "16:9")
|
||||
if (promptData.maybe_aspect_ratio && videoModel?.sizeOptions) {
|
||||
const match = videoModel.sizeOptions.find(
|
||||
(opt) => opt.tauriValue === promptData.maybe_aspect_ratio,
|
||||
);
|
||||
if (match) {
|
||||
videoStore.setAspectRatio(match.textLabel);
|
||||
}
|
||||
}
|
||||
|
||||
// Map API resolution (like "one_k") → video store format (like "1080p")
|
||||
if (promptData.maybe_resolution && videoModel?.resolutionOptions) {
|
||||
const resolutionMap: Record<string, string> = {
|
||||
one_k: "1080p",
|
||||
two_k: "2k",
|
||||
three_k: "3k",
|
||||
four_k: "4k",
|
||||
};
|
||||
const mapped = resolutionMap[promptData.maybe_resolution];
|
||||
if (mapped && videoModel.resolutionOptions.includes(mapped)) {
|
||||
videoStore.setResolution(mapped);
|
||||
}
|
||||
}
|
||||
|
||||
videoStore.setInputMode(inputMode);
|
||||
useTabStore.getState().setActiveTab("VIDEO");
|
||||
} else {
|
||||
// Default to image
|
||||
const imageStore = usePromptImageStore.getState();
|
||||
if (recreatePrompt) imageStore.setPrompt(recreatePrompt);
|
||||
if (refImages.length > 0) imageStore.setReferenceImages(refImages);
|
||||
if (recreateModelType) {
|
||||
const model = IMAGE_MODELS_BY_ID.get(recreateModelType);
|
||||
|
||||
if (promptData.maybe_positive_prompt) {
|
||||
imageStore.setPrompt(promptData.maybe_positive_prompt);
|
||||
}
|
||||
if (imgRefs.length > 0) imageStore.setReferenceImages(imgRefs);
|
||||
if (promptData.maybe_aspect_ratio) {
|
||||
imageStore.setCommonAspectRatio(promptData.maybe_aspect_ratio as CommonAspectRatio);
|
||||
}
|
||||
if (promptData.maybe_resolution) {
|
||||
imageStore.setCommonResolution(promptData.maybe_resolution as CommonResolution);
|
||||
}
|
||||
|
||||
if (promptData.maybe_model_type) {
|
||||
const model = IMAGE_MODELS_BY_ID.get(promptData.maybe_model_type);
|
||||
if (model) modelStore.setSelectedModel(ModelPage.TextToImage, model);
|
||||
}
|
||||
useTabStore.getState().setActiveTab("IMAGE");
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from "./lib/enums/index.js";
|
||||
export * from "./lib/enums/Job.js";
|
||||
export * from "./lib/models/Users.js";
|
||||
export * from "./lib/models/Job.js";
|
||||
export * from "./lib/models/Prompts.js";
|
||||
export * from "./lib/AnalyticsApi.js";
|
||||
export * from "./lib/BillingApi.js";
|
||||
export * from "./lib/CreditsApi.js";
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
export interface PromptContextImage {
|
||||
media_links: {
|
||||
cdn_url: string;
|
||||
maybe_thumbnail_template: string | null;
|
||||
maybe_video_previews: {
|
||||
animated: string;
|
||||
animated_thumbnail_template: string;
|
||||
still: string;
|
||||
still_thumbnail_template: string;
|
||||
} | null;
|
||||
};
|
||||
media_token: string;
|
||||
semantic: string;
|
||||
}
|
||||
|
||||
export interface Prompts {
|
||||
created_at: string;
|
||||
lcm_disabled: boolean;
|
||||
lipsync_enabled: boolean;
|
||||
maybe_inference_duration_millis: number;
|
||||
maybe_moderator_fields: string;
|
||||
maybe_negative_prompt: string;
|
||||
maybe_positive_prompt: string;
|
||||
maybe_strength: number;
|
||||
maybe_style_name: string;
|
||||
maybe_aspect_ratio: string | null;
|
||||
maybe_batch_count: number | null;
|
||||
maybe_context_images: PromptContextImage[] | null;
|
||||
maybe_duration_seconds: number | null;
|
||||
maybe_frame_skip: number | null;
|
||||
maybe_generate_audio: boolean | null;
|
||||
maybe_generation_mode: string | null;
|
||||
maybe_generation_provider: string | null;
|
||||
maybe_global_ipa_image_token: string | null;
|
||||
maybe_inference_duration_millis: number | null;
|
||||
maybe_model_class: string | null;
|
||||
maybe_model_type: string | null;
|
||||
maybe_moderator_fields: string | null;
|
||||
maybe_negative_prompt: string | null;
|
||||
maybe_positive_prompt: string | null;
|
||||
maybe_resolution: string | null;
|
||||
maybe_strength: number | null;
|
||||
maybe_style_name: string | null;
|
||||
maybe_travel_prompt: string | null;
|
||||
prompt_type: string;
|
||||
token: string;
|
||||
use_cinematic: boolean;
|
||||
used_face_detailer: boolean;
|
||||
used_upscaler: boolean;
|
||||
maybe_model_type: string;
|
||||
maybe_generation_provider: string;
|
||||
maybe_context_images: [
|
||||
{
|
||||
media_links: {
|
||||
cdn_url: string;
|
||||
maybe_thumbnail_template: string;
|
||||
maybe_video_previews: {
|
||||
animated: string;
|
||||
animated_thumbnail_template: string;
|
||||
still: string;
|
||||
still_thumbnail_template: string;
|
||||
};
|
||||
};
|
||||
media_token: string;
|
||||
semantic: string;
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { LightboxModal } from "@storyteller/ui-lightbox-modal";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { CloseButton } from "@storyteller/ui-close-button";
|
||||
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
|
||||
import type { Prompts } from "@storyteller/api";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
@@ -252,17 +253,8 @@ interface GalleryModalProps {
|
||||
media_id?: string,
|
||||
) => Promise<void> | void;
|
||||
onRecreateClicked?: (data: {
|
||||
prompt: string | null;
|
||||
promptData: Prompts;
|
||||
mediaClass: string | undefined;
|
||||
modelType: string | null;
|
||||
contextImages: Array<{
|
||||
media_links: {
|
||||
cdn_url: string;
|
||||
maybe_thumbnail_template: string;
|
||||
};
|
||||
media_token: string;
|
||||
semantic: string;
|
||||
}> | null;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from "react";
|
||||
import { gtagEvent } from "@storyteller/google-analytics";
|
||||
import { MediaFilesApi, PromptsApi } from "@storyteller/api";
|
||||
import type { Prompts } from "@storyteller/api";
|
||||
import { toast } from "@storyteller/ui-toaster";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faCopy, faLink, faCheck } from "@fortawesome/pro-solid-svg-icons";
|
||||
@@ -92,17 +93,8 @@ interface LightboxModalProps {
|
||||
media_id?: string,
|
||||
) => Promise<void> | void;
|
||||
onRecreateClicked?: (data: {
|
||||
prompt: string | null;
|
||||
promptData: Prompts;
|
||||
mediaClass: string | undefined;
|
||||
modelType: string | null;
|
||||
contextImages: Array<{
|
||||
media_links: {
|
||||
cdn_url: string;
|
||||
maybe_thumbnail_template: string;
|
||||
};
|
||||
media_token: string;
|
||||
semantic: string;
|
||||
}> | null;
|
||||
}) => void;
|
||||
batchImageToken?: string;
|
||||
onNavigatePrev?: () => void;
|
||||
@@ -148,7 +140,11 @@ export function LightboxModal({
|
||||
|
||||
const [refPreviewUrl, setRefPreviewUrl] = useState<string | null>(null);
|
||||
const [mediaLoaded, setMediaLoaded] = useState<boolean>(false);
|
||||
const [prompt, setPrompt] = useState<string | null>(null);
|
||||
const [promptData, setPromptData] = useState<Prompts | null>(null);
|
||||
const prompt = promptData?.maybe_positive_prompt ?? null;
|
||||
const generationProvider = promptData?.maybe_generation_provider ?? null;
|
||||
const modelType = promptData?.maybe_model_type ?? null;
|
||||
const contextImages = promptData?.maybe_context_images ?? null;
|
||||
const [promptLoading, setPromptLoading] = useState<boolean>(false);
|
||||
const [hasPromptToken, setHasPromptToken] = useState<boolean>(false);
|
||||
const [isPromptExpanded, setIsPromptExpanded] = useState<boolean>(false);
|
||||
@@ -156,18 +152,6 @@ export function LightboxModal({
|
||||
const [isPromptClamped, setIsPromptClamped] = useState<boolean>(false);
|
||||
const [promptCopied, setPromptCopied] = useState<boolean>(false);
|
||||
const promptCopiedTimeoutRef = useRef<number | null>(null);
|
||||
const [generationProvider, setGenerationProvider] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [modelType, setModelType] = useState<string | null>(null);
|
||||
const [contextImages, setContextImages] = useState<Array<{
|
||||
media_links: {
|
||||
cdn_url: string;
|
||||
maybe_thumbnail_template: string;
|
||||
};
|
||||
media_token: string;
|
||||
semantic: string;
|
||||
}> | null>(null);
|
||||
const [batchImages, setBatchImages] = useState<string[] | null>(null);
|
||||
const [batchTokens, setBatchTokens] = useState<string[] | null>(null);
|
||||
const [shareCopied, setShareCopied] = useState<boolean>(false);
|
||||
@@ -204,21 +188,15 @@ export function LightboxModal({
|
||||
// rapid next/prev navigation, with cancellation for stale responses.
|
||||
useEffect(() => {
|
||||
if (!mediaId) {
|
||||
setPrompt(null);
|
||||
setPromptData(null);
|
||||
setHasPromptToken(false);
|
||||
setGenerationProvider(null);
|
||||
setModelType(null);
|
||||
setContextImages(null);
|
||||
setPromptLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Immediately show skeletons & clear stale data
|
||||
setPromptLoading(true);
|
||||
setPrompt(null);
|
||||
setGenerationProvider(null);
|
||||
setModelType(null);
|
||||
setContextImages(null);
|
||||
setPromptData(null);
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
@@ -237,32 +215,19 @@ export function LightboxModal({
|
||||
if (cancelled) return;
|
||||
|
||||
if (promptResponse.success && promptResponse.data) {
|
||||
const promptData = promptResponse.data;
|
||||
setPrompt(promptData.maybe_positive_prompt || null);
|
||||
setGenerationProvider(promptData.maybe_generation_provider || null);
|
||||
setModelType(promptData.maybe_model_type || null);
|
||||
setContextImages(promptData.maybe_context_images || null);
|
||||
setPromptData(promptResponse.data);
|
||||
} else {
|
||||
setPrompt(null);
|
||||
setGenerationProvider(null);
|
||||
setModelType(null);
|
||||
setContextImages(null);
|
||||
setPromptData(null);
|
||||
}
|
||||
} else {
|
||||
setHasPromptToken(false);
|
||||
setPrompt(null);
|
||||
setGenerationProvider(null);
|
||||
setModelType(null);
|
||||
setContextImages(null);
|
||||
setPromptData(null);
|
||||
}
|
||||
} catch (error) {
|
||||
if (cancelled) return;
|
||||
console.error("Error fetching prompt:", error);
|
||||
setHasPromptToken(false);
|
||||
setPrompt(null);
|
||||
setGenerationProvider(null);
|
||||
setModelType(null);
|
||||
setContextImages(null);
|
||||
setPromptData(null);
|
||||
} finally {
|
||||
if (!cancelled) setPromptLoading(false);
|
||||
}
|
||||
@@ -959,6 +924,7 @@ export function LightboxModal({
|
||||
<div className="mt-4 grid grid-cols-2 gap-1.5">
|
||||
{onRecreateClicked &&
|
||||
hasPromptToken &&
|
||||
promptData &&
|
||||
(derivedMediaClass === "image" ||
|
||||
derivedMediaClass === "video") && (
|
||||
<Button
|
||||
@@ -969,10 +935,8 @@ export function LightboxModal({
|
||||
e.stopPropagation();
|
||||
gtagEvent("recreate_clicked");
|
||||
onRecreateClicked({
|
||||
prompt,
|
||||
promptData,
|
||||
mediaClass: derivedMediaClass,
|
||||
modelType,
|
||||
contextImages,
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -905,7 +905,7 @@ export const ImagePromptRow = ({
|
||||
setIsGalleryModalOpen(true);
|
||||
}}
|
||||
icon={faImages}
|
||||
className="w-full"
|
||||
className="w-full bg-base-fg/10 hover:bg-base-fg/20"
|
||||
>
|
||||
Pick from library
|
||||
</Button>
|
||||
@@ -1017,7 +1017,7 @@ export const ImagePromptRow = ({
|
||||
setIsGalleryModalOpen(true);
|
||||
}}
|
||||
icon={faImages}
|
||||
className="w-full"
|
||||
className="w-full bg-base-fg/10 hover:bg-base-fg/20"
|
||||
>
|
||||
Pick from library
|
||||
</Button>
|
||||
|
||||
@@ -354,10 +354,7 @@ export const PromptBoxVideo = ({
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
color: IMAGE_COLORS[idx % IMAGE_COLORS.length],
|
||||
fontWeight: 600,
|
||||
}}
|
||||
style={{ color: IMAGE_COLORS[idx % IMAGE_COLORS.length] }}
|
||||
>
|
||||
{part}
|
||||
</span>
|
||||
@@ -369,10 +366,7 @@ export const PromptBoxVideo = ({
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
color: VIDEO_COLORS[idx % VIDEO_COLORS.length],
|
||||
fontWeight: 600,
|
||||
}}
|
||||
style={{ color: VIDEO_COLORS[idx % VIDEO_COLORS.length] }}
|
||||
>
|
||||
{part}
|
||||
</span>
|
||||
@@ -384,10 +378,7 @@ export const PromptBoxVideo = ({
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
color: AUDIO_COLORS[idx % AUDIO_COLORS.length],
|
||||
fontWeight: 600,
|
||||
}}
|
||||
style={{ color: AUDIO_COLORS[idx % AUDIO_COLORS.length] }}
|
||||
>
|
||||
{part}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user