@rslsp1/fa-app-tools 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +35 -8
- package/dist/index.d.ts +35 -8
- package/dist/index.js +294 -9
- package/dist/index.mjs +279 -9
- package/package.json +4 -1
package/dist/index.d.mts
CHANGED
|
@@ -4,6 +4,32 @@ import { Node, Edge } from '@xyflow/react';
|
|
|
4
4
|
|
|
5
5
|
declare function useOnClickOutside(ref: RefObject<HTMLElement | null>, handler: (e: MouseEvent | TouchEvent) => void): void;
|
|
6
6
|
|
|
7
|
+
declare function injectXMPMetadata(base64: string, prompt: string, seed?: number, model?: string, id?: string, tags?: string[], hierarchy?: string): string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Vollständiger Export: JSON + MD + Bilder (mit XMP Meta)
|
|
11
|
+
*/
|
|
12
|
+
declare function exportProjectToZip(nodes: any[], edges: any[], history: any[], galleryItems: any[], settings: any): Promise<{
|
|
13
|
+
base64: string;
|
|
14
|
+
}>;
|
|
15
|
+
declare function importProjectFromZip(file: File): Promise<any>;
|
|
16
|
+
|
|
17
|
+
interface ExtractedCharacter {
|
|
18
|
+
name: string;
|
|
19
|
+
prompts: {
|
|
20
|
+
title: string;
|
|
21
|
+
content: string;
|
|
22
|
+
}[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parsed Charakter-Dateien für den Import.
|
|
26
|
+
*/
|
|
27
|
+
declare function parsePromptFile(text: string): ExtractedCharacter[];
|
|
28
|
+
/**
|
|
29
|
+
* Wandelt die Knotenstruktur in einen sauberen Markdown-Baum um.
|
|
30
|
+
*/
|
|
31
|
+
declare function formatTreeToMarkdown(nodes: any[], edges: any[]): string;
|
|
32
|
+
|
|
7
33
|
declare function FaToolsBadge(): react_jsx_runtime.JSX.Element;
|
|
8
34
|
|
|
9
35
|
interface PillButtonProps {
|
|
@@ -89,7 +115,7 @@ interface SetupPanelProps {
|
|
|
89
115
|
onWorkspaceImport: (file: File) => void;
|
|
90
116
|
onProjectExport: () => void;
|
|
91
117
|
onProjectImport: (file: File) => void;
|
|
92
|
-
projectActionState: 'idle' | 'working' | 'done' | 'error';
|
|
118
|
+
projectActionState: 'idle' | 'working' | 'working-full' | 'done' | 'error';
|
|
93
119
|
}
|
|
94
120
|
declare const SetupPanel: React.FC<SetupPanelProps>;
|
|
95
121
|
|
|
@@ -98,8 +124,9 @@ interface MediaLibraryProps {
|
|
|
98
124
|
onImport: () => void;
|
|
99
125
|
onDelete: (id: string) => void;
|
|
100
126
|
onSelect: (item: Generation) => void;
|
|
101
|
-
onToggleSelection
|
|
102
|
-
onBatchDownload
|
|
127
|
+
onToggleSelection?: (id: string) => void;
|
|
128
|
+
onBatchDownload?: () => void;
|
|
129
|
+
onGenerateReference?: (item: Generation) => void;
|
|
103
130
|
}
|
|
104
131
|
declare const MediaLibrary: React.FC<MediaLibraryProps>;
|
|
105
132
|
|
|
@@ -109,10 +136,10 @@ interface ListViewProps {
|
|
|
109
136
|
onNodeChange: (id: string, label: string) => void;
|
|
110
137
|
onAddChild: (parentId: string) => void;
|
|
111
138
|
onDeleteNode: (id: string) => void;
|
|
112
|
-
onMoveNode
|
|
113
|
-
onIndentNode
|
|
114
|
-
onOutdentNode
|
|
115
|
-
onAddSibling
|
|
139
|
+
onMoveNode?: (id: string, direction: 'up' | 'down') => void;
|
|
140
|
+
onIndentNode?: (id: string) => void;
|
|
141
|
+
onOutdentNode?: (id: string) => void;
|
|
142
|
+
onAddSibling?: (id: string) => void;
|
|
116
143
|
focusedNodeId: string | null;
|
|
117
144
|
onFocus: (id: string | null) => void;
|
|
118
145
|
activePath: Set<string>;
|
|
@@ -123,4 +150,4 @@ interface ListViewProps {
|
|
|
123
150
|
}
|
|
124
151
|
declare function ListView({ nodes, edges, onNodeChange, onAddChild, onDeleteNode, onMoveNode, onIndentNode, onOutdentNode, onAddSibling, focusedNodeId, onFocus, activePath, onGenerate, onGenerateBranch, onGenerateSubtree, isGeneratingNodeId }: ListViewProps): react_jsx_runtime.JSX.Element;
|
|
125
152
|
|
|
126
|
-
export { CompactDropdown, FaToolsBadge, type Generation, HistoryPanel, InspectPanel, ListView, MediaLibrary, PillButton, type ProjectSettings, SectionLabel, SetupPanel, type WorkspaceTags, useOnClickOutside };
|
|
153
|
+
export { CompactDropdown, type ExtractedCharacter, FaToolsBadge, type Generation, HistoryPanel, InspectPanel, ListView, MediaLibrary, PillButton, type ProjectSettings, SectionLabel, SetupPanel, type WorkspaceTags, exportProjectToZip, formatTreeToMarkdown, importProjectFromZip, injectXMPMetadata, parsePromptFile, useOnClickOutside };
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,32 @@ import { Node, Edge } from '@xyflow/react';
|
|
|
4
4
|
|
|
5
5
|
declare function useOnClickOutside(ref: RefObject<HTMLElement | null>, handler: (e: MouseEvent | TouchEvent) => void): void;
|
|
6
6
|
|
|
7
|
+
declare function injectXMPMetadata(base64: string, prompt: string, seed?: number, model?: string, id?: string, tags?: string[], hierarchy?: string): string;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Vollständiger Export: JSON + MD + Bilder (mit XMP Meta)
|
|
11
|
+
*/
|
|
12
|
+
declare function exportProjectToZip(nodes: any[], edges: any[], history: any[], galleryItems: any[], settings: any): Promise<{
|
|
13
|
+
base64: string;
|
|
14
|
+
}>;
|
|
15
|
+
declare function importProjectFromZip(file: File): Promise<any>;
|
|
16
|
+
|
|
17
|
+
interface ExtractedCharacter {
|
|
18
|
+
name: string;
|
|
19
|
+
prompts: {
|
|
20
|
+
title: string;
|
|
21
|
+
content: string;
|
|
22
|
+
}[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parsed Charakter-Dateien für den Import.
|
|
26
|
+
*/
|
|
27
|
+
declare function parsePromptFile(text: string): ExtractedCharacter[];
|
|
28
|
+
/**
|
|
29
|
+
* Wandelt die Knotenstruktur in einen sauberen Markdown-Baum um.
|
|
30
|
+
*/
|
|
31
|
+
declare function formatTreeToMarkdown(nodes: any[], edges: any[]): string;
|
|
32
|
+
|
|
7
33
|
declare function FaToolsBadge(): react_jsx_runtime.JSX.Element;
|
|
8
34
|
|
|
9
35
|
interface PillButtonProps {
|
|
@@ -89,7 +115,7 @@ interface SetupPanelProps {
|
|
|
89
115
|
onWorkspaceImport: (file: File) => void;
|
|
90
116
|
onProjectExport: () => void;
|
|
91
117
|
onProjectImport: (file: File) => void;
|
|
92
|
-
projectActionState: 'idle' | 'working' | 'done' | 'error';
|
|
118
|
+
projectActionState: 'idle' | 'working' | 'working-full' | 'done' | 'error';
|
|
93
119
|
}
|
|
94
120
|
declare const SetupPanel: React.FC<SetupPanelProps>;
|
|
95
121
|
|
|
@@ -98,8 +124,9 @@ interface MediaLibraryProps {
|
|
|
98
124
|
onImport: () => void;
|
|
99
125
|
onDelete: (id: string) => void;
|
|
100
126
|
onSelect: (item: Generation) => void;
|
|
101
|
-
onToggleSelection
|
|
102
|
-
onBatchDownload
|
|
127
|
+
onToggleSelection?: (id: string) => void;
|
|
128
|
+
onBatchDownload?: () => void;
|
|
129
|
+
onGenerateReference?: (item: Generation) => void;
|
|
103
130
|
}
|
|
104
131
|
declare const MediaLibrary: React.FC<MediaLibraryProps>;
|
|
105
132
|
|
|
@@ -109,10 +136,10 @@ interface ListViewProps {
|
|
|
109
136
|
onNodeChange: (id: string, label: string) => void;
|
|
110
137
|
onAddChild: (parentId: string) => void;
|
|
111
138
|
onDeleteNode: (id: string) => void;
|
|
112
|
-
onMoveNode
|
|
113
|
-
onIndentNode
|
|
114
|
-
onOutdentNode
|
|
115
|
-
onAddSibling
|
|
139
|
+
onMoveNode?: (id: string, direction: 'up' | 'down') => void;
|
|
140
|
+
onIndentNode?: (id: string) => void;
|
|
141
|
+
onOutdentNode?: (id: string) => void;
|
|
142
|
+
onAddSibling?: (id: string) => void;
|
|
116
143
|
focusedNodeId: string | null;
|
|
117
144
|
onFocus: (id: string | null) => void;
|
|
118
145
|
activePath: Set<string>;
|
|
@@ -123,4 +150,4 @@ interface ListViewProps {
|
|
|
123
150
|
}
|
|
124
151
|
declare function ListView({ nodes, edges, onNodeChange, onAddChild, onDeleteNode, onMoveNode, onIndentNode, onOutdentNode, onAddSibling, focusedNodeId, onFocus, activePath, onGenerate, onGenerateBranch, onGenerateSubtree, isGeneratingNodeId }: ListViewProps): react_jsx_runtime.JSX.Element;
|
|
125
152
|
|
|
126
|
-
export { CompactDropdown, FaToolsBadge, type Generation, HistoryPanel, InspectPanel, ListView, MediaLibrary, PillButton, type ProjectSettings, SectionLabel, SetupPanel, type WorkspaceTags, useOnClickOutside };
|
|
153
|
+
export { CompactDropdown, type ExtractedCharacter, FaToolsBadge, type Generation, HistoryPanel, InspectPanel, ListView, MediaLibrary, PillButton, type ProjectSettings, SectionLabel, SetupPanel, type WorkspaceTags, exportProjectToZip, formatTreeToMarkdown, importProjectFromZip, injectXMPMetadata, parsePromptFile, useOnClickOutside };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -29,6 +39,11 @@ __export(index_exports, {
|
|
|
29
39
|
PillButton: () => PillButton,
|
|
30
40
|
SectionLabel: () => SectionLabel,
|
|
31
41
|
SetupPanel: () => SetupPanel,
|
|
42
|
+
exportProjectToZip: () => exportProjectToZip,
|
|
43
|
+
formatTreeToMarkdown: () => formatTreeToMarkdown,
|
|
44
|
+
importProjectFromZip: () => importProjectFromZip,
|
|
45
|
+
injectXMPMetadata: () => injectXMPMetadata,
|
|
46
|
+
parsePromptFile: () => parsePromptFile,
|
|
32
47
|
useOnClickOutside: () => useOnClickOutside
|
|
33
48
|
});
|
|
34
49
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -50,6 +65,265 @@ function useOnClickOutside(ref, handler) {
|
|
|
50
65
|
}, [ref, handler]);
|
|
51
66
|
}
|
|
52
67
|
|
|
68
|
+
// src/lib/metadata.ts
|
|
69
|
+
var crcTable = (() => {
|
|
70
|
+
let c;
|
|
71
|
+
const table = new Uint32Array(256);
|
|
72
|
+
for (let n = 0; n < 256; n++) {
|
|
73
|
+
c = n;
|
|
74
|
+
for (let k = 0; k < 8; k++) {
|
|
75
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
76
|
+
}
|
|
77
|
+
table[n] = c;
|
|
78
|
+
}
|
|
79
|
+
return table;
|
|
80
|
+
})();
|
|
81
|
+
function calculateCRC(bytes) {
|
|
82
|
+
let crc = 4294967295;
|
|
83
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
84
|
+
crc = crcTable[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
|
|
85
|
+
}
|
|
86
|
+
return (crc ^ 4294967295) >>> 0;
|
|
87
|
+
}
|
|
88
|
+
function indexOfBytes(buffer, search, start = 0) {
|
|
89
|
+
for (let i = start; i <= buffer.length - search.length; i++) {
|
|
90
|
+
let found = true;
|
|
91
|
+
for (let j = 0; j < search.length; j++) {
|
|
92
|
+
if (buffer[i + j] !== search[j]) {
|
|
93
|
+
found = false;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (found) return i;
|
|
98
|
+
}
|
|
99
|
+
return -1;
|
|
100
|
+
}
|
|
101
|
+
function createXMPPacket(prompt, seed, model, id, tags = [], hierarchy) {
|
|
102
|
+
const sanitize = (str) => (str || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
103
|
+
const escPrompt = sanitize(prompt);
|
|
104
|
+
const escModel = sanitize(model || "AI Model");
|
|
105
|
+
const escHierarchy = sanitize(hierarchy || "");
|
|
106
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
107
|
+
const allTags = [.../* @__PURE__ */ new Set([...tags, "Avatar Architect", escModel])];
|
|
108
|
+
const rdfTags = allTags.map((t) => `<rdf:li>${sanitize(t)}</rdf:li>`).join("");
|
|
109
|
+
const BOM = "\uFEFF";
|
|
110
|
+
return `<?xpacket begin="${BOM}" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
|
111
|
+
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c140">
|
|
112
|
+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
113
|
+
<rdf:Description rdf:about=""
|
|
114
|
+
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
|
|
115
|
+
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
116
|
+
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
|
|
117
|
+
xmlns:ai="http://ns.flow.creative/ai/1.0/">
|
|
118
|
+
<dc:format>image/png</dc:format>
|
|
119
|
+
<dc:description><rdf:Alt><rdf:li xml:lang="x-default">${escPrompt}</rdf:li></rdf:Alt></dc:description>
|
|
120
|
+
<dc:subject><rdf:Bag>${rdfTags}</rdf:Bag></dc:subject>
|
|
121
|
+
<photoshop:Instructions>Seed: ${seed} | Model: ${escModel}</photoshop:Instructions>
|
|
122
|
+
<xmp:CreateDate>${timestamp}</xmp:CreateDate>
|
|
123
|
+
<ai:prompt>${escPrompt}</ai:prompt>
|
|
124
|
+
<ai:seed>${seed ?? "0"}</ai:seed>
|
|
125
|
+
<ai:model>${escModel}</ai:model>
|
|
126
|
+
<ai:hierarchy>${escHierarchy}</ai:hierarchy>
|
|
127
|
+
</rdf:Description>
|
|
128
|
+
</rdf:RDF>
|
|
129
|
+
</x:xmpmeta>${" ".repeat(1024)}<?xpacket end="w"?>`;
|
|
130
|
+
}
|
|
131
|
+
function bytesToBase64(bytes) {
|
|
132
|
+
let binary = "";
|
|
133
|
+
const len = bytes.byteLength;
|
|
134
|
+
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
|
|
135
|
+
return btoa(binary);
|
|
136
|
+
}
|
|
137
|
+
function base64ToBytes(base64) {
|
|
138
|
+
const clean = base64.includes(",") ? base64.split(",")[1] : base64.trim();
|
|
139
|
+
try {
|
|
140
|
+
const binary = atob(clean);
|
|
141
|
+
const bytes = new Uint8Array(binary.length);
|
|
142
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
143
|
+
return bytes;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return new Uint8Array(0);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function injectXMPMetadata(base64, prompt, seed, model, id, tags = [], hierarchy) {
|
|
149
|
+
try {
|
|
150
|
+
const bytes = base64ToBytes(base64);
|
|
151
|
+
if (bytes.length < 8 || bytes[0] !== 137 || bytes[1] !== 80) {
|
|
152
|
+
console.warn("Metadaten-Injektion \xFCbersprungen: Datei ist kein PNG.");
|
|
153
|
+
return base64;
|
|
154
|
+
}
|
|
155
|
+
const xmpData = createXMPPacket(prompt, seed, model, id, tags, hierarchy);
|
|
156
|
+
const encoder = new TextEncoder();
|
|
157
|
+
const keyword = encoder.encode("XML:com.adobe.xmp");
|
|
158
|
+
const textBytes = encoder.encode(xmpData);
|
|
159
|
+
const chunkData = new Uint8Array(keyword.length + 5 + textBytes.length);
|
|
160
|
+
chunkData.set(keyword, 0);
|
|
161
|
+
chunkData.set(textBytes, keyword.length + 5);
|
|
162
|
+
const type = encoder.encode("iTXt");
|
|
163
|
+
const crcInput = new Uint8Array(type.length + chunkData.length);
|
|
164
|
+
crcInput.set(type, 0);
|
|
165
|
+
crcInput.set(chunkData, type.length);
|
|
166
|
+
const crc = calculateCRC(crcInput);
|
|
167
|
+
const fullChunk = new Uint8Array(4 + 4 + chunkData.length + 4);
|
|
168
|
+
const view = new DataView(fullChunk.buffer);
|
|
169
|
+
view.setUint32(0, chunkData.length);
|
|
170
|
+
fullChunk.set(type, 4);
|
|
171
|
+
fullChunk.set(chunkData, 8);
|
|
172
|
+
view.setUint32(8 + chunkData.length, crc);
|
|
173
|
+
const IDAT = encoder.encode("IDAT");
|
|
174
|
+
const idatPos = indexOfBytes(bytes, IDAT);
|
|
175
|
+
const insertPos = idatPos !== -1 ? idatPos - 4 : 8;
|
|
176
|
+
const result = new Uint8Array(bytes.length + fullChunk.length);
|
|
177
|
+
result.set(bytes.slice(0, insertPos), 0);
|
|
178
|
+
result.set(fullChunk, insertPos);
|
|
179
|
+
result.set(bytes.slice(insertPos), insertPos + fullChunk.length);
|
|
180
|
+
return bytesToBase64(result);
|
|
181
|
+
} catch (e) {
|
|
182
|
+
console.error("Fehler beim Einbetten der Metadaten:", e);
|
|
183
|
+
return base64;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/lib/project.ts
|
|
188
|
+
var import_jszip = __toESM(require("jszip"));
|
|
189
|
+
function generateMarkdownReport(nodes, history) {
|
|
190
|
+
let md = "# Avatar Architect - Projekt Report\n\n";
|
|
191
|
+
md += `Exportiert: ${(/* @__PURE__ */ new Date()).toLocaleString()}
|
|
192
|
+
|
|
193
|
+
`;
|
|
194
|
+
md += "## Hierarchie\n";
|
|
195
|
+
nodes.forEach((n) => {
|
|
196
|
+
md += `- **${n.data?.label || "Unbenannt"}**
|
|
197
|
+
`;
|
|
198
|
+
});
|
|
199
|
+
md += "\n## Historie der Generationen\n";
|
|
200
|
+
history.forEach((h, i) => {
|
|
201
|
+
md += `### ${i + 1}. Generation (${h.model || "AI"})
|
|
202
|
+
`;
|
|
203
|
+
md += `- **Zeitstempel**: ${new Date(h.timestamp).toLocaleString()}
|
|
204
|
+
`;
|
|
205
|
+
md += `- **Seed**: ${h.seed}
|
|
206
|
+
`;
|
|
207
|
+
md += `- **Prompt**:
|
|
208
|
+
> ${h.prompt}
|
|
209
|
+
|
|
210
|
+
`;
|
|
211
|
+
});
|
|
212
|
+
return md;
|
|
213
|
+
}
|
|
214
|
+
async function exportProjectToZip(nodes, edges, history, galleryItems, settings) {
|
|
215
|
+
const zip = new import_jszip.default();
|
|
216
|
+
const projectData = {
|
|
217
|
+
nodes,
|
|
218
|
+
edges,
|
|
219
|
+
history: history.map((h) => ({ ...h, base64: void 0 })),
|
|
220
|
+
galleryItems: galleryItems.map((g) => ({ ...g, base64: void 0 })),
|
|
221
|
+
settings,
|
|
222
|
+
version: "1.2.6"
|
|
223
|
+
};
|
|
224
|
+
zip.file("project.json", JSON.stringify(projectData, null, 2));
|
|
225
|
+
zip.file("projekt_bericht.md", generateMarkdownReport(nodes, history));
|
|
226
|
+
const imgFolder = zip.folder("images");
|
|
227
|
+
if (imgFolder) {
|
|
228
|
+
const allItems = [...history, ...galleryItems];
|
|
229
|
+
for (const item of allItems) {
|
|
230
|
+
if (item.base64) {
|
|
231
|
+
try {
|
|
232
|
+
const cleanB64 = item.base64.includes(",") ? item.base64.split(",")[1] : item.base64;
|
|
233
|
+
const withMeta = injectXMPMetadata(cleanB64, item.prompt || "", item.seed, item.model, item.id, item.tags || []);
|
|
234
|
+
imgFolder.file(`${item.id}.png`, withMeta, { base64: true });
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.error("Fehler beim Packen eines Bildes:", e);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const blob = await zip.generateAsync({ type: "blob", compression: "STORE" });
|
|
242
|
+
return blobToBase64(blob);
|
|
243
|
+
}
|
|
244
|
+
async function blobToBase64(blob) {
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
const reader = new FileReader();
|
|
247
|
+
reader.onloadend = () => {
|
|
248
|
+
const result = reader.result;
|
|
249
|
+
resolve({ base64: result.split(",")[1] });
|
|
250
|
+
};
|
|
251
|
+
reader.onerror = reject;
|
|
252
|
+
reader.readAsDataURL(blob);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async function importProjectFromZip(file) {
|
|
256
|
+
const zip = await import_jszip.default.loadAsync(file);
|
|
257
|
+
const jsonStr = await zip.file("project.json")?.async("string");
|
|
258
|
+
if (!jsonStr) throw new Error("Keine project.json gefunden.");
|
|
259
|
+
const data = JSON.parse(jsonStr);
|
|
260
|
+
const imgFolder = zip.folder("images");
|
|
261
|
+
if (imgFolder) {
|
|
262
|
+
const loadImgs = async (items) => {
|
|
263
|
+
if (!items) return [];
|
|
264
|
+
return Promise.all(items.map(async (h) => {
|
|
265
|
+
const imgFile = zip.file(`images/${h.id}.png`);
|
|
266
|
+
if (!imgFile) return h;
|
|
267
|
+
const b64 = await imgFile.async("base64");
|
|
268
|
+
return { ...h, base64: `data:image/png;base64,${b64}` };
|
|
269
|
+
}));
|
|
270
|
+
};
|
|
271
|
+
data.history = await loadImgs(data.history || []);
|
|
272
|
+
data.galleryItems = await loadImgs(data.galleryItems || []);
|
|
273
|
+
}
|
|
274
|
+
return data;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/lib/parserService.ts
|
|
278
|
+
function parsePromptFile(text) {
|
|
279
|
+
const characters = [];
|
|
280
|
+
const sections = text.split(/^##\s+/m);
|
|
281
|
+
for (let i = 1; i < sections.length; i++) {
|
|
282
|
+
const section = sections[i];
|
|
283
|
+
const lines = section.split("\n");
|
|
284
|
+
const name = lines[0].trim();
|
|
285
|
+
const character = {
|
|
286
|
+
name,
|
|
287
|
+
prompts: []
|
|
288
|
+
};
|
|
289
|
+
const promptRegex = /###\s+(.*?)\s*\n\s*```(?:\w+)?\n([\s\S]*?)```/g;
|
|
290
|
+
let match;
|
|
291
|
+
while ((match = promptRegex.exec(section)) !== null) {
|
|
292
|
+
character.prompts.push({
|
|
293
|
+
title: match[1].trim(),
|
|
294
|
+
content: match[2].trim()
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (character.name) {
|
|
298
|
+
characters.push(character);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return characters;
|
|
302
|
+
}
|
|
303
|
+
function formatTreeToMarkdown(nodes, edges) {
|
|
304
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
305
|
+
nodes.forEach((n) => nodeMap.set(n.id, { ...n, children: [] }));
|
|
306
|
+
edges.forEach((e) => {
|
|
307
|
+
const parent = nodeMap.get(e.source);
|
|
308
|
+
if (parent) parent.children.push(e.target);
|
|
309
|
+
});
|
|
310
|
+
const roots = nodes.filter((n) => !edges.some((e) => e.target === n.id));
|
|
311
|
+
const processNode = (id, depth = 0) => {
|
|
312
|
+
const node = nodeMap.get(id);
|
|
313
|
+
if (!node) return "";
|
|
314
|
+
const indent = " ".repeat(depth);
|
|
315
|
+
let md = `${indent}- ${node.data.label || "Unbenannt"}
|
|
316
|
+
`;
|
|
317
|
+
if (node.children) {
|
|
318
|
+
node.children.forEach((childId) => {
|
|
319
|
+
md += processNode(childId, depth + 1);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return md;
|
|
323
|
+
};
|
|
324
|
+
return roots.map((root) => processNode(root.id)).join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
53
327
|
// src/components/Badge.tsx
|
|
54
328
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
55
329
|
function FaToolsBadge() {
|
|
@@ -175,7 +449,7 @@ var HistoryPanel = ({ history, currentResultId, onSelect, onDelete }) => {
|
|
|
175
449
|
className: `flex gap-3 p-2 bg-white/5 border rounded-2xl cursor-pointer group transition-all relative ${currentResultId === gen.id ? "border-white/40 bg-white/10 shadow-lg" : "border-white/5 hover:border-white/20"}`,
|
|
176
450
|
onClick: () => onSelect(gen),
|
|
177
451
|
children: [
|
|
178
|
-
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-14 h-14 rounded-xl overflow-hidden bg-black shrink-0 border border-white/5", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: gen.base64
|
|
452
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "w-14 h-14 rounded-xl overflow-hidden bg-black shrink-0 border border-white/5", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src: gen.base64 ? gen.base64.startsWith("data:") ? gen.base64 : `data:image/png;base64,${gen.base64}` : "", className: "w-full h-full object-cover", alt: "Thumbnail" }) }),
|
|
179
453
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex-1 py-0.5 overflow-hidden", children: [
|
|
180
454
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex items-center justify-between mb-1", children: [
|
|
181
455
|
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-[7px] font-bold text-white/20 uppercase whitespace-nowrap", children: formatFriendlyTimestamp(gen.timestamp) }),
|
|
@@ -209,10 +483,10 @@ var InspectPanel = ({ currentResult, history, onSelect, workspaceTags, onTagTogg
|
|
|
209
483
|
}
|
|
210
484
|
const fullDateStr = new Date(currentResult.timestamp).toLocaleString([], { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
211
485
|
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_react4.motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, className: "absolute inset-0 flex flex-col overflow-hidden", children: [
|
|
212
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-shrink-0 border-b border-white/5 bg-black/20 py-3 px-4", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex gap-2 overflow-x-auto no-scrollbar pb-1", children: history.map((gen) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: () => onSelect(gen), className: `w-10 h-10 rounded-lg overflow-hidden border shrink-0 transition-all ${currentResult.id === gen.id ? "border-white scale-105 shadow-lg" : "border-white/5 opacity-40 hover:opacity-100"}`, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { src: gen.base64
|
|
486
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-shrink-0 border-b border-white/5 bg-black/20 py-3 px-4", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex gap-2 overflow-x-auto no-scrollbar pb-1", children: history.map((gen) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { onClick: () => onSelect(gen), className: `w-10 h-10 rounded-lg overflow-hidden border shrink-0 transition-all ${currentResult.id === gen.id ? "border-white scale-105 shadow-lg" : "border-white/5 opacity-40 hover:opacity-100"}`, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { src: gen.base64 ? gen.base64.startsWith("data:") ? gen.base64 : `data:image/png;base64,${gen.base64}` : "", className: "w-full h-full object-cover", alt: "Thumbnail" }) }, gen.id)) }) }),
|
|
213
487
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col gap-6 dark-scrollbar pb-10", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col gap-4", children: [
|
|
214
488
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SectionLabel, { children: "Vorschau" }),
|
|
215
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "aspect-square w-full rounded-2xl bg-black overflow-hidden border border-white/10 shadow-2xl", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { src: currentResult.base64
|
|
489
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "aspect-square w-full rounded-2xl bg-black overflow-hidden border border-white/10 shadow-2xl", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { src: currentResult.base64 ? currentResult.base64.startsWith("data:") ? currentResult.base64 : `data:image/png;base64,${currentResult.base64}` : "", className: "w-full h-full object-cover", alt: "Preview" }) }),
|
|
216
490
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "p-4 bg-white/5 rounded-2xl border border-white/5", children: [
|
|
217
491
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-[8px] font-bold text-white/20 uppercase mb-2 tracking-widest", children: "Prompt Details" }),
|
|
218
492
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("p", { className: "text-[10px] text-white/70 italic leading-relaxed font-medium", children: [
|
|
@@ -301,7 +575,7 @@ var SetupPanel = ({ onWorkspaceImport, onProjectExport, onProjectImport, project
|
|
|
301
575
|
// src/components/MediaLibrary.tsx
|
|
302
576
|
var import_react7 = require("motion/react");
|
|
303
577
|
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
304
|
-
var MediaLibrary = ({ items, onImport, onDelete, onSelect, onToggleSelection, onBatchDownload }) => {
|
|
578
|
+
var MediaLibrary = ({ items, onImport, onDelete, onSelect, onToggleSelection, onBatchDownload, onGenerateReference }) => {
|
|
305
579
|
const selectedCount = items.filter((i) => i.selectedForExport).length;
|
|
306
580
|
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col h-full overflow-hidden", children: [
|
|
307
581
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col p-4 border-b border-white/5 gap-3 bg-black/20", children: [
|
|
@@ -336,17 +610,23 @@ var MediaLibrary = ({ items, onImport, onDelete, onSelect, onToggleSelection, on
|
|
|
336
610
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-[10px] italic", children: "Importiere Assets aus deinem Projekt." })
|
|
337
611
|
] })
|
|
338
612
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "grid grid-cols-2 gap-3 pb-10", children: items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_react7.motion.div, { initial: { opacity: 0, scale: 0.9 }, animate: { opacity: 1, scale: 1 }, className: `relative aspect-square group/item rounded-xl overflow-hidden border transition-all bg-white/5 cursor-pointer shadow-lg ${item.selectedForExport ? "border-blue-500 shadow-[0_0_15px_rgba(59,130,246,0.2)]" : "border-white/10 opacity-70 hover:opacity-100"}`, onClick: () => onSelect?.(item), children: [
|
|
339
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("img", { src: item.base64
|
|
613
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("img", { src: item.base64 ? item.base64.startsWith("data:") ? item.base64 : `data:image/png;base64,${item.base64}` : "", className: "w-full h-full object-cover transition-transform duration-500 group-hover/item:scale-110", alt: item.prompt }),
|
|
340
614
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: `absolute top-2 left-2 w-5 h-5 rounded-md border flex items-center justify-center transition-all z-30 pointer-events-auto ${item.selectedForExport ? "bg-blue-500 border-blue-400" : "bg-black/60 border-white/20"}`, onClick: (e) => {
|
|
341
615
|
e.stopPropagation();
|
|
342
616
|
onToggleSelection?.(item.id);
|
|
343
617
|
}, children: item.selectedForExport && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "material-symbols-outlined text-[14px] text-white", children: "check" }) }),
|
|
344
618
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover/item:opacity-100 transition-opacity flex flex-col justify-end p-2 gap-2 pointer-events-none", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center justify-between", children: [
|
|
345
619
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-[8px] font-bold text-white/80 truncate max-w-[80px] uppercase tracking-tighter", children: item.prompt || "Importiert" }),
|
|
346
|
-
/* @__PURE__ */ (0, import_jsx_runtime8.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
620
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-1", children: [
|
|
621
|
+
onGenerateReference && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: (e) => {
|
|
622
|
+
e.stopPropagation();
|
|
623
|
+
onGenerateReference(item);
|
|
624
|
+
}, className: "w-6 h-6 flex items-center justify-center bg-blue-500/20 text-blue-400 hover:bg-blue-500 rounded-md transition-all hover:text-white pointer-events-auto", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "material-symbols-outlined text-[14px]", children: "auto_fix_high" }) }),
|
|
625
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: (e) => {
|
|
626
|
+
e.stopPropagation();
|
|
627
|
+
onDelete?.(item.id);
|
|
628
|
+
}, className: "w-6 h-6 flex items-center justify-center bg-red-500/20 text-red-400 hover:bg-red-500 rounded-md transition-all hover:text-white pointer-events-auto", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "material-symbols-outlined text-[14px]", children: "delete" }) })
|
|
629
|
+
] })
|
|
350
630
|
] }) }),
|
|
351
631
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "absolute top-1 right-1 px-1.5 py-0.5 bg-black/60 backdrop-blur-md rounded text-[7px] font-bold text-white/60 uppercase tracking-tight", children: item.type === "import" ? "Library" : "Gen" })
|
|
352
632
|
] }, item.id)) }) })
|
|
@@ -456,5 +736,10 @@ function ListView({ nodes, edges, onNodeChange, onAddChild, onDeleteNode, onMove
|
|
|
456
736
|
PillButton,
|
|
457
737
|
SectionLabel,
|
|
458
738
|
SetupPanel,
|
|
739
|
+
exportProjectToZip,
|
|
740
|
+
formatTreeToMarkdown,
|
|
741
|
+
importProjectFromZip,
|
|
742
|
+
injectXMPMetadata,
|
|
743
|
+
parsePromptFile,
|
|
459
744
|
useOnClickOutside
|
|
460
745
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -15,6 +15,265 @@ function useOnClickOutside(ref, handler) {
|
|
|
15
15
|
}, [ref, handler]);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// src/lib/metadata.ts
|
|
19
|
+
var crcTable = (() => {
|
|
20
|
+
let c;
|
|
21
|
+
const table = new Uint32Array(256);
|
|
22
|
+
for (let n = 0; n < 256; n++) {
|
|
23
|
+
c = n;
|
|
24
|
+
for (let k = 0; k < 8; k++) {
|
|
25
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
26
|
+
}
|
|
27
|
+
table[n] = c;
|
|
28
|
+
}
|
|
29
|
+
return table;
|
|
30
|
+
})();
|
|
31
|
+
function calculateCRC(bytes) {
|
|
32
|
+
let crc = 4294967295;
|
|
33
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
34
|
+
crc = crcTable[(crc ^ bytes[i]) & 255] ^ crc >>> 8;
|
|
35
|
+
}
|
|
36
|
+
return (crc ^ 4294967295) >>> 0;
|
|
37
|
+
}
|
|
38
|
+
function indexOfBytes(buffer, search, start = 0) {
|
|
39
|
+
for (let i = start; i <= buffer.length - search.length; i++) {
|
|
40
|
+
let found = true;
|
|
41
|
+
for (let j = 0; j < search.length; j++) {
|
|
42
|
+
if (buffer[i + j] !== search[j]) {
|
|
43
|
+
found = false;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (found) return i;
|
|
48
|
+
}
|
|
49
|
+
return -1;
|
|
50
|
+
}
|
|
51
|
+
function createXMPPacket(prompt, seed, model, id, tags = [], hierarchy) {
|
|
52
|
+
const sanitize = (str) => (str || "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
53
|
+
const escPrompt = sanitize(prompt);
|
|
54
|
+
const escModel = sanitize(model || "AI Model");
|
|
55
|
+
const escHierarchy = sanitize(hierarchy || "");
|
|
56
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
57
|
+
const allTags = [.../* @__PURE__ */ new Set([...tags, "Avatar Architect", escModel])];
|
|
58
|
+
const rdfTags = allTags.map((t) => `<rdf:li>${sanitize(t)}</rdf:li>`).join("");
|
|
59
|
+
const BOM = "\uFEFF";
|
|
60
|
+
return `<?xpacket begin="${BOM}" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
|
61
|
+
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c140">
|
|
62
|
+
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
|
63
|
+
<rdf:Description rdf:about=""
|
|
64
|
+
xmlns:xmp="http://ns.adobe.com/xap/1.0/"
|
|
65
|
+
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
66
|
+
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"
|
|
67
|
+
xmlns:ai="http://ns.flow.creative/ai/1.0/">
|
|
68
|
+
<dc:format>image/png</dc:format>
|
|
69
|
+
<dc:description><rdf:Alt><rdf:li xml:lang="x-default">${escPrompt}</rdf:li></rdf:Alt></dc:description>
|
|
70
|
+
<dc:subject><rdf:Bag>${rdfTags}</rdf:Bag></dc:subject>
|
|
71
|
+
<photoshop:Instructions>Seed: ${seed} | Model: ${escModel}</photoshop:Instructions>
|
|
72
|
+
<xmp:CreateDate>${timestamp}</xmp:CreateDate>
|
|
73
|
+
<ai:prompt>${escPrompt}</ai:prompt>
|
|
74
|
+
<ai:seed>${seed ?? "0"}</ai:seed>
|
|
75
|
+
<ai:model>${escModel}</ai:model>
|
|
76
|
+
<ai:hierarchy>${escHierarchy}</ai:hierarchy>
|
|
77
|
+
</rdf:Description>
|
|
78
|
+
</rdf:RDF>
|
|
79
|
+
</x:xmpmeta>${" ".repeat(1024)}<?xpacket end="w"?>`;
|
|
80
|
+
}
|
|
81
|
+
function bytesToBase64(bytes) {
|
|
82
|
+
let binary = "";
|
|
83
|
+
const len = bytes.byteLength;
|
|
84
|
+
for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
|
|
85
|
+
return btoa(binary);
|
|
86
|
+
}
|
|
87
|
+
function base64ToBytes(base64) {
|
|
88
|
+
const clean = base64.includes(",") ? base64.split(",")[1] : base64.trim();
|
|
89
|
+
try {
|
|
90
|
+
const binary = atob(clean);
|
|
91
|
+
const bytes = new Uint8Array(binary.length);
|
|
92
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
93
|
+
return bytes;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return new Uint8Array(0);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function injectXMPMetadata(base64, prompt, seed, model, id, tags = [], hierarchy) {
|
|
99
|
+
try {
|
|
100
|
+
const bytes = base64ToBytes(base64);
|
|
101
|
+
if (bytes.length < 8 || bytes[0] !== 137 || bytes[1] !== 80) {
|
|
102
|
+
console.warn("Metadaten-Injektion \xFCbersprungen: Datei ist kein PNG.");
|
|
103
|
+
return base64;
|
|
104
|
+
}
|
|
105
|
+
const xmpData = createXMPPacket(prompt, seed, model, id, tags, hierarchy);
|
|
106
|
+
const encoder = new TextEncoder();
|
|
107
|
+
const keyword = encoder.encode("XML:com.adobe.xmp");
|
|
108
|
+
const textBytes = encoder.encode(xmpData);
|
|
109
|
+
const chunkData = new Uint8Array(keyword.length + 5 + textBytes.length);
|
|
110
|
+
chunkData.set(keyword, 0);
|
|
111
|
+
chunkData.set(textBytes, keyword.length + 5);
|
|
112
|
+
const type = encoder.encode("iTXt");
|
|
113
|
+
const crcInput = new Uint8Array(type.length + chunkData.length);
|
|
114
|
+
crcInput.set(type, 0);
|
|
115
|
+
crcInput.set(chunkData, type.length);
|
|
116
|
+
const crc = calculateCRC(crcInput);
|
|
117
|
+
const fullChunk = new Uint8Array(4 + 4 + chunkData.length + 4);
|
|
118
|
+
const view = new DataView(fullChunk.buffer);
|
|
119
|
+
view.setUint32(0, chunkData.length);
|
|
120
|
+
fullChunk.set(type, 4);
|
|
121
|
+
fullChunk.set(chunkData, 8);
|
|
122
|
+
view.setUint32(8 + chunkData.length, crc);
|
|
123
|
+
const IDAT = encoder.encode("IDAT");
|
|
124
|
+
const idatPos = indexOfBytes(bytes, IDAT);
|
|
125
|
+
const insertPos = idatPos !== -1 ? idatPos - 4 : 8;
|
|
126
|
+
const result = new Uint8Array(bytes.length + fullChunk.length);
|
|
127
|
+
result.set(bytes.slice(0, insertPos), 0);
|
|
128
|
+
result.set(fullChunk, insertPos);
|
|
129
|
+
result.set(bytes.slice(insertPos), insertPos + fullChunk.length);
|
|
130
|
+
return bytesToBase64(result);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
console.error("Fehler beim Einbetten der Metadaten:", e);
|
|
133
|
+
return base64;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/lib/project.ts
|
|
138
|
+
import JSZip from "jszip";
|
|
139
|
+
function generateMarkdownReport(nodes, history) {
|
|
140
|
+
let md = "# Avatar Architect - Projekt Report\n\n";
|
|
141
|
+
md += `Exportiert: ${(/* @__PURE__ */ new Date()).toLocaleString()}
|
|
142
|
+
|
|
143
|
+
`;
|
|
144
|
+
md += "## Hierarchie\n";
|
|
145
|
+
nodes.forEach((n) => {
|
|
146
|
+
md += `- **${n.data?.label || "Unbenannt"}**
|
|
147
|
+
`;
|
|
148
|
+
});
|
|
149
|
+
md += "\n## Historie der Generationen\n";
|
|
150
|
+
history.forEach((h, i) => {
|
|
151
|
+
md += `### ${i + 1}. Generation (${h.model || "AI"})
|
|
152
|
+
`;
|
|
153
|
+
md += `- **Zeitstempel**: ${new Date(h.timestamp).toLocaleString()}
|
|
154
|
+
`;
|
|
155
|
+
md += `- **Seed**: ${h.seed}
|
|
156
|
+
`;
|
|
157
|
+
md += `- **Prompt**:
|
|
158
|
+
> ${h.prompt}
|
|
159
|
+
|
|
160
|
+
`;
|
|
161
|
+
});
|
|
162
|
+
return md;
|
|
163
|
+
}
|
|
164
|
+
async function exportProjectToZip(nodes, edges, history, galleryItems, settings) {
|
|
165
|
+
const zip = new JSZip();
|
|
166
|
+
const projectData = {
|
|
167
|
+
nodes,
|
|
168
|
+
edges,
|
|
169
|
+
history: history.map((h) => ({ ...h, base64: void 0 })),
|
|
170
|
+
galleryItems: galleryItems.map((g) => ({ ...g, base64: void 0 })),
|
|
171
|
+
settings,
|
|
172
|
+
version: "1.2.6"
|
|
173
|
+
};
|
|
174
|
+
zip.file("project.json", JSON.stringify(projectData, null, 2));
|
|
175
|
+
zip.file("projekt_bericht.md", generateMarkdownReport(nodes, history));
|
|
176
|
+
const imgFolder = zip.folder("images");
|
|
177
|
+
if (imgFolder) {
|
|
178
|
+
const allItems = [...history, ...galleryItems];
|
|
179
|
+
for (const item of allItems) {
|
|
180
|
+
if (item.base64) {
|
|
181
|
+
try {
|
|
182
|
+
const cleanB64 = item.base64.includes(",") ? item.base64.split(",")[1] : item.base64;
|
|
183
|
+
const withMeta = injectXMPMetadata(cleanB64, item.prompt || "", item.seed, item.model, item.id, item.tags || []);
|
|
184
|
+
imgFolder.file(`${item.id}.png`, withMeta, { base64: true });
|
|
185
|
+
} catch (e) {
|
|
186
|
+
console.error("Fehler beim Packen eines Bildes:", e);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const blob = await zip.generateAsync({ type: "blob", compression: "STORE" });
|
|
192
|
+
return blobToBase64(blob);
|
|
193
|
+
}
|
|
194
|
+
async function blobToBase64(blob) {
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const reader = new FileReader();
|
|
197
|
+
reader.onloadend = () => {
|
|
198
|
+
const result = reader.result;
|
|
199
|
+
resolve({ base64: result.split(",")[1] });
|
|
200
|
+
};
|
|
201
|
+
reader.onerror = reject;
|
|
202
|
+
reader.readAsDataURL(blob);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async function importProjectFromZip(file) {
|
|
206
|
+
const zip = await JSZip.loadAsync(file);
|
|
207
|
+
const jsonStr = await zip.file("project.json")?.async("string");
|
|
208
|
+
if (!jsonStr) throw new Error("Keine project.json gefunden.");
|
|
209
|
+
const data = JSON.parse(jsonStr);
|
|
210
|
+
const imgFolder = zip.folder("images");
|
|
211
|
+
if (imgFolder) {
|
|
212
|
+
const loadImgs = async (items) => {
|
|
213
|
+
if (!items) return [];
|
|
214
|
+
return Promise.all(items.map(async (h) => {
|
|
215
|
+
const imgFile = zip.file(`images/${h.id}.png`);
|
|
216
|
+
if (!imgFile) return h;
|
|
217
|
+
const b64 = await imgFile.async("base64");
|
|
218
|
+
return { ...h, base64: `data:image/png;base64,${b64}` };
|
|
219
|
+
}));
|
|
220
|
+
};
|
|
221
|
+
data.history = await loadImgs(data.history || []);
|
|
222
|
+
data.galleryItems = await loadImgs(data.galleryItems || []);
|
|
223
|
+
}
|
|
224
|
+
return data;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/lib/parserService.ts
|
|
228
|
+
function parsePromptFile(text) {
|
|
229
|
+
const characters = [];
|
|
230
|
+
const sections = text.split(/^##\s+/m);
|
|
231
|
+
for (let i = 1; i < sections.length; i++) {
|
|
232
|
+
const section = sections[i];
|
|
233
|
+
const lines = section.split("\n");
|
|
234
|
+
const name = lines[0].trim();
|
|
235
|
+
const character = {
|
|
236
|
+
name,
|
|
237
|
+
prompts: []
|
|
238
|
+
};
|
|
239
|
+
const promptRegex = /###\s+(.*?)\s*\n\s*```(?:\w+)?\n([\s\S]*?)```/g;
|
|
240
|
+
let match;
|
|
241
|
+
while ((match = promptRegex.exec(section)) !== null) {
|
|
242
|
+
character.prompts.push({
|
|
243
|
+
title: match[1].trim(),
|
|
244
|
+
content: match[2].trim()
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (character.name) {
|
|
248
|
+
characters.push(character);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return characters;
|
|
252
|
+
}
|
|
253
|
+
function formatTreeToMarkdown(nodes, edges) {
|
|
254
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
255
|
+
nodes.forEach((n) => nodeMap.set(n.id, { ...n, children: [] }));
|
|
256
|
+
edges.forEach((e) => {
|
|
257
|
+
const parent = nodeMap.get(e.source);
|
|
258
|
+
if (parent) parent.children.push(e.target);
|
|
259
|
+
});
|
|
260
|
+
const roots = nodes.filter((n) => !edges.some((e) => e.target === n.id));
|
|
261
|
+
const processNode = (id, depth = 0) => {
|
|
262
|
+
const node = nodeMap.get(id);
|
|
263
|
+
if (!node) return "";
|
|
264
|
+
const indent = " ".repeat(depth);
|
|
265
|
+
let md = `${indent}- ${node.data.label || "Unbenannt"}
|
|
266
|
+
`;
|
|
267
|
+
if (node.children) {
|
|
268
|
+
node.children.forEach((childId) => {
|
|
269
|
+
md += processNode(childId, depth + 1);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return md;
|
|
273
|
+
};
|
|
274
|
+
return roots.map((root) => processNode(root.id)).join("\n");
|
|
275
|
+
}
|
|
276
|
+
|
|
18
277
|
// src/components/Badge.tsx
|
|
19
278
|
import { jsx } from "react/jsx-runtime";
|
|
20
279
|
function FaToolsBadge() {
|
|
@@ -140,7 +399,7 @@ var HistoryPanel = ({ history, currentResultId, onSelect, onDelete }) => {
|
|
|
140
399
|
className: `flex gap-3 p-2 bg-white/5 border rounded-2xl cursor-pointer group transition-all relative ${currentResultId === gen.id ? "border-white/40 bg-white/10 shadow-lg" : "border-white/5 hover:border-white/20"}`,
|
|
141
400
|
onClick: () => onSelect(gen),
|
|
142
401
|
children: [
|
|
143
|
-
/* @__PURE__ */ jsx5("div", { className: "w-14 h-14 rounded-xl overflow-hidden bg-black shrink-0 border border-white/5", children: /* @__PURE__ */ jsx5("img", { src: gen.base64
|
|
402
|
+
/* @__PURE__ */ jsx5("div", { className: "w-14 h-14 rounded-xl overflow-hidden bg-black shrink-0 border border-white/5", children: /* @__PURE__ */ jsx5("img", { src: gen.base64 ? gen.base64.startsWith("data:") ? gen.base64 : `data:image/png;base64,${gen.base64}` : "", className: "w-full h-full object-cover", alt: "Thumbnail" }) }),
|
|
144
403
|
/* @__PURE__ */ jsxs3("div", { className: "flex-1 py-0.5 overflow-hidden", children: [
|
|
145
404
|
/* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between mb-1", children: [
|
|
146
405
|
/* @__PURE__ */ jsx5("span", { className: "text-[7px] font-bold text-white/20 uppercase whitespace-nowrap", children: formatFriendlyTimestamp(gen.timestamp) }),
|
|
@@ -174,10 +433,10 @@ var InspectPanel = ({ currentResult, history, onSelect, workspaceTags, onTagTogg
|
|
|
174
433
|
}
|
|
175
434
|
const fullDateStr = new Date(currentResult.timestamp).toLocaleString([], { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
176
435
|
return /* @__PURE__ */ jsxs4(motion2.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, className: "absolute inset-0 flex flex-col overflow-hidden", children: [
|
|
177
|
-
/* @__PURE__ */ jsx6("div", { className: "flex-shrink-0 border-b border-white/5 bg-black/20 py-3 px-4", children: /* @__PURE__ */ jsx6("div", { className: "flex gap-2 overflow-x-auto no-scrollbar pb-1", children: history.map((gen) => /* @__PURE__ */ jsx6("button", { onClick: () => onSelect(gen), className: `w-10 h-10 rounded-lg overflow-hidden border shrink-0 transition-all ${currentResult.id === gen.id ? "border-white scale-105 shadow-lg" : "border-white/5 opacity-40 hover:opacity-100"}`, children: /* @__PURE__ */ jsx6("img", { src: gen.base64
|
|
436
|
+
/* @__PURE__ */ jsx6("div", { className: "flex-shrink-0 border-b border-white/5 bg-black/20 py-3 px-4", children: /* @__PURE__ */ jsx6("div", { className: "flex gap-2 overflow-x-auto no-scrollbar pb-1", children: history.map((gen) => /* @__PURE__ */ jsx6("button", { onClick: () => onSelect(gen), className: `w-10 h-10 rounded-lg overflow-hidden border shrink-0 transition-all ${currentResult.id === gen.id ? "border-white scale-105 shadow-lg" : "border-white/5 opacity-40 hover:opacity-100"}`, children: /* @__PURE__ */ jsx6("img", { src: gen.base64 ? gen.base64.startsWith("data:") ? gen.base64 : `data:image/png;base64,${gen.base64}` : "", className: "w-full h-full object-cover", alt: "Thumbnail" }) }, gen.id)) }) }),
|
|
178
437
|
/* @__PURE__ */ jsx6("div", { className: "flex-1 overflow-y-auto p-4 flex flex-col gap-6 dark-scrollbar pb-10", children: /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-4", children: [
|
|
179
438
|
/* @__PURE__ */ jsx6(SectionLabel, { children: "Vorschau" }),
|
|
180
|
-
/* @__PURE__ */ jsx6("div", { className: "aspect-square w-full rounded-2xl bg-black overflow-hidden border border-white/10 shadow-2xl", children: /* @__PURE__ */ jsx6("img", { src: currentResult.base64
|
|
439
|
+
/* @__PURE__ */ jsx6("div", { className: "aspect-square w-full rounded-2xl bg-black overflow-hidden border border-white/10 shadow-2xl", children: /* @__PURE__ */ jsx6("img", { src: currentResult.base64 ? currentResult.base64.startsWith("data:") ? currentResult.base64 : `data:image/png;base64,${currentResult.base64}` : "", className: "w-full h-full object-cover", alt: "Preview" }) }),
|
|
181
440
|
/* @__PURE__ */ jsxs4("div", { className: "p-4 bg-white/5 rounded-2xl border border-white/5", children: [
|
|
182
441
|
/* @__PURE__ */ jsx6("p", { className: "text-[8px] font-bold text-white/20 uppercase mb-2 tracking-widest", children: "Prompt Details" }),
|
|
183
442
|
/* @__PURE__ */ jsxs4("p", { className: "text-[10px] text-white/70 italic leading-relaxed font-medium", children: [
|
|
@@ -266,7 +525,7 @@ var SetupPanel = ({ onWorkspaceImport, onProjectExport, onProjectImport, project
|
|
|
266
525
|
// src/components/MediaLibrary.tsx
|
|
267
526
|
import { motion as motion4 } from "motion/react";
|
|
268
527
|
import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
269
|
-
var MediaLibrary = ({ items, onImport, onDelete, onSelect, onToggleSelection, onBatchDownload }) => {
|
|
528
|
+
var MediaLibrary = ({ items, onImport, onDelete, onSelect, onToggleSelection, onBatchDownload, onGenerateReference }) => {
|
|
270
529
|
const selectedCount = items.filter((i) => i.selectedForExport).length;
|
|
271
530
|
return /* @__PURE__ */ jsxs6("div", { className: "flex flex-col h-full overflow-hidden", children: [
|
|
272
531
|
/* @__PURE__ */ jsxs6("div", { className: "flex flex-col p-4 border-b border-white/5 gap-3 bg-black/20", children: [
|
|
@@ -301,17 +560,23 @@ var MediaLibrary = ({ items, onImport, onDelete, onSelect, onToggleSelection, on
|
|
|
301
560
|
/* @__PURE__ */ jsx8("p", { className: "text-[10px] italic", children: "Importiere Assets aus deinem Projekt." })
|
|
302
561
|
] })
|
|
303
562
|
] }) : /* @__PURE__ */ jsx8("div", { className: "grid grid-cols-2 gap-3 pb-10", children: items.map((item) => /* @__PURE__ */ jsxs6(motion4.div, { initial: { opacity: 0, scale: 0.9 }, animate: { opacity: 1, scale: 1 }, className: `relative aspect-square group/item rounded-xl overflow-hidden border transition-all bg-white/5 cursor-pointer shadow-lg ${item.selectedForExport ? "border-blue-500 shadow-[0_0_15px_rgba(59,130,246,0.2)]" : "border-white/10 opacity-70 hover:opacity-100"}`, onClick: () => onSelect?.(item), children: [
|
|
304
|
-
/* @__PURE__ */ jsx8("img", { src: item.base64
|
|
563
|
+
/* @__PURE__ */ jsx8("img", { src: item.base64 ? item.base64.startsWith("data:") ? item.base64 : `data:image/png;base64,${item.base64}` : "", className: "w-full h-full object-cover transition-transform duration-500 group-hover/item:scale-110", alt: item.prompt }),
|
|
305
564
|
/* @__PURE__ */ jsx8("button", { type: "button", className: `absolute top-2 left-2 w-5 h-5 rounded-md border flex items-center justify-center transition-all z-30 pointer-events-auto ${item.selectedForExport ? "bg-blue-500 border-blue-400" : "bg-black/60 border-white/20"}`, onClick: (e) => {
|
|
306
565
|
e.stopPropagation();
|
|
307
566
|
onToggleSelection?.(item.id);
|
|
308
567
|
}, children: item.selectedForExport && /* @__PURE__ */ jsx8("span", { className: "material-symbols-outlined text-[14px] text-white", children: "check" }) }),
|
|
309
568
|
/* @__PURE__ */ jsx8("div", { className: "absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover/item:opacity-100 transition-opacity flex flex-col justify-end p-2 gap-2 pointer-events-none", children: /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-between", children: [
|
|
310
569
|
/* @__PURE__ */ jsx8("span", { className: "text-[8px] font-bold text-white/80 truncate max-w-[80px] uppercase tracking-tighter", children: item.prompt || "Importiert" }),
|
|
311
|
-
/* @__PURE__ */
|
|
312
|
-
e
|
|
313
|
-
|
|
314
|
-
|
|
570
|
+
/* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1", children: [
|
|
571
|
+
onGenerateReference && /* @__PURE__ */ jsx8("button", { type: "button", onClick: (e) => {
|
|
572
|
+
e.stopPropagation();
|
|
573
|
+
onGenerateReference(item);
|
|
574
|
+
}, className: "w-6 h-6 flex items-center justify-center bg-blue-500/20 text-blue-400 hover:bg-blue-500 rounded-md transition-all hover:text-white pointer-events-auto", children: /* @__PURE__ */ jsx8("span", { className: "material-symbols-outlined text-[14px]", children: "auto_fix_high" }) }),
|
|
575
|
+
/* @__PURE__ */ jsx8("button", { type: "button", onClick: (e) => {
|
|
576
|
+
e.stopPropagation();
|
|
577
|
+
onDelete?.(item.id);
|
|
578
|
+
}, className: "w-6 h-6 flex items-center justify-center bg-red-500/20 text-red-400 hover:bg-red-500 rounded-md transition-all hover:text-white pointer-events-auto", children: /* @__PURE__ */ jsx8("span", { className: "material-symbols-outlined text-[14px]", children: "delete" }) })
|
|
579
|
+
] })
|
|
315
580
|
] }) }),
|
|
316
581
|
/* @__PURE__ */ jsx8("div", { className: "absolute top-1 right-1 px-1.5 py-0.5 bg-black/60 backdrop-blur-md rounded text-[7px] font-bold text-white/60 uppercase tracking-tight", children: item.type === "import" ? "Library" : "Gen" })
|
|
317
582
|
] }, item.id)) }) })
|
|
@@ -420,5 +685,10 @@ export {
|
|
|
420
685
|
PillButton,
|
|
421
686
|
SectionLabel,
|
|
422
687
|
SetupPanel,
|
|
688
|
+
exportProjectToZip,
|
|
689
|
+
formatTreeToMarkdown,
|
|
690
|
+
importProjectFromZip,
|
|
691
|
+
injectXMPMetadata,
|
|
692
|
+
parsePromptFile,
|
|
423
693
|
useOnClickOutside
|
|
424
694
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rslsp1/fa-app-tools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Shared tools and hooks for Fine Art flow apps",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"react": ">=18"
|
|
24
24
|
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"jszip": "^3.10.1"
|
|
27
|
+
},
|
|
25
28
|
"devDependencies": {
|
|
26
29
|
"@types/react": "^18.0.0",
|
|
27
30
|
"@xyflow/react": "^12.10.2",
|