diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 71f27eb1..b7462612 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -251,9 +251,9 @@ export function Header() {
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
title={t("header.openWelcome")}
>
-
+
- popiart-node
+ Popi.TV
diff --git a/src/components/__tests__/Header.test.tsx b/src/components/__tests__/Header.test.tsx
index 84a41691..ce618df4 100644
--- a/src/components/__tests__/Header.test.tsx
+++ b/src/components/__tests__/Header.test.tsx
@@ -75,14 +75,16 @@ describe("Header", () => {
describe("Basic Rendering", () => {
it("should render the app title", () => {
render();
- expect(screen.getByText("popiart-node")).toBeInTheDocument();
+ expect(screen.getByText("Popi.TV")).toBeInTheDocument();
});
it("should render the brand icon", () => {
- render();
- const icon = screen.getByAltText("PopiArt");
+ const { container } = render();
+ const icon = container.querySelector('img[src="/banana_icon.png"]');
expect(icon).toBeInTheDocument();
expect(icon).toHaveAttribute("src", "/banana_icon.png");
+ expect(icon).toHaveAttribute("alt", "");
+ expect(icon).toHaveAttribute("aria-hidden", "true");
});
it("should hide external author link", () => {
diff --git a/src/store/execution/__tests__/nanoBananaExecutor.test.ts b/src/store/execution/__tests__/nanoBananaExecutor.test.ts
index a0e8cb83..00e1df8c 100644
--- a/src/store/execution/__tests__/nanoBananaExecutor.test.ts
+++ b/src/store/execution/__tests__/nanoBananaExecutor.test.ts
@@ -195,6 +195,68 @@ describe("executeNanoBanana", () => {
});
});
+ it("falls back from the default NewApiWG image model on provider overload", async () => {
+ const node = makeNode({
+ selectedModel: {
+ provider: "newapiwg",
+ modelId: "apiyi_nano_banana_2",
+ displayName: "apiyi_nano_banana_2",
+ capabilities: ["text-to-image", "image-to-image"],
+ },
+ });
+
+ mockFetch
+ .mockResolvedValueOnce({
+ ok: false,
+ text: () => Promise.resolve(JSON.stringify({
+ error: "apiyi_nano_banana_2: provider is temporarily overloaded. Please retry in a moment or switch models.",
+ })),
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve({ success: true, image: "data:image/png;base64,fallback" }),
+ });
+
+ const ctx = makeCtx(node);
+ await executeNanoBanana(ctx);
+
+ expect(mockFetch).toHaveBeenCalledTimes(2);
+ const fallbackBody = JSON.parse(mockFetch.mock.calls[1][1].body);
+ expect(fallbackBody.selectedModel).toMatchObject({
+ provider: "newapiwg",
+ modelId: "gpt-image-2-all",
+ });
+
+ const calls = (ctx.updateNodeData as ReturnType).mock.calls;
+ const stampCall = calls.find(
+ (c: unknown[]) => (c[1] as Record).__usedFallback === true
+ );
+ expect(stampCall?.[1]).toMatchObject({
+ __fallbackModelUsed: "gpt-image-2-all",
+ __primaryError: "apiyi_nano_banana_2: provider is temporarily overloaded. Please retry in a moment or switch models.",
+ });
+ });
+
+ it("does not implicitly fall back from the default NewApiWG image model on non-overload errors", async () => {
+ const node = makeNode({
+ selectedModel: {
+ provider: "newapiwg",
+ modelId: "apiyi_nano_banana_2",
+ displayName: "apiyi_nano_banana_2",
+ capabilities: ["text-to-image", "image-to-image"],
+ },
+ });
+
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ text: () => Promise.resolve(JSON.stringify({ error: "bad request" })),
+ });
+
+ const ctx = makeCtx(node);
+ await expect(executeNanoBanana(ctx)).rejects.toThrow("bad request");
+ expect(mockFetch).toHaveBeenCalledTimes(1);
+ });
+
it("reroutes stored Gemini image nodes to NewApiWG when Gemini key is missing", async () => {
const node = makeNode();
mockFetch.mockResolvedValueOnce({
diff --git a/src/store/execution/__tests__/runWithFallback.test.ts b/src/store/execution/__tests__/runWithFallback.test.ts
index 2b379277..8856076e 100644
--- a/src/store/execution/__tests__/runWithFallback.test.ts
+++ b/src/store/execution/__tests__/runWithFallback.test.ts
@@ -73,6 +73,22 @@ describe("runWithFallback", () => {
expect(runOnce).toHaveBeenCalledTimes(1);
});
+ it("primary fails but predicate rejects fallback: rethrows primary error", async () => {
+ const runOnce = vi.fn().mockRejectedValueOnce(new Error("primary boom"));
+ await expect(
+ runWithFallback({
+ nodeId: "n1",
+ primary,
+ fallback,
+ updateNodeData,
+ runOnce,
+ shouldFallback: () => false,
+ })
+ ).rejects.toThrow("primary boom");
+
+ expect(runOnce).toHaveBeenCalledTimes(1);
+ });
+
it("primary fails, fallback succeeds: stamps metadata and clears error", async () => {
const runOnce = vi
.fn()
diff --git a/src/store/execution/nanoBananaExecutor.ts b/src/store/execution/nanoBananaExecutor.ts
index 789d4860..ce2661a5 100644
--- a/src/store/execution/nanoBananaExecutor.ts
+++ b/src/store/execution/nanoBananaExecutor.ts
@@ -14,6 +14,8 @@ import { buildGenerateHeaders } from "@/store/utils/buildApiHeaders";
import {
createDefaultImageModel,
createNewApiWGDefaultImageModel,
+ createNewApiWGDefaultImageFallbackModel,
+ isNewApiWGDefaultImageModel,
shouldPreferNewApiWGImageModel,
} from "@/store/utils/defaultImageModel";
import { pollGenerateTask } from "./pollTaskCompletion";
@@ -31,6 +33,11 @@ export interface NanoBananaOptions {
useStoredFallback?: boolean;
}
+function isProviderOverloadError(error: unknown): boolean {
+ const message = error instanceof Error ? error.message : String(error);
+ return /provider is temporarily overloaded|system\s+disk\s+overloaded/i.test(message);
+}
+
async function loadSavedGenerationImage(directoryPath: string, imageId: string): Promise {
const response = await fetch("/api/load-generation", {
method: "POST",
@@ -419,6 +426,7 @@ export async function executeNanoBanana(
nodeData.selectedModel ?? createDefaultImageModel(nodeData.model, providerSettings, providerEnvStatus);
let primaryParameters: Record | undefined;
let fallbackModel = nodeData.fallbackModel;
+ let shouldFallback: ((error: unknown) => boolean) | undefined;
if (
primaryModel.provider === "gemini" &&
@@ -447,6 +455,11 @@ export async function executeNanoBanana(
});
}
+ if (!fallbackModel && isNewApiWGDefaultImageModel(primaryModel)) {
+ fallbackModel = createNewApiWGDefaultImageFallbackModel();
+ shouldFallback = isProviderOverloadError;
+ }
+
await runWithFallback({
nodeId: node.id,
primary: primaryModel,
@@ -455,6 +468,7 @@ export async function executeNanoBanana(
fallbackParameters: nodeData.fallbackParameters,
updateNodeData,
runOnce,
+ shouldFallback,
clearOutput: { outputImage: null },
});
}
diff --git a/src/store/execution/runWithFallback.ts b/src/store/execution/runWithFallback.ts
index 1b6fd75c..96f11714 100644
--- a/src/store/execution/runWithFallback.ts
+++ b/src/store/execution/runWithFallback.ts
@@ -23,6 +23,7 @@ export interface RunWithFallbackOptions {
fallbackParameters?: Record;
updateNodeData: (id: string, data: Partial) => void;
runOnce: (model: SelectedModel, parametersOverride?: Record) => Promise;
+ shouldFallback?: (error: unknown) => boolean;
/** Data to merge when transitioning to fallback (e.g. { outputImage: null }) */
clearOutput?: Partial;
}
@@ -44,7 +45,7 @@ function isSameModel(a: SelectedModel, b: SelectedModel): boolean {
export async function runWithFallback(
options: RunWithFallbackOptions
): Promise {
- const { nodeId, primary, primaryParameters, fallback, fallbackParameters, updateNodeData, runOnce, clearOutput } = options;
+ const { nodeId, primary, primaryParameters, fallback, fallbackParameters, updateNodeData, runOnce, shouldFallback, clearOutput } = options;
// Clear any prior fallback metadata before we start.
updateNodeData(nodeId, {
@@ -68,6 +69,9 @@ export async function runWithFallback(
}
const primaryErrMsg = errorMessage(primaryError);
+ if (shouldFallback && !shouldFallback(primaryError)) {
+ throw primaryError;
+ }
// Clear error state and stale output, show the fallback is now running.
updateNodeData(nodeId, {
diff --git a/src/store/utils/defaultImageModel.ts b/src/store/utils/defaultImageModel.ts
index 4099453f..b0117139 100644
--- a/src/store/utils/defaultImageModel.ts
+++ b/src/store/utils/defaultImageModel.ts
@@ -3,6 +3,7 @@ import { MODEL_DISPLAY_NAMES } from "@/types";
import { isPopiProviderMode } from "@/lib/providerMode";
const NEWAPIWG_DEFAULT_IMAGE_MODEL_ID = "apiyi_nano_banana_2";
+const NEWAPIWG_DEFAULT_IMAGE_FALLBACK_MODEL_ID = "gpt-image-2-all";
const NEWAPIWG_DEFAULT_VIDEO_MODEL_ID = "doubao-seedance-2-0-260128";
const DEFAULT_AUDIO_MODEL_ID = "elevenlabs/turbo-v2.5";
@@ -15,6 +16,19 @@ export function createNewApiWGDefaultImageModel(): SelectedModel {
};
}
+export function createNewApiWGDefaultImageFallbackModel(): SelectedModel {
+ return {
+ provider: "newapiwg",
+ modelId: NEWAPIWG_DEFAULT_IMAGE_FALLBACK_MODEL_ID,
+ displayName: NEWAPIWG_DEFAULT_IMAGE_FALLBACK_MODEL_ID,
+ capabilities: ["text-to-image", "image-to-image"],
+ };
+}
+
+export function isNewApiWGDefaultImageModel(model: SelectedModel): boolean {
+ return model.provider === "newapiwg" && model.modelId === NEWAPIWG_DEFAULT_IMAGE_MODEL_ID;
+}
+
export function createNewApiWGDefaultVideoModel(): SelectedModel {
return {
provider: "newapiwg",
diff --git a/src/store/workflowStore.ts b/src/store/workflowStore.ts
index b6923699..abe440fd 100644
--- a/src/store/workflowStore.ts
+++ b/src/store/workflowStore.ts
@@ -2689,7 +2689,7 @@ const workflowStoreImpl: StateCreator = (set, get) => ({
const isBrowserPath = isBrowserFileSystemPath(saveDirectoryPath);
// If external media storage is enabled, externalize media before saving
- if (useExternalImageStorage && !isBrowserPath) {
+ if (useExternalImageStorage) {
workflow = await externalizeWorkflowMedia(workflow, saveDirectoryPath);
}
@@ -2710,7 +2710,7 @@ const workflowStoreImpl: StateCreator = (set, get) => ({
// If we externalized media, update store nodes with the refs
// This prevents duplicate media on subsequent saves
- if (useExternalImageStorage && !isBrowserPath && workflow.nodes !== currentNodes) {
+ if (useExternalImageStorage && workflow.nodes !== currentNodes) {
// Merge refs from externalized nodes into current nodes (keeping media data)
const nodesWithRefs = currentNodes.map((node, index) => {
const externalizedNode = workflow.nodes[index];
@@ -2776,7 +2776,7 @@ const workflowStoreImpl: StateCreator = (set, get) => ({
lastSavedAt: timestamp,
hasUnsavedChanges: false,
// Update imageRefBasePath to reflect save location
- imageRefBasePath: useExternalImageStorage && !isBrowserPath ? saveDirectoryPath : null,
+ imageRefBasePath: useExternalImageStorage ? saveDirectoryPath : null,
});
}
diff --git a/src/utils/__tests__/mediaStorage.browserfs.test.ts b/src/utils/__tests__/mediaStorage.browserfs.test.ts
new file mode 100644
index 00000000..2279bc03
--- /dev/null
+++ b/src/utils/__tests__/mediaStorage.browserfs.test.ts
@@ -0,0 +1,204 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { externalizeWorkflowMedia, hydrateWorkflowMedia } from "../mediaStorage";
+import type { WorkflowFile } from "@/store/workflowStore";
+
+const browserFsMocks = vi.hoisted(() => ({
+ writeBrowserGenerationFile: vi.fn(),
+ loadBrowserGenerationFile: vi.fn(),
+}));
+
+vi.mock("../browserFileSystem", () => ({
+ isBrowserFileSystemPath: (path: string | null | undefined) =>
+ typeof path === "string" && path.startsWith("browserfs://"),
+ joinBrowserFileSystemPath: (basePath: string, folderName: string) =>
+ `${basePath.replace(/\/+$/, "")}/${encodeURIComponent(folderName)}`,
+ writeBrowserGenerationFile: browserFsMocks.writeBrowserGenerationFile,
+ loadBrowserGenerationFile: browserFsMocks.loadBrowserGenerationFile,
+}));
+
+const imageData = "data:image/png;base64,aW1hZ2U=";
+const videoData = "data:video/mp4;base64,dmlkZW8=";
+const audioData = "data:audio/mpeg;base64,YXVkaW8=";
+const workflowPath = "browserfs://root/project";
+
+function makeWorkflow(): WorkflowFile {
+ return {
+ version: 1,
+ id: "wf-browserfs",
+ name: "browserfs media",
+ nodes: [
+ {
+ id: "imageInput-1",
+ type: "imageInput",
+ position: { x: 0, y: 0 },
+ data: {
+ image: imageData,
+ filename: "input.png",
+ dimensions: null,
+ },
+ },
+ {
+ id: "generateVideo-1",
+ type: "generateVideo",
+ position: { x: 200, y: 0 },
+ data: {
+ inputImages: [imageData],
+ inputPrompt: "move",
+ outputVideo: videoData,
+ outputVideoRef: undefined,
+ outputVideoRemoteUrl: undefined,
+ outputVideoStorageStatus: undefined,
+ selectedVideoHistoryIndex: 0,
+ videoHistory: [
+ {
+ id: "vid-history-1",
+ video: videoData,
+ timestamp: 1,
+ prompt: "move",
+ },
+ ],
+ status: "completed",
+ error: null,
+ },
+ },
+ {
+ id: "generateAudio-1",
+ type: "generateAudio",
+ position: { x: 400, y: 0 },
+ data: {
+ inputPrompt: "sound",
+ outputAudio: audioData,
+ outputAudioRef: undefined,
+ selectedAudioHistoryIndex: 0,
+ audioHistory: [
+ {
+ id: "aud-history-1",
+ audio: audioData,
+ timestamp: 1,
+ prompt: "sound",
+ },
+ ],
+ status: "completed",
+ error: null,
+ },
+ },
+ ],
+ edges: [],
+ edgeStyle: "angular",
+ } as WorkflowFile;
+}
+
+describe("mediaStorage browserfs support", () => {
+ beforeEach(() => {
+ browserFsMocks.writeBrowserGenerationFile.mockReset();
+ browserFsMocks.loadBrowserGenerationFile.mockReset();
+ browserFsMocks.writeBrowserGenerationFile.mockResolvedValue({ filePath: "browserfs://root/project/media.bin" });
+ vi.stubGlobal("fetch", vi.fn(async () => ({
+ json: async () => ({ success: true, imageId: "server-ref" }),
+ })));
+ });
+
+ it("externalizes browserfs media through browser file helpers instead of API fetch", async () => {
+ const workflow = makeWorkflow();
+
+ const externalized = await externalizeWorkflowMedia(workflow, workflowPath);
+
+ expect(fetch).not.toHaveBeenCalled();
+ expect(browserFsMocks.writeBrowserGenerationFile).toHaveBeenCalledWith(
+ "browserfs://root/project/inputs",
+ expect.any(String),
+ imageData
+ );
+ expect(browserFsMocks.writeBrowserGenerationFile).toHaveBeenCalledWith(
+ "browserfs://root/project/generations",
+ "vid-history-1",
+ videoData
+ );
+ expect(browserFsMocks.writeBrowserGenerationFile).toHaveBeenCalledWith(
+ "browserfs://root/project/generations",
+ "aud-history-1",
+ audioData
+ );
+
+ expect(externalized.nodes[0].data).toMatchObject({
+ image: null,
+ imageRef: expect.any(String),
+ });
+ expect(externalized.nodes[1].data).toMatchObject({
+ inputImages: [],
+ inputImageRefs: [expect.any(String)],
+ outputVideo: null,
+ outputVideoRef: "vid-history-1",
+ outputVideoStorageStatus: "localized",
+ videoHistory: [
+ {
+ id: "vid-history-1",
+ timestamp: 1,
+ prompt: "move",
+ },
+ ],
+ });
+ expect(externalized.nodes[2].data).toMatchObject({
+ outputAudio: null,
+ outputAudioRef: "aud-history-1",
+ });
+ });
+
+ it("hydrates browserfs media refs through browser file helpers instead of API fetch", async () => {
+ browserFsMocks.loadBrowserGenerationFile.mockImplementation(async (directoryPath: string, mediaId: string) => {
+ if (directoryPath.endsWith("/inputs") && mediaId === "input-ref") return imageData;
+ if (directoryPath.endsWith("/generations") && mediaId === "video-ref") return videoData;
+ if (directoryPath.endsWith("/generations") && mediaId === "audio-ref") return audioData;
+ return null;
+ });
+
+ const workflow: WorkflowFile = {
+ ...makeWorkflow(),
+ nodes: [
+ {
+ id: "imageInput-1",
+ type: "imageInput",
+ position: { x: 0, y: 0 },
+ data: { image: null, imageRef: "input-ref" },
+ },
+ {
+ id: "generateVideo-1",
+ type: "generateVideo",
+ position: { x: 200, y: 0 },
+ data: {
+ inputImages: [],
+ inputImageRefs: ["input-ref"],
+ outputVideo: null,
+ outputVideoRef: "video-ref",
+ outputVideoStorageStatus: "localized",
+ status: "idle",
+ error: null,
+ },
+ },
+ {
+ id: "generateAudio-1",
+ type: "generateAudio",
+ position: { x: 400, y: 0 },
+ data: {
+ inputPrompt: "sound",
+ outputAudio: null,
+ outputAudioRef: "audio-ref",
+ status: "idle",
+ error: null,
+ },
+ },
+ ],
+ } as WorkflowFile;
+
+ const hydrated = await hydrateWorkflowMedia(workflow, workflowPath);
+
+ expect(fetch).not.toHaveBeenCalled();
+ expect(hydrated.nodes[0].data).toMatchObject({ image: imageData });
+ expect(hydrated.nodes[1].data).toMatchObject({
+ inputImages: [imageData],
+ outputVideo: videoData,
+ outputVideoStorageStatus: "localized",
+ });
+ expect(hydrated.nodes[2].data).toMatchObject({ outputAudio: audioData });
+ });
+});
diff --git a/src/utils/browserFileSystem.ts b/src/utils/browserFileSystem.ts
index 334722e8..d3ce6837 100644
--- a/src/utils/browserFileSystem.ts
+++ b/src/utils/browserFileSystem.ts
@@ -52,15 +52,49 @@ export function joinBrowserFileSystemPath(basePath: string, folderName: string):
return `${basePath.replace(/\/+$/, "")}/${encodeURIComponent(folderName)}`;
}
-const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "webp", "gif"] as const;
+const MEDIA_EXTENSIONS = [
+ "png",
+ "jpg",
+ "jpeg",
+ "webp",
+ "gif",
+ "mp4",
+ "webm",
+ "mov",
+ "mp3",
+ "wav",
+ "ogg",
+ "flac",
+ "aac",
+] as const;
function extensionFromMime(mime: string | null | undefined): string {
if (mime === "image/jpeg") return "jpg";
if (mime === "image/webp") return "webp";
if (mime === "image/gif") return "gif";
+ if (mime === "video/webm") return "webm";
+ if (mime === "video/quicktime") return "mov";
+ if (mime?.startsWith("video/")) return "mp4";
+ if (mime === "audio/wav") return "wav";
+ if (mime === "audio/ogg") return "ogg";
+ if (mime === "audio/flac") return "flac";
+ if (mime === "audio/aac") return "aac";
+ if (mime?.startsWith("audio/")) return "mp3";
return "png";
}
+function mimeFromExtension(extension: string): string {
+ if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
+ if (extension === "mov") return "video/quicktime";
+ if (extension === "mp3") return "audio/mpeg";
+ if (extension === "wav") return "audio/wav";
+ if (extension === "ogg") return "audio/ogg";
+ if (extension === "flac") return "audio/flac";
+ if (extension === "aac") return "audio/aac";
+ if (extension === "mp4" || extension === "webm") return `video/${extension}`;
+ return `image/${extension}`;
+}
+
function extensionFromDataUrl(dataUrl: string): string {
return extensionFromMime(dataUrl.match(/^data:([^;]+);base64,/)?.[1]);
}
@@ -75,12 +109,16 @@ async function imageSourceToBlob(src: string): Promise {
return response.blob();
}
-async function fileToDataUrl(file: File): Promise {
+async function fileToDataUrl(file: File, fallbackMime?: string): Promise {
+ const blob = file.type || !fallbackMime
+ ? file
+ : file.slice(0, file.size, fallbackMime);
+
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error);
- reader.readAsDataURL(file);
+ reader.readAsDataURL(blob);
});
}
@@ -154,10 +192,10 @@ export async function loadBrowserGenerationFile(
const directoryHandle = await resolveDirectoryHandle(directoryPath, false);
await ensurePermission(directoryHandle, "readwrite");
- for (const extension of IMAGE_EXTENSIONS) {
+ for (const extension of MEDIA_EXTENSIONS) {
try {
const fileHandle = await directoryHandle.getFileHandle(`${imageId}.${extension}`);
- return fileToDataUrl(await fileHandle.getFile());
+ return fileToDataUrl(await fileHandle.getFile(), mimeFromExtension(extension));
} catch {
continue;
}
diff --git a/src/utils/mediaStorage.ts b/src/utils/mediaStorage.ts
index ec1c96aa..40266610 100644
--- a/src/utils/mediaStorage.ts
+++ b/src/utils/mediaStorage.ts
@@ -1,6 +1,12 @@
import { WorkflowNode, WorkflowNodeData } from "@/types";
import { WorkflowFile } from "@/store/workflowStore";
import crypto from "crypto";
+import {
+ isBrowserFileSystemPath,
+ joinBrowserFileSystemPath,
+ loadBrowserGenerationFile,
+ writeBrowserGenerationFile,
+} from "./browserFileSystem";
/**
* Fetch with timeout support using AbortController
@@ -672,6 +678,16 @@ async function saveImageAndGetId(
const imageId = existingId || generateImageId();
const savePromise = (async () => {
+ if (isBrowserFileSystemPath(workflowPath)) {
+ await writeBrowserGenerationFile(
+ joinBrowserFileSystemPath(workflowPath, folder),
+ imageId,
+ imageData
+ );
+ savedImageIds.set(hash, imageId);
+ return imageId;
+ }
+
const response = await fetchWithTimeout(
"/api/workflow-images",
{
@@ -738,6 +754,16 @@ async function saveVideoAndGetRef(
const videoId = existingId || generateMediaId("vid");
const savePromise = (async () => {
+ if (isBrowserFileSystemPath(workflowPath)) {
+ await writeBrowserGenerationFile(
+ joinBrowserFileSystemPath(workflowPath, "generations"),
+ videoId,
+ videoData
+ );
+ savedMediaIds.set(hash, videoId);
+ return videoId;
+ }
+
const response = await fetchWithTimeout(
"/api/save-generation",
{
@@ -806,6 +832,16 @@ async function saveAudioAndGetRef(
const audioId = existingId || generateMediaId("aud");
const savePromise = (async () => {
+ if (isBrowserFileSystemPath(workflowPath)) {
+ await writeBrowserGenerationFile(
+ joinBrowserFileSystemPath(workflowPath, "generations"),
+ audioId,
+ audioData
+ );
+ savedMediaIds.set(hash, audioId);
+ return audioId;
+ }
+
const response = await fetchWithTimeout(
"/api/save-generation",
{
@@ -1242,6 +1278,24 @@ async function loadMediaById(
return loadedMedia.get(mediaId)!;
}
+ if (isBrowserFileSystemPath(workflowPath)) {
+ const folderNames = mediaType === "image" ? ["inputs", "generations"] : ["generations"];
+
+ for (const folderName of folderNames) {
+ const mediaData = await loadBrowserGenerationFile(
+ joinBrowserFileSystemPath(workflowPath, folderName),
+ mediaId
+ );
+ if (mediaData) {
+ loadedMedia.set(mediaId, mediaData);
+ return mediaData;
+ }
+ }
+
+ console.log(`${mediaType} not found: ${mediaId}`);
+ return "";
+ }
+
let response: Response;
if (mediaType === "image") {