Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remember last used workflow per backend #24

Merged
merged 5 commits into from
Dec 17, 2024
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
86 changes: 69 additions & 17 deletions WebUI/src/assets/js/store/imageGeneration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ const ComfyBooleanInputSchema = z.object({
});
export type ComfyBooleanInput = z.infer<typeof ComfyBooleanInputSchema>;

const ComfyDynamicInputSchema = z.discriminatedUnion('type', [
ComfyNumberInputSchema,
ComfyImageInputSchema,
ComfyStringInputSchema,
ComfyBooleanInputSchema,
]);
type ComfyDynamicInput = z.infer<typeof ComfyDynamicInputSchema>;
const ComfyUiWorkflowSchema = z.object({
name: z.string(),
backend: z.literal('comfyui'),
Expand All @@ -126,12 +133,7 @@ const ComfyUiWorkflowSchema = z.object({
tags: z.array(z.string()),
requiredModels: z.array(z.string()).optional(),
requirements: z.array(WorkflowRequirementSchema),
inputs: z.array(z.discriminatedUnion('type',[
ComfyNumberInputSchema,
ComfyImageInputSchema,
ComfyStringInputSchema,
ComfyBooleanInputSchema
])),
inputs: z.array(ComfyDynamicInputSchema),
outputs: z.array(z.object({
name: z.string(),
type: z.literal('image')
Expand All @@ -153,6 +155,7 @@ export type Workflow = z.infer<typeof WorkflowSchema>;


const globalDefaultSettings = {
seed: -1,
width: 512,
height: 512,
inferenceSteps: 20,
Expand Down Expand Up @@ -201,6 +204,7 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
modifiableSettings: [
'resolution',
'seed',
'inferenceSteps',
'negativePrompt',
'batchSize',
'imagePreview',
Expand All @@ -226,12 +230,12 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
'imageModel',
'inpaintModel',
'guidanceScale',
'inferenceSteps',
'scheduler',
],
modifiableSettings: [
'resolution',
'seed',
'inferenceSteps',
'negativePrompt',
'batchSize',
'imagePreview',
Expand All @@ -258,13 +262,13 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
'imageModel',
'inpaintModel',
'guidanceScale',
'inferenceSteps',
'scheduler',
'lora'
],
modifiableSettings: [
'resolution',
'seed',
'inferenceSteps',
'negativePrompt',
'batchSize',
'imagePreview',
Expand All @@ -291,12 +295,12 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
'imageModel',
'inpaintModel',
'guidanceScale',
'inferenceSteps',
'scheduler',
],
modifiableSettings: [
'resolution',
'seed',
'inferenceSteps',
'negativePrompt',
'batchSize',
'imagePreview',
Expand All @@ -323,12 +327,12 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
'imageModel',
'inpaintModel',
'guidanceScale',
'inferenceSteps',
'scheduler',
],
modifiableSettings: [
'resolution',
'seed',
'inferenceSteps',
'negativePrompt',
'batchSize',
'imagePreview',
Expand All @@ -355,12 +359,12 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
'imageModel',
'inpaintModel',
'guidanceScale',
'inferenceSteps',
'scheduler',
],
modifiableSettings: [
'resolution',
'seed',
'inferenceSteps',
'negativePrompt',
'batchSize',
'imagePreview',
Expand Down Expand Up @@ -424,6 +428,7 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
imagePreview.value = generalDefaultSettings.imagePreview;
safeCheck.value = generalDefaultSettings.safeCheck;
settingsPerWorkflow.value[activeWorkflowName.value ?? ''] = undefined;
comfyInputsPerWorkflow.value[activeWorkflowName.value ?? ''] = undefined;
loadSettingsForActiveWorkflow();
}
// model specific settings
Expand All @@ -444,25 +449,68 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
[width.value, height.value] = newValue.split('x').map(Number);
}
})

const settings = { inferenceSteps, width, height, resolution, batchSize, negativePrompt, lora, scheduler, guidanceScale, imageModel, inpaintModel };
const settings = { seed, inferenceSteps, width, height, resolution, batchSize, negativePrompt, lora, scheduler, guidanceScale, imageModel, inpaintModel };
type ModifiableSettings = keyof typeof settings;
const backend = computed({
get() {
return activeWorkflow.value.backend;
},
set(newValue) {
activeWorkflowName.value = workflows.value.find(w => w.backend === newValue)?.name ?? activeWorkflowName.value;
activeWorkflowName.value = workflows.value
.filter(w => w.backend === newValue)
.find(w => w.name === lastWorkflowPerBackend.value[newValue])?.name ?? workflows.value.find(w => w.backend === newValue)?.name ?? activeWorkflowName.value;
}
});
const lastWorkflowPerBackend = ref<Record<Workflow['backend'], string | null>>({
comfyui: null,
default: null
});

const comfyInputs = computed(() => {
if (activeWorkflow.value.backend !== 'comfyui') return []
const inputRef = (input: ComfyDynamicInput): NodeInputReference => `${input.nodeTitle}.${input.nodeInput}`;
const savePerWorkflow = (input: ComfyDynamicInput, newValue: ComfyDynamicInput['defaultValue']) => {
if (!activeWorkflowName.value) return;
comfyInputsPerWorkflow.value[activeWorkflowName.value] = {
...comfyInputsPerWorkflow.value[activeWorkflowName.value],
[inputRef(input)]: newValue
}
console.log('saving', { nodeTitle: input.nodeTitle, nodeInput: input.nodeInput, newValue });
}
const getSavedOrDefault = (input: ComfyDynamicInput) => {
if (!activeWorkflowName.value) return input.defaultValue;
const saved = comfyInputsPerWorkflow.value[activeWorkflowName.value]?.[inputRef(input)];
if (saved) console.log('got saved dynamic input', { nodeTitle: input.nodeTitle, nodeInput: input.nodeInput, saved });
return saved ?? input.defaultValue;
};

return activeWorkflow.value.inputs.map(input => {
const _current = ref(getSavedOrDefault(input))

const current = computed({
get() {
return _current.value;
},
set(newValue) {
_current.value = newValue;
savePerWorkflow(input, newValue)
}
})

const comfyInputs = computed(() => activeWorkflow.value.backend === 'comfyui' ? activeWorkflow.value.inputs.map(input => ({ ...input, current: ref(input.defaultValue) })) : []);
return { ...input, current }
})
});

const settingsPerWorkflow = ref<Record<string, Workflow['defaultSettings']>>({});
type WorkflowName = string;
type NodeInputReference = string; // nodeTitle.nodeInput
const comfyInputsPerWorkflow = ref<Record<WorkflowName, Record<NodeInputReference, ComfyDynamicInput['defaultValue']> | undefined>>({});
const settingsPerWorkflow = ref<Record<WorkflowName, Workflow['defaultSettings']>>({});

const isModifiable = (settingName: ModifiableSettings) => activeWorkflow.value.modifiableSettings.includes(settingName);

watch([activeWorkflowName, workflows], () => {
setTimeout(() => lastWorkflowPerBackend.value[activeWorkflow.value.backend] = activeWorkflowName.value)
loadSettingsForActiveWorkflow();
}, {});

Expand All @@ -484,6 +532,7 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
console.log('saving', { settingName, value: settings[settingName].value });
}
}
saveToSettingsPerWorkflow('seed');
saveToSettingsPerWorkflow('inferenceSteps');
saveToSettingsPerWorkflow('width');
saveToSettingsPerWorkflow('height');
Expand Down Expand Up @@ -516,6 +565,7 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
settings[settingName].value = saved ?? activeWorkflow.value?.defaultSettings?.[settingName] ?? globalDefaultSettings[settingName];
};

getSavedOrDefault('seed');
getSavedOrDefault('inferenceSteps');
getSavedOrDefault('width');
getSavedOrDefault('height');
Expand Down Expand Up @@ -670,7 +720,9 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
batchSize,
negativePrompt,
settingsPerWorkflow,
comfyInputsPerWorkflow,
comfyInputs,
lastWorkflowPerBackend,
resetActiveWorkflowSettings,
loadWorkflowsFromJson,
loadWorkflowsFromIntel,
Expand All @@ -683,7 +735,7 @@ export const useImageGeneration = defineStore("imageGeneration", () => {
}, {
persist: {
debug: true,
pick: ['backend', 'activeWorkflowName', 'settingsPerWorkflow', 'hdWarningDismissed']
pick: ['backend', 'activeWorkflowName', 'settingsPerWorkflow', 'comfyInputsPerWorkflow', 'hdWarningDismissed', 'lastWorkflowPerBackend']
}
});

Expand Down
52 changes: 3 additions & 49 deletions WebUI/src/components/SettingsImageComfyDynamic.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
<template>
<div v-for="input, i in imageGeneration.comfyInputs" class="flex flex-col gap-2">
<div v-for="input, i in imageGeneration.comfyInputs" class="flex flex-col gap-2 py-2">
<p>{{ input.label }}</p>

<!-- Number -->
<slide-bar v-if="input.type === 'number'" v-model:current="(input.current.value as number)" :min="input.min"
:max="input.max" :step="input.step"></slide-bar>

<!-- Image -->
<img ref="imgDropZones" v-if="input.type === 'image'" :src="(input.current.value as string)" alt="Image" class="w-64 h-64 object-scale-down self-center"></img>
<Input v-if="input.type === 'image'" accept="image/jpeg,image/png,image/webp" id="picture" type="file"
v-on:change="(e: Event) => handleFilesEvent(input.current as Ref<string, string>)(e)"></Input>
<LoadImage :id="`${input.nodeTitle}.${input.nodeInput}`" v-if="input.type === 'image'" :image-url-ref="(input.current as WritableComputedRef<string>)"></LoadImage>

<!-- String -->
<Input v-if="input.type === 'string'" type="text" v-model="(input.current.value as string)"></Input>
Expand All @@ -25,53 +23,9 @@
<script setup lang="ts">
import { useImageGeneration } from "@/assets/js/store/imageGeneration";
import { Input } from '../components/ui/input'
import { LoadImage } from '../components/ui/loadImage'
import SlideBar from "../components/SlideBar.vue";
import { useDropZone } from '@vueuse/core';
const imageGeneration = useImageGeneration();
const imgDropZones = useTemplateRef('imgDropZones')
const { isOverDropZone } = useDropZone(imgDropZones.value, {
onDrop: (e) => console.log('Dropped!', e),
// specify the types of data to be received.
dataTypes: ['image/jpeg'],
// control multi-file drop
multiple: true,
// whether to prevent default behavior for unhandled events
preventDefaultForUnhandled: false,
})
onMounted(() => console.log('imgDropZones', imgDropZones.value))
const handleFilesEvent = (inputCurrent: Ref<string, string>) => (event: Event) => {
if (!event.target || !(event.target instanceof HTMLInputElement) || !event.target.files) {
return;
}
const files = event.target.files;
processFiles([...files], inputCurrent);
}
function processFiles(files: File[] | null, inputCurrent: Ref<string, string>) {
if (!files) {
return;
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!file.type.startsWith("image/")) {
continue;
}
const reader = new FileReader();
reader.onload = (e) => {
if (!e.target || !(e.target instanceof FileReader) || !e.target.result || typeof e.target.result !== "string") {
console.error("Failed to read file");
return;
}
inputCurrent.value = e.target.result;
};
reader.readAsDataURL(file);
}
}
</script>
6 changes: 4 additions & 2 deletions WebUI/src/components/SettingsImageGeneration.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</div>
<div class="flex items-center gap-5">
<p>{{ languages.SETTINGS_MODEL_IMAGE_PREVIEW }}</p>
<button v-show=true class="v-checkbox-control flex-none w-5 h-5"
<button v-sh=true class="v-checkbox-control flex-none w-5 h-5"
:class="{ 'v-checkbox-checked': imageGeneration.imagePreview }"
@click="() => imageGeneration.imagePreview = !imageGeneration.imagePreview">
</button>
Expand Down Expand Up @@ -123,7 +123,9 @@
</div>
</div>
<ComfyDynamic></ComfyDynamic>
<button class="mt-4" @click="imageGeneration.resetActiveWorkflowSettings"><div class="svg-icon i-refresh">Reset</div>Load workflow defaults</button>
<div class="border-t border-color-spilter items-center flex-wrap grid grid-cols-1 gap-2">
<button class="mt-4" @click="imageGeneration.resetActiveWorkflowSettings"><div class="svg-icon i-refresh">Reset</div>Load workflow defaults</button>
</div>
</div>
</template>

Expand Down
69 changes: 69 additions & 0 deletions WebUI/src/components/ui/loadImage/LoadImage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/lib/utils'
import { useDropZone } from '@vueuse/core';
const props = defineProps<{
imageUrlRef: WritableComputedRef<string>
defaultValue?: string | number
modelValue?: string | number
class?: HTMLAttributes['class']
id: string
}>()
const acceptedImageTypes = ['image/jpeg', 'image/png', 'image/webp']
const imgDropZone = useTemplateRef('imgDropZone')
const { isOverDropZone } = useDropZone(imgDropZone, {
onDrop: (files) => processFiles(files, props.imageUrlRef),
dataTypes: acceptedImageTypes,
multiple: false,
preventDefaultForUnhandled: false,
})
const handleFilesEvent = (inputCurrent: Ref<string, string>) => (event: Event) => {
if (!event.target || !(event.target instanceof HTMLInputElement) || !event.target.files) {
return;
}
const files = event.target.files;
processFiles([...files], inputCurrent);
}
function processFiles(files: File[] | null, inputCurrent: Ref<string, string>) {
if (!files) {
return;
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!file.type.startsWith("image/")) {
continue;
}
const reader = new FileReader();
reader.onload = (e) => {
if (!e.target || !(e.target instanceof FileReader) || !e.target.result || typeof e.target.result !== "string") {
console.error("Failed to read file");
return;
}
inputCurrent.value = e.target.result;
};
reader.readAsDataURL(file);
}
}
</script>

<template>
<div ref="imgDropZone" class="flex justify-center relative">
<div v-show="isOverDropZone" class="bg-black/70 absolute inset-0 flex items-center justify-center text-white text-lg">Load Image</div>
<img :src="(imageUrlRef.value as string)" alt="Image" class="w-64 py-4 object-scale-down"></img>
</div>
<div class="flex justify-center">
<input :id="id" :accept="acceptedImageTypes.join(',')" type="file" class="hidden"
v-on:change="(e: Event) => handleFilesEvent(imageUrlRef as Ref<string, string>)(e)"
>
<label :for="id" :class="cn('text-base bg-color-active py-1 px-6 rounded hover:opacity-90 hover:cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 ', props.class)">Load Image</label>
</div>

</template>
1 change: 1 addition & 0 deletions WebUI/src/components/ui/loadImage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as LoadImage } from './LoadImage.vue'
Loading