Skip to content

Commit

Permalink
Merge pull request #24 from TNG/remember-last-used-workflow-per-backend
Browse files Browse the repository at this point in the history
Remember last used workflow per backend
  • Loading branch information
florianesser-tng authored Dec 17, 2024
2 parents 220b9aa + 24cc605 commit 507d87f
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 68 deletions.
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'

0 comments on commit 507d87f

Please sign in to comment.