Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
// "extends": ["next/core-web-vitals", "next/typescript"]
"extends": ["next/core-web-vitals", "next/typescript"]
}
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# ComfyUI embedded workflow editor

In-place embedded workflow-exif editing experience for ComfyUI generated images. Edit png exif just in your browser.
In-place embedded workflow-exif editing experience for ComfyUI generated media files. Edit workflow data embedded in PNG, WEBP, FLAC, MP3, and MP4 files directly in your browser.

![screenshot](docs/screenshot.png)

## Usage

1. Open https://comfyui-embeded-workflow-editor.vercel.app/
2. Upload your img (or mount your local directory)
- Supported formats: PNG, WEBP, FLAC, MP3, MP4
- You can also directly load a file via URL parameter: `?url=https://example.com/image.png`
- Or paste a URL into the URL input field
3. Edit as you want
Expand All @@ -19,6 +20,7 @@ In-place embedded workflow-exif editing experience for ComfyUI generated images.
- [x] png read/write
- [x] webp read/write
- [x] Flac read/write
- [x] MP3 read/write
- [x] MP4 read/write
- [ ] jpg (seems not possible yet)
- [x] Show preview img to ensure you are editing the right image (thumbnail)
Expand Down
48 changes: 25 additions & 23 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useSearchParam } from "react-use";
import sflow, { sf } from "sflow";
import useSWR from "swr";
import TimeAgo from "timeago-react";
import useManifestPWA from "use-manifest-pwa";
// import useManifestPWA from "use-manifest-pwa";
import { useSnapshot } from "valtio";
import { persistState } from "./persistState";
import { readWorkflowInfo, setWorkflowInfo } from "./utils/exif";
Expand All @@ -18,23 +18,24 @@ import { readWorkflowInfo, setWorkflowInfo } from "./utils/exif";
* @author snomiao <snomiao@gmail.com> 2024
*/
export default function Home() {
useManifestPWA({
icons: [
{
src: "/favicon.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/favicon.png",
sizes: "512x512",
type: "image/png",
},
],
name: "ComfyUI Embedded Workflow Editor",
short_name: "CWE",
start_url: globalThis.window?.location.origin ?? "/",
});
// todo: enable this in another PR
// useManifestPWA({
// icons: [
// {
// src: "/favicon.png",
// sizes: "192x192",
// type: "image/png",
// },
// {
// src: "/favicon.png",
// sizes: "512x512",
// type: "image/png",
// },
// ],
// name: "ComfyUI Embedded Workflow Editor",
// short_name: "CWE",
// start_url: globalThis.window?.location.origin ?? "/",
// });

const snap = useSnapshot(persistState);
const snapSync = useSnapshot(persistState, { sync: true });
Expand Down Expand Up @@ -68,7 +69,7 @@ export default function Home() {
if (!files.length) return toast.error("No files provided.");
const readedWorkflowInfos = await sflow(files)
.filter((e) => {
if (e.name.match(/\.(png|flac|webp|mp4)$/i)) return true;
if (e.name.match(/\.(png|flac|webp|mp4|mp3)$/i)) return true;
toast.error("Not Supported format discarded: " + e.name);
return null;
})
Expand Down Expand Up @@ -183,15 +184,15 @@ export default function Home() {
<input
readOnly
className="input input-bordered border-dashed input-sm w-full text-center"
placeholder="Way-1. Paste/Drop files here (png, webp, flac, mp4)"
placeholder="Way-1. Paste/Drop files here (png, webp, flac, mp3, mp4)"
onPaste={async (e) => await gotFiles(e.clipboardData.files)}
/>
<div className="flex w-full gap-2">
<input
value={urlInput}
onChange={(e) => setUrlInput(e.target.value)}
className="input input-bordered input-sm flex-1"
placeholder="Way-4. Paste URL here (png, webp, flac, mp4)"
placeholder="Way-4. Paste URL here (png, webp, flac, mp3, mp4)"
onKeyDown={(e) => {
if (e.key === "Enter" && urlInput) {
(
Expand Down Expand Up @@ -232,7 +233,7 @@ export default function Home() {
description: "Supported Files",
accept: {
"image/*": [".png", ".webp"],
"audio/*": [".flac"],
"audio/*": [".flac", ".mp3"],
"video/*": [".mp4"],
},
},
Expand Down Expand Up @@ -413,6 +414,7 @@ export default function Home() {
webp: "img",
mp4: "mp4",
flac: "flac",
mp3: "audio",
};
let typeKey = extTypeMap[ext];
if (!typeKey) {
Expand Down Expand Up @@ -534,7 +536,7 @@ export default function Home() {
const aIter = workingDir.values() as AsyncIterable<FileSystemFileHandle>;
const readed = await sf(aIter)
.filter((e) => e.kind === "file")
.filter((e) => e.name.match(/\.(png|flac|webp|mp4)$/i))
.filter((e) => e.name.match(/\.(png|flac|webp|mp4|mp3)$/i))
.map(async (e) => await e.getFile())
.map(async (e) => await readWorkflowInfo(e))
.filter((e) => e.workflowJson)
Expand Down
99 changes: 99 additions & 0 deletions app/utils/exif-mp3.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { getMp3Metadata, setMp3Metadata } from "./exif-mp3";

test("MP3 metadata extraction", async () => {
// Read test MP3 file
const testFile = Bun.file("tests/mp3/ComfyUI_00047_.mp3");
const buffer = await testFile.arrayBuffer();

// Extract metadata
const metadata = getMp3Metadata(buffer);

// Log the metadata to see what's available
console.log("MP3 metadata:", metadata);

// Basic test to ensure the function runs without errors
expect(metadata).toBeDefined();
});

test("MP3 metadata write and read", async () => {
// Read test MP3 file
const testFile = Bun.file("tests/mp3/ComfyUI_00047_.mp3");
const buffer = await testFile.arrayBuffer();

// Create test workflow JSON
const testWorkflow = JSON.stringify({
test: "workflow",
nodes: [{ id: 1, name: "Test Node" }],
});

// Set metadata - now we can pass the Buffer directly
const modified = setMp3Metadata(buffer, { workflow: testWorkflow });

// Read back the metadata
const readMetadata = getMp3Metadata(modified);

// Verify the workflow was written and read correctly
expect(readMetadata.workflow).toBe(testWorkflow);
});

test("MP3 metadata update", async () => {
// Read test MP3 file
const testFile = Bun.file("tests/mp3/ComfyUI_00047_.mp3");
const buffer = await testFile.arrayBuffer();

// First, add some metadata - now we can pass the Buffer directly
const modified1 = setMp3Metadata(buffer, {
title: "Test Title",
artist: "ComfyUI",
});

// Then, update the title but keep the artist - no need for conversion
const modified2 = setMp3Metadata(modified1, {
title: "Updated Title",
workflow: "Test Workflow",
});

// Read back the metadata
const readMetadata = getMp3Metadata(modified2);

// Verify updates
expect(readMetadata.title).toBe("Updated Title");
expect(readMetadata.workflow).toBe("Test Workflow");
expect(readMetadata.artist).toBe("ComfyUI"); // Artist should be preserved
});

test("MP3 metadata preservation", async () => {
// Read test MP3 file
const testFile = Bun.file("tests/mp3/ComfyUI_00047_.mp3");
const originalBuffer = await testFile.arrayBuffer();

// Get original metadata
const originalMetadata = getMp3Metadata(originalBuffer);
console.log("Original metadata keys:", Object.keys(originalMetadata));

// Sample workflow data
const sampleWorkflow = JSON.stringify({
test: "workflow data",
nodes: { id1: { class_type: "TestNode" } },
});

// Update only the workflow
const modifiedBuffer = setMp3Metadata(originalBuffer, {
workflow: sampleWorkflow,
});

// Get the updated metadata
const updatedMetadata = getMp3Metadata(modifiedBuffer);

// Verify the workflow was updated
expect(updatedMetadata.workflow).toBeDefined();
expect(updatedMetadata.workflow).toEqual(sampleWorkflow);

// Verify other existing metadata is preserved
for (const key of Object.keys(originalMetadata)) {
if (key !== "workflow") {
console.log(`Checking preservation of ${key}`);
expect(updatedMetadata[key]).toEqual(originalMetadata[key]);
}
}
});
Loading