V 2.0.0 - Universal api #66 - VEO
This commit is contained in:
+47
-13
@@ -24,6 +24,7 @@ from ..components.API import api_json_to_requestbody
|
||||
from ..components.API import external_api_backend
|
||||
from ..components.API import api_schema_registry
|
||||
from ..components import file_output
|
||||
from server import PromptServer
|
||||
|
||||
class PrimereApiProcessor:
|
||||
CATEGORY = TREE_API
|
||||
@@ -75,12 +76,13 @@ class PrimereApiProcessor:
|
||||
|
||||
hidden_inputs = {
|
||||
"extra_pnginfo": "EXTRA_PNGINFO",
|
||||
"prompt_extra": "PROMPT"
|
||||
"prompt_extra": "PROMPT",
|
||||
"unique_id": "UNIQUE_ID",
|
||||
}
|
||||
|
||||
return {"required": cls.required_inputs, "optional": cls.optional_inputs, "hidden": hidden_inputs}
|
||||
|
||||
def process_uniapi(self, processor, api_provider, api_service, prompt, negative_prompt = None, batch = 1, reference_images = None, first_image = None, last_image = None, width = 1024, height = 1024, aspect_ratio = '1:1', seed = None, debug_mode = False, **kwargs):
|
||||
def process_uniapi(self, processor, api_provider, api_service, prompt, negative_prompt = None, batch = 1, reference_images = None, first_image = None, last_image = None, width = 1024, height = 1024, aspect_ratio = '1:1', seed = None, debug_mode = False, unique_id = None, **kwargs):
|
||||
API_SCHEMAS_PATH = os.path.join(PRIMERE_ROOT, 'front_end', 'api_schemas.json')
|
||||
API_CONFIG_PATH = os.path.join(PRIMERE_ROOT, 'json', 'apiconfig.json')
|
||||
API_SCHEMA_REGISTRY = api_schema_registry.load_and_validate_api_schema_registry(API_SCHEMAS_PATH, API_CONFIG_PATH)
|
||||
@@ -188,6 +190,20 @@ class PrimereApiProcessor:
|
||||
elif img_binary_api not in (None, "") and (not isinstance(img_binary_api, list) or len(img_binary_api) > 0):
|
||||
selected_parameters["reference_images"] = img_binary_api
|
||||
|
||||
if first_image is not None:
|
||||
first_image_source = first_image
|
||||
elif reference_images is not None:
|
||||
first_image_source = reference_images[0] if isinstance(reference_images, list) else reference_images
|
||||
else:
|
||||
first_image_source = None
|
||||
if first_image_source is not None:
|
||||
first_image_result = external_api_backend.apply_reference_images_handler(
|
||||
schema, api_provider,
|
||||
{"img_binary_api": first_image_source, "loaded_client_for_upload": loaded_client_for_upload}
|
||||
)
|
||||
if isinstance(first_image_result, dict) and first_image_result:
|
||||
selected_parameters.update(first_image_result)
|
||||
|
||||
possible_parameters = schema.get("possible_parameters", {}) if isinstance(schema, dict) else {}
|
||||
if isinstance(possible_parameters, dict):
|
||||
for key in possible_parameters.keys():
|
||||
@@ -306,6 +322,11 @@ class PrimereApiProcessor:
|
||||
# --- File naming and output path resolution ---
|
||||
auto_save_result = kwargs.get('auto_save_result', False)
|
||||
if auto_save_result and result_image is not None:
|
||||
if type(result_image).__name__ == "str":
|
||||
SAVED_IMAGE_PATH = os.path.join(PRIMERE_ROOT, 'front_end', 'images')
|
||||
SAVED_IMAGE = os.path.join(SAVED_IMAGE_PATH, "file_saved.jpg")
|
||||
result_image = utility.ImageLoaderFromPath(SAVED_IMAGE)
|
||||
|
||||
output_path_input = kwargs.get('output_path', '[time(%Y-%m-%d)]')
|
||||
subpath = kwargs.get('subpath', 'None')
|
||||
add_provider_to_path = kwargs.get('add_provider_to_path', False)
|
||||
@@ -350,17 +371,30 @@ class PrimereApiProcessor:
|
||||
)
|
||||
|
||||
Path(folder_paths.temp_directory).mkdir(parents=True, exist_ok=True)
|
||||
file_output.save_bytes_to_file(save_bytes, output_file, image_extension, image_quality, folder_paths.temp_directory)
|
||||
try:
|
||||
saved_path = file_output.save_bytes_to_file(save_bytes, output_file, image_extension, image_quality, folder_paths.temp_directory)
|
||||
|
||||
save_data = {
|
||||
"provider": api_provider,
|
||||
"service": selected_service or api_service,
|
||||
"selected_parameters": selected_parameters_output,
|
||||
"used_values": used_values_output,
|
||||
# "rendered": rendered_payload,
|
||||
"raw_payload": raw_payload,
|
||||
# "api_result": api_result_debug,
|
||||
}
|
||||
file_output.save_metadata(save_data, json_file, txt_file, save_data_to_json, save_data_to_txt, used_values_output)
|
||||
save_data = {
|
||||
"provider": api_provider,
|
||||
"service": selected_service or api_service,
|
||||
"selected_parameters": selected_parameters_output,
|
||||
"used_values": used_values_output,
|
||||
# "rendered": rendered_payload,
|
||||
"raw_payload": raw_payload,
|
||||
# "api_result": api_result_debug,
|
||||
}
|
||||
file_output.save_metadata(save_data, json_file, txt_file, save_data_to_json, save_data_to_txt, used_values_output)
|
||||
|
||||
PromptServer.instance.send_sync("primere.save_result", {
|
||||
"node_id": unique_id,
|
||||
"status": "success",
|
||||
"path": saved_path,
|
||||
})
|
||||
except Exception as save_error:
|
||||
PromptServer.instance.send_sync("primere.save_result", {
|
||||
"node_id": unique_id,
|
||||
"status": "failed",
|
||||
"error": str(save_error),
|
||||
})
|
||||
|
||||
return (result_image, client, api_provider, schema, rendered_payload, raw_payload, used_values_output, api_schemas, api_result_debug)
|
||||
File diff suppressed because one or more lines are too long
@@ -811,7 +811,7 @@ File saving only runs when `auto_save_result` is ON and the API returned a valid
|
||||
|
||||
| Input | Default | Description |
|
||||
|---|---|---|
|
||||
| `output_path` | `[time(%Y-%m-%d)]` | Base output directory. Supports `[time(...)]` tokens. Relative paths are anchored to the ComfyUI output directory. Absolute paths are used as-is. |
|
||||
| `output_path` | `[time(%Y-%m-%d)]` | Base output directory. Supports `[time(...)]` tokens. Relative paths are anchored to the ComfyUI output directory. Absolute paths are used as-is — e.g. `e:\!Works\AI\AIPICS\ComfyAPI\[time(%Y-%m-Week-%W)]` saves outside ComfyUI to any location on disk. |
|
||||
| `subpath` | `Project` | Fixed subdirectory appended after provider/service/model dirs. Select `None` to skip. |
|
||||
| `add_provider_to_path` | OFF | Adds the API provider name as a subdirectory (e.g. `Gemini`). |
|
||||
| `add_service_to_path` | OFF | Adds the selected service name as a subdirectory (e.g. `Imagen`). |
|
||||
@@ -855,6 +855,9 @@ The actual file type is detected from the API response bytes (MIME detection), n
|
||||
| `text/*` | `.txt` (UTF-8) |
|
||||
| other / unknown | extension from `image_extension` |
|
||||
|
||||
> **Important — non-image results have no display or player.**
|
||||
> Video, audio, and text results are saved to disk only. There is no preview, no image output, and no playback inside ComfyUI. When the file is saved successfully, a toast notification appears on the node with the saved file path. If `auto_save_result` is **OFF**, the result is permanently lost — it exists only in memory for the duration of that execution and is not recoverable afterward.
|
||||
|
||||
### Metadata files
|
||||
|
||||
| Input | Default | Description |
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from typing import Any
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from google.genai import types
|
||||
|
||||
|
||||
def handle_reference_images(img_binary_api: Any = None, temp_file_ref: str = "", loaded_client_for_upload: Any = None, **_: Any):
|
||||
if img_binary_api is None:
|
||||
return {}
|
||||
if type(img_binary_api).__name__ == "Tensor":
|
||||
img_array = (img_binary_api[0].cpu().numpy() * 255).astype(np.uint8)
|
||||
pil_image = Image.fromarray(img_array)
|
||||
buffer = io.BytesIO()
|
||||
pil_image.save(buffer, format="PNG")
|
||||
image_bytes = buffer.getvalue()
|
||||
return {"first_image": types.Image(image_bytes=image_bytes, mime_type="image/png")}
|
||||
return {}
|
||||
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
import os
|
||||
import random
|
||||
import folder_paths
|
||||
|
||||
|
||||
def handle_response(api_result, schema=None, loaded_client=None, response_url=None, client=None, sdk_context=None):
|
||||
if api_result is None:
|
||||
return [None, None]
|
||||
|
||||
operation = api_result
|
||||
while not operation.done:
|
||||
time.sleep(10)
|
||||
operation = client.operations.get(operation)
|
||||
|
||||
generated_videos = getattr(getattr(operation, 'response', None), 'generated_videos', None)
|
||||
if not generated_videos:
|
||||
return [None, None]
|
||||
|
||||
generated_video = generated_videos[0]
|
||||
tmp_path = os.path.join(folder_paths.temp_directory, f"veo_{random.randint(10000, 99999)}.mp4")
|
||||
client.files.download(file=generated_video.video)
|
||||
generated_video.video.save(tmp_path)
|
||||
|
||||
with open(tmp_path, 'rb') as f:
|
||||
video_bytes = f.read()
|
||||
|
||||
return ["video_result", video_bytes]
|
||||
@@ -68,16 +68,17 @@
|
||||
"VEO": {
|
||||
"provider": "Gemini",
|
||||
"service": "VEO",
|
||||
"reference_images_handler": "Gemini_VEO.py",
|
||||
"import_modules": [
|
||||
"from google import genai",
|
||||
"from google.genai import types"
|
||||
],
|
||||
"possible_parameters": {
|
||||
"aspect_ratio": ["9:16", "16:9"],
|
||||
"durationSeconds": ["4", "6", "8"],
|
||||
"personGeneration": ["allow_all", "allow_adult", "dont_allow"],
|
||||
"resolution": ["720p", "1080p", "4K"],
|
||||
"model": ["veo-3.0-generate", "veo-3.0-fast-generate", "veo-3.1-generate-preview", "veo-3.1-generate", "veo-3.1-fast-generate-preview", "veo-3.1-fast-generate"]
|
||||
"model": ["veo-2.0-generate-001", "veo-3.1-fast-generate-preview", "veo-3.1-generate-preview"],
|
||||
"aspect_ratio": ["16:9", "16:10", "9:16", "10:16"],
|
||||
"duration_seconds": [4, 5, 6, 7, 8],
|
||||
"person_generation": ["allow_adult", "allow_all", "dont_allow"],
|
||||
"resolution": ["720p", "1080p", "4K"]
|
||||
},
|
||||
"request": {
|
||||
"method": "SDK",
|
||||
@@ -93,11 +94,13 @@
|
||||
"$args": [],
|
||||
"$kwargs": {
|
||||
"aspectRatio": "{{aspect_ratio}}",
|
||||
"number_of_videos": "{{batch}}",
|
||||
"resolution": "{{config_resolution}}",
|
||||
"durationSeconds": "{{durationSeconds}}",
|
||||
"personGeneration": "{{personGeneration}}",
|
||||
"durationSeconds": "{{duration_seconds}}",
|
||||
"personGeneration": "{{person_generation}}",
|
||||
"reference_images": "{{reference_images}}",
|
||||
"last_frame": "{{last_image}}"
|
||||
"last_frame": "{{last_image}}",
|
||||
"negative_prompt": "{{negative_prompt}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
+44
-38
@@ -389,46 +389,52 @@ class PreviewSaver {
|
||||
saveImageName = state.targetFileName;
|
||||
}
|
||||
|
||||
const renderToCanvas = (blob) => new Promise((resolve, reject) => {
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
const canvas = document.createElement('canvas');
|
||||
let w = img.naturalWidth;
|
||||
let h = img.naturalHeight;
|
||||
if (maxWidth && w > maxWidth) { h = Math.round(h * maxWidth / w); w = maxWidth; }
|
||||
if (maxHeight && h > maxHeight) { w = Math.round(w * maxHeight / h); h = maxHeight; }
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
resolve(canvas);
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
reject(new Error('Failed to decode image: ' + imageName));
|
||||
};
|
||||
img.src = blobUrl;
|
||||
});
|
||||
|
||||
fetch(imageSource)
|
||||
.then((res) => res.blob())
|
||||
.then((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const file = dataURLtoFile(reader.result, imageName);
|
||||
loadImage(file, function (img) {
|
||||
if (typeof img.toDataURL === "function") {
|
||||
const resampledImage = img.toDataURL(imgMime, wv.targetQuality);
|
||||
if (wv.saveMode === false) {
|
||||
downloadImage(resampledImage, extension, saveImageName);
|
||||
} else {
|
||||
sendPOSTmessage(JSON.stringify({
|
||||
"PreviewTarget": wv.previewTarget,
|
||||
"PreviewTargetOriginal": state.selectedTarget,
|
||||
"extension": extension,
|
||||
"ImageName": imageName,
|
||||
"ImagePath": ImagePath,
|
||||
"SaveImageName": saveImageName,
|
||||
"maxWidth": img.width,
|
||||
"maxHeight": img.height,
|
||||
"TargetQuality": wv.targetQuality,
|
||||
"PrwSaveMode": wv.prwSaveMode,
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
alert('Source image: ' + imageName + ' does not exist, maybe deleted.');
|
||||
}
|
||||
}, {
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
canvas: true,
|
||||
pixelRatio: 1,
|
||||
downsamplingRatio: wv.targetQuality / 100,
|
||||
orientation: true,
|
||||
imageSmoothingEnabled: true,
|
||||
imageSmoothingQuality: 'high',
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
.then((blob) => renderToCanvas(blob))
|
||||
.then((canvas) => {
|
||||
const resampledImage = canvas.toDataURL(imgMime, wv.targetQuality / 100);
|
||||
if (wv.saveMode === false) {
|
||||
downloadImage(resampledImage, extension, saveImageName);
|
||||
} else {
|
||||
sendPOSTmessage(JSON.stringify({
|
||||
"PreviewTarget": wv.previewTarget,
|
||||
"PreviewTargetOriginal": state.selectedTarget,
|
||||
"extension": extension,
|
||||
"ImageName": imageName,
|
||||
"ImagePath": ImagePath,
|
||||
"SaveImageName": saveImageName,
|
||||
"maxWidth": canvas.width,
|
||||
"maxHeight": canvas.height,
|
||||
"TargetQuality": wv.targetQuality,
|
||||
"PrwSaveMode": wv.prwSaveMode,
|
||||
}));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
alert('Failed to load source image: ' + err.message);
|
||||
|
||||
@@ -6,6 +6,106 @@ const SCHEMA_URL = new URL("/extensions/ComfyUI_Primere_Nodes/api_schemas.json",
|
||||
let schemaCache = null;
|
||||
let schemaPromise = null;
|
||||
|
||||
const TOAST_DURATION_MS = 10000; // toast auto-dismiss delay in milliseconds
|
||||
|
||||
function showToast(status, message) {
|
||||
let container = document.getElementById("primere-toast-container");
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = "primere-toast-container";
|
||||
Object.assign(container.style, {
|
||||
position: "fixed",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
zIndex: "99999",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
pointerEvents: "none",
|
||||
});
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
const isSuccess = status === "success";
|
||||
const accentColor = isSuccess ? "#4caf50" : "#f44336";
|
||||
const icon = isSuccess ? "✔" : "✖";
|
||||
|
||||
const toast = document.createElement("div");
|
||||
Object.assign(toast.style, {
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
background: "#1e1e1e",
|
||||
border: `1px solid ${accentColor}`,
|
||||
borderLeft: `4px solid ${accentColor}`,
|
||||
borderRadius: "6px",
|
||||
padding: "10px 14px 10px 12px",
|
||||
minWidth: "280px",
|
||||
maxWidth: "440px",
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
gap: "10px",
|
||||
pointerEvents: "all",
|
||||
boxShadow: "0 4px 16px rgba(0,0,0,0.6)",
|
||||
opacity: "0",
|
||||
transform: "translateY(-12px)",
|
||||
transition: "opacity 0.25s ease, transform 0.25s ease",
|
||||
fontFamily: "sans-serif",
|
||||
fontSize: "15px",
|
||||
color: "#e0e0e0",
|
||||
cursor: "default",
|
||||
});
|
||||
|
||||
const iconEl = document.createElement("span");
|
||||
iconEl.textContent = icon;
|
||||
Object.assign(iconEl.style, { color: accentColor, fontSize: "15px", flexShrink: "0", lineHeight: "1.5" });
|
||||
|
||||
const textEl = document.createElement("span");
|
||||
textEl.textContent = message;
|
||||
Object.assign(textEl.style, { flex: "1", lineHeight: "1.5", wordBreak: "break-all" });
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.textContent = "✕";
|
||||
Object.assign(closeBtn.style, {
|
||||
background: "none", border: "none", color: "#888", cursor: "pointer",
|
||||
fontSize: "13px", padding: "0 0 0 6px", flexShrink: "0", lineHeight: "1.5",
|
||||
});
|
||||
closeBtn.addEventListener("mouseover", () => { closeBtn.style.color = "#ccc"; });
|
||||
closeBtn.addEventListener("mouseout", () => { closeBtn.style.color = "#888"; });
|
||||
|
||||
const progress = document.createElement("div");
|
||||
Object.assign(progress.style, {
|
||||
position: "absolute", bottom: "0", left: "0",
|
||||
height: "3px", width: "100%",
|
||||
background: accentColor,
|
||||
transformOrigin: "left",
|
||||
transform: "scaleX(1)",
|
||||
transition: `transform ${TOAST_DURATION_MS / 1000}s linear`,
|
||||
});
|
||||
|
||||
toast.appendChild(iconEl);
|
||||
toast.appendChild(textEl);
|
||||
toast.appendChild(closeBtn);
|
||||
toast.appendChild(progress);
|
||||
container.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
toast.style.opacity = "1";
|
||||
toast.style.transform = "translateY(0)";
|
||||
progress.style.transform = "scaleX(0)";
|
||||
}));
|
||||
|
||||
function dismiss() {
|
||||
clearTimeout(timer);
|
||||
toast.style.opacity = "0";
|
||||
toast.style.transform = "translateY(-12px)";
|
||||
setTimeout(() => toast.remove(), 280);
|
||||
}
|
||||
|
||||
const timer = setTimeout(dismiss, TOAST_DURATION_MS);
|
||||
closeBtn.addEventListener("click", dismiss);
|
||||
}
|
||||
|
||||
async function loadSchemas() {
|
||||
if (schemaCache) {
|
||||
return schemaCache;
|
||||
@@ -350,6 +450,22 @@ async function initializeUniApiNode(node) {
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
const onSaveResult = (event) => {
|
||||
const detail = event.detail;
|
||||
if (String(detail?.node_id) !== String(node.id)) return;
|
||||
if (detail.status === "success") {
|
||||
showToast("success", `Saved: ${detail.path}`);
|
||||
} else {
|
||||
showToast("error", `Save failed: ${detail.error}`);
|
||||
}
|
||||
};
|
||||
app.api.addEventListener("primere.save_result", onSaveResult);
|
||||
|
||||
const originalOnRemoved = node.onRemoved;
|
||||
node.onRemoved = function () {
|
||||
originalOnRemoved?.call(this);
|
||||
app.api.removeEventListener("primere.save_result", onSaveResult);
|
||||
};
|
||||
|
||||
const originalOnWidgetChanged = node.onWidgetChanged;
|
||||
node.onWidgetChanged = function (name, value, oldValue, widget) {
|
||||
|
||||
Reference in New Issue
Block a user