Start restore parameters

This commit is contained in:
space-nuko
2023-05-29 00:16:02 -05:00
parent fde480cb43
commit 26ab7989c8
10 changed files with 485 additions and 110 deletions

View File

@@ -224,13 +224,13 @@ const Metadata = z.object({
extra_data: ExtraData
})
const ComfyBoxStdPrompt = z.object({
const StdPrompt = z.object({
version: z.number(),
metadata: Metadata,
parameters: Parameters
})
export default ComfyBoxStdPrompt
export default StdPrompt
/*
* A standardized Stable Diffusion parameter format that should be used with an
@@ -260,4 +260,4 @@ export default ComfyBoxStdPrompt
* "see" width 1024 and height 1024, even though the only parameter exposed from
* the frontend was the scale of 2.)
*/
export type ComfyBoxStdPrompt = z.infer<typeof ComfyBoxStdPrompt>
export type ComfyBoxStdPrompt = z.infer<typeof StdPrompt>

View File

@@ -1,31 +1,88 @@
import type { ComfyBoxStdGroupLoRA, ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
import type { SerializedPrompt, SerializedPromptInputs } from "./components/ComfyApp";
import StdPrompt from "$lib/ComfyBoxStdPrompt";
import type { SafeParseReturnType, ZodError } from "zod";
import type { ComfyNodeID } from "./api";
import type { SerializedAppState, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll } from "./components/ComfyApp";
import { ComfyComboNode, type ComfyWidgetNode } from "./nodes/widgets";
import { basename, isSerializedPromptInputLink } from "./utils";
export type ComfyPromptConverter = (stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs, nodeID: ComfyNodeID) => void;
function LoraLoader(stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs) {
const params = stdPrompt.parameters
const lora: ComfyBoxStdGroupLoRA = {
model_name: inputs["lora_name"],
strength_unet: inputs["strength_model"],
strength_tenc: inputs["strength_clip"]
}
if (params.lora)
params.lora.push(lora)
else
params.lora = [lora]
export type ComfyPromptConverter = {
encoder: ComfyPromptEncoder,
decoder: ComfyPromptDecoder
}
const ALL_CONVERTERS: Record<string, ComfyPromptConverter> = {
LoraLoader
//
export type ComfyDecodeArgument = {
groupName: string,
keyName: string,
value: any,
widgetNode: ComfyWidgetNode
};
export type ComfyPromptEncoder = (stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs, nodeID: ComfyNodeID) => void;
export type ComfyPromptDecoder = (args: ComfyDecodeArgument[]) => void;
const LoraLoader: ComfyPromptConverter = {
encoder: (stdPrompt: ComfyBoxStdPrompt, inputs: SerializedPromptInputs) => {
const params = stdPrompt.parameters
const loras: ComfyBoxStdGroupLoRA[] = params.lora
for (const lora of loras) {
lora.model_hashes = {
addnet_shorthash: null // TODO find hashes for model!
}
}
},
decoder: (args: ComfyDecodeArgument[]) => {
// Find corresponding model names in the ComfyUI models folder from the model base filename
for (const arg of args) {
if (arg.groupName === "lora" && arg.keyName === "model_name" && arg.widgetNode.is(ComfyComboNode)) {
const modelBasename = basename(arg.value);
const found = arg.widgetNode.properties.values.find(k => k.indexOf(modelBasename) !== -1)
if (found)
arg.value = found;
}
}
}
}
// input name -> group/key in standard prompt
type ComfyStdPromptMapping = Record<string, string>
type ComfyStdPromptSpec = {
paramMapping: ComfyStdPromptMapping,
extraParams?: Record<string, string>,
converter?: ComfyPromptConverter,
}
const ALL_SPECS: Record<string, ComfyStdPromptSpec> = {
"KSampler": {
paramMapping: {
cfg: "k_sampler.cfg_scale",
seed: "k_sampler.seed",
steps: "k_sampler.steps",
sampler_name: "k_sampler.sampler_name",
scheduler: "k_sampler.scheduler",
denoise: "k_sampler.denoise",
},
},
"LoraLoader": {
paramMapping: {
lora_name: "lora.model_name",
strength_model: "lora.strength_unet",
strength_clip: "lora.strength_tenc",
},
extraParams: {
"lora.module_name": "LoRA",
},
converter: LoraLoader,
}
}
const COMMIT_HASH: string = __GIT_COMMIT_HASH__;
export default class ComfyBoxStdPromptSerializer {
serialize(prompt: SerializedPrompt): ComfyBoxStdPrompt {
serialize(prompt: SerializedPromptInputsAll, workflow?: SerializedAppState): [SafeParseReturnType<any, ComfyBoxStdPrompt>, any] {
const stdPrompt: ComfyBoxStdPrompt = {
version: 1,
metadata: {
@@ -33,23 +90,57 @@ export default class ComfyBoxStdPromptSerializer {
commit_hash: COMMIT_HASH,
extra_data: {
comfybox: {
workflows: [] // TODO!!!
}
}
},
parameters: {}
}
for (const [nodeID, inputs] of Object.entries(prompt.output)) {
for (const [nodeID, inputs] of Object.entries(prompt)) {
const classType = inputs.class_type
const converter = ALL_CONVERTERS[classType]
if (converter) {
converter(stdPrompt, inputs.inputs, nodeID)
const spec = ALL_SPECS[classType]
if (spec) {
console.warn("SPEC", spec, inputs)
let targets = {}
for (const [comfyKey, stdPromptKey] of Object.entries(spec.paramMapping)) {
const inputValue = inputs.inputs[comfyKey];
if (inputValue != null && !isSerializedPromptInputLink(inputValue)) {
console.warn("GET", comfyKey, inputValue)
const trail = stdPromptKey.split(".");
let target = null;
console.warn(trail, trail.length - 2);
for (let index = 0; index < trail.length - 1; index++) {
const name = trail[index];
if (index === 0) {
targets[name] ||= {}
target = targets[name]
}
else {
target = target[name]
}
console.warn(index, name, target)
}
let name = trail[trail.length - 1]
target[name] = inputValue
console.warn(stdPrompt.parameters)
}
}
// TODO converter.encode
for (const [groupName, group] of Object.entries(targets)) {
stdPrompt.parameters[groupName] ||= []
stdPrompt.parameters[groupName].push(group)
}
}
else {
console.warn("No StdPrompt type converter for comfy class!", classType)
console.warn("No StdPrompt type spec for comfy class!", classType)
}
}
return stdPrompt
return [StdPrompt.safeParse(stdPrompt), stdPrompt];
}
}

View File

@@ -1064,9 +1064,6 @@ export default class ComfyApp {
// console.debug(graphToGraphVis(workflow.graph))
// console.debug(promptToGraphVis(p))
const stdPrompt = this.stdPromptSerializer.serialize(p);
// console.warn("STD", stdPrompt);
const extraData: ComfyBoxPromptExtraData = {
extra_pnginfo: {
comfyBoxWorkflow: wf,

View File

@@ -32,6 +32,7 @@
import ComfyQueueGridDisplay from "./ComfyQueueGridDisplay.svelte";
import { WORKFLOWS_VIEW } from "./ComfyBoxWorkflowsView.svelte";
import uiQueueState from "$lib/stores/uiQueueState";
import type { SerializedAppState, SerializedPromptInputsAll } from "./ComfyApp";
export let app: ComfyApp;
@@ -124,19 +125,22 @@
let showModal = false;
let expandAll = false;
let selectedPrompt = null;
let selectedPrompt: SerializedPromptInputsAll | null = null;
let selectedWorkflow: SerializedAppState | null = null;
let selectedImages = [];
function showPrompt(entry: QueueUIEntry) {
if (entry.error != null) {
showModal = false;
expandAll = false;
selectedPrompt = null;
selectedWorkflow = null;
selectedImages = [];
showError(entry.entry.promptID);
}
else {
selectedPrompt = entry.entry.prompt;
selectedPrompt = entry.entry.prompt,
selectedWorkflow = entry.entry.extraData.extra_pnginfo.comfyBoxWorkflow
selectedImages = entry.images;
showModal = true;
expandAll = false
@@ -145,6 +149,7 @@
function closeModal() {
selectedPrompt = null
selectedWorkflow = null;
selectedImages = []
showModal = false;
expandAll = false;
@@ -165,7 +170,7 @@
</div>
<svelte:fragment let:closeDialog>
{#if selectedPrompt}
<PromptDisplay closeModal={() => { closeModal(); closeDialog(); }} {app} prompt={selectedPrompt} images={selectedImages} {expandAll} />
<PromptDisplay closeModal={() => { closeModal(); closeDialog(); }} {app} prompt={selectedPrompt} workflow={selectedWorkflow} images={selectedImages} {expandAll} />
{/if}
</svelte:fragment>
<div slot="buttons" let:closeDialog>

View File

@@ -173,7 +173,7 @@
}
.comfy-settings-entries {
padding: 3rem 3rem;
padding: 2rem 0.75rem;
height: 100%;
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { TextBox } from "@gradio/form";
import type { SerializedPromptInput, SerializedPromptInputsAll } from "./ComfyApp";
import type { SerializedAppState, SerializedPrompt, SerializedPromptInput, SerializedPromptInputsAll } from "./ComfyApp";
import { Block, BlockLabel, BlockTitle } from "@gradio/atoms";
import { JSON as JSONComponent } from "@gradio/json";
import { JSON as JSONIcon, Copy, Check } from "@gradio/icons";
@@ -13,16 +13,39 @@
import workflowState, { type ComfyBoxWorkflow, type WorkflowReceiveOutputTargets } from "$lib/stores/workflowState";
import type { ComfyReceiveOutputNode } from "$lib/nodes/actions";
import type ComfyApp from "./ComfyApp";
import { TabItem, Tabs } from "@gradio/tabs";
import { type ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
import JsonView from "./JsonView.svelte";
import type { ZodError } from "zod";
const splitLength = 50;
export let prompt: SerializedPromptInputsAll;
export let workflow: SerializedAppState | null;
export let images: string[] = []; // list of image URLs to ComfyUI's /view? endpoint
export let isMobile: boolean = false;
export let expandAll: boolean = false;
export let closeModal: () => void;
export let app: ComfyApp;
let stdPrompt: ComfyBoxStdPrompt | null;
let stdPromptError: ZodError<any> | null;
$: {
const [result, orig] = new ComfyBoxStdPromptSerializer().serialize(prompt, workflow);
if (result.success === true) {
stdPrompt = result.data;
stdPromptError = null;
}
else {
stdPrompt = orig;
stdPromptError = result.error;
}
}
let selectedTab: "restore-parameters" | "send-outputs" | "standard-prompt" | "prompt" = "standard-prompt";
let selected_image: number | null = null;
let galleryStyle: Styles = {
@@ -127,62 +150,14 @@
<div class="prompt-display">
<div class="prompt-and-sends">
<Block>
<Accordion label="Prompt" open={expandAll || comfyBoxImages.length === 0}>
<div class="scroll-container">
<Block>
{#each Object.entries(prompt) as [nodeID, inputs], i}
{@const classType = inputs.class_type}
{@const filtered = Object.entries(inputs.inputs).filter((i) => !isInputLink(i[1]))}
{#if filtered.length > 0}
<div class="accordion">
<Block padding={true}>
<Accordion label="Node {i+1}: {classType}" open={expandAll}>
{#each filtered as [inputName, input]}
<Block>
<button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}>
{#if copiedNodeID === nodeID && copiedInputName === inputName}
<span class="copied-icon">
<Check />
</span>
{:else}
<span class="copy-text"><Copy /></span>
{/if}
</button>
<div>
{#if isInputLink(input)}
Link {input[0]} -> {input[1]}
{:else if typeof input === "object"}
<Block>
<BlockLabel
Icon={JSONIcon}
show_label={true}
label={inputName}
float={true}
/>
<JSONComponent value={input} />
</Block>
{:else if isMultiline(input)}
{@const lines = Math.max(countNewLines(input), input.length / splitLength)}
<TextBox label={inputName} value={formatInput(input)} {lines} max_lines={lines} />
{:else}
<TextBox label={inputName} value={formatInput(input)} lines={1} max_lines={1} />
{/if}
</div>
</Block>
{/each}
</Accordion>
</Block>
</div>
{/if}
{/each}
</Block>
</div>
</Accordion>
</Block>
{#if comfyBoxImages.length > 0}
<Block>
<Accordion label="Send Outputs To..." open={true}>
<Tabs bind:selected={selectedTab}>
<TabItem id="restore-parameters" name="Restore Parameters">
<Block>
<BlockTitle>Parameters</BlockTitle>
</Block>
</TabItem>
{#if comfyBoxImages.length > 0}
<TabItem id="send-outputs" name="Send Outputs">
<Block>
<BlockTitle>Output type: {litegraphType}</BlockTitle>
{#if receiveTargets.length > 0}
@@ -191,9 +166,94 @@
<div class="outputs-message">No receive output targets found across all workflows.</div>
{/if}
</Block>
</Accordion>
</Block>
{/if}
</TabItem>
{/if}
<TabItem id="standard-prompt" name="Standard Prompt">
{#if stdPromptError}
<Block>
<BlockTitle><div style:color="#F88">Parsing Error</div></BlockTitle>
<div class="scroll-container">
<div class="json">
<JsonView json={stdPromptError} />
</div>
</div>
</Block>
<Block>
<BlockTitle><div>Original Data</div></BlockTitle>
<div class="scroll-container">
<div class="json">
<JsonView json={stdPrompt} />
</div>
</div>
</Block>
{:else if stdPrompt}
<Block>
<div class="scroll-container">
<div class="json">
<JsonView json={stdPrompt} />
</div>
</div>
</Block>
{:else}
<Block>
(No standard prompt)
</Block>
{/if}
</TabItem>
<TabItem id="prompt" name="Prompt">
<Block>
<div class="scroll-container">
<Block>
{#each Object.entries(prompt) as [nodeID, inputs], i}
{@const classType = inputs.class_type}
{@const filtered = Object.entries(inputs.inputs).filter((i) => !isInputLink(i[1]))}
{#if filtered.length > 0}
<div class="accordion">
<Block padding={true}>
<Accordion label="Node {i+1}: {classType}" open={expandAll}>
{#each filtered as [inputName, input]}
<Block>
<button class="copy-button" on:click={() => handleCopy(nodeID, inputName, input)}>
{#if copiedNodeID === nodeID && copiedInputName === inputName}
<span class="copied-icon">
<Check />
</span>
{:else}
<span class="copy-text"><Copy /></span>
{/if}
</button>
<div>
{#if isInputLink(input)}
Link {input[0]} -> {input[1]}
{:else if typeof input === "object"}
<Block>
<BlockLabel
Icon={JSONIcon}
show_label={true}
label={inputName}
float={true}
/>
<JSONComponent value={input} />
</Block>
{:else if isMultiline(input)}
{@const lines = Math.max(countNewLines(input), input.length / splitLength)}
<TextBox label={inputName} value={formatInput(input)} {lines} max_lines={lines} />
{:else}
<TextBox label={inputName} value={formatInput(input)} lines={1} max_lines={1} />
{/if}
</div>
</Block>
{/each}
</Accordion>
</Block>
</div>
{/if}
{/each}
</Block>
</div>
</Block>
</TabItem>
</Tabs>
</div>
{#if images.length > 0}
<div class="image-container">
@@ -221,21 +281,34 @@
display: flex;
flex-wrap: nowrap;
overflow-y: auto;
flex-direction: column;
@media (min-width: 1600px) {
@media (min-width: 1200px) {
flex-direction: row;
}
}
.scroll-container {
position: relative;
/* overflow-y: auto; */
flex: 1 1 0%;
}
.json {
@include json-view;
}
.prompt-and-sends {
width: 50%;
.scroll-container {
position: relative;
/* overflow-y: auto; */
flex: 1 1 0%;
overflow-y: auto;
:global(>.tabs) {
height: 100%;
:global(>.tabitem) {
overflow-y: auto;
}
}
.copy-button {

View File

@@ -0,0 +1,205 @@
import type { INodeInputSlot, NodeID } from "@litegraph-ts/core";
import type { SerializedPrompt } from "./components/ComfyApp";
import type { ComfyWidgetNode } from "./nodes/widgets";
import type { SerializedComfyWidgetNode } from "./nodes/widgets/ComfyWidgetNode";
import { isComfyWidgetNode } from "./stores/layoutStates";
import type { ComfyBoxWorkflow } from "./stores/workflowState";
import { isSerializedPromptInputLink } from "./utils";
import ComfyBoxStdPromptSerializer from "./ComfyBoxStdPromptSerializer";
interface RestoreParamSource {
finalValue: any
}
/*
* A serialized ComfyWidgetNode from the saved workflow that corresponds
* *exactly* to a node with the same ID in the current workflow. Easiest case
* since the parameter value can just be copied without much fuss.
*/
interface RestoreParamSourceWorkflowNode extends RestoreParamSource {
sourceNode: SerializedComfyWidgetNode
}
/*
* A value received by the ComfyUI *backend* that corresponds to a value that
* was held in a ComfyWidgetNode. These may not necessarily be one-to-one
* because there can be extra frontend-only processing nodes between the two.
*
* (Example: a node that converts a random prompt template into a final prompt
* string, then passes *that* prompt string to the backend. The backend will not
* see the template string, so it will be missing in the arguments to ComfyUI's
* prompt endpoint. Hence this parameter source won't account for those kinds of
* values.)
*/
interface RestoreParamSourceBackendNodeInput extends RestoreParamSource {
backendNode: SerializedComfyWidgetNode,
/*
* If false, this node was connected to the backend node across one or more
* additional frontend nodes, so the value in the source may not correspond
* exactly to the widget's original value
*/
isDirectAttachment: boolean
}
/*
* A value contained in the standard prompt extracted from the saved workflow.
*/
interface RestoreParamSourceStdPrompt<T, K extends keyof T> extends RestoreParamSource {
/*
* Name of the group containing the value to pass
*
* "lora"
*/
groupName: string,
/*
* The standard prompt group containing the value and metadata like
* "positive"/"negative" for identification use
*
* { "$meta": { ... }, model_name: "...", model_hashes: [...], ... }
*/
group: T,
/*
* Key of the group parameter holding the actual value
* "model_name"
*/
key: K,
/*
* The raw value as saved to the prompt, not accounting for stuff like hashes
*
* "contrastFix"
*/
rawValue: T[K]
/*
* The *actual* value that will be copied into the ComfyWidgetNode, after
* conversion to account for filepaths/etc. from prompt adapters has been
* completed
*
* "models/lora/contrastFix.safetensors"
*/
finalValue: any
}
export type RestoreParamTarget = {
/*
* Node that will receive the parameter from the prompt
*/
targetNode: ComfyWidgetNode;
/*
* Possible sources of values to insert into the target node
*/
sources: RestoreParamSource[]
}
export type RestoreParamTargets = Record<NodeID, RestoreParamTarget>
function isSerializedComfyWidgetNode(param: any): param is SerializedComfyWidgetNode {
return param != null && typeof param === "object" && "id" in param && "comfyValue" in param
}
function findUpstreamSerializedWidgetNode(prompt: SerializedPrompt, input: INodeInputSlot): [SerializedComfyWidgetNode | null, boolean | null] {
let linkID = input.link;
let isDirectAttachment = true;
while (linkID) {
const link = prompt.workflow.links[linkID]
if (link == null)
return [null, null];
const originNode = prompt.workflow.nodes.find(n => n.id === link[1])
if (isSerializedComfyWidgetNode(originNode))
return [originNode, isDirectAttachment]
isDirectAttachment = false;
// TODO: getUpstreamLink() for serialized nodes?
if (originNode.inputs && originNode.inputs.length === 1)
linkID = originNode.inputs[0].link
else
linkID = null;
}
return [null, null];
}
export default function restoreParameters(workflow: ComfyBoxWorkflow, prompt: SerializedPrompt): RestoreParamTargets {
const result = {}
const addSource = (targetNode: ComfyWidgetNode, source: RestoreParamSource) => {
result[targetNode.id] ||= { targetNode, sources: [] }
result[targetNode.id].sources.push(source);
}
const graph = workflow.graph;
// Step 1: Find nodes that correspond to *this* workflow exactly, since we
// can easily match up the nodes between each (their IDs will be the same)
for (const serNode of prompt.workflow.nodes) {
const foundNode = graph.getNodeByIdRecursive(serNode.id);
if (isComfyWidgetNode(foundNode) && foundNode.type === serNode.type) {
const finalValue = (serNode as SerializedComfyWidgetNode).comfyValue;
if (finalValue != null) {
const source: RestoreParamSourceWorkflowNode = {
finalValue,
sourceNode: serNode
}
addSource(foundNode, source)
}
}
}
// Step 2: Figure out what parameters the backend received. If there was a
// widget node attached to a backend node's input upstream, then we can
// use that value.
for (const [serNodeID, inputs] of Object.entries(prompt.output)) {
const serNode = prompt.workflow.nodes.find(sn => sn.id === serNodeID)
if (serNode == null)
continue;
for (const [inputName, inputValue] of Object.entries(inputs)) {
const input = serNode.inputs.find(i => i.name === inputName);
if (input == null)
continue;
if (isSerializedPromptInputLink(inputValue))
continue;
const [originNode, isDirectAttachment] = findUpstreamSerializedWidgetNode(prompt, input)
if (originNode) {
const foundNode = graph.getNodeByIdRecursive(serNode.id);
if (isComfyWidgetNode(foundNode) && foundNode.type === serNode.type) {
const source: RestoreParamSourceBackendNodeInput = {
finalValue: inputValue,
backendNode: serNode,
isDirectAttachment
}
addSource(foundNode, source)
}
}
}
}
// Step 3: Extract the standard prompt from the workflow and use that to
// infer parameter types
const serializer = new ComfyBoxStdPromptSerializer();
const stdPrompt = serializer.serialize(prompt);
const allWidgetNodes = Array.from(graph.iterateNodesInOrderRecursive()).filter(isComfyWidgetNode);
for (const widgetNode of allWidgetNodes) {
}
// for (const [groupName, groups] of Object.entries(stdPrompt)) {
// }
return result;
}

View File

@@ -4,8 +4,8 @@ import type { FileData as GradioFileData } from "@gradio/upload";
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID, type NodeID, type SlotType, type Vector4, type SerializedLGraphNode } from "@litegraph-ts/core";
import { get } from "svelte/store";
import type { ComfyNodeID } from "./api";
import ComfyApp, { type SerializedPrompt } from "./components/ComfyApp";
import workflowState, { type WorkflowReceiveOutputTargets } from "./stores/workflowState";
import ComfyApp, { type SerializedPrompt, type SerializedPromptInput, type SerializedPromptInputLink } from "./components/ComfyApp";
import { ImageViewer } from "./ImageViewer";
import configState from "$lib/stores/configState";
import SendOutputModal, { type SendOutputModalResult } from "$lib/components/modal/SendOutputModal.svelte";
@@ -143,6 +143,10 @@ export function stopDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore)
layoutState.notifyWorkflowModified();
};
export function isSerializedPromptInputLink(inputValue: SerializedPromptInput): inputValue is SerializedPromptInputLink {
return Array.isArray(inputValue) && inputValue.length === 2 && typeof inputValue[0] === "string" && typeof inputValue[1] === "number"
}
export function graphToGraphVis(graph: LGraph): string {
let links: string[] = []
let seenLinks = new Set()
@@ -247,7 +251,7 @@ export function promptToGraphVis(prompt: SerializedPrompt): string {
for (const pair2 of Object.entries(o.inputs)) {
const [inpName, i] = pair2;
if (Array.isArray(i) && i.length === 2 && typeof i[0] === "string" && typeof i[1] === "number") {
if (isSerializedPromptInputLink(i)) {
// Link
const [inpID, inpSlot] = i;
if (ids[inpID] == null)

View File

@@ -259,18 +259,18 @@
<Row>
{#if canMask}
<div>
{#if editMask}
<Button variant="secondary" on:click={() => { clearMask(); notify("Mask cleared."); }}>
Clear Mask
</Button>
{/if}
<Button disabled={!_value} on:click={toggleEditMask}>
<Button variant="primary" disabled={!_value} on:click={toggleEditMask}>
{#if editMask}
Show Image
{:else}
Edit Mask
{/if}
</Button>
{#if editMask}
<Button variant="secondary" on:click={() => { clearMask(); notify("Mask cleared."); }}>
Clear Mask
</Button>
{/if}
</div>
{/if}
<div>

View File

@@ -12,7 +12,7 @@ import { visualizer } from "rollup-plugin-visualizer";
const isProduction = process.env.NODE_ENV === "production";
console.log("Production build: " + isProduction)
const commitHash = execSync('git rev-parse HEAD').toString();
const commitHash = execSync('git rev-parse HEAD').toString().trim();
console.log("Commit: " + commitHash)
export default defineConfig({