From c22c6ca135ddf617cf5d3d053bd767dd651d2ce8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 30 May 2023 15:57:50 +1000 Subject: [PATCH] fix(ui): fix img2img fit --- .../graphBuilders/buildImageToImageGraph.ts | 306 +++++++++++++++++- .../graphBuilders/buildTextToImageGraph.ts | 215 +++++++++++- .../nodes/util/nodeBuilders/addNoiseNodes.ts | 208 ------------ 3 files changed, 504 insertions(+), 225 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts index bd3d8a5460..728d75c2ae 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildImageToImageGraph.ts @@ -2,21 +2,31 @@ import { RootState } from 'app/store/store'; import { CompelInvocation, Graph, + ImageResizeInvocation, ImageToLatentsInvocation, + IterateInvocation, LatentsToImageInvocation, LatentsToLatentsInvocation, + NoiseInvocation, + RandomIntInvocation, + RangeOfSizeInvocation, } from 'services/api'; import { NonNullableGraph } from 'features/nodes/types/types'; -import { addNoiseNodes } from '../nodeBuilders/addNoiseNodes'; import { log } from 'app/logging/useLogger'; +import { set } from 'lodash-es'; -const moduleLog = log.child({ namespace: 'buildImageToImageGraph' }); +const moduleLog = log.child({ namespace: 'nodes' }); const POSITIVE_CONDITIONING = 'positive_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning'; const IMAGE_TO_LATENTS = 'image_to_latents'; const LATENTS_TO_LATENTS = 'latents_to_latents'; const LATENTS_TO_IMAGE = 'latents_to_image'; +const RESIZE = 'resize_image'; +const NOISE = 'noise'; +const RANDOM_INT = 'rand_int'; +const RANGE_OF_SIZE = 'range_of_size'; +const ITERATE = 'iterate'; /** * Builds the Image to Image tab graph. @@ -31,6 +41,12 @@ export const buildImageToImageGraph = (state: RootState): Graph => { steps, initialImage, img2imgStrength: strength, + shouldFitToWidthHeight, + width, + height, + iterations, + seed, + shouldRandomizeSeed, } = state.generation; if (!initialImage) { @@ -38,12 +54,12 @@ export const buildImageToImageGraph = (state: RootState): Graph => { throw new Error('No initial image found in state'); } - let graph: NonNullableGraph = { + const graph: NonNullableGraph = { nodes: {}, edges: [], }; - // Create the conditioning, t2l and l2i nodes + // Create the positive conditioning (prompt) node const positiveConditioningNode: CompelInvocation = { id: POSITIVE_CONDITIONING, type: 'compel', @@ -51,6 +67,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => { model, }; + // Negative conditioning const negativeConditioningNode: CompelInvocation = { id: NEGATIVE_CONDITIONING, type: 'compel', @@ -58,16 +75,15 @@ export const buildImageToImageGraph = (state: RootState): Graph => { model, }; + // This will encode the raster image to latents - but it may get its `image` from a resize node, + // so we do not set its `image` property yet const imageToLatentsNode: ImageToLatentsInvocation = { id: IMAGE_TO_LATENTS, type: 'i2l', model, - image: { - image_name: initialImage?.image_name, - image_origin: initialImage?.image_origin, - }, }; + // This does the actual img2img inference const latentsToLatentsNode: LatentsToLatentsInvocation = { id: LATENTS_TO_LATENTS, type: 'l2l', @@ -78,20 +94,21 @@ export const buildImageToImageGraph = (state: RootState): Graph => { strength, }; + // Finally we decode the latents back to an image const latentsToImageNode: LatentsToImageInvocation = { id: LATENTS_TO_IMAGE, type: 'l2i', model, }; - // Add to the graph + // Add all those nodes to the graph graph.nodes[POSITIVE_CONDITIONING] = positiveConditioningNode; graph.nodes[NEGATIVE_CONDITIONING] = negativeConditioningNode; graph.nodes[IMAGE_TO_LATENTS] = imageToLatentsNode; graph.nodes[LATENTS_TO_LATENTS] = latentsToLatentsNode; graph.nodes[LATENTS_TO_IMAGE] = latentsToImageNode; - // Connect them + // Connect the prompt nodes to the imageToLatents node graph.edges.push({ source: { node_id: POSITIVE_CONDITIONING, field: 'conditioning' }, destination: { @@ -99,7 +116,6 @@ export const buildImageToImageGraph = (state: RootState): Graph => { field: 'positive_conditioning', }, }); - graph.edges.push({ source: { node_id: NEGATIVE_CONDITIONING, field: 'conditioning' }, destination: { @@ -108,6 +124,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => { }, }); + // Connect the image-encoding node graph.edges.push({ source: { node_id: IMAGE_TO_LATENTS, field: 'latents' }, destination: { @@ -116,6 +133,7 @@ export const buildImageToImageGraph = (state: RootState): Graph => { }, }); + // Connect the image-decoding node graph.edges.push({ source: { node_id: LATENTS_TO_LATENTS, field: 'latents' }, destination: { @@ -124,8 +142,270 @@ export const buildImageToImageGraph = (state: RootState): Graph => { }, }); - // Create and add the noise nodes - graph = addNoiseNodes(graph, latentsToLatentsNode.id, state); + /** + * Now we need to handle iterations and random seeds. There are four possible scenarios: + * - Single iteration, explicit seed + * - Single iteration, random seed + * - Multiple iterations, explicit seed + * - Multiple iterations, random seed + * + * They all have different graphs and connections. + */ + + // Single iteration, explicit seed + if (!shouldRandomizeSeed && iterations === 1) { + // Noise node using the explicit seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + seed: seed, + }; + + graph.nodes[NOISE] = noiseNode; + + // Connect noise to l2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Single iteration, random seed + if (shouldRandomizeSeed && iterations === 1) { + // Random int node to generate the seed + const randomIntNode: RandomIntInvocation = { + id: RANDOM_INT, + type: 'rand_int', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + }; + + graph.nodes[RANDOM_INT] = randomIntNode; + graph.nodes[NOISE] = noiseNode; + + // Connect random int to the seed of the noise node + graph.edges.push({ + source: { node_id: RANDOM_INT, field: 'a' }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to l2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Multiple iterations, explicit seed + if (!shouldRandomizeSeed && iterations > 1) { + // Range of size node to generate `iterations` count of seeds - range of size generates a collection + // of ints from `start` to `start + size`. The `start` is the seed, and the `size` is the number of + // iterations. + const rangeOfSizeNode: RangeOfSizeInvocation = { + id: RANGE_OF_SIZE, + type: 'range_of_size', + start: seed, + size: iterations, + }; + + // Iterate node to iterate over the seeds generated by the range of size node + const iterateNode: IterateInvocation = { + id: ITERATE, + type: 'iterate', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + }; + + // Adding to the graph + graph.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; + graph.nodes[ITERATE] = iterateNode; + graph.nodes[NOISE] = noiseNode; + + // Connect range of size to iterate + graph.edges.push({ + source: { node_id: RANGE_OF_SIZE, field: 'collection' }, + destination: { + node_id: ITERATE, + field: 'collection', + }, + }); + + // Connect iterate to noise + graph.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to l2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Multiple iterations, random seed + if (shouldRandomizeSeed && iterations > 1) { + // Random int node to generate the seed + const randomIntNode: RandomIntInvocation = { + id: RANDOM_INT, + type: 'rand_int', + }; + + // Range of size node to generate `iterations` count of seeds - range of size generates a collection + const rangeOfSizeNode: RangeOfSizeInvocation = { + id: RANGE_OF_SIZE, + type: 'range_of_size', + size: iterations, + }; + + // Iterate node to iterate over the seeds generated by the range of size node + const iterateNode: IterateInvocation = { + id: ITERATE, + type: 'iterate', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + width, + height, + }; + + // Adding to the graph + graph.nodes[RANDOM_INT] = randomIntNode; + graph.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; + graph.nodes[ITERATE] = iterateNode; + graph.nodes[NOISE] = noiseNode; + + // Connect random int to the start of the range of size so the range starts on the random first seed + graph.edges.push({ + source: { node_id: RANDOM_INT, field: 'a' }, + destination: { node_id: RANGE_OF_SIZE, field: 'start' }, + }); + + // Connect range of size to iterate + graph.edges.push({ + source: { node_id: RANGE_OF_SIZE, field: 'collection' }, + destination: { + node_id: ITERATE, + field: 'collection', + }, + }); + + // Connect iterate to noise + graph.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to l2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: LATENTS_TO_LATENTS, + field: 'noise', + }, + }); + } + + if (shouldFitToWidthHeight) { + // The init image needs to be resized to the specified width and height before being passed to `IMAGE_TO_LATENTS` + + // Create a resize node, explicitly setting its image + const resizeNode: ImageResizeInvocation = { + id: RESIZE, + type: 'img_resize', + image: { + image_name: initialImage.image_name, + image_origin: initialImage.image_origin, + }, + height, + width, + }; + + graph.nodes[RESIZE] = resizeNode; + + // The `RESIZE` node then passes its image to `IMAGE_TO_LATENTS` + graph.edges.push({ + source: { node_id: RESIZE, field: 'image' }, + destination: { + node_id: IMAGE_TO_LATENTS, + field: 'image', + }, + }); + + // The `RESIZE` node also passes its width and height to `NOISE` + graph.edges.push({ + source: { node_id: RESIZE, field: 'width' }, + destination: { + node_id: NOISE, + field: 'width', + }, + }); + + graph.edges.push({ + source: { node_id: RESIZE, field: 'height' }, + destination: { + node_id: NOISE, + field: 'height', + }, + }); + } else { + // We are not resizing, so we need to set the image on the `LATENTS_TO_LATENTS` node explicitly + set(graph.nodes[LATENTS_TO_LATENTS], 'image', { + image_name: initialImage.image_name, + image_origin: initialImage.image_origin, + }); + + // Pass the image's dimensions to the `NOISE` node + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'width' }, + destination: { + node_id: NOISE, + field: 'width', + }, + }); + graph.edges.push({ + source: { node_id: IMAGE_TO_LATENTS, field: 'height' }, + destination: { + node_id: NOISE, + field: 'height', + }, + }); + } return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts index 51f89e8f74..737d8d5b61 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graphBuilders/buildTextToImageGraph.ts @@ -2,16 +2,23 @@ import { RootState } from 'app/store/store'; import { CompelInvocation, Graph, + IterateInvocation, LatentsToImageInvocation, + NoiseInvocation, + RandomIntInvocation, + RangeOfSizeInvocation, TextToLatentsInvocation, } from 'services/api'; import { NonNullableGraph } from 'features/nodes/types/types'; -import { addNoiseNodes } from '../nodeBuilders/addNoiseNodes'; const POSITIVE_CONDITIONING = 'positive_conditioning'; const NEGATIVE_CONDITIONING = 'negative_conditioning'; const TEXT_TO_LATENTS = 'text_to_latents'; const LATENTS_TO_IMAGE = 'latents_to_image'; +const NOISE = 'noise'; +const RANDOM_INT = 'rand_int'; +const RANGE_OF_SIZE = 'range_of_size'; +const ITERATE = 'iterate'; /** * Builds the Text to Image tab graph. @@ -24,9 +31,14 @@ export const buildTextToImageGraph = (state: RootState): Graph => { cfgScale: cfg_scale, scheduler, steps, + width, + height, + iterations, + seed, + shouldRandomizeSeed, } = state.generation; - let graph: NonNullableGraph = { + const graph: NonNullableGraph = { nodes: {}, edges: [], }; @@ -92,8 +104,203 @@ export const buildTextToImageGraph = (state: RootState): Graph => { }, }); - // Create and add the noise nodes - graph = addNoiseNodes(graph, TEXT_TO_LATENTS, state); + /** + * Now we need to handle iterations and random seeds. There are four possible scenarios: + * - Single iteration, explicit seed + * - Single iteration, random seed + * - Multiple iterations, explicit seed + * - Multiple iterations, random seed + * + * They all have different graphs and connections. + */ + // Single iteration, explicit seed + if (!shouldRandomizeSeed && iterations === 1) { + // Noise node using the explicit seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + seed: seed, + }; + + graph.nodes[NOISE] = noiseNode; + + // Connect noise to l2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Single iteration, random seed + if (shouldRandomizeSeed && iterations === 1) { + // Random int node to generate the seed + const randomIntNode: RandomIntInvocation = { + id: RANDOM_INT, + type: 'rand_int', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + }; + + graph.nodes[RANDOM_INT] = randomIntNode; + graph.nodes[NOISE] = noiseNode; + + // Connect random int to the seed of the noise node + graph.edges.push({ + source: { node_id: RANDOM_INT, field: 'a' }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to t2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Multiple iterations, explicit seed + if (!shouldRandomizeSeed && iterations > 1) { + // Range of size node to generate `iterations` count of seeds - range of size generates a collection + // of ints from `start` to `start + size`. The `start` is the seed, and the `size` is the number of + // iterations. + const rangeOfSizeNode: RangeOfSizeInvocation = { + id: RANGE_OF_SIZE, + type: 'range_of_size', + start: seed, + size: iterations, + }; + + // Iterate node to iterate over the seeds generated by the range of size node + const iterateNode: IterateInvocation = { + id: ITERATE, + type: 'iterate', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + }; + + // Adding to the graph + graph.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; + graph.nodes[ITERATE] = iterateNode; + graph.nodes[NOISE] = noiseNode; + + // Connect range of size to iterate + graph.edges.push({ + source: { node_id: RANGE_OF_SIZE, field: 'collection' }, + destination: { + node_id: ITERATE, + field: 'collection', + }, + }); + + // Connect iterate to noise + graph.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to t2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'noise', + }, + }); + } + + // Multiple iterations, random seed + if (shouldRandomizeSeed && iterations > 1) { + // Random int node to generate the seed + const randomIntNode: RandomIntInvocation = { + id: RANDOM_INT, + type: 'rand_int', + }; + + // Range of size node to generate `iterations` count of seeds - range of size generates a collection + const rangeOfSizeNode: RangeOfSizeInvocation = { + id: RANGE_OF_SIZE, + type: 'range_of_size', + size: iterations, + }; + + // Iterate node to iterate over the seeds generated by the range of size node + const iterateNode: IterateInvocation = { + id: ITERATE, + type: 'iterate', + }; + + // Noise node without any seed + const noiseNode: NoiseInvocation = { + id: NOISE, + type: 'noise', + width, + height, + }; + + // Adding to the graph + graph.nodes[RANDOM_INT] = randomIntNode; + graph.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; + graph.nodes[ITERATE] = iterateNode; + graph.nodes[NOISE] = noiseNode; + + // Connect random int to the start of the range of size so the range starts on the random first seed + graph.edges.push({ + source: { node_id: RANDOM_INT, field: 'a' }, + destination: { node_id: RANGE_OF_SIZE, field: 'start' }, + }); + + // Connect range of size to iterate + graph.edges.push({ + source: { node_id: RANGE_OF_SIZE, field: 'collection' }, + destination: { + node_id: ITERATE, + field: 'collection', + }, + }); + + // Connect iterate to noise + graph.edges.push({ + source: { + node_id: ITERATE, + field: 'item', + }, + destination: { + node_id: NOISE, + field: 'seed', + }, + }); + + // Connect noise to t2l + graph.edges.push({ + source: { node_id: NOISE, field: 'noise' }, + destination: { + node_id: TEXT_TO_LATENTS, + field: 'noise', + }, + }); + } return graph; }; diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts deleted file mode 100644 index ba3d4d8168..0000000000 --- a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/addNoiseNodes.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { RootState } from 'app/store/store'; -import { - IterateInvocation, - NoiseInvocation, - RandomIntInvocation, - RangeOfSizeInvocation, -} from 'services/api'; -import { NonNullableGraph } from 'features/nodes/types/types'; -import { cloneDeep } from 'lodash-es'; - -const NOISE = 'noise'; -const RANDOM_INT = 'rand_int'; -const RANGE_OF_SIZE = 'range_of_size'; -const ITERATE = 'iterate'; -/** - * Adds the appropriate noise nodes to a linear UI t2l or l2l graph. - * - * @param graph The graph to add the noise nodes to. - * @param baseNodeId The id of the base node to connect the noise nodes to. - * @param state The app state.. - */ -export const addNoiseNodes = ( - graph: NonNullableGraph, - baseNodeId: string, - state: RootState -): NonNullableGraph => { - const graphClone = cloneDeep(graph); - - // Create and add the noise nodes - const { width, height, seed, iterations, shouldRandomizeSeed } = - state.generation; - - // Single iteration, explicit seed - if (!shouldRandomizeSeed && iterations === 1) { - const noiseNode: NoiseInvocation = { - id: NOISE, - type: 'noise', - seed: seed, - width, - height, - }; - - graphClone.nodes[NOISE] = noiseNode; - - // Connect them - graphClone.edges.push({ - source: { node_id: NOISE, field: 'noise' }, - destination: { - node_id: baseNodeId, - field: 'noise', - }, - }); - } - - // Single iteration, random seed - if (shouldRandomizeSeed && iterations === 1) { - // TODO: This assumes the `high` value is the max seed value - const randomIntNode: RandomIntInvocation = { - id: RANDOM_INT, - type: 'rand_int', - }; - - const noiseNode: NoiseInvocation = { - id: NOISE, - type: 'noise', - width, - height, - }; - - graphClone.nodes[RANDOM_INT] = randomIntNode; - graphClone.nodes[NOISE] = noiseNode; - - graphClone.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, - destination: { - node_id: NOISE, - field: 'seed', - }, - }); - - graphClone.edges.push({ - source: { node_id: NOISE, field: 'noise' }, - destination: { - node_id: baseNodeId, - field: 'noise', - }, - }); - } - - // Multiple iterations, explicit seed - if (!shouldRandomizeSeed && iterations > 1) { - const rangeOfSizeNode: RangeOfSizeInvocation = { - id: RANGE_OF_SIZE, - type: 'range_of_size', - start: seed, - size: iterations, - }; - - const iterateNode: IterateInvocation = { - id: ITERATE, - type: 'iterate', - }; - - const noiseNode: NoiseInvocation = { - id: NOISE, - type: 'noise', - width, - height, - }; - - graphClone.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; - graphClone.nodes[ITERATE] = iterateNode; - graphClone.nodes[NOISE] = noiseNode; - - graphClone.edges.push({ - source: { node_id: RANGE_OF_SIZE, field: 'collection' }, - destination: { - node_id: ITERATE, - field: 'collection', - }, - }); - - graphClone.edges.push({ - source: { - node_id: ITERATE, - field: 'item', - }, - destination: { - node_id: NOISE, - field: 'seed', - }, - }); - - graphClone.edges.push({ - source: { node_id: NOISE, field: 'noise' }, - destination: { - node_id: baseNodeId, - field: 'noise', - }, - }); - } - - // Multiple iterations, random seed - if (shouldRandomizeSeed && iterations > 1) { - // TODO: This assumes the `high` value is the max seed value - const randomIntNode: RandomIntInvocation = { - id: RANDOM_INT, - type: 'rand_int', - }; - - const rangeOfSizeNode: RangeOfSizeInvocation = { - id: RANGE_OF_SIZE, - type: 'range_of_size', - size: iterations, - }; - - const iterateNode: IterateInvocation = { - id: ITERATE, - type: 'iterate', - }; - - const noiseNode: NoiseInvocation = { - id: NOISE, - type: 'noise', - width, - height, - }; - - graphClone.nodes[RANDOM_INT] = randomIntNode; - graphClone.nodes[RANGE_OF_SIZE] = rangeOfSizeNode; - graphClone.nodes[ITERATE] = iterateNode; - graphClone.nodes[NOISE] = noiseNode; - - graphClone.edges.push({ - source: { node_id: RANDOM_INT, field: 'a' }, - destination: { node_id: RANGE_OF_SIZE, field: 'start' }, - }); - - graphClone.edges.push({ - source: { node_id: RANGE_OF_SIZE, field: 'collection' }, - destination: { - node_id: ITERATE, - field: 'collection', - }, - }); - - graphClone.edges.push({ - source: { - node_id: ITERATE, - field: 'item', - }, - destination: { - node_id: NOISE, - field: 'seed', - }, - }); - - graphClone.edges.push({ - source: { node_id: NOISE, field: 'noise' }, - destination: { - node_id: baseNodeId, - field: 'noise', - }, - }); - } - - return graphClone; -};