reprompt/recreate updated to use prompts api (#1208)

This commit is contained in:
BFlat
2026-03-24 21:59:38 +07:00
committed by GitHub
parent 4bdb1c89a4
commit d1266b7591
7 changed files with 166 additions and 128 deletions
@@ -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");
+1
View File
@@ -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";
+34 -24
View File
@@ -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>