Skip to content

Commit 3dc7797

Browse files
authored
Feat: add mp3 workflow support (#11)
* fix(sno-mp3): stage stage * fix(sno-mp3): mp3 supported mp3 supported * fix(sno-mp3): all tests passed all tests passed * format: Apply prettier --fix changes * chore(bun): fix lockfile fix lockfile * fix: update supported file formats to include MP3 in the workflow editor * fix(sno-mp3): upgrade usemanifestpwa upgrade usemanifestpwa * fix: remove mp3 debug script * fix: restore next lint * fix: restore use-manifest-pwa version * fix(sno-mp3): tmp disable useManifestPWA tmp disable useManifestPWA
1 parent 9d6c1cf commit 3dc7797

File tree

9 files changed

+2613
-30
lines changed

9 files changed

+2613
-30
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
// "extends": ["next/core-web-vitals", "next/typescript"]
2+
"extends": ["next/core-web-vitals", "next/typescript"]
33
}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# ComfyUI embedded workflow editor
22

3-
In-place embedded workflow-exif editing experience for ComfyUI generated images. Edit png exif just in your browser.
3+
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.
44

55
![screenshot](docs/screenshot.png)
66

77
## Usage
88

99
1. Open https://comfyui-embeded-workflow-editor.vercel.app/
1010
2. Upload your img (or mount your local directory)
11+
- Supported formats: PNG, WEBP, FLAC, MP3, MP4
1112
- You can also directly load a file via URL parameter: `?url=https://example.com/image.png`
1213
- Or paste a URL into the URL input field
1314
3. Edit as you want
@@ -19,6 +20,7 @@ In-place embedded workflow-exif editing experience for ComfyUI generated images.
1920
- [x] png read/write
2021
- [x] webp read/write
2122
- [x] Flac read/write
23+
- [x] MP3 read/write
2224
- [x] MP4 read/write
2325
- [ ] jpg (seems not possible yet)
2426
- [x] Show preview img to ensure you are editing the right image (thumbnail)

app/page.tsx

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useSearchParam } from "react-use";
99
import sflow, { sf } from "sflow";
1010
import useSWR from "swr";
1111
import TimeAgo from "timeago-react";
12-
import useManifestPWA from "use-manifest-pwa";
12+
// import useManifestPWA from "use-manifest-pwa";
1313
import { useSnapshot } from "valtio";
1414
import { persistState } from "./persistState";
1515
import { readWorkflowInfo, setWorkflowInfo } from "./utils/exif";
@@ -18,23 +18,24 @@ import { readWorkflowInfo, setWorkflowInfo } from "./utils/exif";
1818
* @author snomiao <[email protected]> 2024
1919
*/
2020
export default function Home() {
21-
useManifestPWA({
22-
icons: [
23-
{
24-
src: "/favicon.png",
25-
sizes: "192x192",
26-
type: "image/png",
27-
},
28-
{
29-
src: "/favicon.png",
30-
sizes: "512x512",
31-
type: "image/png",
32-
},
33-
],
34-
name: "ComfyUI Embedded Workflow Editor",
35-
short_name: "CWE",
36-
start_url: globalThis.window?.location.origin ?? "/",
37-
});
21+
// todo: enable this in another PR
22+
// useManifestPWA({
23+
// icons: [
24+
// {
25+
// src: "/favicon.png",
26+
// sizes: "192x192",
27+
// type: "image/png",
28+
// },
29+
// {
30+
// src: "/favicon.png",
31+
// sizes: "512x512",
32+
// type: "image/png",
33+
// },
34+
// ],
35+
// name: "ComfyUI Embedded Workflow Editor",
36+
// short_name: "CWE",
37+
// start_url: globalThis.window?.location.origin ?? "/",
38+
// });
3839

3940
const snap = useSnapshot(persistState);
4041
const snapSync = useSnapshot(persistState, { sync: true });
@@ -68,7 +69,7 @@ export default function Home() {
6869
if (!files.length) return toast.error("No files provided.");
6970
const readedWorkflowInfos = await sflow(files)
7071
.filter((e) => {
71-
if (e.name.match(/\.(png|flac|webp|mp4)$/i)) return true;
72+
if (e.name.match(/\.(png|flac|webp|mp4|mp3)$/i)) return true;
7273
toast.error("Not Supported format discarded: " + e.name);
7374
return null;
7475
})
@@ -183,15 +184,15 @@ export default function Home() {
183184
<input
184185
readOnly
185186
className="input input-bordered border-dashed input-sm w-full text-center"
186-
placeholder="Way-1. Paste/Drop files here (png, webp, flac, mp4)"
187+
placeholder="Way-1. Paste/Drop files here (png, webp, flac, mp3, mp4)"
187188
onPaste={async (e) => await gotFiles(e.clipboardData.files)}
188189
/>
189190
<div className="flex w-full gap-2">
190191
<input
191192
value={urlInput}
192193
onChange={(e) => setUrlInput(e.target.value)}
193194
className="input input-bordered input-sm flex-1"
194-
placeholder="Way-4. Paste URL here (png, webp, flac, mp4)"
195+
placeholder="Way-4. Paste URL here (png, webp, flac, mp3, mp4)"
195196
onKeyDown={(e) => {
196197
if (e.key === "Enter" && urlInput) {
197198
(
@@ -232,7 +233,7 @@ export default function Home() {
232233
description: "Supported Files",
233234
accept: {
234235
"image/*": [".png", ".webp"],
235-
"audio/*": [".flac"],
236+
"audio/*": [".flac", ".mp3"],
236237
"video/*": [".mp4"],
237238
},
238239
},
@@ -413,6 +414,7 @@ export default function Home() {
413414
webp: "img",
414415
mp4: "mp4",
415416
flac: "flac",
417+
mp3: "audio",
416418
};
417419
let typeKey = extTypeMap[ext];
418420
if (!typeKey) {
@@ -534,7 +536,7 @@ export default function Home() {
534536
const aIter = workingDir.values() as AsyncIterable<FileSystemFileHandle>;
535537
const readed = await sf(aIter)
536538
.filter((e) => e.kind === "file")
537-
.filter((e) => e.name.match(/\.(png|flac|webp|mp4)$/i))
539+
.filter((e) => e.name.match(/\.(png|flac|webp|mp4|mp3)$/i))
538540
.map(async (e) => await e.getFile())
539541
.map(async (e) => await readWorkflowInfo(e))
540542
.filter((e) => e.workflowJson)

app/utils/exif-mp3.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { getMp3Metadata, setMp3Metadata } from "./exif-mp3";
2+
3+
test("MP3 metadata extraction", async () => {
4+
// Read test MP3 file
5+
const testFile = Bun.file("tests/mp3/ComfyUI_00047_.mp3");
6+
const buffer = await testFile.arrayBuffer();
7+
8+
// Extract metadata
9+
const metadata = getMp3Metadata(buffer);
10+
11+
// Log the metadata to see what's available
12+
console.log("MP3 metadata:", metadata);
13+
14+
// Basic test to ensure the function runs without errors
15+
expect(metadata).toBeDefined();
16+
});
17+
18+
test("MP3 metadata write and read", async () => {
19+
// Read test MP3 file
20+
const testFile = Bun.file("tests/mp3/ComfyUI_00047_.mp3");
21+
const buffer = await testFile.arrayBuffer();
22+
23+
// Create test workflow JSON
24+
const testWorkflow = JSON.stringify({
25+
test: "workflow",
26+
nodes: [{ id: 1, name: "Test Node" }],
27+
});
28+
29+
// Set metadata - now we can pass the Buffer directly
30+
const modified = setMp3Metadata(buffer, { workflow: testWorkflow });
31+
32+
// Read back the metadata
33+
const readMetadata = getMp3Metadata(modified);
34+
35+
// Verify the workflow was written and read correctly
36+
expect(readMetadata.workflow).toBe(testWorkflow);
37+
});
38+
39+
test("MP3 metadata update", async () => {
40+
// Read test MP3 file
41+
const testFile = Bun.file("tests/mp3/ComfyUI_00047_.mp3");
42+
const buffer = await testFile.arrayBuffer();
43+
44+
// First, add some metadata - now we can pass the Buffer directly
45+
const modified1 = setMp3Metadata(buffer, {
46+
title: "Test Title",
47+
artist: "ComfyUI",
48+
});
49+
50+
// Then, update the title but keep the artist - no need for conversion
51+
const modified2 = setMp3Metadata(modified1, {
52+
title: "Updated Title",
53+
workflow: "Test Workflow",
54+
});
55+
56+
// Read back the metadata
57+
const readMetadata = getMp3Metadata(modified2);
58+
59+
// Verify updates
60+
expect(readMetadata.title).toBe("Updated Title");
61+
expect(readMetadata.workflow).toBe("Test Workflow");
62+
expect(readMetadata.artist).toBe("ComfyUI"); // Artist should be preserved
63+
});
64+
65+
test("MP3 metadata preservation", async () => {
66+
// Read test MP3 file
67+
const testFile = Bun.file("tests/mp3/ComfyUI_00047_.mp3");
68+
const originalBuffer = await testFile.arrayBuffer();
69+
70+
// Get original metadata
71+
const originalMetadata = getMp3Metadata(originalBuffer);
72+
console.log("Original metadata keys:", Object.keys(originalMetadata));
73+
74+
// Sample workflow data
75+
const sampleWorkflow = JSON.stringify({
76+
test: "workflow data",
77+
nodes: { id1: { class_type: "TestNode" } },
78+
});
79+
80+
// Update only the workflow
81+
const modifiedBuffer = setMp3Metadata(originalBuffer, {
82+
workflow: sampleWorkflow,
83+
});
84+
85+
// Get the updated metadata
86+
const updatedMetadata = getMp3Metadata(modifiedBuffer);
87+
88+
// Verify the workflow was updated
89+
expect(updatedMetadata.workflow).toBeDefined();
90+
expect(updatedMetadata.workflow).toEqual(sampleWorkflow);
91+
92+
// Verify other existing metadata is preserved
93+
for (const key of Object.keys(originalMetadata)) {
94+
if (key !== "workflow") {
95+
console.log(`Checking preservation of ${key}`);
96+
expect(updatedMetadata[key]).toEqual(originalMetadata[key]);
97+
}
98+
}
99+
});

0 commit comments

Comments
 (0)