Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c8c158b
Refactor SearchBar component for improved readability and structure
Nynxz Dec 30, 2025
eaf4a07
Refactor SettingsForm component for improved structure and readability
Nynxz Dec 30, 2025
de22357
Add ImageDialogProvider to EmbeddrPanel rendering in sidebar
Nynxz Dec 30, 2025
d4fb4c7
feat: enhance ImageGrid component with right-click functionality and …
Nynxz Dec 30, 2025
7b1f214
prettier lint
Nynxz Dec 30, 2025
b85f911
feat: implement ExploreTab and SettingsForm components; refactor Embe…
Nynxz Dec 31, 2025
9f730f5
feat: add ExternalNavProvider to EmbeddrPanel rendering; update sideb…
Nynxz Dec 31, 2025
cbc8e39
feat: add shadcn dependency and update globals.css for select compone…
Nynxz Dec 31, 2025
9c56e1f
lint
Nynxz Dec 31, 2025
ccaef06
feat: add GlobalDialog component and integrate image selection functi…
Nynxz Jan 4, 2026
9854d78
feat: integrate GlobalDialog component and enhance Embeddr.LoadImage …
Nynxz Jan 4, 2026
21eb371
feat: add EmbeddrFindSimilarNode and EmbeddrFindSimilarTextNode for i…
Nynxz Jan 4, 2026
f544618
feat: update EmbeddrLoadImageNode to load images by Embeddr Image ID …
Nynxz Jan 4, 2026
0f76222
feat: add EmbeddrMergeIDsNode for merging multiple Embeddr Image IDs …
Nynxz Jan 4, 2026
d73d3f5
feat: add EmbeddrUploadVideo node for video uploading with support fo…
Nynxz Jan 4, 2026
e20cb9c
feat: add new utility functions for fetching libraries and collection…
Nynxz Jan 4, 2026
261327f
chore: update tsconfig.json to exclude test files from compilation
Nynxz Jan 4, 2026
c2d4c98
feat: update version of embeddr-extension to 1.2.0 in pyproject.toml …
Nynxz Jan 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 24 additions & 24 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
name: CI

on:
push:
branches:
- dev
pull_request:
branches:
- main
push:
branches:
- dev
pull_request:
branches:
- main

jobs:
build:
name: Build Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
build:
name: Build Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 9

- name: Install dependencies
run: pnpm install
- name: Install dependencies
run: pnpm install

- name: Build frontend
run: pnpm build
- name: Build frontend
run: pnpm build
328 changes: 164 additions & 164 deletions LICENSE.md

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

from .nodes.EmbeddrUploadImage import EmbeddrSaveToFolderNode
from .nodes.EmbeddrLoadImage import EmbeddrLoadImageNode
from .nodes.EmbeddrLoadImages import EmbeddrLoadImagesNode
from .nodes.EmbeddrMergeIDs import EmbeddrMergeIDsNode
from .nodes.EmbeddrFindSimilar import EmbeddrFindSimilarNode
from .nodes.EmbeddrFindSimilarText import EmbeddrFindSimilarTextNode
from .nodes.EmbeddrUploadVideo import EmbeddrUploadVideo

CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")

Expand Down Expand Up @@ -81,8 +86,13 @@ async def get_config(request):
class EmbeddrComfyUIExtension(ComfyExtension):
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [
EmbeddrSaveToFolderNode,
EmbeddrFindSimilarNode,
EmbeddrFindSimilarTextNode,
EmbeddrLoadImageNode,
EmbeddrLoadImagesNode,
EmbeddrMergeIDsNode,
EmbeddrSaveToFolderNode,
EmbeddrUploadVideo,
]


Expand Down
111 changes: 111 additions & 0 deletions nodes/EmbeddrFindSimilar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import requests
import torch
import numpy as np
from PIL import Image
import io as pyio
from comfy_api.latest import io, ui
from .utils import get_config
from .utils.api import get_libraries, get_collections


class EmbeddrFindSimilarNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
# Fetch dynamic options
libraries = ["All"] + get_libraries()
collections = ["All"] + get_collections()

return io.Schema(
node_id="embeddr.FindSimilar",
display_name="Embeddr Find Similar",
description="Finds similar images in your Embeddr library using an input image.",
category="Embeddr",
inputs=[
io.Image.Input("image"),
io.Combo.Input("library", options=libraries, default="All"),
io.Combo.Input(
"collection", options=collections, default="All"),
io.Int.Input("limit", default=5, min=1, max=50),
io.Float.Input("threshold", default=0.0, min=0.0,
max=1.0, step=0.01, display_name="Min Score"),
],
outputs=[
io.Image.Output("images", is_output_list=True),
io.String.Output("embeddr_ids", is_output_list=True),
],
)

@classmethod
def execute(cls, image, library="All", collection="All", limit=5, threshold=0.0):
config = get_config()
endpoint = config.get("endpoint", "http://localhost:8003")
api_url = endpoint.rstrip("/") + "/api/v1/images/search/image"

# Prepare image (take first of batch)
img_array = (image[0].cpu().numpy() * 255).astype(np.uint8)
img = Image.fromarray(np.clip(img_array, 0, 255))

buf = pyio.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)

files = {"file": ("image.png", buf, "image/png")}
data = {
"limit": limit,
}

# Parse IDs from "ID: Name" format
if library != "All":
try:
lib_id = int(library.split(":")[0])
data["library_id"] = lib_id
except:
pass

if collection != "All":
try:
col_id = int(collection.split(":")[0])
data["collection_id"] = col_id
except:
pass

try:
response = requests.post(api_url, files=files, data=data)
response.raise_for_status()
results = response.json()
items = results.get("items", [])

if not items:
# Return empty
empty = torch.zeros(
(1, 64, 64, 3), dtype=torch.float32, device="cpu")
return io.NodeOutput(empty, "[]")

# Load images
output_images = []
output_ids = []

for item in items:
# Fetch image file
img_url = endpoint.rstrip(
"/") + f"/api/v1/images/{item['id']}/file"
img_resp = requests.get(img_url)
if img_resp.status_code == 200:
i = Image.open(pyio.BytesIO(img_resp.content))
i = i.convert("RGB")
i = np.array(i).astype(np.float32) / 255.0
output_images.append(torch.from_numpy(i))
output_ids.append(str(item['id']))

if not output_images:
return io.NodeOutput([], [])

# Return list of images (unsqueeze to add batch dimension 1)
final_images = [img.unsqueeze(0) for img in output_images]
return io.NodeOutput(final_images, output_ids)

except Exception as e:
print(f"[Embeddr] Search failed: {e}")
empty = torch.zeros(
(1, 64, 64, 3), dtype=torch.float32, device="cpu")
return io.NodeOutput(empty, "[]")
105 changes: 105 additions & 0 deletions nodes/EmbeddrFindSimilarText.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import requests
import torch
import numpy as np
from PIL import Image, ImageOps
import io as pyio
from comfy_api.latest import io, ui
from .utils import get_config
from .utils.api import get_libraries, get_collections


class EmbeddrFindSimilarTextNode(io.ComfyNode):
@classmethod
def define_schema(cls) -> io.Schema:
# Fetch dynamic options
libraries = ["All"] + get_libraries()
collections = ["All"] + get_collections()

return io.Schema(
node_id="embeddr.FindSimilarText",
display_name="Embeddr Find Similar (Text)",
description="Finds similar images in your Embeddr library using a text prompt.",
category="Embeddr",
inputs=[
io.String.Input("prompt", multiline=True),
io.Combo.Input("library", options=libraries, default="All"),
io.Combo.Input(
"collection", options=collections, default="All"),
io.Int.Input("limit", default=5, min=1, max=50),
],
outputs=[
io.Image.Output("images", is_output_list=True),
io.String.Output("embeddr_ids", is_output_list=True),
],
)

@classmethod
def execute(cls, prompt, library="All", collection="All", limit=5):
config = get_config()
endpoint = config.get("endpoint", "http://localhost:8003")
api_url = endpoint.rstrip("/") + "/api/v1/images"

params = {
"q": prompt,
"limit": limit,
}

# Parse IDs from "ID: Name" format
if library != "All":
try:
lib_id = int(library.split(":")[0])
params["library_id"] = lib_id
except:
pass

if collection != "All":
try:
col_id = int(collection.split(":")[0])
params["collection_id"] = col_id
except:
pass

try:
response = requests.get(api_url, params=params)
response.raise_for_status()
results = response.json()
items = results.get("items", [])

if not items:
# Return empty
empty = torch.zeros(
(1, 64, 64, 3), dtype=torch.float32, device="cpu")
return io.NodeOutput(empty, "")

# Load images
output_images = []
output_ids = []

first_width = 0
first_height = 0

for item in items:
# Fetch image file
img_url = endpoint.rstrip(
"/") + f"/api/v1/images/{item['id']}/file"
img_resp = requests.get(img_url)
if img_resp.status_code == 200:
i = Image.open(pyio.BytesIO(img_resp.content))
i = ImageOps.exif_transpose(i)

i = i.convert("RGB")
i = np.array(i).astype(np.float32) / 255.0
# Add batch dimension (1, H, W, C)
output_images.append(torch.from_numpy(i).unsqueeze(0))
output_ids.append(str(item['id']))

if not output_images:
return io.NodeOutput([], [])

return io.NodeOutput(output_images, output_ids)

except Exception as e:
print(f"[Embeddr] Find Similar Text error: {e}")
empty = torch.zeros(
(1, 64, 64, 3), dtype=torch.float32, device="cpu")
return io.NodeOutput(empty, "")
36 changes: 22 additions & 14 deletions nodes/EmbeddrLoadImage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from PIL import Image, ImageOps
from io import BytesIO
from comfy_api.latest import io, ui
from .utils import get_config


class EmbeddrLoadImageNode(io.ComfyNode):
Expand All @@ -14,33 +15,40 @@ def define_schema(cls) -> io.Schema:
return io.Schema(
node_id="embeddr.LoadImage",
display_name="Embeddr Load Image",
description="Loads an image from a URL (Embeddr).",
description="Loads an image from a Embeddr Image ID.",
category="Embeddr",
inputs=[
io.String.Input("image_url", default=""),
io.String.Input("image_id", default=""),
],
outputs=[
io.Image.Output("image"),
io.Mask.Output("mask"),
io.String.Output("embeddr_id"),
],
)

@classmethod
def execute(cls, image_url):
if not image_url:
# Return empty black image if no URL
def execute(cls, image_id):
if not image_id:
# Return empty black image if no ID
empty_image = torch.zeros(
(1, 64, 64, 3), dtype=torch.float32, device="cpu")
empty_mask = torch.zeros(
(1, 64, 64), dtype=torch.float32, device="cpu")
return io.NodeOutput(empty_image, empty_mask)
return io.NodeOutput(empty_image, empty_mask, "")

if image_url in cls._cache:
image, mask = cls._cache[image_url]
return io.NodeOutput(image, mask)
if image_id in cls._cache:
image, mask = cls._cache[image_id]
return io.NodeOutput(image, mask, image_id)

try:
response = requests.get(image_url)
config = get_config()
endpoint = config.get("endpoint", "http://localhost:8003")
# Ensure endpoint doesn't end with slash
endpoint = endpoint.rstrip("/")
api_url = f"{endpoint}/api/v1/images/{image_id}/file"

response = requests.get(api_url)
response.raise_for_status()
img = Image.open(BytesIO(response.content))

Expand All @@ -55,13 +63,13 @@ def execute(cls, image_url):
else:
mask = torch.zeros((64, 64), dtype=torch.float32, device="cpu")

cls._cache[image_url] = (image, mask)
return io.NodeOutput(image, mask, ui=ui.PreviewImage(image))
cls._cache[image_id] = (image, mask)
return io.NodeOutput(image, mask, image_id, ui=ui.PreviewImage(image))

except Exception as e:
print(f"[Embeddr] Error loading image: {e}")
print(f"[Embeddr] Error loading image {image_id}: {e}")
empty_image = torch.zeros(
(1, 64, 64, 3), dtype=torch.float32, device="cpu")
empty_mask = torch.zeros(
(1, 64, 64), dtype=torch.float32, device="cpu")
return io.NodeOutput(empty_image, empty_mask)
return io.NodeOutput(empty_image, empty_mask, "")
Loading