Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5392aa237 | ||
|
|
16a2b55fa1 | ||
|
|
b423b09ff3 | ||
|
|
e66add88cb | ||
|
|
32b22c39cb | ||
|
|
259baac177 | ||
|
|
67ef8c13a8 | ||
|
|
b2bb1876de | ||
|
|
cda4e626e7 | ||
|
|
d835aff0cb | ||
|
|
21e1967c5e | ||
|
|
c9b5baf4d9 | ||
|
|
67c974c96e | ||
|
|
b46ccb03c9 | ||
|
|
0ecf98e08b | ||
|
|
f024034724 | ||
|
|
327a21f009 | ||
|
|
cfc51532b8 | ||
|
|
00988f92e4 | ||
|
|
868c6085a8 | ||
|
|
a47a56bda0 | ||
|
|
3667b42b2f | ||
|
|
7d142d7d62 | ||
|
|
24863e2ed3 | ||
|
|
fe8b526bbb | ||
|
|
6298be393a | ||
|
|
3a7853f9cc | ||
|
|
4a9413c83d | ||
|
|
21b04d62ae |
@@ -7,15 +7,19 @@ on:
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'shadowcz007' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
|
||||
@@ -10,7 +10,9 @@ For business cooperation, please contact email 389570357@qq.com
|
||||
|
||||
##### `最新`:
|
||||
|
||||
- 新增 SimulateDevDesignDiscussions,需要安装[swarm](https://github.com/openai/swarm)和[Comfyui-ChatTTS](https://github.com/shadowcz007/Comfyui-ChatTTS),工作流下载[./workflow/swarm制作的播客节点workflow.json]
|
||||
- 新增[fal.ai](https://fal.ai/dashboard)的视频生成:Kling、RunwayGen3、LumaDreamMachine,[工作流下载](./workflow/video-all-in-one-test-workflow.json)
|
||||
|
||||
- 新增 SimulateDevDesignDiscussions,需要安装[swarm](https://github.com/openai/swarm)和[Comfyui-ChatTTS](https://github.com/shadowcz007/Comfyui-ChatTTS),[工作流下载](./workflow/swarm制作的播客节点workflow.json)
|
||||
|
||||
- 新增 SenseVoice
|
||||
|
||||
|
||||
+19
@@ -1455,4 +1455,23 @@ try:
|
||||
except Exception as e:
|
||||
logging.info('Whisper.available False' )
|
||||
|
||||
|
||||
try:
|
||||
from .nodes.FalVideo import VideoGenKlingNode,VideoGenLumaDreamMachineNode,VideoGenRunwayGen3Node,LoadVideoFromURL
|
||||
logging.info('FalVideo.available')
|
||||
# Update Node class mappings
|
||||
NODE_CLASS_MAPPINGS['VideoGenKlingNode']=VideoGenKlingNode
|
||||
NODE_CLASS_MAPPINGS['VideoGenRunwayGen3Node']=VideoGenRunwayGen3Node
|
||||
NODE_CLASS_MAPPINGS['VideoGenLumaDreamMachineNode']=VideoGenLumaDreamMachineNode
|
||||
NODE_CLASS_MAPPINGS['LoadVideoFromURL']=LoadVideoFromURL
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS["VideoGenKlingNode"]= "Kling Video Generation @fal"
|
||||
NODE_DISPLAY_NAME_MAPPINGS["VideoGenRunwayGen3Node"]= "Runway Gen3 Image-to-Video @fal"
|
||||
NODE_DISPLAY_NAME_MAPPINGS["VideoGenLumaDreamMachineNode"]= "Luma Dream Machine @fal"
|
||||
NODE_DISPLAY_NAME_MAPPINGS["LoadVideoFromURL"]= "Load Video from URL"
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.info('FalVideo.available False' )
|
||||
|
||||
logging.info('\033[93m -------------- \033[0m')
|
||||
|
||||
+867
-121
File diff suppressed because it is too large
Load Diff
+1
-6
@@ -843,12 +843,7 @@ class JsonRepair:
|
||||
|
||||
# 以下为固定提示词的LLM节点示例
|
||||
class SimulateDevDesignDiscussions:
|
||||
def __init__(self):
|
||||
# self.__client = OpenAI()
|
||||
self.session_history = [] # 用于存储会话历史的列表
|
||||
# self.seed=0
|
||||
self.system_content="You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible."
|
||||
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
# 修改自 https://github.com/gokayfem/ComfyUI-fal-API/blob/main/nodes/video_node.py
|
||||
# image-to-video all in one
|
||||
|
||||
import os,sys
|
||||
import torch
|
||||
from PIL import Image
|
||||
import tempfile
|
||||
import numpy as np
|
||||
import requests
|
||||
import cv2
|
||||
import subprocess
|
||||
import importlib.util
|
||||
python = sys.executable
|
||||
|
||||
def is_installed(package, package_overwrite=None,auto_install=True):
|
||||
is_has=False
|
||||
try:
|
||||
spec = importlib.util.find_spec(package)
|
||||
is_has=spec is not None
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
package = package_overwrite or package
|
||||
|
||||
if spec is None:
|
||||
if auto_install==True:
|
||||
print(f"Installing {package}...")
|
||||
# 清华源 -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
command = f'"{python}" -m pip install {package}'
|
||||
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=os.environ)
|
||||
|
||||
is_has=True
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Couldn't install\nCommand: {command}\nError code: {result.returncode}")
|
||||
is_has=False
|
||||
else:
|
||||
print(package+'## OK')
|
||||
|
||||
return is_has
|
||||
|
||||
|
||||
try:
|
||||
if is_installed('fal_client','fal-client')==True:
|
||||
from fal_client import submit, upload_file
|
||||
except:
|
||||
print("#install fal-client error")
|
||||
|
||||
|
||||
def upload_image(image):
|
||||
try:
|
||||
# Convert the image tensor to a numpy array
|
||||
if isinstance(image, torch.Tensor):
|
||||
image_np = image.cpu().numpy()
|
||||
else:
|
||||
image_np = np.array(image)
|
||||
|
||||
# Ensure the image is in the correct format (H, W, C)
|
||||
if image_np.ndim == 4:
|
||||
image_np = image_np.squeeze(0) # Remove batch dimension if present
|
||||
if image_np.ndim == 2:
|
||||
image_np = np.stack([image_np] * 3, axis=-1) # Convert grayscale to RGB
|
||||
elif image_np.shape[0] == 3:
|
||||
image_np = np.transpose(image_np, (1, 2, 0)) # Change from (C, H, W) to (H, W, C)
|
||||
|
||||
# Normalize the image data to 0-255 range
|
||||
if image_np.dtype == np.float32 or image_np.dtype == np.float64:
|
||||
image_np = (image_np * 255).astype(np.uint8)
|
||||
|
||||
# Convert to PIL Image
|
||||
pil_image = Image.fromarray(image_np)
|
||||
|
||||
# Save the image to a temporary file
|
||||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
|
||||
pil_image.save(temp_file, format="PNG")
|
||||
temp_file_path = temp_file.name
|
||||
|
||||
# Upload the temporary file
|
||||
image_url = upload_file(temp_file_path)
|
||||
return image_url
|
||||
except Exception as e:
|
||||
print(f"Error uploading image: {str(e)}")
|
||||
return None
|
||||
finally:
|
||||
# Clean up the temporary file
|
||||
if 'temp_file_path' in locals():
|
||||
os.unlink(temp_file_path)
|
||||
|
||||
|
||||
class VideoGenKlingNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"duration": (["5", "10"], {"default": "5"}),
|
||||
"aspect_ratio": (["16:9", "9:16", "1:1"], {"default": "16:9"}),
|
||||
"mode": (["standard", "pro"], {"default": "standard"}),
|
||||
"fal_key":("STRING", {"forceInput": True,}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "generate_video"
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
|
||||
def generate_video(self, prompt, duration, aspect_ratio,mode,fal_key, image=None):
|
||||
arguments = {
|
||||
"prompt": prompt,
|
||||
"duration": duration,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
}
|
||||
|
||||
os.environ["FAL_KEY"] = fal_key
|
||||
|
||||
api_url="fal-ai/kling-video/v1/"+mode
|
||||
|
||||
try:
|
||||
if image is not None:
|
||||
image_url = upload_image(image)
|
||||
if image_url:
|
||||
arguments["image_url"] = image_url
|
||||
handler = submit(api_url+"/image-to-video", arguments=arguments)
|
||||
else:
|
||||
return ("Error: Unable to upload image.",)
|
||||
else:
|
||||
handler = submit(api_url+"/text-to-video", arguments=arguments)
|
||||
|
||||
result = handler.get()
|
||||
video_url = result["video"]["url"]
|
||||
return (video_url,)
|
||||
except Exception as e:
|
||||
print(f"Error generating video: {str(e)}")
|
||||
return ("Error: Unable to generate video.",)
|
||||
|
||||
|
||||
class VideoGenRunwayGen3Node:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"image": ("IMAGE",),
|
||||
"duration": (["5", "10"], {"default": "5"}),
|
||||
"aspect_ratio": (["16:9", "9:16"], {"default": "16:9"}),
|
||||
"fal_key":("STRING", {"forceInput": True,}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "generate_video"
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
|
||||
def generate_video(self, prompt, image, duration,aspect_ratio,fal_key):
|
||||
os.environ["FAL_KEY"] = fal_key
|
||||
try:
|
||||
image_url = upload_image(image)
|
||||
if not image_url:
|
||||
return ("Error: Unable to upload image.",)
|
||||
|
||||
arguments = {
|
||||
"prompt": prompt,
|
||||
"image_url": image_url,
|
||||
"duration": duration,
|
||||
"ratio":aspect_ratio
|
||||
}
|
||||
|
||||
handler = submit("fal-ai/runway-gen3/turbo/image-to-video", arguments=arguments)
|
||||
result = handler.get()
|
||||
video_url = result["video"]["url"]
|
||||
return (video_url,)
|
||||
except Exception as e:
|
||||
print(f"Error generating video: {str(e)}")
|
||||
return ("Error: Unable to generate video.",)
|
||||
|
||||
class VideoGenLumaDreamMachineNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"prompt": ("STRING", {"default": "", "multiline": True}),
|
||||
"aspect_ratio": (["16:9", "9:16", "4:3", "3:4", "21:9", "9:21"], {"default": "16:9"}),
|
||||
"fal_key":("STRING", {"forceInput": True,}),
|
||||
},
|
||||
"optional": {
|
||||
"image": ("IMAGE",),
|
||||
"loop": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
FUNCTION = "generate_video"
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
|
||||
def generate_video(self, prompt, aspect_ratio,fal_key, image=None, loop=True):
|
||||
|
||||
os.environ["FAL_KEY"] = fal_key
|
||||
|
||||
arguments = {
|
||||
"prompt": prompt,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
"loop": loop,
|
||||
}
|
||||
|
||||
try:
|
||||
if image is not None:
|
||||
image_url = upload_image(image)
|
||||
if not image_url:
|
||||
return ("Error: Unable to upload image.",)
|
||||
arguments["image_url"] = image_url
|
||||
endpoint = "fal-ai/luma-dream-machine/image-to-video"
|
||||
else:
|
||||
endpoint = "fal-ai/luma-dream-machine"
|
||||
|
||||
handler = submit(endpoint, arguments=arguments)
|
||||
result = handler.get()
|
||||
video_url = result["video"]["url"]
|
||||
return (video_url,)
|
||||
except Exception as e:
|
||||
print(f"Error generating video: {str(e)}")
|
||||
return ("Error: Unable to generate video.",)
|
||||
|
||||
class LoadVideoFromURL:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"url": ("STRING", {"default": "https://example.com/video.mp4"}),
|
||||
"force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1}),
|
||||
"force_size": (["Disabled", "Custom Height", "Custom Width", "Custom", "256x?", "?x256", "256x256", "512x?", "?x512", "512x512"],),
|
||||
"custom_width": ("INT", {"default": 512, "min": 0, "max": 8192, "step": 8}),
|
||||
"custom_height": ("INT", {"default": 512, "min": 0, "max": 8192, "step": 8}),
|
||||
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": 1000000, "step": 1}),
|
||||
"skip_first_frames": ("INT", {"default": 0, "min": 0, "max": 1000000, "step": 1}),
|
||||
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "INT", "VHS_VIDEOINFO")
|
||||
RETURN_NAMES = ("frames", "frame_count", "video_info")
|
||||
FUNCTION = "load_video_from_url"
|
||||
CATEGORY = "♾️Mixlab/Video"
|
||||
|
||||
def load_video_from_url(self, url, force_rate, force_size, custom_width, custom_height, frame_load_cap, skip_first_frames, select_every_nth):
|
||||
# Download the video to a temporary file
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_file:
|
||||
response = requests.get(url, stream=True)
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
temp_file.write(chunk)
|
||||
temp_file_path = temp_file.name
|
||||
|
||||
# Load the video using OpenCV
|
||||
cap = cv2.VideoCapture(temp_file_path)
|
||||
|
||||
# Get video properties
|
||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
duration = total_frames / fps
|
||||
|
||||
# Calculate target size
|
||||
if force_size != "Disabled":
|
||||
if force_size == "Custom Width":
|
||||
new_height = int(height * (custom_width / width))
|
||||
new_width = custom_width
|
||||
elif force_size == "Custom Height":
|
||||
new_width = int(width * (custom_height / height))
|
||||
new_height = custom_height
|
||||
elif force_size == "Custom":
|
||||
new_width, new_height = custom_width, custom_height
|
||||
else:
|
||||
target_width, target_height = map(int, force_size.replace("?", "0").split("x"))
|
||||
if target_width == 0:
|
||||
new_width = int(width * (target_height / height))
|
||||
new_height = target_height
|
||||
else:
|
||||
new_height = int(height * (target_width / width))
|
||||
new_width = target_width
|
||||
else:
|
||||
new_width, new_height = width, height
|
||||
|
||||
frames = []
|
||||
frame_count = 0
|
||||
|
||||
for i in range(total_frames):
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
if i < skip_first_frames:
|
||||
continue
|
||||
|
||||
if (i - skip_first_frames) % select_every_nth != 0:
|
||||
continue
|
||||
|
||||
if force_size != "Disabled":
|
||||
frame = cv2.resize(frame, (new_width, new_height))
|
||||
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
frame = torch.from_numpy(frame).float() / 255.0
|
||||
frames.append(frame)
|
||||
|
||||
frame_count += 1
|
||||
|
||||
if frame_load_cap > 0 and frame_count >= frame_load_cap:
|
||||
break
|
||||
|
||||
cap.release()
|
||||
os.unlink(temp_file_path)
|
||||
|
||||
frames = torch.stack(frames)
|
||||
|
||||
video_info = {
|
||||
"source_fps": fps,
|
||||
"source_frame_count": total_frames,
|
||||
"source_duration": duration,
|
||||
"source_width": width,
|
||||
"source_height": height,
|
||||
"loaded_fps": fps if force_rate == 0 else force_rate,
|
||||
"loaded_frame_count": frame_count,
|
||||
"loaded_duration": frame_count / (fps if force_rate == 0 else force_rate),
|
||||
"loaded_width": new_width,
|
||||
"loaded_height": new_height,
|
||||
}
|
||||
|
||||
return (frames, frame_count, video_info)
|
||||
|
||||
+22
-10
@@ -123,8 +123,13 @@ def composite_images(foreground, background, mask, is_multiply_blend=False, posi
|
||||
}
|
||||
|
||||
# Resize the foreground image with antialiasing
|
||||
layer_image = layer['image'].resize((layer['width'], layer['height']), Image.ANTIALIAS)
|
||||
layer_mask = layer['mask'].resize((layer['width'], layer['height']), Image.ANTIALIAS)
|
||||
try:
|
||||
resampling_method = Image.Resampling.LANCZOS
|
||||
except AttributeError:
|
||||
resampling_method = Image.ANTIALIAS
|
||||
|
||||
layer_image = layer['image'].resize((layer['width'], layer['height']), resampling_method)
|
||||
layer_mask = layer['mask'].resize((layer['width'], layer['height']), resampling_method)
|
||||
|
||||
bg_image.paste(layer_image, (layer['x'], layer['y']), layer_mask)
|
||||
|
||||
@@ -1041,29 +1046,33 @@ def generate_text_image(text,
|
||||
|
||||
if layout == "vertical":
|
||||
for line in lines:
|
||||
max_char_width = max(font.getsize(char)[0] for char in line)
|
||||
max_char_width = max(font.getbbox(char)[2] - font.getbbox(char)[0] for char in line)
|
||||
for char in line:
|
||||
char_width, char_height = font.getsize(char)
|
||||
left, top, right, bottom = font.getbbox(char)
|
||||
char_width = right - left
|
||||
char_height = bottom - top
|
||||
char_coordinates.append((x, y))
|
||||
y += char_height + spacing
|
||||
max_height = max(max_height, y + padding)
|
||||
x += max_char_width + line_spacing
|
||||
y = padding
|
||||
max_width = x
|
||||
total_line_width = sum(font.getsize(line)[1] for line in lines)
|
||||
total_line_width = sum(font.getbbox(line)[2] - font.getbbox(line)[0] for line in lines)
|
||||
total_spacing = line_spacing * (len(lines) - 1)
|
||||
max_width = total_line_width + total_spacing + padding * 2
|
||||
else:
|
||||
for line in lines:
|
||||
line_width, line_height = font.getsize(line)
|
||||
line_width, line_height = font.getbbox(line)[2] - font.getbbox(line)[0], font.getbbox(line)[3] - font.getbbox(line)[1]
|
||||
for char in line:
|
||||
char_width, char_height = font.getsize(char)
|
||||
left, top, right, bottom = font.getbbox(char)
|
||||
char_width = right - left
|
||||
char_height = bottom - top
|
||||
char_coordinates.append((x, y))
|
||||
x += char_width + spacing
|
||||
max_width = max(max_width, x + padding)
|
||||
y += line_height + line_spacing
|
||||
x = padding
|
||||
total_line_heights = sum(font.getsize(line)[1] for line in lines)
|
||||
total_line_heights = sum(font.getbbox(line)[3] - font.getbbox(line)[1] for line in lines)
|
||||
total_spacing = line_spacing * (len(lines) - 1)
|
||||
max_height = total_line_heights + total_spacing + padding * 2
|
||||
|
||||
@@ -2951,11 +2960,14 @@ class ResizeImage:
|
||||
im=tensor2pil(im)
|
||||
|
||||
im=im.convert('RGB')
|
||||
a_im,hex=get_average_color_image(im)
|
||||
|
||||
a_im,hex=get_average_color_image(im)
|
||||
|
||||
if average_color=='on':
|
||||
fill_color=hex
|
||||
|
||||
|
||||
a_im=resize_image(a_im,scale_option,w,h,fill_color)
|
||||
|
||||
im=resize_image(im,scale_option,w,h,fill_color)
|
||||
|
||||
im=pil2tensor(im)
|
||||
|
||||
+1
-1
@@ -234,7 +234,7 @@ class StyleAlignedSampleReferenceLatents:
|
||||
"steps": ("INT", {"default": 20, "min": 1, "max": 10000}),
|
||||
"cfg": ("FLOAT", {"default": 8.0, "min": 0.0, "max": 100.0, "step":0.1, "round": 0.01}),
|
||||
|
||||
"scheduler": (comfy.samplers.KSampler.SCHEDULERS.reverse(), ),
|
||||
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, ),
|
||||
"denoise": ("FLOAT", {"default": 1, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
|
||||
}
|
||||
|
||||
+6
-4
@@ -7,6 +7,7 @@ import os
|
||||
import folder_paths
|
||||
import node_helpers
|
||||
import hashlib
|
||||
from uuid import uuid4
|
||||
|
||||
# Tensor to PIL
|
||||
def tensor2pil(image):
|
||||
@@ -26,7 +27,7 @@ def tensor_to_hash(tensor):
|
||||
return hash_value
|
||||
|
||||
|
||||
def create_temp_file(image):
|
||||
def create_temp_file(image, uuid):
|
||||
output_dir = folder_paths.get_temp_directory()
|
||||
|
||||
(
|
||||
@@ -35,7 +36,7 @@ def create_temp_file(image):
|
||||
counter,
|
||||
subfolder,
|
||||
_,
|
||||
) = folder_paths.get_save_image_path('material', output_dir)
|
||||
) = folder_paths.get_save_image_path(f'material_{uuid}', output_dir)
|
||||
|
||||
|
||||
image=tensor2pil(image)
|
||||
@@ -59,6 +60,7 @@ class EditMask:
|
||||
|
||||
def __init__(self):
|
||||
self.image_id = None
|
||||
self.uuid = str(uuid4())
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
@@ -117,13 +119,13 @@ class EditMask:
|
||||
image_path = os.path.join(base_dir,subfolder, name)
|
||||
|
||||
if image_path==None:
|
||||
image_path,images=create_temp_file(image)
|
||||
image_path,images=create_temp_file(image, self.uuid)
|
||||
|
||||
print('#image_path',os.path.exists(image_path),image_path)
|
||||
# image_path = folder_paths.get_annotated_filepath(image) #文件名
|
||||
|
||||
if not os.path.exists(image_path):
|
||||
image_path,images=create_temp_file(image)
|
||||
image_path,images=create_temp_file(image, self.uuid)
|
||||
|
||||
|
||||
img = node_helpers.pillow(Image.open, image_path)
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-mixlab-nodes"
|
||||
description = "3D, ScreenShareNode & FloatingVideoNode, SpeechRecognition & SpeechSynthesis, GPT, LoadImagesFromLocal, Layers, Other Nodes, ..."
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
license = "MIT"
|
||||
dependencies = ["numpy", "pyOpenSSL", "watchdog", "opencv-python-headless", "matplotlib", "openai", "simple-lama-inpainting", "clip-interrogator==0.6.0", "transformers>=4.36.0", "lark-parser", "imageio-ffmpeg", "rembg[gpu]", "omegaconf==2.3.0", "Pillow>=9.5.0", "einops==0.7.0", "trimesh>=4.0.5", "huggingface-hub", "scikit-image"]
|
||||
|
||||
|
||||
@@ -267,7 +267,13 @@ function downloadJsonFile (jsonData, fileName = 'mix_app.json') {
|
||||
}
|
||||
|
||||
async function save (json, download = false, showInfo = true) {
|
||||
let nodesAll = window._nodesAll || (await getObjectInfo())
|
||||
if (!window._nodesAll) {
|
||||
window._nodesAll = await getObjectInfo();
|
||||
}
|
||||
|
||||
let nodesAll = window._nodesAll;
|
||||
|
||||
// let nodesAll = window._nodesAll || (await getObjectInfo())
|
||||
|
||||
console.log('####SAVE', nodesAll, json)
|
||||
|
||||
@@ -417,9 +423,9 @@ function getInputsAndOutputs () {
|
||||
app.registerExtension({
|
||||
name: 'Mixlab.utils.AppInfo',
|
||||
init () {
|
||||
if (!window._nodesAll) {
|
||||
getObjectInfo().then(r => (window._nodesAll = r))
|
||||
}
|
||||
// if (!window._nodesAll) {
|
||||
// getObjectInfo().then(r => (window._nodesAll = r))
|
||||
// }
|
||||
},
|
||||
async beforeRegisterNodeDef (nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass == 'AppInfo') {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { app } from '../../../scripts/app.js'
|
||||
const repoOwner = 'shadowcz007' // 替换为仓库的所有者
|
||||
const repoName = 'comfyui-mixlab-nodes' // 替换为仓库的名称
|
||||
|
||||
const version = 'v0.45.0'
|
||||
const version = 'v0.46.0'
|
||||
|
||||
fetch(`https://api.github.com/repos/${repoOwner}/${repoName}/releases/latest`)
|
||||
.then(response => response.json())
|
||||
|
||||
@@ -3,7 +3,7 @@ import { api } from '../../../scripts/api.js'
|
||||
import { ComfyWidgets } from '../../../scripts/widgets.js'
|
||||
import { $el } from '../../../scripts/ui.js'
|
||||
|
||||
import WaveSurfer from 'https://cdn.jsdelivr.net/npm/wavesurfer.js@7/dist/wavesurfer.esm.js'
|
||||
import WaveSurfer from './wavesurfer.esm.js'
|
||||
|
||||
function get_position_style (ctx, widget_width, y, node_height) {
|
||||
const MARGIN = 4 // the margin around the html element
|
||||
|
||||
@@ -1735,14 +1735,16 @@ app.registerExtension({
|
||||
|
||||
// 把json往里 拖
|
||||
document.addEventListener('drop', async event => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
// Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that
|
||||
// Only intercept the JSON files handled here. Calling preventDefault()
|
||||
// unconditionally swallowed every drop, so ComfyUI's native drag&drop
|
||||
// (guarded by event.defaultPrevented) never loaded dropped workflows
|
||||
// (PNG / JSON / etc.). Keep preventDefault scoped to the handled case.
|
||||
if (
|
||||
event.dataTransfer.files.length &&
|
||||
event.dataTransfer.files[0].type == 'application/json'
|
||||
) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const reader = new FileReader()
|
||||
reader.onload = async () => {
|
||||
loadAppJson(reader.result)
|
||||
@@ -2171,6 +2173,10 @@ app.registerExtension({
|
||||
|
||||
fetch('manager/badge_mode').then(r => {
|
||||
if (r.status === 404) {
|
||||
// 已有ComfyUI自带的badge
|
||||
if(node.badges?.[0]?.()){
|
||||
return
|
||||
}
|
||||
// 右上角的badge是否已经绘制
|
||||
if (!node.badge_enabled) {
|
||||
if (!node.getNickname) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,775 @@
|
||||
{
|
||||
"last_node_id": 18,
|
||||
"last_link_id": 15,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 6,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
-32,
|
||||
79
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 314
|
||||
},
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"1.jpg",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"type": "KeyInput",
|
||||
"pos": [
|
||||
-34,
|
||||
585
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 94
|
||||
},
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"outputs": [
|
||||
{
|
||||
"name": "key",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
4,
|
||||
5,
|
||||
6
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "KeyInput"
|
||||
},
|
||||
"widgets_values": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"type": "LoadVideoFromURL",
|
||||
"pos": [
|
||||
1038,
|
||||
21
|
||||
],
|
||||
"size": [
|
||||
315,
|
||||
266
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "url",
|
||||
"type": "STRING",
|
||||
"link": 7,
|
||||
"widget": {
|
||||
"name": "url"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "frames",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
8
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadVideoFromURL"
|
||||
},
|
||||
"widgets_values": [
|
||||
"https://example.com/video.mp4",
|
||||
0,
|
||||
"Disabled",
|
||||
512,
|
||||
512,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "LoadVideoFromURL",
|
||||
"pos": [
|
||||
1032,
|
||||
715
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 266
|
||||
},
|
||||
"flags": {},
|
||||
"order": 9,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "url",
|
||||
"type": "STRING",
|
||||
"link": 10,
|
||||
"widget": {
|
||||
"name": "url"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "frames",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
12
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadVideoFromURL"
|
||||
},
|
||||
"widgets_values": [
|
||||
"https://example.com/video.mp4",
|
||||
0,
|
||||
"Disabled",
|
||||
512,
|
||||
512,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1436,
|
||||
6
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 11,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 8
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "VideoGenKlingNode",
|
||||
"pos": [
|
||||
527.4816383216086,
|
||||
19.100152539671797
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 200
|
||||
},
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 1
|
||||
},
|
||||
{
|
||||
"name": "fal_key",
|
||||
"type": "STRING",
|
||||
"link": 4,
|
||||
"widget": {
|
||||
"name": "fal_key"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
7,
|
||||
13
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VideoGenKlingNode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"The man is shaking his head with a wry smile.\n\n",
|
||||
"5",
|
||||
"16:9",
|
||||
"standard",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "ShowTextForGPT",
|
||||
"pos": [
|
||||
1702,
|
||||
-85
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 13,
|
||||
"widget": {
|
||||
"name": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "output_dir",
|
||||
"type": "STRING",
|
||||
"link": null,
|
||||
"widget": {
|
||||
"name": "output_dir"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": null,
|
||||
"shape": 6
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ShowTextForGPT"
|
||||
},
|
||||
"widgets_values": [
|
||||
"",
|
||||
"",
|
||||
"https://v2.fal.media/files/0f9f44093c7442d5b0819616880bca65_output.mp4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "VideoGenRunwayGen3Node",
|
||||
"pos": [
|
||||
529,
|
||||
297
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 200
|
||||
},
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 2
|
||||
},
|
||||
{
|
||||
"name": "fal_key",
|
||||
"type": "STRING",
|
||||
"link": 5,
|
||||
"widget": {
|
||||
"name": "fal_key"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
9,
|
||||
14
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VideoGenRunwayGen3Node"
|
||||
},
|
||||
"widgets_values": [
|
||||
"The man is shaking his head with a wry smile.\n\n",
|
||||
"5",
|
||||
"16:9",
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "VideoGenLumaDreamMachineNode",
|
||||
"pos": [
|
||||
534,
|
||||
570
|
||||
],
|
||||
"size": {
|
||||
"0": 400,
|
||||
"1": 200
|
||||
},
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"name": "fal_key",
|
||||
"type": "STRING",
|
||||
"link": 6,
|
||||
"widget": {
|
||||
"name": "fal_key"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
10,
|
||||
15
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "VideoGenLumaDreamMachineNode"
|
||||
},
|
||||
"widgets_values": [
|
||||
"The man is shaking his head with a wry smile.\n\n",
|
||||
"16:9",
|
||||
"",
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"type": "ShowTextForGPT",
|
||||
"pos": [
|
||||
1719,
|
||||
665
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 10,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 15,
|
||||
"widget": {
|
||||
"name": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "output_dir",
|
||||
"type": "STRING",
|
||||
"link": null,
|
||||
"widget": {
|
||||
"name": "output_dir"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": null,
|
||||
"shape": 6
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ShowTextForGPT"
|
||||
},
|
||||
"widgets_values": [
|
||||
"",
|
||||
"",
|
||||
"https://v2.fal.media/files/a622a5aac002452ba0e75f7d8871389d_output.mp4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"type": "LoadVideoFromURL",
|
||||
"pos": [
|
||||
1049,
|
||||
349
|
||||
],
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 266
|
||||
},
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "url",
|
||||
"type": "STRING",
|
||||
"link": 9,
|
||||
"widget": {
|
||||
"name": "url"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "frames",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
11
|
||||
],
|
||||
"shape": 3,
|
||||
"slot_index": 0
|
||||
},
|
||||
{
|
||||
"name": "frame_count",
|
||||
"type": "INT",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
},
|
||||
{
|
||||
"name": "video_info",
|
||||
"type": "VHS_VIDEOINFO",
|
||||
"links": null,
|
||||
"shape": 3
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "LoadVideoFromURL"
|
||||
},
|
||||
"widgets_values": [
|
||||
"https://example.com/video.mp4",
|
||||
0,
|
||||
"Disabled",
|
||||
512,
|
||||
512,
|
||||
0,
|
||||
0,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"type": "ShowTextForGPT",
|
||||
"pos": [
|
||||
1711,
|
||||
318
|
||||
],
|
||||
"size": [
|
||||
400,
|
||||
200
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "text",
|
||||
"type": "STRING",
|
||||
"link": 14,
|
||||
"widget": {
|
||||
"name": "text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "output_dir",
|
||||
"type": "STRING",
|
||||
"link": null,
|
||||
"widget": {
|
||||
"name": "output_dir"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": null,
|
||||
"shape": 6
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ShowTextForGPT"
|
||||
},
|
||||
"widgets_values": [
|
||||
"",
|
||||
"",
|
||||
"https://v2.fal.media/files/755d51c8984a4445a852268a209b07e4_output.mp4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1444,
|
||||
650
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 13,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 12
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1416,
|
||||
310
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 12,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 11
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
1,
|
||||
6,
|
||||
0,
|
||||
3,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
2,
|
||||
6,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
3,
|
||||
6,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
4,
|
||||
7,
|
||||
0,
|
||||
3,
|
||||
1,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
5,
|
||||
7,
|
||||
0,
|
||||
4,
|
||||
1,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
6,
|
||||
7,
|
||||
0,
|
||||
5,
|
||||
1,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
7,
|
||||
3,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
8,
|
||||
10,
|
||||
0,
|
||||
11,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
9,
|
||||
4,
|
||||
0,
|
||||
12,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
10,
|
||||
5,
|
||||
0,
|
||||
13,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
11,
|
||||
12,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
12,
|
||||
13,
|
||||
0,
|
||||
15,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
13,
|
||||
3,
|
||||
0,
|
||||
16,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
14,
|
||||
4,
|
||||
0,
|
||||
17,
|
||||
0,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
15,
|
||||
5,
|
||||
0,
|
||||
18,
|
||||
0,
|
||||
"STRING"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.7247295000000012,
|
||||
"offset": [
|
||||
-59.526177135875514,
|
||||
197.10624904779425
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
Reference in New Issue
Block a user