Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2025-02-28 14:11:14 -05:00
committed by GitHub
parent e106fded37
commit 24e560bb2d
82 changed files with 1662 additions and 106 deletions

View File

@@ -1,4 +1,8 @@
import { Locator, Page } from '@playwright/test'
import path from 'path'
import { CORE_TEMPLATES } from '../../src/constants/coreTemplates'
import { TemplateInfo } from '../../src/types/workflowTemplateTypes'
export class ComfyTemplates {
readonly content: Locator
@@ -8,6 +12,17 @@ export class ComfyTemplates {
}
async loadTemplate(id: string) {
await this.content.getByTestId(`template-workflow-${id}`).click()
await this.content
.getByTestId(`template-workflow-${id}`)
.getByRole('img')
.click()
}
getAllTemplates(): TemplateInfo[] {
return CORE_TEMPLATES.flatMap((category) => category.templates)
}
getTemplatePath(filename: string): string {
return path.join('public', 'templates', filename)
}
}

View File

@@ -1,15 +1,58 @@
import { expect } from '@playwright/test'
import fs from 'fs'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Templates', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false)
})
test('should have a JSON workflow file for each template', async ({
comfyPage
}) => {
for (const template of comfyPage.templates.getAllTemplates()) {
const workflowPath = comfyPage.templates.getTemplatePath(
`${template.name}.json`
)
expect(
fs.existsSync(workflowPath),
`Missing workflow: ${template.name}`
).toBe(true)
}
})
test('should have all required thumbnail media for each template', async ({
comfyPage
}) => {
for (const template of comfyPage.templates.getAllTemplates()) {
const { name, mediaSubtype, thumbnailVariant } = template
const baseMedia = `${name}-1.${mediaSubtype}`
const basePath = comfyPage.templates.getTemplatePath(baseMedia)
// Check base thumbnail
expect(
fs.existsSync(basePath),
`Missing base thumbnail: ${baseMedia}`
).toBe(true)
// Check second thumbnail for variants that need it
if (
thumbnailVariant === 'compareSlider' ||
thumbnailVariant === 'hoverDissolve'
) {
const secondMedia = `${name}-2.${mediaSubtype}`
const secondPath = comfyPage.templates.getTemplatePath(secondMedia)
expect(
fs.existsSync(secondPath),
`Missing second thumbnail: ${secondMedia} required for ${thumbnailVariant}`
).toBe(true)
}
}
})
test('Can load template workflows', async ({ comfyPage }) => {
// This test will need expanding on once the templates are decided
// Clear the workflow
await comfyPage.menu.workflowsTab.open()
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.10.15",
"version": "1.10.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@comfyorg/comfyui-frontend",
"version": "1.10.15",
"version": "1.10.16",
"license": "GPL-3.0-only",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.10.15",
"version": "1.10.16",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -0,0 +1,582 @@
{
"last_node_id": 57,
"last_link_id": 113,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
1,
-17
],
"size": [
389.7508239746094,
98
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
14
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
65
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8,
85
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd3.5_large_fp8_scaled.safetensors"
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
0,
145
],
"size": [
388.7348327636719,
188.959716796875
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 65
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
98,
109
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"crystal butterfly above the sea, white, hyper detailed, with diamonds"
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
770,
310
],
"size": [
278.8823547363281,
46.5799446105957
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 63
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
13
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 9,
"type": "SaveImage",
"pos": [
1097,
-14
],
"size": [
845.74560546875,
898.2359619140625
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 13
}
],
"outputs": [],
"properties": {},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 33,
"type": "EmptySD3LatentImage",
"pos": [
420,
250
],
"size": [
300.9447021484375,
106
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"shape": 3,
"links": [
66
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptySD3LatentImage"
},
"widgets_values": [
1024,
1024,
1
]
},
{
"id": 50,
"type": "ConditioningZeroOut",
"pos": [
94,
404
],
"size": [
317.4000244140625,
26
],
"flags": {
"collapsed": true
},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "conditioning",
"type": "CONDITIONING",
"link": 98
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"shape": 3,
"links": [
108
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ConditioningZeroOut"
},
"widgets_values": []
},
{
"id": 46,
"type": "ControlNetLoader",
"pos": [
-15,
472
],
"size": [
411.968017578125,
58.06914520263672
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "CONTROL_NET",
"type": "CONTROL_NET",
"shape": 3,
"links": [
87
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ControlNetLoader"
},
"widgets_values": [
"sd3.5_large_controlnet_blur.safetensors"
]
},
{
"id": 57,
"type": "LoadImage",
"pos": [
449,
478
],
"size": [
470.65765380859375,
461.4942932128906
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
113
],
"slot_index": 0
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"ComfyUI_00204_.png",
"image"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
770,
-10
],
"size": [
284.1198425292969,
262
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 14
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 83,
"slot_index": 1
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 84
},
{
"name": "latent_image",
"type": "LATENT",
"link": 66
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
63
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
268264726798396,
"randomize",
30,
4,
"euler",
"simple",
1
]
},
{
"id": 44,
"type": "ControlNetApplySD3",
"pos": [
420,
-20
],
"size": [
315,
186
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "positive",
"type": "CONDITIONING",
"link": 109
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 108
},
{
"name": "control_net",
"type": "CONTROL_NET",
"link": 87,
"slot_index": 2
},
{
"name": "vae",
"type": "VAE",
"link": 85,
"slot_index": 3
},
{
"name": "image",
"type": "IMAGE",
"link": 113,
"slot_index": 4
}
],
"outputs": [
{
"name": "positive",
"type": "CONDITIONING",
"shape": 3,
"links": [
83
],
"slot_index": 0
},
{
"name": "negative",
"type": "CONDITIONING",
"shape": 3,
"links": [
84
],
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "ControlNetApplySD3"
},
"widgets_values": [
0.7000000000000001,
0,
1
]
}
],
"links": [
[
8,
4,
2,
8,
1,
"VAE"
],
[
13,
8,
0,
9,
0,
"IMAGE"
],
[
14,
4,
0,
3,
0,
"MODEL"
],
[
63,
3,
0,
8,
0,
"LATENT"
],
[
65,
4,
1,
6,
0,
"CLIP"
],
[
66,
33,
0,
3,
3,
"LATENT"
],
[
83,
44,
0,
3,
1,
"CONDITIONING"
],
[
84,
44,
1,
3,
2,
"CONDITIONING"
],
[
85,
4,
2,
44,
3,
"VAE"
],
[
87,
46,
0,
44,
2,
"CONTROL_NET"
],
[
92,
48,
0,
47,
0,
"IMAGE"
],
[
98,
6,
0,
50,
0,
"CONDITIONING"
],
[
102,
47,
0,
53,
0,
"IMAGE"
],
[
108,
50,
0,
44,
1,
"CONDITIONING"
],
[
109,
6,
0,
44,
0,
"CONDITIONING"
],
[
113,
57,
0,
44,
4,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.555991731349224,
"offset": [
224.63848966184364,
330.6857814538206
]
}
},
"version": 0.4
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,713 @@
{
"last_node_id": 60,
"last_link_id": 121,
"nodes": [
{
"id": 9,
"type": "SaveImage",
"pos": [
1097,
-14
],
"size": [
845.74560546875,
898.2359619140625
],
"flags": {},
"order": 12,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 13
}
],
"outputs": [],
"properties": {},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 33,
"type": "EmptySD3LatentImage",
"pos": [
430,
250
],
"size": [
300.9447021484375,
106
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"shape": 3,
"links": [
115
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptySD3LatentImage"
},
"widgets_values": [
1024,
1024,
1
]
},
{
"id": 45,
"type": "LoadImage",
"pos": [
-10,
600
],
"size": [
288,
336
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"shape": 3,
"links": [
91
]
},
{
"name": "MASK",
"type": "MASK",
"shape": 3,
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"vcmd_create_dragon_mascot_characters_that_best_suitable_for_Sin_5ba6beab-2ad7-4810-997e-387c27bea297.png",
"image"
]
},
{
"id": 46,
"type": "ControlNetLoader",
"pos": [
0,
490
],
"size": [
623.134033203125,
58
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "CONTROL_NET",
"type": "CONTROL_NET",
"shape": 3,
"links": [
87
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ControlNetLoader"
},
"widgets_values": [
"sd3.5_large_controlnet_depth.safetensors"
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
780,
315
],
"size": [
278.8823547363281,
46.5799446105957
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 116
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
13
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
},
{
"id": 56,
"type": "KSampler",
"pos": [
765,
-15
],
"size": [
315,
262
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 112
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 113
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 114
},
{
"name": "latent_image",
"type": "LATENT",
"link": 115
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
116
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 58,
"type": "ConditioningZeroOut",
"pos": [
135,
420
],
"size": [
317.4000244140625,
26
],
"flags": {
"collapsed": true
},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "conditioning",
"type": "CONDITIONING",
"link": 119
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
121
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ConditioningZeroOut"
}
},
{
"id": 57,
"type": "CLIPTextEncode",
"pos": [
-15,
150
],
"size": [
400,
200
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 117
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
119,
120
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"hairy dragon stuffed toy with light green color in a fairy tale background, fluffy hair, standing with 2 legs"
]
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
-15,
0
],
"size": [
387.85345458984375,
98
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
112
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
117
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8,
85
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"sd3.5_large_fp8_scaled.safetensors"
]
},
{
"id": 44,
"type": "ControlNetApplySD3",
"pos": [
420,
15
],
"size": [
315,
186
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "positive",
"type": "CONDITIONING",
"link": 120
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 121
},
{
"name": "control_net",
"type": "CONTROL_NET",
"link": 87,
"slot_index": 2
},
{
"name": "vae",
"type": "VAE",
"link": 85,
"slot_index": 3
},
{
"name": "image",
"type": "IMAGE",
"link": 110,
"slot_index": 4
}
],
"outputs": [
{
"name": "positive",
"type": "CONDITIONING",
"shape": 3,
"links": [
113
],
"slot_index": 0
},
{
"name": "negative",
"type": "CONDITIONING",
"shape": 3,
"links": [
114
],
"slot_index": 1
}
],
"properties": {
"Node name for S&R": "ControlNetApplySD3"
},
"widgets_values": [
0.7000000000000001,
0,
1
]
},
{
"id": 54,
"type": "PreviewImage",
"pos": [
660,
495
],
"size": [
366.44989013671875,
340.7085266113281
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 109
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 48,
"type": "ImageScale",
"pos": [
310,
600
],
"size": [
315,
130
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 91
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"shape": 3,
"links": [
103,
108
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ImageScale"
},
"widgets_values": [
"bilinear",
1024,
1024,
"center"
]
},
{
"id": 55,
"type": "DepthAnythingPreprocessor",
"pos": [
310,
770
],
"size": [
315,
82
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 108
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
109,
110
]
}
],
"properties": {
"Node name for S&R": "DepthAnythingPreprocessor"
},
"widgets_values": [
"depth_anything_vitl14.pth",
1024
]
}
],
"links": [
[
8,
4,
2,
8,
1,
"VAE"
],
[
13,
8,
0,
9,
0,
"IMAGE"
],
[
85,
4,
2,
44,
3,
"VAE"
],
[
87,
46,
0,
44,
2,
"CONTROL_NET"
],
[
91,
45,
0,
48,
0,
"IMAGE"
],
[
92,
48,
0,
47,
0,
"IMAGE"
],
[
102,
47,
0,
53,
0,
"IMAGE"
],
[
108,
48,
0,
55,
0,
"IMAGE"
],
[
109,
55,
0,
54,
0,
"IMAGE"
],
[
110,
55,
0,
44,
4,
"IMAGE"
],
[
112,
4,
0,
56,
0,
"MODEL"
],
[
113,
44,
0,
56,
1,
"CONDITIONING"
],
[
114,
44,
1,
56,
2,
"CONDITIONING"
],
[
115,
33,
0,
56,
3,
"LATENT"
],
[
116,
56,
0,
8,
0,
"LATENT"
],
[
117,
4,
1,
57,
0,
"CLIP"
],
[
119,
57,
0,
58,
0,
"CONDITIONING"
],
[
120,
57,
0,
44,
0,
"CONDITIONING"
],
[
121,
58,
0,
44,
1,
"CONDITIONING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.2331581182307068,
"offset": [
692.0972183417064,
84.29928193157562
]
}
},
"version": 0.4
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,38 +1,45 @@
<template>
<Card :data-testid="`template-workflow-${template.name}`" class="w-64">
<Card
ref="cardRef"
:data-testid="`template-workflow-${template.name}`"
class="w-64 group"
>
<template #header>
<div class="flex items-center justify-center">
<div class="relative overflow-hidden rounded-t-lg cursor-pointer">
<div
class="flex items-center justify-center cursor-pointer"
@click="$emit('loadWorkflow', template.name)"
>
<div class="relative overflow-hidden rounded-t-lg">
<template v-if="template.mediaType === 'audio'">
<div class="w-64 h-64 flex items-center justify-center p-4 z-20">
<audio
controls
class="w-full relative z-20"
:src="thumbnailSrc"
@error="imageError = true"
@click.stop
/>
</div>
<AudioThumbnail :src="baseThumbnailSrc" />
</template>
<template v-else-if="template.thumbnailVariant === 'compareSlider'">
<CompareSliderThumbnail
:base-image-src="baseThumbnailSrc"
:overlay-image-src="overlayThumbnailSrc"
:alt="title"
:is-hovered="isHovered"
/>
</template>
<template v-else-if="template.thumbnailVariant === 'hoverDissolve'">
<HoverDissolveThumbnail
:base-image-src="baseThumbnailSrc"
:overlay-image-src="overlayThumbnailSrc"
:alt="title"
:is-hovered="isHovered"
/>
</template>
<template v-else>
<img
v-if="!imageError"
:src="thumbnailSrc"
<DefaultThumbnail
:src="baseThumbnailSrc"
:alt="title"
class="w-64 h-64 rounded-t-lg object-cover thumbnail"
@error="imageError = true"
:hover-zoom="
template.thumbnailVariant === 'zoomHover'
? UPSCALE_ZOOM_SCALE
: DEFAULT_ZOOM_SCALE
"
/>
<div v-else class="w-64 h-64 content-center text-center">
<i class="pi pi-file" style="font-size: 4rem"></i>
</div>
</template>
<a @click="$emit('loadWorkflow', template.name)">
<div
class="absolute top-0 left-0 w-64 h-64 overflow-hidden opacity-0 transition duration-300 ease-in-out hover:opacity-100 bg-opacity-50 bg-black flex items-center justify-center z-10"
>
<i class="pi pi-play-circle" style="color: white"></i>
</div>
</a>
<ProgressSpinner
v-if="loading"
class="absolute inset-0 z-1 w-3/12 h-full"
@@ -41,7 +48,9 @@
</div>
</template>
<template #subtitle>
<div class="text-center">
<div
class="text-center py-2 opacity-85 group-hover:opacity-100 transition-opacity"
>
{{ title }}
</div>
</template>
@@ -49,14 +58,22 @@
</template>
<script setup lang="ts">
import { useElementHover } from '@vueuse/core'
import Card from 'primevue/card'
import ProgressSpinner from 'primevue/progressspinner'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue'
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
import { TemplateInfo } from '@/types/workflowTemplateTypes'
import { normalizeI18nKey } from '@/utils/formatUtil'
const UPSCALE_ZOOM_SCALE = 38 // for upscale templates, exaggerate the hover zoom
const DEFAULT_ZOOM_SCALE = 6
const { sourceModule, categoryTitle, loading, template } = defineProps<{
sourceModule: string
categoryTitle: string
@@ -66,13 +83,23 @@ const { sourceModule, categoryTitle, loading, template } = defineProps<{
const { t } = useI18n()
const imageError = ref(false)
const cardRef = ref<HTMLElement | null>(null)
const isHovered = useElementHover(cardRef)
const thumbnailSrc = computed(() =>
sourceModule === 'default'
? `/templates/${template.name}.${template.mediaSubtype}`
: `/api/workflow_templates/${sourceModule}/${template.name}.${template.mediaSubtype}`
? `/templates/${template.name}`
: `/api/workflow_templates/${sourceModule}/${template.name}`
)
const baseThumbnailSrc = computed(
() => `${thumbnailSrc.value}-1.${template.mediaSubtype}`
)
const overlayThumbnailSrc = computed(
() => `${thumbnailSrc.value}-2.${template.mediaSubtype}`
)
const title = computed(() => {
return sourceModule === 'default'
? t(

View File

@@ -0,0 +1,15 @@
<template>
<BaseThumbnail>
<div class="w-64 h-64 flex items-center justify-center p-4">
<audio controls class="w-full relative" :src="src" @click.stop />
</div>
</BaseThumbnail>
</template>
<script setup lang="ts">
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
defineProps<{
src: string
}>()
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div class="relative w-64 h-64 rounded-t-lg overflow-hidden select-none">
<div v-if="!error" ref="contentRef">
<slot />
</div>
<div
v-else
class="w-full h-full flex items-center justify-center bg-surface-card"
>
<i class="pi pi-file text-4xl text-surface-600" />
</div>
</div>
</template>
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import { onMounted, ref } from 'vue'
const error = ref(false)
const contentRef = ref<HTMLElement | null>(null)
onMounted(() => {
const images = Array.from(contentRef.value?.getElementsByTagName('img') ?? [])
images.forEach((img) => {
useEventListener(img, 'error', () => {
error.value = true
})
})
})
</script>

View File

@@ -0,0 +1,51 @@
<template>
<BaseThumbnail>
<img :src="baseImageSrc" :alt="alt" class="w-full h-full object-cover" />
<div ref="containerRef" class="absolute inset-0">
<img
:src="overlayImageSrc"
:alt="alt"
class="w-full h-full object-cover"
:style="{
clipPath: `inset(0 ${100 - sliderPosition}% 0 0)`
}"
/>
<div
class="absolute inset-y-0 w-0.5 bg-white/30 backdrop-blur-sm z-10 pointer-events-none"
:style="{
left: `${sliderPosition}%`
}"
/>
</div>
</BaseThumbnail>
</template>
<script setup lang="ts">
import { useMouseInElement } from '@vueuse/core'
import { ref, watch } from 'vue'
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
const { isHovered } = defineProps<{
baseImageSrc: string
overlayImageSrc: string
alt: string
isHovered?: boolean
}>()
const sliderPosition = ref(21)
const containerRef = ref<HTMLElement | null>(null)
const { elementX, elementWidth, isOutside } = useMouseInElement(containerRef)
// Update slider position based on mouse position when hovered
watch(
[() => isHovered, elementX, elementWidth, isOutside],
([isHovered, x, width, outside]) => {
if (!isHovered) return
if (!outside) {
sliderPosition.value = (x / width) * 100
}
}
)
</script>

View File

@@ -0,0 +1,37 @@
<template>
<BaseThumbnail>
<div ref="containerRef" class="overflow-hidden">
<img
:src="src"
:alt="alt"
draggable="false"
class="w-64 h-64 object-cover transform-gpu transition-transform duration-300 ease-out"
:style="
isHovered ? { transform: `scale(${1 + hoverZoom / 100})` } : undefined
"
/>
</div>
</BaseThumbnail>
</template>
<script setup lang="ts">
import { useElementHover } from '@vueuse/core'
import { ref } from 'vue'
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
const { hoverZoom = 8 } = defineProps<{
src: string
alt: string
hoverZoom?: number
}>()
const containerRef = ref<HTMLElement | null>(null)
const isHovered = useElementHover(containerRef)
</script>
<style scoped>
img {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<BaseThumbnail>
<div class="relative w-full h-full">
<img
:src="baseImageSrc"
:alt="alt"
draggable="false"
class="absolute inset-0 w-64 h-64 object-cover"
/>
<img
:src="overlayImageSrc"
:alt="alt"
draggable="false"
class="absolute inset-0 w-64 h-64 object-cover transition-opacity duration-300"
:class="{ 'opacity-100': isHovered, 'opacity-0': !isHovered }"
/>
</div>
</BaseThumbnail>
</template>
<script setup lang="ts">
import BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'
defineProps<{
baseImageSrc: string
overlayImageSrc: string
alt: string
isHovered: boolean
}>()
</script>

View File

@@ -7,52 +7,54 @@ export const CORE_TEMPLATES = [
{
name: 'default',
mediaType: 'image',
mediaSubtype: 'png'
mediaSubtype: 'webp'
},
{
name: 'image2image',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/img2img/'
},
{
name: 'lora',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/lora/'
},
{
name: 'inpaint_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'compareSlider',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/inpaint/'
},
{
name: 'inpain_model_outpainting',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'compareSlider',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/inpaint/#outpainting'
},
{
name: 'embedding_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/textual_inversion_embeddings/'
},
{
name: 'gligen_textbox_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/gligen/'
},
{
name: 'lora_multiple',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/lora/'
}
]
@@ -65,51 +67,55 @@ export const CORE_TEMPLATES = [
{
name: 'flux_dev_checkpoint_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-dev-1'
},
{
name: 'flux_schnell',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#flux-schnell-1'
},
{
name: 'flux_fill_inpaint_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'compareSlider',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#fill-inpainting-model'
},
{
name: 'flux_fill_outpaint_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'compareSlider',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#fill-inpainting-model'
},
{
name: 'flux_canny_model_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'hoverDissolve',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#canny-and-depth'
},
{
name: 'flux_depth_lora_example',
mediaType: 'image',
mediaSubtype: 'webp',
thumbnailVariant: 'hoverDissolve',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#canny-and-depth'
},
{
name: 'flux_redux_model_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#redux'
},
{
name: 'flux_depth_lora_example',
mediaType: 'image',
mediaSubtype: 'png',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/flux/#canny-and-depth'
}
]
},
@@ -121,35 +127,40 @@ export const CORE_TEMPLATES = [
{
name: 'controlnet_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'hoverDissolve',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/'
},
{
name: '2_pass_pose_worship',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'hoverDissolve',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#pose-controlnet'
},
{
name: 'depth_controlnet',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'hoverDissolve',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#t2i-adapter-vs-controlnets'
},
{
name: 'depth_t2i_adapter',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'hoverDissolve',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#t2i-adapter-vs-controlnets'
},
{
name: 'mixing_controlnets',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'hoverDissolve',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/controlnet/#mixing-controlnets'
}
@@ -161,30 +172,34 @@ export const CORE_TEMPLATES = [
type: 'image',
templates: [
{
name: 'upscale',
name: 'hiresfix_latent_workflow',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'zoomHover',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/'
},
{
name: 'esrgan_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'zoomHover',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/upscale_models/'
},
{
name: 'hiresfix_esrgan_workflow',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'zoomHover',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/#non-latent-upscaling'
},
{
name: 'latent_upscale_different_prompt_model',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'zoomHover',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/2_pass_txt2img/#more-examples'
}
@@ -195,20 +210,6 @@ export const CORE_TEMPLATES = [
title: 'Video',
type: 'video',
templates: [
{
name: 'image_to_video',
mediaType: 'image',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/video/#image-to-video'
},
{
name: 'txt_to_image_to_video',
mediaType: 'image',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/video/#image-to-video'
},
{
name: 'ltxv_image_to_video',
mediaType: 'image',
@@ -233,6 +234,20 @@ export const CORE_TEMPLATES = [
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/hunyuan_video/'
},
{
name: 'image_to_video',
mediaType: 'image',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/video/#image-to-video'
},
{
name: 'txt_to_image_to_video',
mediaType: 'image',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/video/#image-to-video'
}
]
},
@@ -244,28 +259,31 @@ export const CORE_TEMPLATES = [
{
name: 'sd3.5_simple_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35'
},
{
name: 'sd3.5_large_canny_controlnet_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'hoverDissolve',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
},
{
name: 'sd3.5_large_depth',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'hoverDissolve',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
},
{
name: 'sd3.5_large_blur',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
thumbnailVariant: 'hoverDissolve',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/sd3/#sd35-controlnets'
}
@@ -279,33 +297,33 @@ export const CORE_TEMPLATES = [
{
name: 'sdxl_simple_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/'
},
{
name: 'sdxl_refiner_prompt_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl: 'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/'
},
{
name: 'sdxl_revision_text_prompts',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/#revision'
},
{
name: 'sdxl_revision_zero_positive',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/sdxl/#revision'
},
{
name: 'sdxlturbo_example',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/sdturbo/'
}
@@ -319,21 +337,21 @@ export const CORE_TEMPLATES = [
{
name: 'area_composition',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/'
},
{
name: 'area_composition_reversed',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/'
},
{
name: 'area_composition_square_area_for_subject',
mediaType: 'image',
mediaSubtype: 'png',
mediaSubtype: 'webp',
tutorialUrl:
'https://comfyanonymous.github.io/ComfyUI_examples/area_composition/#increasing-consistency-of-images-with-area-composition'
}

View File

@@ -393,9 +393,8 @@
"mixing_controlnets": "Mixing ControlNets"
},
"Upscaling": {
"upscale": "Upscale",
"esrgan_example": "ESRGAN",
"hiresfix_latent_workflow": "HiresFix Latent Workflow",
"hiresfix_latent_workflow": "Upscale",
"hiresfix_esrgan_workflow": "HiresFix ESRGAN Workflow",
"latent_upscale_different_prompt_model": "Latent Upscale Different Prompt Model"
},

View File

@@ -820,8 +820,7 @@
"esrgan_example": "ESRGAN",
"hiresfix_esrgan_workflow": "Flux de Travail ESRGAN HiresFix",
"hiresfix_latent_workflow": "Flux de Travail Latent HiresFix",
"latent_upscale_different_prompt_model": "Modèle d'Agrandissement Latent Différent Prompt",
"upscale": "Agrandissement"
"latent_upscale_different_prompt_model": "Modèle d'Agrandissement Latent Différent Prompt"
},
"Video": {
"hunyuan_video_text_to_video": "Texte à Vidéo Hunyuan",

View File

@@ -820,8 +820,7 @@
"esrgan_example": "ESRGAN",
"hiresfix_esrgan_workflow": "HiresFix ESRGANワークフロー",
"hiresfix_latent_workflow": "HiresFix Latentワークフロー",
"latent_upscale_different_prompt_model": "Latent Upscale異なるプロンプトモデル",
"upscale": "アップスケール"
"latent_upscale_different_prompt_model": "Latent Upscale異なるプロンプトモデル"
},
"Video": {
"hunyuan_video_text_to_video": "Hunyuanビデオテキストからビデオへ",

View File

@@ -813,8 +813,7 @@
"esrgan_example": "ESRGAN",
"hiresfix_esrgan_workflow": "HiresFix ESRGAN 워크플로우",
"hiresfix_latent_workflow": "HiresFix Latent 워크플로우",
"latent_upscale_different_prompt_model": "Latent Upscale 다른 프롬프트 모델",
"upscale": "업스케일"
"latent_upscale_different_prompt_model": "Latent Upscale 다른 프롬프트 모델"
},
"Video": {
"hunyuan_video_text_to_video": "텍스트 -> 비디오 (Hunyuan Video)",

View File

@@ -820,8 +820,7 @@
"esrgan_example": "ESRGAN",
"hiresfix_esrgan_workflow": "HiresFix ESRGAN Workflow",
"hiresfix_latent_workflow": "HiresFix Latent Workflow",
"latent_upscale_different_prompt_model": "Latent Upscale Different Prompt Model",
"upscale": "Увеличение"
"latent_upscale_different_prompt_model": "Latent Upscale Different Prompt Model"
},
"Video": {
"hunyuan_video_text_to_video": "Hunyuan Video Text to Video",

View File

@@ -820,8 +820,7 @@
"esrgan_example": "ESRGAN",
"hiresfix_esrgan_workflow": "HiresFix ESRGAN工作流",
"hiresfix_latent_workflow": "HiresFix潜在工作流",
"latent_upscale_different_prompt_model": "潜在升级不同提示模型",
"upscale": "升级"
"latent_upscale_different_prompt_model": "潜在升级不同提示模型"
},
"Video": {
"hunyuan_video_text_to_video": "Hunyuan视频文本到视频",

View File

@@ -3,6 +3,7 @@ export interface TemplateInfo {
tutorialUrl?: string
mediaType: string
mediaSubtype: string
thumbnailVariant?: string
}
export interface WorkflowTemplates {