Compare commits
11
Commits
main
...
dev-v2schema
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f934d3d9e | ||
|
|
7e1391535c | ||
|
|
c54bdbbb97 | ||
|
|
e4caea1af6 | ||
|
|
19c9a3f212 | ||
|
|
2ef0200c4c | ||
|
|
b9b43e7ef5 | ||
|
|
b5bc49753e | ||
|
|
67d4747e9c | ||
|
|
583b7afa78 | ||
|
|
e6266cc049 |
@@ -3,6 +3,7 @@ test.py
|
|||||||
js/test.js
|
js/test.js
|
||||||
veniceai_config.json
|
veniceai_config.json
|
||||||
data/*
|
data/*
|
||||||
|
testing_video/*
|
||||||
|
|
||||||
# Ignore Mac system files
|
# Ignore Mac system files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -1,110 +1,4 @@
|
|||||||
# ComfyUI Venice.AI API Custom Nodes
|
### Hi, hello
|
||||||
|
|
||||||
An unofficial custom node implementation for ComfyUI that integrates with venice.ai's Generative AI services such as Image, Text and TTS Generation models (as well as Upscale and Enhance). This project is adapted from [ComfyUI-FLUX-TOGETHER-API](https://github.com/BZcreativ/ComfyUI-FLUX-TOGETHER-API) to work with the venice.ai API.
|
WIP dev branch changing things woohee
|
||||||
**Note**: TTS is in Beta per Venice (as of 15th June 2025). The node for it "Generate Speech (Venice)" is set to BETA/Experimental too. To be able to find it with node search you have to enable "Show experimental nodes in search" setting in ComfyUI.
|
see [TODO](TODO.md) for some sparse todo stuff
|
||||||
|
|
||||||
Disclaimer: I originally made this on a whim because someone wanted something similar to Together.AI custom nodes but have them use venice instead. I'm also not affiliated with Venice.AI. Idk is this good enough for a disclaimer or something
|
|
||||||
|
|
||||||
## Nodes: (Text gen node is missing but will be updated soon)
|
|
||||||
|
|
||||||

|
|
||||||
Image and Text models (as of 15th June 2025 | for Speech gen only tts-kokoro is available)
|
|
||||||
For updated text generation experience please use `Generate Text Advanced BETA (Venice)` node and additionally `Textgen Parameters (Venice) for extra venice.ai specific parameters to pass onto the generation process.
|
|
||||||
 
|
|
||||||
|
|
||||||
~~# todo: inpainting~~ Deprecated by venice, new thing is coming for it at some point
|
|
||||||
|
|
||||||
todo: actual log, maybe separate logging file for less clutter from comfyui stuff, maybe, maybe... eeeee
|
|
||||||
|
|
||||||
todo: add settings to set default model by user
|
|
||||||
|
|
||||||
todo: less convoluted approach to downloading and loading model/character/styles lists
|
|
||||||
|
|
||||||
~~todo: LLM characters list~~ done, but its in beta so subject to big changes, use with `Textgen Parameters (Venice)` node
|
|
||||||
|
|
||||||
todo: use variants api (currently in beta) | idk what happened to this, probably gone
|
|
||||||
|
|
||||||
Todo: chat history/memory/context for LLM | got a vague idea but thats pretty much it
|
|
||||||
|
|
||||||
todo
|
|
||||||
|
|
||||||
- Some Validation on queue for API limits like prompt max length or width/height
|
|
||||||
- these are different for different models so this isnt planned to be implemented unless the api exposes those limits somehow
|
|
||||||
- i'd have an error thrown before it gets sent to api about some value being too high or too low (though api should send error back for now either way)
|
|
||||||
- maybe the api already exposes those limits, the steps limit is afaik at least, need to look into it
|
|
||||||
|
|
||||||
### Below ReadMe text is only slightly altered from original Flux Together API readme, it was not really reworked or anything so its likely not correct or up to date
|
|
||||||
|
|
||||||
### Installation - these instructions are a mess
|
|
||||||
|
|
||||||
0. Before proceeding, check if you can find these nodes through ComfyUI-Manager interface rather than following the instructions below.
|
|
||||||
|
|
||||||
<details><summary>Expand me to see harder instructions</summary>
|
|
||||||
1. Clone this repository into your ComfyUI custom_nodes directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ComfyUI/custom_nodes
|
|
||||||
git clone https://github.com/DraconicDragon/ComfyUI-Venice-API.git
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install the required dependencies: (this might be done by comfyui automatically on restart already?)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
OR From the Comfyui Folder (this one is usually preferred if you have portable edition)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./python_embeded\python.exe -m pip install -r ComfyUI\custom_nodes\ComfyUI-Venice-API\requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
1. Get your API key from [venice.ai](https://venice.ai)
|
|
||||||
2. Add your API key in ComfyUI settings > VeniceAI
|
|
||||||
|
|
||||||
### Parameters for Generate Image (Venice) node
|
|
||||||
|
|
||||||
| Parameter | Type | Range | Default | Description |
|
|
||||||
|-----------------|---------|------------|---------|-----------------------------------------|
|
|
||||||
| prompt | string | 1-1500 | "A flying cat made of lettuce" | Main generation prompt |
|
|
||||||
| negative_prompt | string | 0-1500 | "" | Elements to avoid |
|
|
||||||
| width | integer | 0-1280? | 1024 | Image width |
|
|
||||||
| height | integer | 0-1280? | 1024 | Image height |
|
|
||||||
| batch_size | integer | 1-4 | 1 | Number of Images to gen in a single run |
|
|
||||||
| steps | integer | 1-30 or 50 | 20 | Number of generation steps |
|
|
||||||
| cfg/guidance | float | 0-20.0 | 3.0 | Guidance scale |
|
|
||||||
| style_preset | string | N/A | none | The Style preset to apply |
|
|
||||||
| hide_watermark | boolean | N/A | true | Whether to hide watermark (NSFW = false)|
|
|
||||||
| safe_mode | boolean | N/A | false | Whether to blur NSFW images |
|
|
||||||
| seed | integer | -999999999 to 999999999 | -1 | Generation seed |
|
|
||||||
|
|
||||||
### License
|
|
||||||
|
|
||||||
MIT License - see [LICENSE](LICENSE) file for details.
|
|
||||||
|
|
||||||
### Credits
|
|
||||||
|
|
||||||
- This project is adapted from [ComfyUI-FLUX-TOGETHER-API](https://github.com/BZcreativ/ComfyUI-FLUX-TOGETHER-API)
|
|
||||||
- venice.ai for providing the generative AI services and API
|
|
||||||
- [ComfyUI-FLUX-TOGETHER-API](https://github.com/BZcreativ/ComfyUI-FLUX-TOGETHER-API) for their work
|
|
||||||
- ComfyUI team for the amazing framework
|
|
||||||
|
|
||||||
### Author
|
|
||||||
|
|
||||||
Created by [BZcreativ](https://github.com/BZcreativ)
|
|
||||||
|
|
||||||
venice.ai rewrite by [DraconicDragon](https://github.com/DraconicDragon)
|
|
||||||
|
|
||||||
### Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Feel free to submit a Pull Request.
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
todo
|
|
||||||
|
|
||||||
For detailed usage instructions, see [USAGE.md](USAGE.md) (not reworked)
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
- [ ] Support quote video gen button label for price estimates
|
||||||
|
- [ ] Convert to node schema v3
|
||||||
|
- [ ] Use DynamicCombo for all applicable nodes
|
||||||
|
- [ ] Combine DynamicCombo with other limits such as prompt length
|
||||||
|
- [ ] Check for TODOs in code files
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# dont follow stuff thats in here i dont think its correct
|
|
||||||
|
|
||||||
|
|
||||||
# ComfyUI venice.ai API Node Usage Guide
|
|
||||||
|
|
||||||
### Disclaimer: I just made this on a whim because someone wanted something similar to Together.AI custom nodes but use venice instead and I don't have access to any API keys for any of the mentioned services.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
1. Ensure you have a venice.ai account and API key
|
|
||||||
2. Configure your API key in `config.ini`
|
|
||||||
3. Install all required dependencies (should be done automatically by comfy?)
|
|
||||||
|
|
||||||
## Node Configuration
|
|
||||||
|
|
||||||
### Input Parameters
|
|
||||||
|
|
||||||
#### Required Parameters:
|
|
||||||
- **Prompt** (String)
|
|
||||||
- Your main generation prompt
|
|
||||||
- Be specific and detailed for best results if using Flux
|
|
||||||
|
|
||||||
- **Negative Prompt** (String)
|
|
||||||
- Elements you want to avoid in the generation
|
|
||||||
- Leave empty if not needed | Will be ignored if flux-dev or flux-dev-uncensored is selected as model
|
|
||||||
|
|
||||||
- **Steps** (Integer)
|
|
||||||
- Range: 1-30
|
|
||||||
- Default: 20
|
|
||||||
|
|
||||||
- **Width** (Integer)
|
|
||||||
- Range: 512-2048
|
|
||||||
- Default: 1024
|
|
||||||
- Must be a multiple of 32
|
|
||||||
- Common values: 512, 768, 1024 (1MP), 1440 (Flux | 2MP)
|
|
||||||
|
|
||||||
- **Height** (Integer)
|
|
||||||
- Range: 512-2048
|
|
||||||
- Default: 1024
|
|
||||||
- Must be a multiple of 32
|
|
||||||
- Common values: 512, 768, 1024 (1MP), 1440 (Flux | 2MP)
|
|
||||||
|
|
||||||
- **Seed** (Integer)
|
|
||||||
- Range: 0 to max 64-bit integer
|
|
||||||
- Default: -1 | Random
|
|
||||||
- Reuse a seed with same prompt to reproduce an image
|
|
||||||
|
|
||||||
- **CFG (Guidance Scale)** (Float)
|
|
||||||
- Range: 0.1-15.0
|
|
||||||
- Default: 3.5
|
|
||||||
- Recommended range: 5.0-10.0 (SDXL) | ~3.5 (Flux)
|
|
||||||
|
|
||||||
### Output
|
|
||||||
|
|
||||||
The node outputs a single image tensor compatible with other ComfyUI nodes.
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Prompt Engineering**
|
|
||||||
- Be specific and detailed in your prompts
|
|
||||||
- Use descriptive adjectives
|
|
||||||
- Include style references when needed
|
|
||||||
|
|
||||||
2. **Performance**
|
|
||||||
- Start with lower step counts (20-30) for testing
|
|
||||||
- Increase steps for final generations
|
|
||||||
- Use reasonable image dimensions (1024x1024 is standard)
|
|
||||||
|
|
||||||
3. **Error Handling/Troubleshooting**
|
|
||||||
- Check console for error messages
|
|
||||||
- Verify API key is correctly configured
|
|
||||||
- Ensure parameters are within valid ranges
|
|
||||||
|
|
||||||
## Common Workflows
|
|
||||||
|
|
||||||
### Basic Image Generation
|
|
||||||
1. Add Together API Node to workspace
|
|
||||||
2. Connect to a Load Image node
|
|
||||||
3. Configure prompt and basic parameters
|
|
||||||
4. Execute workflow
|
|
||||||
|
|
||||||
### Advanced Usage
|
|
||||||
1. Combine with other ComfyUI nodes
|
|
||||||
2. Use seed control for consistent results
|
|
||||||
3. Experiment with guidance scale for style control
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
1. **API Key Errors**
|
|
||||||
- Verify key in config.ini
|
|
||||||
- Check API key validity
|
|
||||||
- Ensure proper formatting
|
|
||||||
|
|
||||||
2. **Generation Errors**
|
|
||||||
- Verify parameter ranges
|
|
||||||
- Check prompt length
|
|
||||||
- Monitor API rate limits
|
|
||||||
|
|
||||||
3. **Image Quality Issues**
|
|
||||||
- Adjust step count
|
|
||||||
- Modify guidance scale
|
|
||||||
- Refine prompt
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### Basic Prompt Example
|
|
||||||
```
|
|
||||||
A beautiful landscape with mountains and lakes, cinematic lighting, high detail
|
|
||||||
```
|
|
||||||
|
|
||||||
### Advanced Prompt Example
|
|
||||||
```
|
|
||||||
A stunning mountain landscape at sunset, volumetric lighting,
|
|
||||||
golden hour, ultra detailed, professional photography,
|
|
||||||
8k resolution, artistic composition
|
|
||||||
```
|
|
||||||
|
|
||||||
### Negative Prompt Example
|
|
||||||
```
|
|
||||||
blur, haze, low quality, distortion, bad composition,
|
|
||||||
oversaturated, unrealistic lighting
|
|
||||||
```
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues and feature requests, please use the GitHub issue tracker.
|
|
||||||
+23
-21
@@ -1,39 +1,41 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from .pyserver import (
|
from .pyserver import routes # noqa: F401
|
||||||
get_key_from_jssetting, # noqa: F401
|
|
||||||
update_characters, # noqa: F401
|
NODE_PACKAGE = Path(__file__).with_name("nodes")
|
||||||
update_models, # noqa: F401
|
node_list = sorted(
|
||||||
update_styles, # noqa: F401
|
module.stem for module in NODE_PACKAGE.glob("*.py") if module.is_file() and not module.name.startswith("_")
|
||||||
)
|
)
|
||||||
|
|
||||||
node_list = [
|
|
||||||
# "things_n_stuff_node",
|
|
||||||
"gen_image_node",
|
|
||||||
# "gen_image_inpaint_node",
|
|
||||||
"gen_text_node",
|
|
||||||
"gen_text_advanced_node",
|
|
||||||
"gen_text_venice_params_node",
|
|
||||||
"i2i_enhance_upscale_node",
|
|
||||||
"gen_speech_node",
|
|
||||||
"util_nodes",
|
|
||||||
]
|
|
||||||
|
|
||||||
NODE_CLASS_MAPPINGS = {}
|
NODE_CLASS_MAPPINGS = {}
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||||
|
NODE_MODULE_PREFIX = f"{__name__}.nodes"
|
||||||
|
|
||||||
for module_name in node_list:
|
for module_name in node_list:
|
||||||
try:
|
try:
|
||||||
imported_module = importlib.import_module(f".nodes.{module_name}", __name__)
|
imported_module = importlib.import_module(f"{NODE_MODULE_PREFIX}.{module_name}")
|
||||||
NODE_CLASS_MAPPINGS.update(imported_module.NODE_CLASS_MAPPINGS)
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS.update(imported_module.NODE_DISPLAY_NAME_MAPPINGS)
|
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logging.warning(f"Could not import module '{module_name}': {e}")
|
logging.warning(f"Could not import module '{module_name}': {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
has_classes = hasattr(imported_module, "NODE_CLASS_MAPPINGS")
|
||||||
|
has_display = hasattr(imported_module, "NODE_DISPLAY_NAME_MAPPINGS")
|
||||||
|
|
||||||
|
if has_classes and has_display:
|
||||||
|
NODE_CLASS_MAPPINGS.update(imported_module.NODE_CLASS_MAPPINGS)
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS.update(imported_module.NODE_DISPLAY_NAME_MAPPINGS)
|
||||||
|
elif has_classes or has_display:
|
||||||
|
logging.warning(
|
||||||
|
"Module '%s' defines '%s' but is missing '%s'; module skipped",
|
||||||
|
module_name,
|
||||||
|
"NODE_CLASS_MAPPINGS" if has_classes else "NODE_DISPLAY_NAME_MAPPINGS",
|
||||||
|
"NODE_DISPLAY_NAME_MAPPINGS" if has_classes else "NODE_CLASS_MAPPINGS",
|
||||||
|
)
|
||||||
|
# modules without either mapping are ignored silently
|
||||||
|
|
||||||
WEB_DIRECTORY = os.path.join(os.path.dirname(__file__), "js")
|
WEB_DIRECTORY = os.path.join(os.path.dirname(__file__), "js")
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 438 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
+12
-2
@@ -1,3 +1,5 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
API_ENDPOINTS = {
|
API_ENDPOINTS = {
|
||||||
"list_models": "/models", # response type is list of strings
|
"list_models": "/models", # response type is list of strings
|
||||||
"list_styles": "/image/styles", #
|
"list_styles": "/image/styles", #
|
||||||
@@ -6,9 +8,17 @@ API_ENDPOINTS = {
|
|||||||
"upscale_image": "/image/upscale", # NOTE: apparently doesnt even work yet? idk; response type is image/png file, content type is multipart/form-data
|
"upscale_image": "/image/upscale", # NOTE: apparently doesnt even work yet? idk; response type is image/png file, content type is multipart/form-data
|
||||||
"text_generate": "/chat/completions", # has much info, text response is in choices: content, can have multiple choices apparently but dosnt seem to be utilized
|
"text_generate": "/chat/completions", # has much info, text response is in choices: content, can have multiple choices apparently but dosnt seem to be utilized
|
||||||
"speech_generate": "/audio/speech", # type: file (audio/aac; audio/mpeg; audio/wav.. etc)
|
"speech_generate": "/audio/speech", # type: file (audio/aac; audio/mpeg; audio/wav.. etc)
|
||||||
|
"video_queue": "/video/queue", #
|
||||||
|
"video_quote": "/video/quote", # price estimate, takes same payload as video_queue
|
||||||
|
"video_retrieve": "/video/retrieve", # get video file by job id
|
||||||
|
"list_api_keys": "/api_keys",
|
||||||
}
|
}
|
||||||
|
|
||||||
VENICEAI_BASE_URL = "https://api.venice.ai/api/v1"
|
VENICEAI_BASE_URL = "https://api.venice.ai/api/v1"
|
||||||
|
|
||||||
# unused right now
|
# request hygiene
|
||||||
headers = {"User-Agent": "ComfyUI-Venice-API/1.0 (by draconicdragon on github)"}
|
USER_AGENT = "ComfyUI-Venice-API/1.0 (by draconicdragon on github)"
|
||||||
|
|
||||||
|
|
||||||
|
os.environ["VENICE_CLIENT_DRY_RUN"] = "0"
|
||||||
|
os.environ["VENICE_CLIENT_DEBUG"] = "1"
|
||||||
|
|||||||
-125
@@ -1,125 +0,0 @@
|
|||||||
import { api } from "../../scripts/api.js";
|
|
||||||
import { app } from "../../scripts/app.js";
|
|
||||||
|
|
||||||
|
|
||||||
// Helper function to fetch data and assign to widget
|
|
||||||
async function fetchAndAssignWidget(widget, url, dataKey, logMsg, errorMsg) {
|
|
||||||
if (!widget) return;
|
|
||||||
try {
|
|
||||||
console.log(`(VeniceAI.NodeSpawn) ${logMsg}`);
|
|
||||||
const response = await api.fetchApi(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawText = await response.text();
|
|
||||||
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
data = JSON.parse(rawText);
|
|
||||||
} catch (jsonError) {
|
|
||||||
throw new Error(`Failed to parse JSON: ${jsonError.message}. Raw response: ${rawText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
widget.options.values = data[dataKey];
|
|
||||||
if (widget.onChange) {
|
|
||||||
widget.onChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setDirtyCanvas(true);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`(VeniceAI.NodeSpawn) ${errorMsg}:`, error);
|
|
||||||
alert(`(VeniceAI.NodeSpawn) ${errorMsg}:\n${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
app.registerExtension({
|
|
||||||
name: "VeniceAI.NodeSpawn",
|
|
||||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
|
||||||
if (nodeData.name === "GenerateImage_VENICE" || nodeData.name === "InpaintImage_VENICE") {
|
|
||||||
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
||||||
nodeType.prototype.onNodeCreated = async function () {
|
|
||||||
if (originalOnNodeCreated) {
|
|
||||||
originalOnNodeCreated.apply(this);
|
|
||||||
}
|
|
||||||
await fetchAndAssignWidget.call(
|
|
||||||
this,
|
|
||||||
this.widgets.find(w => w.name === "model"),
|
|
||||||
"/veniceai/get_models_list",
|
|
||||||
"image_models",
|
|
||||||
"Trying to fetch image models...",
|
|
||||||
"Failed to fetch image models"
|
|
||||||
);
|
|
||||||
await fetchAndAssignWidget.call(
|
|
||||||
this,
|
|
||||||
this.widgets.find(w => w.name === "style_preset"),
|
|
||||||
"/veniceai/get_styles_list",
|
|
||||||
"data",
|
|
||||||
"Trying to fetch styles...",
|
|
||||||
"Failed to fetch styles"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodeData.name === "GenerateText_VENICE" || nodeData.name === "GenerateTextAdvanced_VENICE") {
|
|
||||||
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
||||||
nodeType.prototype.onNodeCreated = async function () {
|
|
||||||
if (originalOnNodeCreated) {
|
|
||||||
originalOnNodeCreated.apply(this);
|
|
||||||
}
|
|
||||||
await fetchAndAssignWidget.call(
|
|
||||||
this,
|
|
||||||
this.widgets.find(w => w.name === "model"),
|
|
||||||
"/veniceai/get_models_list",
|
|
||||||
"text_models",
|
|
||||||
"Trying to fetch text models...",
|
|
||||||
"Failed to fetch text models"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodeData.name === "GenerateTextVeniceParameters_VENICE") {
|
|
||||||
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
||||||
nodeType.prototype.onNodeCreated = async function () {
|
|
||||||
if (originalOnNodeCreated) {
|
|
||||||
originalOnNodeCreated.apply(this);
|
|
||||||
}
|
|
||||||
await fetchAndAssignWidget.call(
|
|
||||||
this,
|
|
||||||
this.widgets.find(w => w.name === "character_slug"),
|
|
||||||
"/veniceai/get_characters_list",
|
|
||||||
"characters",
|
|
||||||
"Trying to fetch character slugs...",
|
|
||||||
"Failed to fetch character slugs"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nodeData.name === "GenerateSpeech_VENICE") {
|
|
||||||
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
|
|
||||||
nodeType.prototype.onNodeCreated = async function () {
|
|
||||||
if (originalOnNodeCreated) {
|
|
||||||
originalOnNodeCreated.apply(this);
|
|
||||||
}
|
|
||||||
await fetchAndAssignWidget.call(
|
|
||||||
this,
|
|
||||||
this.widgets.find(w => w.name === "model"),
|
|
||||||
"/veniceai/get_models_list",
|
|
||||||
"tts_models",
|
|
||||||
"Trying to fetch tts models...",
|
|
||||||
"Failed to fetch tts models"
|
|
||||||
);
|
|
||||||
await fetchAndAssignWidget.call(
|
|
||||||
this,
|
|
||||||
this.widgets.find(w => w.name === "voice"),
|
|
||||||
"/veniceai/get_models_list",
|
|
||||||
"tts_voices",
|
|
||||||
"Trying to fetch tts voices...",
|
|
||||||
"Failed to fetch tts voices"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
+5
-2
@@ -5,11 +5,15 @@ app.registerExtension({
|
|||||||
name: "VeniceAI.Settings",
|
name: "VeniceAI.Settings",
|
||||||
settings: [
|
settings: [
|
||||||
{
|
{
|
||||||
|
category: ["VeniceAI", "API Key", "VeniceAI API Key"],
|
||||||
id: "VeniceAI.apikey",
|
id: "VeniceAI.apikey",
|
||||||
name: "VeniceAI API Key",
|
name: "VeniceAI API Key",
|
||||||
type: "text",
|
type: "text",
|
||||||
defaultValue: "your_venice_api_key_here",
|
defaultValue: "your_venice_api_key_here",
|
||||||
tooltip: "Enter your VeniceAI API Bearer Token Key here",
|
tooltip: "Enter your VeniceAI API Key/Bearer Token here",
|
||||||
|
attrs: {
|
||||||
|
type: "password",
|
||||||
|
},
|
||||||
onChange: async (newVal) => {
|
onChange: async (newVal) => {
|
||||||
api.fetchApi("/veniceai/save_apikey", {
|
api.fetchApi("/veniceai/save_apikey", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -19,4 +23,3 @@ app.registerExtension({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+5
-43
@@ -5,51 +5,13 @@ app.registerExtension({
|
|||||||
name: "VeniceAI.Startup",
|
name: "VeniceAI.Startup",
|
||||||
|
|
||||||
async setup() {
|
async setup() {
|
||||||
// Load saved value from server on startup
|
|
||||||
console.log("(VeniceAI.Startup) Fetching VeniceAI API key from config file...");
|
|
||||||
const api_key_response = await api.fetchApi("/veniceai/get_apikey");
|
|
||||||
const savedKey = await api_key_response.json();
|
|
||||||
|
|
||||||
// update the settings UI
|
|
||||||
app.extensionManager.setting.set("VeniceAI.apikey", savedKey.apikey);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// update the model list
|
console.log("(VeniceAI.Startup) Fetching VeniceAI API key from config file...");
|
||||||
console.log("(VeniceAI.Startup) Updating model list...");
|
const api_key_response = await api.fetchApi("/veniceai/get_apikey");
|
||||||
//alert("fetching model list")
|
const savedKey = await api_key_response.json();
|
||||||
const response = await api.fetchApi("/veniceai/update_models_list");
|
app.extensionManager.setting.set("VeniceAI.apikey", savedKey.apikey);
|
||||||
const data = await response.json();
|
|
||||||
//alert(`response status ${JSON.stringify(data)}`);
|
|
||||||
if (data.error) {
|
|
||||||
alert(`${data.message}`);
|
|
||||||
console.log(`(VeniceAI.Startup) ${data.message}`);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
// update the style list if not model list error
|
|
||||||
console.log("(VeniceAI.Startup) Updating styles list...");
|
|
||||||
//alert("fetching styles list")
|
|
||||||
const response_s = await api.fetchApi("/veniceai/update_styles_list");
|
|
||||||
//alert(`response status ${await response_s.text()}`);
|
|
||||||
const data_s = await response_s.json();
|
|
||||||
if (data_s.error) {
|
|
||||||
alert(`${data_s.message}`);
|
|
||||||
console.log(`(VeniceAI.Startup) ${data_s.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update the characters list
|
|
||||||
console.log("(VeniceAI.Startup) Updating characters list...");
|
|
||||||
const response_c = await api.fetchApi("/veniceai/update_characters_list");
|
|
||||||
const data_c = await response_c.json();
|
|
||||||
if (data_c.error) {
|
|
||||||
alert(`${data_c.message}`);
|
|
||||||
console.log(`(VeniceAI.Startup) ${data_c.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle any unexpected errors
|
console.error("(VeniceAI.Startup) Failed to load Venice API key", error);
|
||||||
alert(`(VeniceAI.Startup) Unexpected Error: ${error.message}`);
|
|
||||||
console.error("(VeniceAI.Startup) Unexpected Error:", error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Iterable, Sequence, Tuple
|
||||||
|
|
||||||
|
from ..venice_catalog import get_characters, get_models, get_styles
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_values(
|
||||||
|
loader,
|
||||||
|
key: str,
|
||||||
|
fallback: Sequence[str] = ("This list is unavailable; check logs for details?",),
|
||||||
|
) -> Tuple[str, ...]:
|
||||||
|
"""
|
||||||
|
Safely retrieves a sequence of string values from a loader payload keyed by `key`.
|
||||||
|
|
||||||
|
Attempts to call the provided `loader` callable to obtain a payload, logging failures
|
||||||
|
and falling back to the provided default values. Extracts and normalizes the value
|
||||||
|
associated with `key`, ensuring it is returned as a tuple of strings. If the extracted
|
||||||
|
value is missing, empty, or otherwise falsy, the fallback values are returned instead.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = loader()
|
||||||
|
except Exception as exc:
|
||||||
|
log.debug("Failed to load %s catalog: %s", key, exc)
|
||||||
|
return tuple(fallback)
|
||||||
|
|
||||||
|
raw = payload.get(key)
|
||||||
|
if raw is None:
|
||||||
|
return tuple(fallback)
|
||||||
|
|
||||||
|
if isinstance(raw, Iterable) and not isinstance(raw, (str, bytes)):
|
||||||
|
values = tuple(str(item) for item in raw if item)
|
||||||
|
else:
|
||||||
|
values = tuple(str(raw)) if raw else ()
|
||||||
|
|
||||||
|
return values or tuple(fallback)
|
||||||
|
|
||||||
|
|
||||||
|
def image_model_choices() -> Tuple[str, ...]:
|
||||||
|
return _safe_values(lambda: get_models(), "image_models")
|
||||||
|
|
||||||
|
|
||||||
|
def text2video_model_choices() -> Tuple[str, ...]:
|
||||||
|
return _safe_values(lambda: get_models(), "text2video_models")
|
||||||
|
|
||||||
|
def image2video_model_choices() -> Tuple[str, ...]:
|
||||||
|
return _safe_values(lambda: get_models(), "image2video_models")
|
||||||
|
|
||||||
|
|
||||||
|
def text_model_choices() -> Tuple[str, ...]:
|
||||||
|
return _safe_values(lambda: get_models(), "text_models")
|
||||||
|
|
||||||
|
|
||||||
|
def tts_model_choices() -> Tuple[str, ...]:
|
||||||
|
return _safe_values(lambda: get_models(), "tts_models")
|
||||||
|
|
||||||
|
|
||||||
|
def tts_voice_choices() -> Tuple[str, ...]:
|
||||||
|
return _safe_values(lambda: get_models(), "tts_voices")
|
||||||
|
|
||||||
|
|
||||||
|
def style_choices() -> Tuple[str, ...]:
|
||||||
|
return _safe_values(lambda: get_styles(), "data")
|
||||||
|
|
||||||
|
|
||||||
|
def character_choices() -> Tuple[str, ...]:
|
||||||
|
return _safe_values(lambda: get_characters(), "characters")
|
||||||
@@ -3,11 +3,12 @@ import io
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch # type: ignore
|
import torch # type: ignore
|
||||||
import torchvision.transforms as transforms # type: ignore
|
import torchvision.transforms as transforms # type: ignore
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateImageBase:
|
class GenerateImageBase:
|
||||||
RETURN_TYPES = ("IMAGE",)
|
RETURN_TYPES = ("IMAGE",)
|
||||||
FUNCTION = "generate"
|
FUNCTION = "generate"
|
||||||
@@ -34,12 +35,9 @@ class GenerateImageBase:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Error processing image result: {str(e)}") from e
|
raise Exception(f"Error processing image result: {str(e)}") from e
|
||||||
|
|
||||||
|
# unused
|
||||||
def create_blank_image(self):
|
def create_blank_image(self):
|
||||||
blank_img = Image.new("RGB", (64, 64), color="black")
|
blank_img = Image.new("RGB", (64, 64), color="black")
|
||||||
img_array = np.array(blank_img).astype(np.float32) / 255.0
|
img_array = np.array(blank_img).astype(np.float32) / 255.0
|
||||||
img_tensor = torch.from_numpy(img_array)[None,]
|
img_tensor = torch.from_numpy(img_array)[None,]
|
||||||
return (img_tensor,)
|
return (img_tensor,)
|
||||||
|
|
||||||
def check_multiple_of_32(self, width, height):
|
|
||||||
if width % 32 != 0 or height % 32 != 0:
|
|
||||||
raise ValueError(f"Width {width} and height {height} must be multiples of 32.")
|
|
||||||
|
|||||||
+70
-30
@@ -1,33 +1,45 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
import requests
|
import torch
|
||||||
import torch # type: ignore
|
|
||||||
|
|
||||||
from ..globals import API_ENDPOINTS, VENICEAI_BASE_URL
|
from ..globals import API_ENDPOINTS
|
||||||
|
from ..nodes.catalog_utils import image_model_choices, style_choices
|
||||||
from ..nodes.gen_image_base import GenerateImageBase
|
from ..nodes.gen_image_base import GenerateImageBase
|
||||||
|
from ..nodes.utils import ensure_multiple_of, ensure_prompt_length
|
||||||
|
from ..venice_client import client
|
||||||
|
|
||||||
|
|
||||||
class GenerateImage(GenerateImageBase):
|
class GenerateImage(GenerateImageBase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
|
model_choices = image_model_choices()
|
||||||
|
style_preset_options = style_choices()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"model": (
|
"model": (
|
||||||
"COMBO",
|
model_choices,
|
||||||
{
|
{
|
||||||
"default": "flux-dev",
|
"default": model_choices[0],
|
||||||
"tooltip": "Model to use for image generation, if this just says flux-dev or C O M B O then something failed oopsie.",
|
"tooltip": "Model to use for image generation",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"prompt": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": "A flying cat made of lettuce",
|
||||||
|
"multiline": True,
|
||||||
|
"tooltip": "The text prompt to guide the image generation",
|
||||||
|
"placeholder": "Positive Prompt. Example: A flying cat made of lettuce",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"prompt": ("STRING", {"default": "A flying cat made of lettuce", "multiline": True}),
|
|
||||||
"neg_prompt": (
|
"neg_prompt": (
|
||||||
"STRING",
|
"STRING",
|
||||||
{
|
{
|
||||||
"placeholder": "Negative Prompt. (Ignored for Flux based models.)\nBad composition, rating_explicit, Text, signature, lowres, faded image, out of focus, cropped, out of frame, vacant scene, bad quality, worst quality,",
|
"placeholder": "Negative Prompt. (Ignored for Flux based models.)\n Example: bad composition, rating_explicit, bad quality,",
|
||||||
"multiline": True,
|
"multiline": True,
|
||||||
"tooltip": "Negative prompt. This is ignored when using flux-dev or flux-dev-uncensored",
|
"tooltip": "Negative prompt. This is ignored when using flux-dev or flux-dev-uncensored or similar models that do not support CFG (Classifier-Free-Guidance)",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"width": (
|
"width": (
|
||||||
@@ -50,7 +62,15 @@ class GenerateImage(GenerateImageBase):
|
|||||||
"tooltip": "Must be a multiple of 32. Maximum allowed by venice.ai at time of writing is 1280",
|
"tooltip": "Must be a multiple of 32. Maximum allowed by venice.ai at time of writing is 1280",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"batch_size": ("INT", {"default": 1, "min": 1, "max": 4}),
|
"batch_size": (
|
||||||
|
"INT",
|
||||||
|
{
|
||||||
|
"default": 1,
|
||||||
|
"min": 1,
|
||||||
|
"max": 4,
|
||||||
|
"tooltip": "Number of images to generate in a single batch. IMPORTANT: Doesn't do actual batches like ComfyUI would, just sends batches amount of different requests to Venice.",
|
||||||
|
},
|
||||||
|
),
|
||||||
"steps": (
|
"steps": (
|
||||||
"INT",
|
"INT",
|
||||||
{
|
{
|
||||||
@@ -66,9 +86,23 @@ class GenerateImage(GenerateImageBase):
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"guidance": ("FLOAT", {"default": 3.0, "min": 0.0, "max": 20.0, "step": 0.05}),
|
"guidance": (
|
||||||
|
"FLOAT",
|
||||||
|
{
|
||||||
|
"default": 3.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 20.0,
|
||||||
|
"step": 0.05,
|
||||||
|
"tooltip": "CFG scale parameter or 'Guidance' for Flux",
|
||||||
|
},
|
||||||
|
),
|
||||||
# "lora_strength": ("INT", {"default": 50, "min": 0, "max": 100}), # check docs idk how to work this yet
|
# "lora_strength": ("INT", {"default": 50, "min": 0, "max": 100}), # check docs idk how to work this yet
|
||||||
"style_preset": ("COMBO", {"default": "none"}),
|
"style_preset": (
|
||||||
|
style_preset_options,
|
||||||
|
{
|
||||||
|
"default": style_preset_options[0],
|
||||||
|
},
|
||||||
|
),
|
||||||
"hide_watermark": (
|
"hide_watermark": (
|
||||||
"BOOLEAN",
|
"BOOLEAN",
|
||||||
{
|
{
|
||||||
@@ -90,6 +124,26 @@ class GenerateImage(GenerateImageBase):
|
|||||||
}, # 0xffffffffffffffff is 64 bit integer limit, current hex is 999999999, venice max
|
}, # 0xffffffffffffffff is 64 bit integer limit, current hex is 999999999, venice max
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# todo: add variants for batch size
|
||||||
|
# todo: implement lora and lora_strength
|
||||||
|
# todo: see if aspect_ratio needs to be added (some models incl nano banana pro use this)
|
||||||
|
# https://docs.venice.ai/api-reference/endpoint/image/generate
|
||||||
|
# todo: see if resolution needs to be added (some models incl nano banana pro use this)
|
||||||
|
# todo: add enable_web_search, mention it charges extra credits
|
||||||
|
# todo: change up default limits, prompt length max is 7500 now
|
||||||
|
@classmethod
|
||||||
|
def VALIDATE_INPUTS(cls, input_types, **kwargs):
|
||||||
|
width = input_types.get("width", kwargs.get("width"))
|
||||||
|
height = input_types.get("height", kwargs.get("height"))
|
||||||
|
prompt = input_types.get("prompt", kwargs.get("prompt"))
|
||||||
|
neg_prompt = input_types.get("neg_prompt", kwargs.get("neg_prompt"))
|
||||||
|
|
||||||
|
ensure_multiple_of(width, height)
|
||||||
|
ensure_prompt_length(prompt, 1500, "Prompt")
|
||||||
|
ensure_prompt_length(neg_prompt, 1500, "Negative Prompt", allow_empty=True)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def generate(
|
def generate(
|
||||||
self,
|
self,
|
||||||
model,
|
model,
|
||||||
@@ -107,10 +161,6 @@ class GenerateImage(GenerateImageBase):
|
|||||||
# format,
|
# format,
|
||||||
seed=-1,
|
seed=-1,
|
||||||
):
|
):
|
||||||
if prompt == "" or len(prompt) > 1500:
|
|
||||||
raise ValueError("VeniceAI Generate Image Node: Prompt cannot be empty or above 1500 characters")
|
|
||||||
if len(neg_prompt) > 1500:
|
|
||||||
raise ValueError("VeniceAI Generate Image Node: Negative prompt cannot be above 1500 characters")
|
|
||||||
if re.match(r"^flux.*", model):
|
if re.match(r"^flux.*", model):
|
||||||
logging.info(f"VeniceAPI INFO: Ignoring negative prompt for {model}.")
|
logging.info(f"VeniceAPI INFO: Ignoring negative prompt for {model}.")
|
||||||
neg_prompt = ""
|
neg_prompt = ""
|
||||||
@@ -118,11 +168,6 @@ class GenerateImage(GenerateImageBase):
|
|||||||
images_tensor = () # empty tuple for tensors
|
images_tensor = () # empty tuple for tensors
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.check_multiple_of_32(width, height) # todo: make this be validate node instead
|
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {os.getenv('VENICEAI_API_KEY')}", "Content-Type": "application/json"}
|
|
||||||
url = VENICEAI_BASE_URL + API_ENDPOINTS["image_generate"]
|
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
@@ -138,20 +183,15 @@ class GenerateImage(GenerateImageBase):
|
|||||||
"hide_watermark": hide_watermark,
|
"hide_watermark": hide_watermark,
|
||||||
"safe_mode": safe_mode,
|
"safe_mode": safe_mode,
|
||||||
"format": "png", # hardcoded because, change to format var and uncomment related stuff above if want dynamic
|
"format": "png", # hardcoded because, change to format var and uncomment related stuff above if want dynamic
|
||||||
|
"embed_exif_metadata": True, # this might not work and be overriden by comfyui on image save
|
||||||
}
|
}
|
||||||
if style_preset == "none":
|
if style_preset == "none":
|
||||||
del payload["style_preset"]
|
del payload["style_preset"]
|
||||||
|
|
||||||
for i in range(batch_size):
|
for i in range(batch_size):
|
||||||
payload["seed"] = seed + i
|
payload["seed"] = seed + i
|
||||||
response = requests.request("POST", url, json=payload, headers=headers)
|
response_json = client.post_json(API_ENDPOINTS["image_generate"], payload)
|
||||||
|
images_tensor += self.process_result(response_json)
|
||||||
if response.status_code != 200:
|
|
||||||
raise requests.exceptions.HTTPError(
|
|
||||||
f"HTTP error: {response.status_code}, Response: {response.text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
images_tensor += self.process_result(response.json())
|
|
||||||
|
|
||||||
merged = torch.cat(images_tensor, dim=0)
|
merged = torch.cat(images_tensor, dim=0)
|
||||||
return (merged,)
|
return (merged,)
|
||||||
|
|||||||
+21
-14
@@ -2,21 +2,27 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import torch # type: ignore
|
import torch
|
||||||
import torchaudio # type: ignore
|
import torchaudio
|
||||||
|
|
||||||
from ..globals import API_ENDPOINTS, VENICEAI_BASE_URL
|
from ..globals import API_ENDPOINTS
|
||||||
|
from ..nodes.catalog_utils import tts_model_choices, tts_voice_choices
|
||||||
|
from ..nodes.utils import ensure_prompt_length
|
||||||
|
from ..venice_client import client
|
||||||
|
|
||||||
|
|
||||||
class GenerateSpeech:
|
class GenerateSpeech:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
|
model_options = tts_model_choices()
|
||||||
|
voice_options = tts_voice_choices()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"model": (
|
"model": (
|
||||||
"COMBO",
|
model_options,
|
||||||
{
|
{
|
||||||
"default": "tts-kokoro",
|
"default": model_options[0],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"input": (
|
"input": (
|
||||||
@@ -73,9 +79,9 @@ class GenerateSpeech:
|
|||||||
# },
|
# },
|
||||||
# ),
|
# ),
|
||||||
"voice": (
|
"voice": (
|
||||||
"COMBO",
|
voice_options,
|
||||||
{
|
{
|
||||||
"default": "af_sky - tts-kokoro",
|
"default": voice_options[0],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -89,10 +95,9 @@ class GenerateSpeech:
|
|||||||
EXPERIMENTAL = True
|
EXPERIMENTAL = True
|
||||||
|
|
||||||
def gen_speech(self, model, input, response_format, speed, voice):
|
def gen_speech(self, model, input, response_format, speed, voice):
|
||||||
if len(input) > 4096 or len(input) == 0:
|
ensure_prompt_length(input, 4096, label="Speech input")
|
||||||
raise ValueError("Generate Speech (Venice) Input exceeds the max length of 4096 characters or is empty.")
|
|
||||||
|
|
||||||
url = VENICEAI_BASE_URL + API_ENDPOINTS["speech_generate"]
|
url = API_ENDPOINTS["speech_generate"]
|
||||||
|
|
||||||
# remove everything from voice string after and including the hyphen " - blabla"
|
# remove everything from voice string after and including the hyphen " - blabla"
|
||||||
voice = voice.split(" - ")[0] if " - " in voice else voice
|
voice = voice.split(" - ")[0] if " - " in voice else voice
|
||||||
@@ -107,12 +112,14 @@ class GenerateSpeech:
|
|||||||
"streaming": False,
|
"streaming": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {os.getenv('VENICEAI_API_KEY')}", "Content-Type": "application/json"}
|
|
||||||
|
|
||||||
# Send request
|
# Send request
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, json=payload, headers=headers)
|
response = client.request(
|
||||||
response.raise_for_status()
|
"POST",
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
raise RuntimeError(f"Generate Speech (Venice) API request failed: {str(e)}")
|
raise RuntimeError(f"Generate Speech (Venice) API request failed: {str(e)}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import base64
|
from ..globals import API_ENDPOINTS
|
||||||
import io
|
from ..nodes.catalog_utils import text_model_choices
|
||||||
import os
|
from ..nodes.utils import encode_tensor_for_vision, ensure_prompt_length
|
||||||
|
from ..venice_client import client
|
||||||
import numpy as np
|
|
||||||
import requests
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from ..globals import API_ENDPOINTS, VENICEAI_BASE_URL
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateTextAdvanced:
|
class GenerateTextAdvanced:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
|
model_options = text_model_choices()
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"model": ("COMBO", {"default": "llama-3.1-405b", "tooltip": ("The model to use for text generation.")}),
|
"model": (
|
||||||
|
model_options,
|
||||||
|
{
|
||||||
|
"default": model_options[0],
|
||||||
|
"tooltip": ("The model to use for text generation."),
|
||||||
|
},
|
||||||
|
),
|
||||||
"prompt": (
|
"prompt": (
|
||||||
"STRING",
|
"STRING",
|
||||||
{
|
{
|
||||||
@@ -228,80 +230,17 @@ class GenerateTextAdvanced:
|
|||||||
# endregion
|
# endregion
|
||||||
):
|
):
|
||||||
|
|
||||||
url = VENICEAI_BASE_URL + API_ENDPOINTS["text_generate"]
|
ensure_prompt_length(prompt, 1500, label="Prompt")
|
||||||
|
|
||||||
user_content = []
|
user_content = []
|
||||||
venice_parameters = kwargs.get("venice_parameters", None)
|
venice_parameters = kwargs.get("venice_parameters")
|
||||||
image_for_vision = kwargs.get("image_for_vision", None)
|
image_for_vision = kwargs.get("image_for_vision")
|
||||||
|
|
||||||
if image_for_vision is not None and enable_vision:
|
if image_for_vision is not None and enable_vision:
|
||||||
# Convert tensor to PIL Image
|
encoded_image = encode_tensor_for_vision(image_for_vision[0])
|
||||||
image_tensor = image_for_vision[0] # shape: (H, W, 3)
|
|
||||||
image_np = image_tensor.cpu().numpy() # Still in (H, W, 3)
|
|
||||||
image_np = (image_np * 255).astype(np.uint8) # Scale from [0, 1] to [0, 255] if needed
|
|
||||||
pil_image = Image.fromarray(image_np)
|
|
||||||
|
|
||||||
# Resize image to meet constraints
|
|
||||||
original_width, original_height = pil_image.size
|
|
||||||
aspect_ratio = original_width / original_height
|
|
||||||
|
|
||||||
# Determine target dimensions
|
|
||||||
if original_width > original_height:
|
|
||||||
target_width = 1024
|
|
||||||
target_height = int(target_width / aspect_ratio)
|
|
||||||
if target_height < 256:
|
|
||||||
target_height = 256
|
|
||||||
target_width = int(target_height * aspect_ratio)
|
|
||||||
else:
|
|
||||||
target_height = 1024
|
|
||||||
target_width = int(target_height * aspect_ratio)
|
|
||||||
if target_width < 256:
|
|
||||||
target_width = 256
|
|
||||||
target_height = int(target_width / aspect_ratio)
|
|
||||||
|
|
||||||
# Round dimensions to multiples of 14
|
|
||||||
def round_down_to_multiple(value, multiple):
|
|
||||||
return (value // multiple) * multiple
|
|
||||||
|
|
||||||
target_width = round_down_to_multiple(target_width, 14)
|
|
||||||
target_height = round_down_to_multiple(target_height, 14)
|
|
||||||
|
|
||||||
# Ensure minimum dimension is 256 after rounding
|
|
||||||
if min(target_width, target_height) < 256:
|
|
||||||
if target_width < target_height:
|
|
||||||
target_width = ((256 + 13) // 14) * 14
|
|
||||||
target_height = round_down_to_multiple(int(target_width / aspect_ratio), 14)
|
|
||||||
else:
|
|
||||||
target_height = ((256 + 13) // 14) * 14
|
|
||||||
target_width = round_down_to_multiple(int(target_height * aspect_ratio), 14)
|
|
||||||
|
|
||||||
pil_image = pil_image.resize((target_width, target_height), Image.LANCZOS) # type: ignore
|
|
||||||
|
|
||||||
# Convert to base64 and check size
|
|
||||||
buffered = io.BytesIO()
|
|
||||||
pil_image.save(buffered, format="PNG")
|
|
||||||
img_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
||||||
|
|
||||||
# Resize further if base64 exceeds 4.5MB
|
|
||||||
while len(img_base64) > 4500000:
|
|
||||||
scaling_factor = (4500000 / len(img_base64)) ** 0.5
|
|
||||||
new_width = int(target_width * scaling_factor)
|
|
||||||
new_height = int(target_height * scaling_factor)
|
|
||||||
|
|
||||||
new_width = max(round_down_to_multiple(new_width, 14), 256)
|
|
||||||
new_height = max(round_down_to_multiple(new_height, 14), 256)
|
|
||||||
|
|
||||||
pil_image = pil_image.resize((new_width, new_height), Image.LANCZOS) # type: ignore
|
|
||||||
target_width, target_height = new_width, new_height
|
|
||||||
|
|
||||||
buffered = io.BytesIO()
|
|
||||||
pil_image.save(buffered, format="PNG")
|
|
||||||
img_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
||||||
|
|
||||||
user_content.extend(
|
user_content.extend(
|
||||||
[
|
[
|
||||||
{"type": "text", "text": prompt},
|
{"type": "text", "text": prompt},
|
||||||
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_base64}"}},
|
{"type": "image_url", "image_url": {"url": encoded_image}},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -330,13 +269,7 @@ class GenerateTextAdvanced:
|
|||||||
if venice_parameters is not None:
|
if venice_parameters is not None:
|
||||||
payload["venice_parameters"] = venice_parameters
|
payload["venice_parameters"] = venice_parameters
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {os.getenv('VENICEAI_API_KEY')}", "Content-Type": "application/json"}
|
json_response = client.post_json(API_ENDPOINTS["text_generate"], payload)
|
||||||
response = requests.post(url, json=payload, headers=headers)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
raise requests.exceptions.HTTPError(f"HTTP error: {response.status_code}, Response: {response.text}")
|
|
||||||
|
|
||||||
json_response = response.json()
|
|
||||||
try:
|
try:
|
||||||
content = json_response["choices"][0]["message"]["content"]
|
content = json_response["choices"][0]["message"]["content"]
|
||||||
except (KeyError, IndexError, TypeError) as e:
|
except (KeyError, IndexError, TypeError) as e:
|
||||||
|
|||||||
+13
-85
@@ -1,23 +1,20 @@
|
|||||||
import base64
|
from ..globals import API_ENDPOINTS
|
||||||
import io
|
from ..nodes.catalog_utils import text_model_choices
|
||||||
import os
|
from ..nodes.utils import encode_tensor_for_vision, ensure_prompt_length
|
||||||
|
from ..venice_client import client
|
||||||
import numpy as np
|
|
||||||
import requests
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
from ..globals import API_ENDPOINTS, VENICEAI_BASE_URL
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateText:
|
class GenerateText:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
|
model_choices = text_model_choices()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"model": (
|
"model": (
|
||||||
"COMBO",
|
model_choices,
|
||||||
{
|
{
|
||||||
"default": "llama-3.3-70b",
|
"default": model_choices[0],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
"system_prompt": ("STRING", {"default": "", "multiline": True}),
|
"system_prompt": ("STRING", {"default": "", "multiline": True}),
|
||||||
@@ -50,79 +47,16 @@ class GenerateText:
|
|||||||
enable_vision,
|
enable_vision,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
url = VENICEAI_BASE_URL + API_ENDPOINTS["text_generate"]
|
ensure_prompt_length(prompt, 1500, label="Prompt")
|
||||||
|
|
||||||
user_content = []
|
user_content = []
|
||||||
image_for_vision = kwargs.get("image_for_vision", None)
|
image_for_vision = kwargs.get("image_for_vision")
|
||||||
|
|
||||||
if image_for_vision is not None and enable_vision:
|
if image_for_vision is not None and enable_vision:
|
||||||
# Convert tensor to PIL Image
|
encoded_image = encode_tensor_for_vision(image_for_vision[0])
|
||||||
image_tensor = image_for_vision[0] # shape: (H, W, 3)
|
|
||||||
image_np = image_tensor.cpu().numpy() # Still in (H, W, 3)
|
|
||||||
image_np = (image_np * 255).astype(np.uint8) # Scale from [0, 1] to [0, 255] if needed
|
|
||||||
pil_image = Image.fromarray(image_np)
|
|
||||||
|
|
||||||
# Resize image to meet constraints
|
|
||||||
original_width, original_height = pil_image.size
|
|
||||||
aspect_ratio = original_width / original_height
|
|
||||||
|
|
||||||
# Determine target dimensions
|
|
||||||
if original_width > original_height:
|
|
||||||
target_width = 1024
|
|
||||||
target_height = int(target_width / aspect_ratio)
|
|
||||||
if target_height < 256:
|
|
||||||
target_height = 256
|
|
||||||
target_width = int(target_height * aspect_ratio)
|
|
||||||
else:
|
|
||||||
target_height = 1024
|
|
||||||
target_width = int(target_height * aspect_ratio)
|
|
||||||
if target_width < 256:
|
|
||||||
target_width = 256
|
|
||||||
target_height = int(target_width / aspect_ratio)
|
|
||||||
|
|
||||||
# Round dimensions to multiples of 14
|
|
||||||
def round_down_to_multiple(value, multiple):
|
|
||||||
return (value // multiple) * multiple
|
|
||||||
|
|
||||||
target_width = round_down_to_multiple(target_width, 14)
|
|
||||||
target_height = round_down_to_multiple(target_height, 14)
|
|
||||||
|
|
||||||
# Ensure minimum dimension is 256 after rounding
|
|
||||||
if min(target_width, target_height) < 256:
|
|
||||||
if target_width < target_height:
|
|
||||||
target_width = ((256 + 13) // 14) * 14
|
|
||||||
target_height = round_down_to_multiple(int(target_width / aspect_ratio), 14)
|
|
||||||
else:
|
|
||||||
target_height = ((256 + 13) // 14) * 14
|
|
||||||
target_width = round_down_to_multiple(int(target_height * aspect_ratio), 14)
|
|
||||||
|
|
||||||
pil_image = pil_image.resize((target_width, target_height), Image.LANCZOS) # type: ignore
|
|
||||||
|
|
||||||
# Convert to base64 and check size
|
|
||||||
buffered = io.BytesIO()
|
|
||||||
pil_image.save(buffered, format="PNG")
|
|
||||||
img_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
||||||
|
|
||||||
# Resize further if base64 exceeds 4.5MB
|
|
||||||
while len(img_base64) > 4500000:
|
|
||||||
scaling_factor = (4500000 / len(img_base64)) ** 0.5
|
|
||||||
new_width = int(target_width * scaling_factor)
|
|
||||||
new_height = int(target_height * scaling_factor)
|
|
||||||
|
|
||||||
new_width = max(round_down_to_multiple(new_width, 14), 256)
|
|
||||||
new_height = max(round_down_to_multiple(new_height, 14), 256)
|
|
||||||
|
|
||||||
pil_image = pil_image.resize((new_width, new_height), Image.LANCZOS) # type: ignore
|
|
||||||
target_width, target_height = new_width, new_height
|
|
||||||
|
|
||||||
buffered = io.BytesIO()
|
|
||||||
pil_image.save(buffered, format="PNG")
|
|
||||||
img_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
||||||
|
|
||||||
user_content.extend(
|
user_content.extend(
|
||||||
[
|
[
|
||||||
{"type": "text", "text": prompt},
|
{"type": "text", "text": prompt},
|
||||||
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_base64}"}},
|
{"type": "image_url", "image_url": {"url": encoded_image}},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -140,13 +74,7 @@ class GenerateText:
|
|||||||
"top_p": top_p,
|
"top_p": top_p,
|
||||||
}
|
}
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {os.getenv('VENICEAI_API_KEY')}", "Content-Type": "application/json"}
|
json_response = client.post_json(API_ENDPOINTS["text_generate"], payload)
|
||||||
response = requests.post(url, json=payload, headers=headers)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
raise requests.exceptions.HTTPError(f"HTTP error: {response.status_code}, Response: {response.text}")
|
|
||||||
|
|
||||||
json_response = response.json()
|
|
||||||
content = json_response["choices"][0]["message"]["content"]
|
content = json_response["choices"][0]["message"]["content"]
|
||||||
# print(content)
|
# print(content)
|
||||||
return (content,)
|
return (content,)
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
|
from ..nodes.catalog_utils import character_choices
|
||||||
|
|
||||||
|
|
||||||
class GenerateTextVeniceParameters:
|
class GenerateTextVeniceParameters:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
|
character_options = character_choices()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"required": {
|
"required": {
|
||||||
"character_slug": (
|
"character_slug": (
|
||||||
"COMBO",
|
character_options,
|
||||||
{
|
{
|
||||||
"default": "strawberry-the-cat",
|
"default": character_options[0],
|
||||||
"tooltip": ("The character slug of a public Venice character."),
|
"tooltip": ("The character slug of a public Venice character."),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
from comfy_api.latest import InputImpl, io
|
||||||
|
|
||||||
|
from ..nodes.catalog_utils import image2video_model_choices, text2video_model_choices
|
||||||
|
from ..nodes.utils import ensure_prompt_length
|
||||||
|
from ..nodes.video_utils import (
|
||||||
|
get_testing_video_path,
|
||||||
|
list_testing_videos,
|
||||||
|
poll_video_until_ready,
|
||||||
|
queue_video_job,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class GenerateVideoFromText:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
i2v_models = image2video_model_choices()
|
||||||
|
t2v_models = text2video_model_choices()
|
||||||
|
model_choices = i2v_models + t2v_models
|
||||||
|
# todo: make model choices show model name instead of id for readability and prepend i2v/t2v
|
||||||
|
|
||||||
|
video_choices = list_testing_videos()
|
||||||
|
existing_default = video_choices[0] if video_choices else "none_available"
|
||||||
|
choices_for_combo = video_choices or ["none_available"]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"model": (
|
||||||
|
model_choices,
|
||||||
|
{
|
||||||
|
# "default": model_choices[0],
|
||||||
|
"default": "longcat-distilled-text-to-video",
|
||||||
|
"tooltip": "Model to use for text-to-video generation",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"prompt": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": "A cat made of lettuce flying through space",
|
||||||
|
"placeholder": "Positive Prompt. Example: A cat made of lettuce flying through space",
|
||||||
|
"tooltip": "Text prompt to generate the video from",
|
||||||
|
"multiline": True,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"negative_prompt": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": "low resolution, error, worst quality, low quality, defects",
|
||||||
|
"placeholder": "Negative Prompt",
|
||||||
|
"multiline": True,
|
||||||
|
"tooltip": "Negative prompt to avoid elements in the video",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"duration": (
|
||||||
|
["4s", "5s", "6s", "8s", "10s", "12s", "14s", "15s", "16s", "18s", "20s"],
|
||||||
|
{
|
||||||
|
"default": "5s",
|
||||||
|
"tooltip": "Duration of the generated video",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"aspect_ratio": (
|
||||||
|
["16:9", "9:16", "1:1"],
|
||||||
|
{
|
||||||
|
"default": "16:9",
|
||||||
|
"tooltip": "Aspect ratio for the video",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"resolution": (
|
||||||
|
["1080p", "720p", "480p"],
|
||||||
|
{
|
||||||
|
"default": "720p",
|
||||||
|
"tooltip": "Resolution of the generated video",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"audio": (
|
||||||
|
"BOOLEAN",
|
||||||
|
{
|
||||||
|
"default": True,
|
||||||
|
"tooltip": "Generate audio if the model supports it",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"use_existing_video": (
|
||||||
|
"BOOLEAN",
|
||||||
|
{
|
||||||
|
"default": True, # NOTE: IMPORTANT DEFAULT TO TRUE FOR TESTING PURPOSES THE WHOLE TIME DO NOT REMOVE UNTIL DEPLOYMENT
|
||||||
|
"tooltip": "Use a cached video from testing_video instead of calling the Venice API",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"existing_video": (
|
||||||
|
choices_for_combo,
|
||||||
|
{
|
||||||
|
"default": existing_default,
|
||||||
|
"tooltip": "Select the cached video file that should be emitted when bypassing the API",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("VIDEO",)
|
||||||
|
RETURN_NAMES = ("video",)
|
||||||
|
FUNCTION = "execute"
|
||||||
|
CATEGORY = "venice.ai"
|
||||||
|
|
||||||
|
def execute(
|
||||||
|
self,
|
||||||
|
model,
|
||||||
|
prompt,
|
||||||
|
negative_prompt,
|
||||||
|
duration,
|
||||||
|
aspect_ratio,
|
||||||
|
resolution,
|
||||||
|
audio,
|
||||||
|
use_existing_video,
|
||||||
|
existing_video,
|
||||||
|
):
|
||||||
|
ensure_prompt_length(prompt, 2500, label="Prompt")
|
||||||
|
ensure_prompt_length(negative_prompt, 2500, label="Negative Prompt", allow_empty=True)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"negative_prompt": negative_prompt,
|
||||||
|
"duration": duration,
|
||||||
|
"aspect_ratio": aspect_ratio,
|
||||||
|
"resolution": resolution,
|
||||||
|
# "audio": audio, # todo: this will error with bad request if model without audio support is used, fix with node schema v3 rewrite
|
||||||
|
}
|
||||||
|
|
||||||
|
if use_existing_video:
|
||||||
|
if not existing_video:
|
||||||
|
raise ValueError("No cached video selected")
|
||||||
|
cached_files = list_testing_videos()
|
||||||
|
if existing_video not in cached_files:
|
||||||
|
raise ValueError("Selected cached video does not exist anymore")
|
||||||
|
video_path = get_testing_video_path(existing_video)
|
||||||
|
if not video_path.exists():
|
||||||
|
raise ValueError("Cached video file disappeared")
|
||||||
|
return io.NodeOutput(InputImpl.VideoFromFile(video_path))
|
||||||
|
|
||||||
|
model_id, queue_id = queue_video_job(payload)
|
||||||
|
video_path, _ = poll_video_until_ready(model=model_id, queue_id=queue_id)
|
||||||
|
|
||||||
|
return io.NodeOutput(InputImpl.VideoFromFile(video_path))
|
||||||
|
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"TextToVideo_VENICE": GenerateVideoFromText,
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"TextToVideo_VENICE": "Generate Video from Text (Venice)",
|
||||||
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import base64
|
import base64
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from torchvision.transforms import ToPILImage, ToTensor # type: ignore
|
from torchvision.transforms import ToPILImage, ToTensor # type: ignore
|
||||||
|
|
||||||
from ..globals import API_ENDPOINTS, VENICEAI_BASE_URL
|
from ..globals import API_ENDPOINTS
|
||||||
|
from ..nodes.utils import ensure_prompt_length
|
||||||
|
from ..venice_client import client
|
||||||
|
|
||||||
|
|
||||||
class I2IEnhanceUpscale:
|
class I2IEnhanceUpscale:
|
||||||
@@ -86,10 +87,9 @@ class I2IEnhanceUpscale:
|
|||||||
CATEGORY = "venice.ai"
|
CATEGORY = "venice.ai"
|
||||||
|
|
||||||
def i2i_enhance_upscale(self, image, scale, enhance, enhance_creativity, enhance_prompt, replication):
|
def i2i_enhance_upscale(self, image, scale, enhance, enhance_creativity, enhance_prompt, replication):
|
||||||
url = VENICEAI_BASE_URL + API_ENDPOINTS["upscale_image"]
|
response = None
|
||||||
|
|
||||||
if len(enhance_prompt) > 1500:
|
ensure_prompt_length(enhance_prompt, 1500, label="Enhance prompt", allow_empty=True)
|
||||||
raise ValueError("Upscale Image (Venice) enhance_prompt cannot be above 1500 characters")
|
|
||||||
if scale == 1:
|
if scale == 1:
|
||||||
raise ValueError("Upscale Image (Venice) 'enhance' must be set to 'True' if scale is 1.")
|
raise ValueError("Upscale Image (Venice) 'enhance' must be set to 'True' if scale is 1.")
|
||||||
if scale == 4:
|
if scale == 4:
|
||||||
@@ -134,12 +134,14 @@ class I2IEnhanceUpscale:
|
|||||||
"replication": replication,
|
"replication": replication,
|
||||||
}
|
}
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {os.getenv('VENICEAI_API_KEY')}", "Content-Type": "application/json"}
|
|
||||||
|
|
||||||
# Send request
|
# Send request
|
||||||
try:
|
try:
|
||||||
response = requests.post(url, json=payload, headers=headers)
|
response = client.request(
|
||||||
response.raise_for_status()
|
"POST",
|
||||||
|
API_ENDPOINTS["upscale_image"],
|
||||||
|
json=payload,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
)
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
raise RuntimeError(f"Upscale Image (Venice) API request failed: {str(e)}")
|
raise RuntimeError(f"Upscale Image (Venice) API request failed: {str(e)}")
|
||||||
|
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
# Todo: chat history/memory/context for LLM
|
|
||||||
|
|
||||||
# todo: inpainting
|
|
||||||
|
|
||||||
# todo: LLM characters
|
|
||||||
|
|
||||||
# todo: use variants api (currently in beta)
|
|
||||||
|
|
||||||
|
|
||||||
# import base64
|
|
||||||
# import io
|
|
||||||
# import os
|
|
||||||
|
|
||||||
# import numpy as np
|
|
||||||
# import requests
|
|
||||||
# import torch
|
|
||||||
# from PIL import Image
|
|
||||||
|
|
||||||
# from ..globals import API_ENDPOINTS, VENICEAI_BASE_URL
|
|
||||||
# from ..nodes.gen_image_base import GenerateImageBase
|
|
||||||
|
|
||||||
# todo: https://docs.comfy.org/custom-nodes/backend/more_on_inputs#dynamically-created-inputs
|
|
||||||
# for model list update?
|
|
||||||
|
|
||||||
|
|
||||||
# region inpaint
|
|
||||||
# class InpaintImage(GenerateImageBase):
|
|
||||||
# @classmethod
|
|
||||||
# def INPUT_TYPES(cls):
|
|
||||||
# return {
|
|
||||||
# "required": {
|
|
||||||
# "image": ("IMAGE",),
|
|
||||||
# "model": (
|
|
||||||
# "COMBO",
|
|
||||||
# {"default": "flux-dev"},
|
|
||||||
# ),
|
|
||||||
# "prompt": ("STRING", {"default": "A flying cat made of lettuce", "multiline": True}),
|
|
||||||
# "neg_prompt": (
|
|
||||||
# "STRING",
|
|
||||||
# {
|
|
||||||
# "placeholder": "Bad composition, rating_explicit, Text, signature, lowres, lowres, low details, faded image, out of focus, cropped, clipped, cut-off, out of frame, deserted scene, empty scene, vacant scene, desolate scene, sparse décor, bad quality, worst quality,",
|
|
||||||
# "multiline": True,
|
|
||||||
# "tooltip": "Negative prompt. This is ignored when using flux-dev or flux-dev-uncensored",
|
|
||||||
# },
|
|
||||||
# ),
|
|
||||||
# "width": (
|
|
||||||
# "INT",
|
|
||||||
# {
|
|
||||||
# "default": 1024,
|
|
||||||
# "min": 256,
|
|
||||||
# "max": 2048, # limit is 1280 but i dont want to restrict this in case of future updates, https://docs.venice.ai/api-reference/endpoint/image/generate#body-height
|
|
||||||
# "step": 32,
|
|
||||||
# "tooltip": "Must be a multiple of 32. Maximum allowed by venice.ai is 1280",
|
|
||||||
# },
|
|
||||||
# ),
|
|
||||||
# "height": (
|
|
||||||
# "INT",
|
|
||||||
# {
|
|
||||||
# "default": 1024,
|
|
||||||
# "min": 256,
|
|
||||||
# "max": 2048,
|
|
||||||
# "step": 32,
|
|
||||||
# "tooltip": "Must be a multiple of 32. Maximum allowed by venice.ai is 1280",
|
|
||||||
# },
|
|
||||||
# ),
|
|
||||||
# "batch_size": ("INT", {"default": 1, "min:": 1, "max": 4}),
|
|
||||||
# "steps": ("INT", {"default": 20, "min": 1, "max": 30}),
|
|
||||||
# "guidance": ("FLOAT", {"default": 3.0, "min": 0.1, "max": 15.0}),
|
|
||||||
# "style_preset": ("COMBO", {"default": "none"}),
|
|
||||||
# "hide_watermark": ("BOOLEAN", {"default": True}),
|
|
||||||
# "inpaint_strength": ("INT", {"default": 50, "min": 0, "max": 100}),
|
|
||||||
# },
|
|
||||||
# "optional": {"seed": ("INT", {"default": -1})},
|
|
||||||
# }
|
|
||||||
|
|
||||||
# def generate_image(
|
|
||||||
# self,
|
|
||||||
# image,
|
|
||||||
# model,
|
|
||||||
# prompt,
|
|
||||||
# neg_prompt,
|
|
||||||
# width,
|
|
||||||
# height,
|
|
||||||
# batch_size,
|
|
||||||
# steps,
|
|
||||||
# guidance,
|
|
||||||
# style_preset,
|
|
||||||
# hide_watermark,
|
|
||||||
# inpaint_strength,
|
|
||||||
# seed=-1,
|
|
||||||
# ):
|
|
||||||
# images_tensor = () # empty tuple for tensors
|
|
||||||
|
|
||||||
# if model in ["flux-dev", "flux-dev-uncensored"]:
|
|
||||||
# print(f"VeniceAPI INFO: Ignoring negative prompt for {model}.")
|
|
||||||
# neg_prompt = ""
|
|
||||||
|
|
||||||
# try:
|
|
||||||
# self.check_multiple_of_32(width, height) # todo: make this be validate node instead
|
|
||||||
|
|
||||||
# # Convert input image tensor to base64
|
|
||||||
# if image is None or image.size(0) == 0:
|
|
||||||
# raise ValueError("Input image is required for inpainting")
|
|
||||||
|
|
||||||
# # Process first image in the batch
|
|
||||||
# img_tensor = image[0].cpu() # Convert to CPU tensor
|
|
||||||
# np_image = img_tensor.numpy()
|
|
||||||
# np_image = (np_image * 255).astype(np.uint8) # Convert to 0-255 range
|
|
||||||
|
|
||||||
# # Create PIL Image and convert to base64
|
|
||||||
# pil_image = Image.fromarray(np_image, "RGB")
|
|
||||||
# buffered = io.BytesIO()
|
|
||||||
# pil_image.save(buffered, format="PNG")
|
|
||||||
# img_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
||||||
# source_image_base64 = f"data:image/png;base64,{img_base64}"
|
|
||||||
|
|
||||||
# headers = {"Authorization": f"Bearer {os.getenv('VENICEAI_API_KEY')}", "Content-Type": "application/json"}
|
|
||||||
# url = VENICEAI_BASE_URL + API_ENDPOINTS["image_generate"]
|
|
||||||
|
|
||||||
# payload = {
|
|
||||||
# "model": model,
|
|
||||||
# "prompt": prompt,
|
|
||||||
# "negative_prompt": neg_prompt,
|
|
||||||
# "style_preset": style_preset,
|
|
||||||
# "height": height,
|
|
||||||
# "width": width,
|
|
||||||
# "steps": steps,
|
|
||||||
# "cfg_scale": guidance,
|
|
||||||
# "seed": seed,
|
|
||||||
# "return_binary": False,
|
|
||||||
# "hide_watermark": hide_watermark,
|
|
||||||
# "format": "png",
|
|
||||||
# "inpaint": {
|
|
||||||
# "strength": inpaint_strength,
|
|
||||||
# "source_image_base64": source_image_base64,
|
|
||||||
# "mask": {
|
|
||||||
# "image_prompt": "Generate a high-resolution image...",
|
|
||||||
# "object_target": "rabbit's face",
|
|
||||||
# "inferred_object": "rabbit's face wearing round silver spectacles",
|
|
||||||
# },
|
|
||||||
# },
|
|
||||||
# }
|
|
||||||
# if style_preset == "none":
|
|
||||||
# del payload["style_preset"]
|
|
||||||
|
|
||||||
# for i in range(batch_size):
|
|
||||||
# payload["seed"] = seed + i
|
|
||||||
# response = requests.request("POST", url, json=payload, headers=headers)
|
|
||||||
|
|
||||||
# if response.status_code != 200:
|
|
||||||
# raise requests.exceptions.HTTPError(
|
|
||||||
# f"HTTP error: {response.status_code}, Response: {response.text}"
|
|
||||||
# )
|
|
||||||
|
|
||||||
# images_tensor += self.process_result(response.json())
|
|
||||||
|
|
||||||
# merged = torch.cat(images_tensor, dim=0)
|
|
||||||
# return (merged,)
|
|
||||||
|
|
||||||
# except Exception as e:
|
|
||||||
# raise Exception(f"Error processing image result: {str(e)}") from e
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
|
|
||||||
# region text gen
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
|
|
||||||
# region upscale img
|
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
|
|
||||||
# NODE_CLASS_MAPPINGS = {
|
|
||||||
# "InpaintImage_VENICE": InpaintImage,
|
|
||||||
# }
|
|
||||||
|
|
||||||
# NODE_DISPLAY_NAME_MAPPINGS = {
|
|
||||||
# "InpaintImage_VENICE": "Inpaint Image (Venice)",
|
|
||||||
# }
|
|
||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
from PIL import Image
|
||||||
|
from requests import RequestException
|
||||||
|
|
||||||
|
from ..globals import API_ENDPOINTS
|
||||||
|
from ..venice_client import VeniceAPIError, client
|
||||||
|
from ..venice_config import config as venice_config
|
||||||
|
|
||||||
|
|
||||||
|
def _round_down(value: int, multiple: int) -> int:
|
||||||
|
return (value // multiple) * multiple
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_prompt_length(text: str, maximum: int, label: str, allow_empty: bool = False) -> None:
|
||||||
|
if not text:
|
||||||
|
if allow_empty:
|
||||||
|
return
|
||||||
|
raise ValueError(f"{label} is required and cannot be empty")
|
||||||
|
if len(text) > maximum:
|
||||||
|
raise ValueError(f"{label} exceeds the maximum length of {maximum} characters")
|
||||||
|
|
||||||
|
|
||||||
|
def encode_tensor_for_vision(image_tensor: torch.Tensor, *, max_encoded_bytes: int = 4_500_000) -> str:
|
||||||
|
"""
|
||||||
|
Encode a vision tensor into a Base64-encoded PNG string suitable for transmission.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
image_tensor : torch.Tensor
|
||||||
|
A height×width×(3 or 4) tensor representing an image in the range [0, 1].
|
||||||
|
max_encoded_bytes : int, optional
|
||||||
|
Maximum allowed length of the Base64 payload. Defaults to 4,500,000 bytes.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
A data URI containing the PNG image encoded in Base64.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
ValueError
|
||||||
|
If the tensor does not have 3 dimensions or the last dimension is not 3 or 4.
|
||||||
|
"""
|
||||||
|
tensor = image_tensor.detach().cpu()
|
||||||
|
if tensor.ndim != 3 or tensor.shape[-1] not in {3, 4}:
|
||||||
|
raise ValueError("Vision images must be height×width×(3 or 4 channels)")
|
||||||
|
|
||||||
|
if tensor.shape[-1] > 3:
|
||||||
|
tensor = tensor[:, :, :3]
|
||||||
|
|
||||||
|
array = (tensor.numpy() * 255).clip(0, 255).astype(np.uint8)
|
||||||
|
pil_image = Image.fromarray(array, "RGB")
|
||||||
|
|
||||||
|
original_width, original_height = pil_image.size
|
||||||
|
aspect_ratio = original_width / original_height
|
||||||
|
|
||||||
|
if original_width > original_height:
|
||||||
|
target_width = 1024
|
||||||
|
target_height = int(target_width / aspect_ratio)
|
||||||
|
if target_height < 256:
|
||||||
|
target_height = 256
|
||||||
|
target_width = int(target_height * aspect_ratio)
|
||||||
|
else:
|
||||||
|
target_height = 1024
|
||||||
|
target_width = int(target_height * aspect_ratio)
|
||||||
|
if target_width < 256:
|
||||||
|
target_width = 256
|
||||||
|
target_height = int(target_width / aspect_ratio)
|
||||||
|
|
||||||
|
target_width = _round_down(target_width, 14)
|
||||||
|
target_height = _round_down(target_height, 14)
|
||||||
|
|
||||||
|
if min(target_width, target_height) < 256:
|
||||||
|
if target_width < target_height:
|
||||||
|
target_width = _round_down(256 + 13, 14)
|
||||||
|
target_height = _round_down(int(target_width / aspect_ratio), 14)
|
||||||
|
else:
|
||||||
|
target_height = _round_down(256 + 13, 14)
|
||||||
|
target_width = _round_down(int(target_height * aspect_ratio), 14)
|
||||||
|
|
||||||
|
pil_image = pil_image.resize((target_width, target_height), Image.LANCZOS)
|
||||||
|
|
||||||
|
buffered = io.BytesIO()
|
||||||
|
pil_image.save(buffered, format="PNG")
|
||||||
|
img_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||||
|
|
||||||
|
while len(img_base64) > max_encoded_bytes:
|
||||||
|
scaling_factor = (max_encoded_bytes / len(img_base64)) ** 0.5
|
||||||
|
new_width = max(_round_down(int(target_width * scaling_factor), 14), 256)
|
||||||
|
new_height = max(_round_down(int(target_height * scaling_factor), 14), 256)
|
||||||
|
|
||||||
|
pil_image = pil_image.resize((new_width, new_height), Image.LANCZOS)
|
||||||
|
target_width, target_height = new_width, new_height
|
||||||
|
|
||||||
|
buffered = io.BytesIO()
|
||||||
|
pil_image.save(buffered, format="PNG")
|
||||||
|
img_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
||||||
|
|
||||||
|
return f"data:image/png;base64,{img_base64}"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_multiple_of(width: int, height: int, *, multiple: int = 32) -> None:
|
||||||
|
bad_dimensions = []
|
||||||
|
if width % multiple != 0:
|
||||||
|
bad_dimensions.append(f"width ({width})")
|
||||||
|
if height % multiple != 0:
|
||||||
|
bad_dimensions.append(f"height ({height})")
|
||||||
|
if bad_dimensions:
|
||||||
|
dimensions = " and ".join(bad_dimensions)
|
||||||
|
raise ValueError(f"{dimensions} must be multiples of {multiple}")
|
||||||
|
|
||||||
|
# unused right now, might be useful, or not
|
||||||
|
def ensure_api_key_valid() -> None:
|
||||||
|
key = venice_config.apikey.strip()
|
||||||
|
if not key:
|
||||||
|
raise ValueError("VeniceAI API key is missing; set it in the VeniceAI settings first.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
client.get_json(API_ENDPOINTS["list_api_keys"])
|
||||||
|
except VeniceAPIError as exc:
|
||||||
|
raise ValueError("Unable to validate the VeniceAI API key.", exc) from exc
|
||||||
|
except RequestException as exc:
|
||||||
|
raise ValueError("Unable to reach VeniceAI while validating the API key.", exc) from exc
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
from comfy.utils import ProgressBar # type: ignore
|
||||||
|
|
||||||
|
from ..globals import API_ENDPOINTS
|
||||||
|
from ..venice_client import VeniceAPIError, client
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
POLL_INTERVAL_SECONDS = 5
|
||||||
|
MAX_POLLS = 100 # MAX_POLLS * POLL_INTERVAL_SECONDS = x minutes total wait time
|
||||||
|
PROGRESS_BAR_TOTAL = 100
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
VIDEO_OUTPUT_DIR = PROJECT_ROOT / "testing_video"
|
||||||
|
VIDEO_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
DEBUG_SAVE_API_VIDEOS = os.environ.get("VENICE_CLIENT_DEBUG", "").lower() in {"1", "true"}
|
||||||
|
|
||||||
|
|
||||||
|
def queue_video_job(payload: dict) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Enqueue a Venice video generation job and return (model, queue_id).
|
||||||
|
Raises VeniceAPIError on failure.
|
||||||
|
"""
|
||||||
|
resp = client.post_json(API_ENDPOINTS["video_queue"], payload)
|
||||||
|
model = resp.get("model") or payload.get("model") or ""
|
||||||
|
queue_id = resp.get("queue_id")
|
||||||
|
if not queue_id:
|
||||||
|
raise VeniceAPIError(f"Video queue response missing queue_id: {resp}")
|
||||||
|
LOG.info("Venice video queued: model=%s queue_id=%s", model, queue_id)
|
||||||
|
return model, queue_id
|
||||||
|
|
||||||
|
|
||||||
|
def _guess_suffix(content_type: str) -> str:
|
||||||
|
if "mp4" in content_type:
|
||||||
|
return ".mp4"
|
||||||
|
if "webm" in content_type:
|
||||||
|
return ".webm"
|
||||||
|
if "quicktime" in content_type or "mov" in content_type:
|
||||||
|
return ".mov"
|
||||||
|
return ".bin"
|
||||||
|
|
||||||
|
|
||||||
|
def _build_video_path(queue_id: str, suffix: str) -> Path:
|
||||||
|
clean_id = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in queue_id)
|
||||||
|
timestamp = int(time.time())
|
||||||
|
filename = f"{clean_id or 'queue'}_{timestamp}{suffix}"
|
||||||
|
return VIDEO_OUTPUT_DIR / filename
|
||||||
|
|
||||||
|
|
||||||
|
def poll_video_until_ready(
|
||||||
|
*,
|
||||||
|
model: str,
|
||||||
|
queue_id: str,
|
||||||
|
progress_bar: Optional[ProgressBar] = None,
|
||||||
|
delete_on_completion: bool = True,
|
||||||
|
max_polls: int = MAX_POLLS,
|
||||||
|
poll_interval: float = POLL_INTERVAL_SECONDS,
|
||||||
|
) -> Tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Poll Venice /video/retrieve until the video is ready.
|
||||||
|
Returns (video_filepath, queue_id). Raises VeniceAPIError on failure/timeout.
|
||||||
|
"""
|
||||||
|
pbar = progress_bar or ProgressBar(PROGRESS_BAR_TOTAL)
|
||||||
|
last_progress = 0
|
||||||
|
|
||||||
|
for attempt in range(max_polls):
|
||||||
|
time.sleep(poll_interval)
|
||||||
|
|
||||||
|
retrieve_payload = {
|
||||||
|
"model": model,
|
||||||
|
"queue_id": queue_id,
|
||||||
|
"delete_media_on_completion": delete_on_completion,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp = client.request(
|
||||||
|
"POST",
|
||||||
|
API_ENDPOINTS["video_retrieve"],
|
||||||
|
json=retrieve_payload,
|
||||||
|
headers={
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "*/*", # allow binary video or JSON status
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ctype = (resp.headers or {}).get("Content-Type", "").lower()
|
||||||
|
|
||||||
|
is_status_json = ctype.startswith("application/json") or ctype.startswith("text/")
|
||||||
|
if is_status_json:
|
||||||
|
data = resp.json()
|
||||||
|
status = (data.get("status") or "UNKNOWN").upper()
|
||||||
|
exec_dur = float(data.get("execution_duration") or 0)
|
||||||
|
avg_exec = float(data.get("average_execution_time") or 0)
|
||||||
|
|
||||||
|
LOG.debug(
|
||||||
|
"Venice video status: status=%s queue_id=%s exec_ms=%s avg_ms=%s attempt=%s/%s",
|
||||||
|
status,
|
||||||
|
queue_id,
|
||||||
|
exec_dur,
|
||||||
|
avg_exec,
|
||||||
|
attempt + 1,
|
||||||
|
max_polls,
|
||||||
|
)
|
||||||
|
|
||||||
|
if avg_exec and avg_exec > 0:
|
||||||
|
progress_value = min(int(exec_dur / avg_exec * PROGRESS_BAR_TOTAL), PROGRESS_BAR_TOTAL)
|
||||||
|
else:
|
||||||
|
progress_value = min(last_progress + 1, PROGRESS_BAR_TOTAL)
|
||||||
|
|
||||||
|
delta = progress_value - last_progress
|
||||||
|
if delta > 0:
|
||||||
|
pbar.update(delta)
|
||||||
|
last_progress = progress_value
|
||||||
|
|
||||||
|
if status not in {"PROCESSING", "QUEUED"}:
|
||||||
|
raise VeniceAPIError(f"Video job reported unexpected status '{status}' for queue_id {queue_id}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Got binary content (video)
|
||||||
|
suffix = _guess_suffix(ctype)
|
||||||
|
if DEBUG_SAVE_API_VIDEOS:
|
||||||
|
video_path = _build_video_path(queue_id, suffix)
|
||||||
|
with open(video_path, "wb") as fp:
|
||||||
|
fp.write(resp.content)
|
||||||
|
else:
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp_file:
|
||||||
|
tmp_file.write(resp.content)
|
||||||
|
video_path = Path(tmp_file.name)
|
||||||
|
|
||||||
|
LOG.debug(
|
||||||
|
"Venice video ready: queue_id=%s saved_to=%s content_type=%s",
|
||||||
|
queue_id,
|
||||||
|
video_path,
|
||||||
|
ctype,
|
||||||
|
)
|
||||||
|
|
||||||
|
if last_progress < PROGRESS_BAR_TOTAL:
|
||||||
|
pbar.update(PROGRESS_BAR_TOTAL - last_progress)
|
||||||
|
|
||||||
|
return str(video_path), queue_id
|
||||||
|
|
||||||
|
raise VeniceAPIError(
|
||||||
|
f"Timed out waiting for Venice video. queue_id={queue_id} after {max_polls * poll_interval:.0f}s"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_testing_videos() -> List[str]:
|
||||||
|
"""Return cached video filenames that can be selected inside a node."""
|
||||||
|
if not VIDEO_OUTPUT_DIR.exists():
|
||||||
|
return []
|
||||||
|
return sorted(entry.name for entry in VIDEO_OUTPUT_DIR.iterdir() if entry.is_file())
|
||||||
|
|
||||||
|
|
||||||
|
def get_testing_video_path(filename: str) -> Path:
|
||||||
|
"""Resolve the video file inside testing_video and ensure it exists."""
|
||||||
|
video_path = VIDEO_OUTPUT_DIR / filename
|
||||||
|
if not video_path.exists() or not video_path.is_file():
|
||||||
|
raise FileNotFoundError(f"Test video not found: {filename}")
|
||||||
|
return video_path
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from . import routes # noqa: F401
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
from server import PromptServer
|
|
||||||
|
|
||||||
routes = PromptServer.instance.routes
|
|
||||||
|
|
||||||
CONFIG_FILE = Path(__file__).parent.parent / "veniceai_config.json"
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_config_file_exists():
|
|
||||||
# Create the config file with a default value if it doesn't exist
|
|
||||||
if not CONFIG_FILE.exists():
|
|
||||||
default_key = "your_venice_api_key_here" # Set a default value (e.g., empty string)
|
|
||||||
set_venice_key(default_key)
|
|
||||||
|
|
||||||
|
|
||||||
def set_venice_key(apikey):
|
|
||||||
os.environ["VENICEAI_API_KEY"] = apikey
|
|
||||||
|
|
||||||
with open(CONFIG_FILE, "w") as f:
|
|
||||||
json.dump({"apikey": apikey}, f)
|
|
||||||
|
|
||||||
|
|
||||||
@routes.post("/veniceai/save_apikey")
|
|
||||||
async def post_key_server(request):
|
|
||||||
data = await request.json()
|
|
||||||
|
|
||||||
set_venice_key(data.get("apikey", ""))
|
|
||||||
|
|
||||||
return web.json_response({})
|
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/veniceai/get_apikey")
|
|
||||||
async def get_key_server(request):
|
|
||||||
return web.json_response({"apikey": os.getenv("VENICEAI_API_KEY")})
|
|
||||||
|
|
||||||
|
|
||||||
ensure_config_file_exists()
|
|
||||||
|
|
||||||
|
|
||||||
if CONFIG_FILE.exists():
|
|
||||||
with open(CONFIG_FILE) as f:
|
|
||||||
saved_key = json.load(f).get("apikey", "")
|
|
||||||
set_venice_key(saved_key)
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from server import PromptServer # type: ignore
|
||||||
|
|
||||||
|
from ..venice_catalog import (
|
||||||
|
get_characters,
|
||||||
|
get_models,
|
||||||
|
get_styles,
|
||||||
|
refresh_characters,
|
||||||
|
refresh_models,
|
||||||
|
refresh_styles,
|
||||||
|
)
|
||||||
|
from ..venice_config import config as venice_config
|
||||||
|
|
||||||
|
routes = PromptServer.instance.routes
|
||||||
|
|
||||||
|
|
||||||
|
def _error_response(message: str) -> web.Response:
|
||||||
|
return web.json_response({"message": message, "error": True})
|
||||||
|
|
||||||
|
|
||||||
|
@routes.post("/veniceai/save_apikey")
|
||||||
|
async def save_key_server(request: web.Request) -> web.Response:
|
||||||
|
payload = await request.json()
|
||||||
|
venice_config.save_apikey(payload.get("apikey", ""))
|
||||||
|
return web.json_response({"message": "API key saved", "error": False})
|
||||||
|
|
||||||
|
|
||||||
|
@routes.get("/veniceai/get_apikey")
|
||||||
|
async def get_key_server(_: web.Request) -> web.Response:
|
||||||
|
return web.json_response({"apikey": venice_config.apikey})
|
||||||
|
|
||||||
|
|
||||||
|
# @routes.get("/veniceai/update_models_list")
|
||||||
|
# async def update_models_list_server(request: web.Request) -> web.Response:
|
||||||
|
# model_type = request.rel_url.query.get("type")
|
||||||
|
# try:
|
||||||
|
# payload = await asyncio.to_thread(refresh_models, model_type)
|
||||||
|
# except Exception as exc:
|
||||||
|
# return _error_response(str(exc))
|
||||||
|
# return web.json_response({"message": "Model list updated", "error": False, "data": payload})
|
||||||
|
|
||||||
|
|
||||||
|
# @routes.get("/veniceai/get_models_list")
|
||||||
|
# async def get_models_list_server(request: web.Request) -> web.Response:
|
||||||
|
# model_type = request.rel_url.query.get("type")
|
||||||
|
# try:
|
||||||
|
# payload = await asyncio.to_thread(get_models, model_type)
|
||||||
|
# except Exception as exc:
|
||||||
|
# return _error_response(str(exc))
|
||||||
|
# return web.json_response(payload)
|
||||||
|
|
||||||
|
|
||||||
|
# @routes.get("/veniceai/update_styles_list")
|
||||||
|
# async def update_styles_list_server(_: web.Request) -> web.Response:
|
||||||
|
# try:
|
||||||
|
# payload = await asyncio.to_thread(refresh_styles)
|
||||||
|
# except Exception as exc:
|
||||||
|
# return _error_response(str(exc))
|
||||||
|
# return web.json_response({"message": "Styles updated", "error": False, "data": payload})
|
||||||
|
|
||||||
|
|
||||||
|
# @routes.get("/veniceai/get_styles_list")
|
||||||
|
# async def get_styles_list_server(_: web.Request) -> web.Response:
|
||||||
|
# try:
|
||||||
|
# payload = await asyncio.to_thread(get_styles)
|
||||||
|
# except Exception as exc:
|
||||||
|
# return _error_response(str(exc))
|
||||||
|
# return web.json_response(payload)
|
||||||
|
|
||||||
|
|
||||||
|
# @routes.get("/veniceai/update_characters_list")
|
||||||
|
# async def update_characters_list_server(_: web.Request) -> web.Response:
|
||||||
|
# try:
|
||||||
|
# payload = await asyncio.to_thread(refresh_characters)
|
||||||
|
# except Exception as exc:
|
||||||
|
# return _error_response(str(exc))
|
||||||
|
# return web.json_response({"message": "Characters updated", "error": False, "data": payload})
|
||||||
|
|
||||||
|
|
||||||
|
# @routes.get("/veniceai/get_characters_list")
|
||||||
|
# async def get_characters_list_server(_: web.Request) -> web.Response:
|
||||||
|
# try:
|
||||||
|
# payload = await asyncio.to_thread(get_characters)
|
||||||
|
# except Exception as exc:
|
||||||
|
# return _error_response(str(exc))
|
||||||
|
# return web.json_response(payload)
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
from server import PromptServer # type: ignore
|
|
||||||
|
|
||||||
from ..globals import API_ENDPOINTS, VENICEAI_BASE_URL
|
|
||||||
|
|
||||||
routes = PromptServer.instance.routes
|
|
||||||
|
|
||||||
script_dir = Path(__file__).resolve().parent
|
|
||||||
data_dir = script_dir.parent / "data"
|
|
||||||
data_dir.mkdir(exist_ok=True)
|
|
||||||
characters_list_path = data_dir / "characters_list.json"
|
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/veniceai/update_characters_list")
|
|
||||||
async def update_characters_list_server(request):
|
|
||||||
try:
|
|
||||||
headers = {"Authorization": f"Bearer {os.getenv('VENICEAI_API_KEY')}"}
|
|
||||||
url = f"{VENICEAI_BASE_URL}{API_ENDPOINTS['characters']}"
|
|
||||||
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
response.raise_for_status() # Raises HTTPError for bad responses
|
|
||||||
|
|
||||||
response_data = response.json()
|
|
||||||
|
|
||||||
# remove "object" key from response_data
|
|
||||||
response_data.pop("object", None)
|
|
||||||
|
|
||||||
response_data["data"] = sorted(response_data.get("data", []), key=lambda item: item.get("slug", ""))
|
|
||||||
|
|
||||||
with open(characters_list_path, "w") as f:
|
|
||||||
json.dump(response_data, f, indent=4)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
return (f"Unexpected error: {e}", True)
|
|
||||||
|
|
||||||
response = (response_data, False)
|
|
||||||
return web.json_response({"message": response[0], "error": response[1]})
|
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/veniceai/get_characters_list")
|
|
||||||
async def get_local_characters_list(request):
|
|
||||||
with open(characters_list_path, "r") as f:
|
|
||||||
characters_list_json = json.load(f)
|
|
||||||
|
|
||||||
characters = []
|
|
||||||
for item in characters_list_json.get("data", []):
|
|
||||||
if isinstance(item, dict) and item.get("slug"):
|
|
||||||
characters.append(item.get("slug"))
|
|
||||||
|
|
||||||
return web.json_response({"characters": characters})
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
from server import PromptServer # type: ignore
|
|
||||||
|
|
||||||
from ..globals import API_ENDPOINTS, VENICEAI_BASE_URL
|
|
||||||
|
|
||||||
routes = PromptServer.instance.routes
|
|
||||||
|
|
||||||
script_dir = Path(__file__).resolve().parent
|
|
||||||
data_dir = script_dir.parent / "data"
|
|
||||||
data_dir.mkdir(exist_ok=True)
|
|
||||||
all_model_list_path = data_dir / "all_model_list.json"
|
|
||||||
|
|
||||||
|
|
||||||
async def fetch_models_list():
|
|
||||||
try:
|
|
||||||
headers = {"Authorization": f"Bearer {os.getenv('VENICEAI_API_KEY')}"}
|
|
||||||
url = f"{VENICEAI_BASE_URL}{API_ENDPOINTS['list_models']}"
|
|
||||||
params = {"type": "all"}
|
|
||||||
|
|
||||||
response = requests.get(url, headers=headers, params=params)
|
|
||||||
response.raise_for_status() # Raises HTTPError for bad responses
|
|
||||||
|
|
||||||
response_data = response.json()
|
|
||||||
|
|
||||||
# params = {"type": "embedding"}
|
|
||||||
|
|
||||||
# r2 = requests.get(url, headers=headers, params=params)
|
|
||||||
# r2.raise_for_status() # Raises HTTPError for bad responses
|
|
||||||
|
|
||||||
# r2_data = r2.json()
|
|
||||||
|
|
||||||
# # concatenate response_data and r2_data
|
|
||||||
# if isinstance(response_data, list) and isinstance(r2_data, list):
|
|
||||||
# response_data = response_data + r2_data
|
|
||||||
|
|
||||||
# except requests.exceptions.HTTPError as http_err:
|
|
||||||
# print(f"HTTP error occurred: {http_err}")
|
|
||||||
# except requests.exceptions.RequestException as req_err:
|
|
||||||
# print(f"Request error occurred: {req_err}")
|
|
||||||
# except (ValueError, KeyError) as data_err:
|
|
||||||
# print(f"Data parsing error: {data_err}")
|
|
||||||
except Exception as e:
|
|
||||||
return (f"Unexpected error: {e}", True)
|
|
||||||
|
|
||||||
return (response_data, False)
|
|
||||||
|
|
||||||
|
|
||||||
async def humanize_name(model_id: str) -> str:
|
|
||||||
|
|
||||||
# Handle number-letter combinations without space
|
|
||||||
model_id = re.sub(r"(\d+)([a-zA-Z]+)", lambda m: f"{m.group(1)}{m.group(2).upper()}", model_id)
|
|
||||||
|
|
||||||
# Replace remaining separators with spaces and title case
|
|
||||||
model_id = re.sub(r"[-_]", " ", model_id).title()
|
|
||||||
|
|
||||||
humanized = model_id.replace("xl", "XL").replace("vl", "VL").replace("sd", "SD").replace("llama", "LLaMA")
|
|
||||||
|
|
||||||
return humanized
|
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/veniceai/update_models_list")
|
|
||||||
async def update_models_list_server(request):
|
|
||||||
# img_model_json = await fetch_model_list("image")
|
|
||||||
# txt_model_json = await fetch_model_list("text")
|
|
||||||
# merged_json = {"object": "list", "data": img_model_json.get("data", []) + txt_model_json.get("data", [])}
|
|
||||||
|
|
||||||
response = await fetch_models_list()
|
|
||||||
|
|
||||||
if response[1]: # true = error happened
|
|
||||||
return response
|
|
||||||
|
|
||||||
merged_json = response[0]
|
|
||||||
|
|
||||||
# Validating response structure
|
|
||||||
if not isinstance(merged_json, dict) or "data" not in merged_json:
|
|
||||||
raise ValueError("Unexpected API response format")
|
|
||||||
|
|
||||||
# Create lookup dictionary and enhanced JSON
|
|
||||||
enhanced_data = []
|
|
||||||
model_dict = {}
|
|
||||||
|
|
||||||
# Process all models for enhanced JSON
|
|
||||||
for model in merged_json["data"]:
|
|
||||||
model_id = model["id"]
|
|
||||||
model_spec = model.get("model_spec", {})
|
|
||||||
|
|
||||||
# Format traits excluding function_calling_default
|
|
||||||
traits = [
|
|
||||||
trait.replace("_", " ").title()
|
|
||||||
for trait in model_spec.get("traits", [])
|
|
||||||
if trait != "function_calling_default"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create humanized description
|
|
||||||
parts = [await humanize_name(model_id)]
|
|
||||||
if "availableContextTokens" in model_spec:
|
|
||||||
parts.append(f"ctx: {model_spec['availableContextTokens']}")
|
|
||||||
if traits:
|
|
||||||
parts.append(" | ".join(traits))
|
|
||||||
|
|
||||||
# Add to enhanced data
|
|
||||||
enhanced_model = model.copy()
|
|
||||||
enhanced_model["humanized"] = " | ".join(parts)
|
|
||||||
enhanced_data.append(enhanced_model)
|
|
||||||
model_dict[model_id] = enhanced_model
|
|
||||||
|
|
||||||
# Save enhanced JSON
|
|
||||||
enhanced_json = {"object": "list", "data": enhanced_data}
|
|
||||||
with open(all_model_list_path, "w") as f:
|
|
||||||
json.dump(enhanced_json, f, indent=2)
|
|
||||||
|
|
||||||
response = ("Model list update success", False)
|
|
||||||
return web.json_response({"message": response[0], "error": response[1]})
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE: routes are frozen and don't update with ComfyUI-HotReloadHack
|
|
||||||
@routes.get("/veniceai/get_models_list")
|
|
||||||
async def get_local_models_list_server(request):
|
|
||||||
with open(all_model_list_path, "r") as f:
|
|
||||||
model_list_json = json.load(f)
|
|
||||||
|
|
||||||
data = model_list_json["data"]
|
|
||||||
|
|
||||||
# Create final sorted lists of names/ids
|
|
||||||
img_models = [m["id"] for m in data if m.get("type") == "image"]
|
|
||||||
txt_models = [m["id"] for m in data if m.get("type") == "text"]
|
|
||||||
tts_models = [m["id"] for m in data if m.get("type") == "tts"]
|
|
||||||
|
|
||||||
# Collect voices with humanized model name
|
|
||||||
tts_voices = []
|
|
||||||
for m in data:
|
|
||||||
if m.get("type") == "tts":
|
|
||||||
voices = m.get("model_spec", {}).get("voices", [])
|
|
||||||
# humanized = m.get("humanized", m.get("id", ""))
|
|
||||||
humanized = m.get("id", "")
|
|
||||||
tts_voices.extend([f"{humanized} - {voice}" for voice in voices])
|
|
||||||
|
|
||||||
# if tts_voices:
|
|
||||||
# print(f"TTS Voices: {', '.join(tts_voices[:100])}")
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"image_models": sorted(img_models),
|
|
||||||
"text_models": sorted(txt_models),
|
|
||||||
"tts_models": sorted(tts_models),
|
|
||||||
"tts_voices": sorted(tts_voices),
|
|
||||||
"model_list_json": model_list_json,
|
|
||||||
}
|
|
||||||
return web.json_response(data)
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from aiohttp import web
|
|
||||||
|
|
||||||
from server import PromptServer # type: ignore
|
|
||||||
|
|
||||||
from ..globals import API_ENDPOINTS, VENICEAI_BASE_URL
|
|
||||||
|
|
||||||
routes = PromptServer.instance.routes
|
|
||||||
|
|
||||||
script_dir = Path(__file__).resolve().parent
|
|
||||||
data_dir = script_dir.parent / "data"
|
|
||||||
data_dir.mkdir(exist_ok=True)
|
|
||||||
styles_list_path = data_dir / "styles_list.json"
|
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/veniceai/update_styles_list")
|
|
||||||
async def update_styles_list_server(request):
|
|
||||||
try:
|
|
||||||
headers = {"Authorization": f"Bearer {os.getenv('VENICEAI_API_KEY')}"}
|
|
||||||
url = f"{VENICEAI_BASE_URL}{API_ENDPOINTS['list_styles']}"
|
|
||||||
|
|
||||||
response = requests.get(url, headers=headers)
|
|
||||||
response.raise_for_status() # Raises HTTPError for bad responses
|
|
||||||
|
|
||||||
response_data = response.json()
|
|
||||||
|
|
||||||
# remove "object" key from response_data
|
|
||||||
response_data.pop("object", None)
|
|
||||||
|
|
||||||
response_data["data"] = sorted(response_data.get("data", []))
|
|
||||||
response_data["data"].insert(0, "none")
|
|
||||||
|
|
||||||
with open(styles_list_path, "w") as f:
|
|
||||||
json.dump(response_data, f, indent=2)
|
|
||||||
|
|
||||||
# except requests.exceptions.HTTPError as http_err:
|
|
||||||
# print(f"HTTP error occurred: {http_err}")
|
|
||||||
# except requests.exceptions.RequestException as req_err:
|
|
||||||
# print(f"Request error occurred: {req_err}")
|
|
||||||
# except (ValueError, KeyError) as data_err:
|
|
||||||
# print(f"Data parsing error: {data_err}")
|
|
||||||
except Exception as e:
|
|
||||||
return (f"Unexpected error: {e}", True)
|
|
||||||
|
|
||||||
response = (response_data, False)
|
|
||||||
return web.json_response({"message": response[0], "error": response[1]})
|
|
||||||
|
|
||||||
|
|
||||||
@routes.get("/veniceai/get_styles_list")
|
|
||||||
async def get_local_styles_list(requests):
|
|
||||||
with open(styles_list_path, "r") as f:
|
|
||||||
styles_list_json = json.load(f)
|
|
||||||
|
|
||||||
return web.json_response(styles_list_json)
|
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Dict, Iterable, List, Optional
|
||||||
|
|
||||||
|
from .venice_client import VeniceAPIError, VeniceClient
|
||||||
|
|
||||||
|
DATA_DIR = Path(__file__).with_name("data")
|
||||||
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
class CatalogStore:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
default_factory: Optional[Callable[[], Dict[str, Any]]] = None,
|
||||||
|
legacy_name: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
self.file = DATA_DIR / name
|
||||||
|
self._default_factory = default_factory or (lambda: {"object": "list", "data": []})
|
||||||
|
self._legacy_name = legacy_name
|
||||||
|
|
||||||
|
def load(self) -> Dict[str, Any]:
|
||||||
|
if not self.file.exists():
|
||||||
|
return self._load_legacy() or self._default_factory()
|
||||||
|
try:
|
||||||
|
with self.file.open("r", encoding="utf-8") as fp:
|
||||||
|
return json.load(fp)
|
||||||
|
except (ValueError, json.JSONDecodeError):
|
||||||
|
return self._load_legacy() or self._default_factory()
|
||||||
|
|
||||||
|
def save(self, payload: Dict[str, Any]) -> None:
|
||||||
|
with self.file.open("w", encoding="utf-8") as fp:
|
||||||
|
json.dump(payload, fp, indent=2)
|
||||||
|
|
||||||
|
def _load_legacy(self) -> Dict[str, Any] | None:
|
||||||
|
if not self._legacy_name:
|
||||||
|
return None
|
||||||
|
legacy_file = self.file.with_name(self._legacy_name)
|
||||||
|
if not legacy_file.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with legacy_file.open("r", encoding="utf-8") as fp:
|
||||||
|
return json.load(fp)
|
||||||
|
except (ValueError, json.JSONDecodeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def filter_by_type(self, payload: Dict[str, Any], *types: str) -> List[Dict[str, Any]]:
|
||||||
|
data = payload.get("data") or []
|
||||||
|
return [entry for entry in data if entry.get("type") in types]
|
||||||
|
|
||||||
|
|
||||||
|
_model_store = CatalogStore("models_list.json", legacy_name="all_model_list.json")
|
||||||
|
_styles_store = CatalogStore(
|
||||||
|
"styles_list.json",
|
||||||
|
lambda: {"data": ["none"], "object": "list"},
|
||||||
|
legacy_name="styles_list.json",
|
||||||
|
)
|
||||||
|
_characters_store = CatalogStore(
|
||||||
|
"characters_list.json",
|
||||||
|
lambda: {"data": [], "object": "list"},
|
||||||
|
legacy_name="characters_list.json",
|
||||||
|
)
|
||||||
|
_client = VeniceClient()
|
||||||
|
|
||||||
|
# to prevent spamming api.
|
||||||
|
# can be bypassed by setting force_refresh=True in get_* calls
|
||||||
|
_CACHE_TTL = float(os.environ.get("VENICE_CATALOG_TTL", "900"))
|
||||||
|
_models_last_refresh = 0.0
|
||||||
|
_styles_last_refresh = 0.0
|
||||||
|
_characters_last_refresh = 0.0
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _enrich_models(raw: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
return raw.copy()
|
||||||
|
|
||||||
|
|
||||||
|
def _should_refresh(last_refresh: float) -> bool:
|
||||||
|
if _CACHE_TTL <= 0:
|
||||||
|
return False
|
||||||
|
return (time.monotonic() - last_refresh) > _CACHE_TTL
|
||||||
|
|
||||||
|
|
||||||
|
def _record_models_refresh() -> None:
|
||||||
|
global _models_last_refresh
|
||||||
|
_models_last_refresh = time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
|
def _record_styles_refresh() -> None:
|
||||||
|
global _styles_last_refresh
|
||||||
|
_styles_last_refresh = time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
|
def _record_characters_refresh() -> None:
|
||||||
|
global _characters_last_refresh
|
||||||
|
_characters_last_refresh = time.monotonic()
|
||||||
|
|
||||||
|
|
||||||
|
def _should_attempt_refresh(payload: Dict[str, Any], should_refresh: bool) -> bool:
|
||||||
|
if not should_refresh:
|
||||||
|
return False
|
||||||
|
if _client.dry_run and payload.get("data"):
|
||||||
|
LOG.debug("Dry-run mode skipping catalog refresh because cached data is available.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_models(model_type: str = "all") -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
raw = _client.list_models(model_type or "all")
|
||||||
|
except VeniceAPIError as exc:
|
||||||
|
LOG.error("Failed to refresh Venice model list: %s", exc)
|
||||||
|
raise
|
||||||
|
enriched = _enrich_models(raw)
|
||||||
|
if not enriched.get("data"):
|
||||||
|
raise VeniceAPIError("Venice returned an empty model catalog")
|
||||||
|
_model_store.save(enriched)
|
||||||
|
_record_models_refresh()
|
||||||
|
return enriched
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_styles() -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
raw = _client.list_styles()
|
||||||
|
except VeniceAPIError as exc:
|
||||||
|
LOG.error("Failed to refresh Venice styles list: %s", exc)
|
||||||
|
raise
|
||||||
|
|
||||||
|
data = raw.get("data")
|
||||||
|
if not data:
|
||||||
|
raise VeniceAPIError("Venice returned an empty styles catalog")
|
||||||
|
|
||||||
|
sorted_data = sorted(data)
|
||||||
|
sorted_data.insert(0, "none")
|
||||||
|
payload = {"object": raw.get("object", "list"), "data": sorted_data}
|
||||||
|
_styles_store.save(payload)
|
||||||
|
_record_styles_refresh()
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_characters() -> Dict[str, Any]:
|
||||||
|
try:
|
||||||
|
raw = _client.list_characters()
|
||||||
|
except VeniceAPIError as exc:
|
||||||
|
LOG.error("Failed to refresh Venice characters list: %s", exc)
|
||||||
|
raise
|
||||||
|
|
||||||
|
items = raw.get("data")
|
||||||
|
if not items:
|
||||||
|
raise VeniceAPIError("Venice returned an empty characters catalog")
|
||||||
|
|
||||||
|
sorted_items = sorted(items, key=lambda item: item.get("slug", "")) if isinstance(items, Iterable) else []
|
||||||
|
payload = {"object": raw.get("object", "list"), "data": sorted_items}
|
||||||
|
_characters_store.save(payload)
|
||||||
|
_record_characters_refresh()
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def get_models(model_type: Optional[str] = None, *, force_refresh: bool = False) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Return model metadata from the Venice catalog, optionally filtered by type.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
model_type : Optional[str]
|
||||||
|
If provided, filters models by the requested type (e.g., "image", "text", "tts") and
|
||||||
|
returns only that subset; when omitted, the response contains categorized lists
|
||||||
|
of model IDs, available TTS voices, and the raw payload.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Dict[str, Any]
|
||||||
|
The filtered or fully categorized model information, keyed by categories such as
|
||||||
|
"image_models", "text_models", "tts_models", "tts_voices", and "model_list_json".
|
||||||
|
When a model_type filter is applied, returns a single "models" key with matching entries.
|
||||||
|
"""
|
||||||
|
payload = _model_store.load()
|
||||||
|
should_refresh = force_refresh or not payload.get("data") or _should_refresh(_models_last_refresh)
|
||||||
|
if _should_attempt_refresh(payload, should_refresh):
|
||||||
|
payload = refresh_models()
|
||||||
|
if model_type:
|
||||||
|
return {"models": _model_store.filter_by_type(payload, model_type)}
|
||||||
|
|
||||||
|
filtered = {
|
||||||
|
"image_models": sorted([m.get("id") for m in payload.get("data", []) if m.get("type") == "image"]),
|
||||||
|
"text_models": sorted([m.get("id") for m in payload.get("data", []) if m.get("type") == "text"]),
|
||||||
|
"tts_models": sorted([m.get("id") for m in payload.get("data", []) if m.get("type") == "tts"]),
|
||||||
|
"tts_voices": sorted(
|
||||||
|
[
|
||||||
|
f"{m.get('id', '')} - {voice}"
|
||||||
|
for m in payload.get("data", [])
|
||||||
|
if m.get("type") == "tts"
|
||||||
|
for voice in (m.get("model_spec", {}).get("voices") or [])
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"text2video_models": sorted(
|
||||||
|
[
|
||||||
|
m.get("id")
|
||||||
|
for m in payload.get("data", [])
|
||||||
|
if m.get("type") == "video"
|
||||||
|
and isinstance(m.get("model_spec"), dict)
|
||||||
|
and isinstance(m["model_spec"].get("constraints"), dict)
|
||||||
|
and m["model_spec"]["constraints"].get("model_type") == "text-to-video"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"image2video_models": sorted(
|
||||||
|
[
|
||||||
|
m.get("id")
|
||||||
|
for m in payload.get("data", [])
|
||||||
|
if m.get("type") == "video"
|
||||||
|
and isinstance(m.get("model_spec"), dict)
|
||||||
|
and isinstance(m["model_spec"].get("constraints"), dict)
|
||||||
|
and m["model_spec"]["constraints"].get("model_type") == "image-to-video"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"model_list_json": payload,
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
def get_styles(*, force_refresh: bool = False) -> Dict[str, Any]:
|
||||||
|
payload = _styles_store.load()
|
||||||
|
should_refresh = force_refresh or not payload.get("data") or _should_refresh(_styles_last_refresh)
|
||||||
|
if _should_attempt_refresh(payload, should_refresh):
|
||||||
|
payload = refresh_styles()
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def get_characters(*, force_refresh: bool = False) -> Dict[str, Any]:
|
||||||
|
payload = _characters_store.load()
|
||||||
|
should_refresh = force_refresh or not payload.get("data") or _should_refresh(_characters_last_refresh)
|
||||||
|
if _should_attempt_refresh(payload, should_refresh):
|
||||||
|
payload = refresh_characters()
|
||||||
|
data = payload.get("data", [])
|
||||||
|
characters = [item.get("slug") for item in data if isinstance(item, dict) and item.get("slug")]
|
||||||
|
return {"characters": characters}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Any, Dict, Mapping, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from .globals import API_ENDPOINTS, USER_AGENT, VENICEAI_BASE_URL
|
||||||
|
from .venice_config import config as venice_config
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
if os.environ.get("VENICE_CLIENT_DEBUG", "").lower() in {"1", "true"}:
|
||||||
|
LOG.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
class VeniceAPIError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DummyResponse:
|
||||||
|
def __init__(self, payload: dict | None = None, status_code: int = 200) -> None:
|
||||||
|
self._payload = payload or {}
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
def raise_for_status(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text(self) -> str:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def __getattr__(self, item: str) -> Any:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class VeniceClient:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._session = requests.Session()
|
||||||
|
self._session.headers.update({"User-Agent": USER_AGENT})
|
||||||
|
self._dry_run = os.environ.get("VENICE_CLIENT_DRY_RUN", "").lower() in {"1", "true"}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dry_run(self) -> bool:
|
||||||
|
return self._dry_run
|
||||||
|
|
||||||
|
def _ensure_api_key(self) -> str:
|
||||||
|
key = venice_config.apikey.strip()
|
||||||
|
if not key:
|
||||||
|
raise VeniceAPIError(
|
||||||
|
"VeniceAI API key is missing. Set it via the VeniceAI settings before using the nodes."
|
||||||
|
)
|
||||||
|
return key
|
||||||
|
|
||||||
|
def _build_headers(self, extra: Optional[Mapping[str, str]] = None) -> Dict[str, str]:
|
||||||
|
headers: Dict[str, str] = {"Authorization": f"Bearer {self._ensure_api_key()}"}
|
||||||
|
if extra:
|
||||||
|
headers.update(extra)
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def _friendly_status_hint(self, status_code: int) -> str:
|
||||||
|
if status_code == 401:
|
||||||
|
return "The API key may be invalid, expired, or lack permissions."
|
||||||
|
if status_code == 404:
|
||||||
|
return "The requested VeniceAI endpoint was not found. Ensure the node and catalog data are up to date."
|
||||||
|
if status_code == 429:
|
||||||
|
return "Request rate limits were hit. Wait a moment before retrying."
|
||||||
|
if 500 <= status_code < 600:
|
||||||
|
return "VeniceAI appears to be experiencing server issues; try again in a bit."
|
||||||
|
return "Check your request parameters and ensure your API key is valid."
|
||||||
|
|
||||||
|
def _friendly_network_hint(self) -> str:
|
||||||
|
return "Unable to reach VeniceAI. Confirm your internet connection and that api.venice.ai is reachable."
|
||||||
|
|
||||||
|
def request(self, method: str, endpoint: str, **kwargs: Any) -> requests.Response:
|
||||||
|
if self._dry_run:
|
||||||
|
logging.debug("VeniceClient dry run skipping %s %s", method, endpoint)
|
||||||
|
return DummyResponse()
|
||||||
|
|
||||||
|
url = VENICEAI_BASE_URL + endpoint
|
||||||
|
headers = kwargs.pop("headers", None)
|
||||||
|
kwargs.setdefault("timeout", 30)
|
||||||
|
try:
|
||||||
|
response = self._session.request(method, url, headers=self._build_headers(headers), **kwargs)
|
||||||
|
response.raise_for_status()
|
||||||
|
except requests.HTTPError as exc:
|
||||||
|
self._log_response(method, endpoint, exc.response)
|
||||||
|
hint = (
|
||||||
|
self._friendly_status_hint(exc.response.status_code)
|
||||||
|
if exc.response is not None
|
||||||
|
else "Unexpected response from VeniceAI."
|
||||||
|
)
|
||||||
|
message = f"Venice request failed ({method} {endpoint}): {exc}. \n{hint}"
|
||||||
|
LOG.debug("Venice request failed: %s %s %s", method, endpoint, exc)
|
||||||
|
raise VeniceAPIError(message) from exc
|
||||||
|
except requests.RequestException as exc:
|
||||||
|
message = f"{self._friendly_network_hint()} Details: {exc}"
|
||||||
|
LOG.debug("Venice network error: %s %s %s", method, endpoint, exc)
|
||||||
|
raise VeniceAPIError(message) from exc
|
||||||
|
self._log_response(method, endpoint, response)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _log_response(self, method: str, endpoint: str, response: Optional[requests.Response | DummyResponse]) -> None:
|
||||||
|
if not LOG.isEnabledFor(logging.DEBUG) or response is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
headers = getattr(response, "headers", None)
|
||||||
|
header_snapshot: Dict[str, Any] = dict(headers) if headers else {}
|
||||||
|
content_type = (header_snapshot.get("Content-Type") or "").lower()
|
||||||
|
|
||||||
|
body = ""
|
||||||
|
readable_body = content_type.startswith("application/json") or content_type.startswith("text/")
|
||||||
|
if readable_body or not content_type:
|
||||||
|
try:
|
||||||
|
body = response.text or ""
|
||||||
|
except Exception as exc:
|
||||||
|
body = f"<unable to read body: {exc}>"
|
||||||
|
if len(body) > 2000:
|
||||||
|
body = body[:2000] + "...[truncated]"
|
||||||
|
else:
|
||||||
|
body = f"<{content_type} response body omitted>"
|
||||||
|
|
||||||
|
LOG.debug(
|
||||||
|
"Venice response %s %s status=%s headers=%s body=%s",
|
||||||
|
method,
|
||||||
|
endpoint,
|
||||||
|
getattr(response, "status_code", "<?>"),
|
||||||
|
header_snapshot,
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
|
||||||
|
def post_json(self, endpoint: str, payload: Mapping[str, Any], **kwargs: Any) -> Dict[str, Any]:
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
headers.update(kwargs.pop("headers", {}))
|
||||||
|
response = self.request("POST", endpoint, json=payload, headers=headers, **kwargs)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_json(self, endpoint: str, params: Optional[Mapping[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
response = self.request("GET", endpoint, params=params)
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def list_models(self, model_type: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
params = {"type": model_type} if model_type else None
|
||||||
|
return self.get_json(API_ENDPOINTS["list_models"], params=params)
|
||||||
|
|
||||||
|
def list_styles(self) -> Dict[str, Any]:
|
||||||
|
return self.get_json(API_ENDPOINTS["list_styles"])
|
||||||
|
|
||||||
|
def list_characters(self) -> Dict[str, Any]:
|
||||||
|
return self.get_json(API_ENDPOINTS["characters"])
|
||||||
|
|
||||||
|
|
||||||
|
client = VeniceClient()
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
DEFAULT_CONFIG: Dict[str, Any] = {"apikey": ""}
|
||||||
|
|
||||||
|
|
||||||
|
class VeniceConfig:
|
||||||
|
_instance: "VeniceConfig" | None = None
|
||||||
|
|
||||||
|
def __new__(cls) -> "VeniceConfig":
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance._initialized = False
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
if self._initialized:
|
||||||
|
return
|
||||||
|
self._initialized = True
|
||||||
|
self.path = Path(__file__).with_name("veniceai_config.json")
|
||||||
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._data = self._read()
|
||||||
|
self._sync_env()
|
||||||
|
|
||||||
|
def _read(self) -> Dict[str, Any]:
|
||||||
|
if not self.path.exists():
|
||||||
|
self._write(DEFAULT_CONFIG)
|
||||||
|
return DEFAULT_CONFIG.copy()
|
||||||
|
try:
|
||||||
|
with self.path.open("r", encoding="utf-8") as fp:
|
||||||
|
return json.load(fp)
|
||||||
|
except (ValueError, json.JSONDecodeError):
|
||||||
|
self._write(DEFAULT_CONFIG)
|
||||||
|
return DEFAULT_CONFIG.copy()
|
||||||
|
|
||||||
|
def _write(self, data: Dict[str, Any]) -> None:
|
||||||
|
with self.path.open("w", encoding="utf-8") as fp:
|
||||||
|
json.dump(data, fp, indent=2)
|
||||||
|
|
||||||
|
def _sync_env(self) -> None:
|
||||||
|
os.environ.setdefault("VENICEAI_API_KEY", self._data.get("apikey", ""))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def apikey(self) -> str:
|
||||||
|
return self._data.get("apikey", "") or ""
|
||||||
|
|
||||||
|
def save_apikey(self, key: str) -> None:
|
||||||
|
self._data["apikey"] = key
|
||||||
|
self._write(self._data)
|
||||||
|
os.environ["VENICEAI_API_KEY"] = key
|
||||||
|
|
||||||
|
|
||||||
|
config = VeniceConfig()
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
{
|
|
||||||
"last_node_id": 22,
|
|
||||||
"last_link_id": 6,
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": 20,
|
|
||||||
"type": "SaveImage",
|
|
||||||
"pos": {
|
|
||||||
"0": 645,
|
|
||||||
"1": 514
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"0": 315,
|
|
||||||
"1": 270
|
|
||||||
},
|
|
||||||
"flags": {},
|
|
||||||
"order": 4,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "images",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"link": 6
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"properties": {},
|
|
||||||
"widgets_values": [
|
|
||||||
"ComfyUIPro"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 15,
|
|
||||||
"type": "SaveImage",
|
|
||||||
"pos": {
|
|
||||||
"0": 641,
|
|
||||||
"1": 913
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"0": 315,
|
|
||||||
"1": 270
|
|
||||||
},
|
|
||||||
"flags": {},
|
|
||||||
"order": 6,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "images",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"link": 5
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"properties": {},
|
|
||||||
"widgets_values": [
|
|
||||||
"ComfyUIP11"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 5,
|
|
||||||
"type": "SaveImage",
|
|
||||||
"pos": {
|
|
||||||
"0": 624,
|
|
||||||
"1": 91
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"0": 315,
|
|
||||||
"1": 270
|
|
||||||
},
|
|
||||||
"flags": {},
|
|
||||||
"order": 5,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"name": "images",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"link": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [],
|
|
||||||
"properties": {},
|
|
||||||
"widgets_values": [
|
|
||||||
"ComfyUIDev"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 9,
|
|
||||||
"type": "FluxPro_TOGETHER",
|
|
||||||
"pos": {
|
|
||||||
"0": 116,
|
|
||||||
"1": 513
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"0": 400,
|
|
||||||
"1": 292
|
|
||||||
},
|
|
||||||
"flags": {},
|
|
||||||
"order": 0,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "IMAGE",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"links": [
|
|
||||||
6
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "FluxPro_TOGETHER"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
"The image showcases a striking 3D CGI representation of a stylized angry muscle Doberman, characterized by a sleek black and silver coat that glistens under the soft lighting, enhancing its muscular physique. The dog is prominently positioned in the center of the composition against a stark black background, which accentuates its fierce expression and well-defined features. Its piercing eyes are shielded by trendy green sunglasses, adding a fashionable flair that contrasts with the dark tones of its fur. The meticulous detailing highlights the texture of the Doberman’s coat, revealing subtle variations in color that suggest a glossy sheen. Surrounding the Doberman, hints of a countryside landscape can be inferred in the background, possibly indicating an advertising theme, although it remains blurred and understated to maintain focus on the dog itself. The overall quality of the image is exceptional, marked by ultra-detailed craftsmanship that embodies a masterpiece in high-resolution, making it a captivating visual spectacle.\n\n",
|
|
||||||
1024,
|
|
||||||
1024,
|
|
||||||
28,
|
|
||||||
true,
|
|
||||||
"2",
|
|
||||||
2.5,
|
|
||||||
2,
|
|
||||||
324,
|
|
||||||
"randomize"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 6,
|
|
||||||
"type": "FluxDev_TOGETHER",
|
|
||||||
"pos": {
|
|
||||||
"0": 112,
|
|
||||||
"1": 91
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"0": 400,
|
|
||||||
"1": 268
|
|
||||||
},
|
|
||||||
"flags": {},
|
|
||||||
"order": 1,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "IMAGE",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"links": [
|
|
||||||
4
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "FluxDev_TOGETHER"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
"The image showcases a striking 3D CGI representation of a stylized angry muscle Doberman, characterized by a sleek black and silver coat that glistens under the soft lighting, enhancing its muscular physique. The dog is prominently positioned in the center of the composition against a stark black background, which accentuates its fierce expression and well-defined features. Its piercing eyes are shielded by trendy green sunglasses, adding a fashionable flair that contrasts with the dark tones of its fur. The meticulous detailing highlights the texture of the Doberman’s coat, revealing subtle variations in color that suggest a glossy sheen. Surrounding the Doberman, hints of a countryside landscape can be inferred in the background, possibly indicating an advertising theme, although it remains blurred and understated to maintain focus on the dog itself. The overall quality of the image is exceptional, marked by ultra-detailed craftsmanship that embodies a masterpiece in high-resolution, making it a captivating visual spectacle.\n\n",
|
|
||||||
1024,
|
|
||||||
1024,
|
|
||||||
4,
|
|
||||||
true,
|
|
||||||
"1",
|
|
||||||
2.5,
|
|
||||||
675,
|
|
||||||
"randomize"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 14,
|
|
||||||
"type": "FluxPro11_TOGETHER",
|
|
||||||
"pos": {
|
|
||||||
"0": 115,
|
|
||||||
"1": 912
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"0": 400,
|
|
||||||
"1": 244
|
|
||||||
},
|
|
||||||
"flags": {},
|
|
||||||
"order": 2,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "IMAGE",
|
|
||||||
"type": "IMAGE",
|
|
||||||
"links": [
|
|
||||||
5
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"Node name for S&R": "FluxPro11_TOGETHER"
|
|
||||||
},
|
|
||||||
"widgets_values": [
|
|
||||||
"The image showcases a striking 3D CGI representation of a stylized angry muscle Doberman, characterized by a sleek black and silver coat that glistens under the soft lighting, enhancing its muscular physique. The dog is prominently positioned in the center of the composition against a stark black background, which accentuates its fierce expression and well-defined features. Its piercing eyes are shielded by trendy green sunglasses, adding a fashionable flair that contrasts with the dark tones of its fur. The meticulous detailing highlights the texture of the Doberman’s coat, revealing subtle variations in color that suggest a glossy sheen. Surrounding the Doberman, hints of a countryside landscape can be inferred in the background, possibly indicating an advertising theme, although it remains blurred and understated to maintain focus on the dog itself. The overall quality of the image is exceptional, marked by ultra-detailed craftsmanship that embodies a masterpiece in high-resolution, making it a captivating visual spectacle.\n",
|
|
||||||
1024,
|
|
||||||
1024,
|
|
||||||
true,
|
|
||||||
1,
|
|
||||||
"2",
|
|
||||||
1148,
|
|
||||||
"randomize"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 22,
|
|
||||||
"type": "Fast Groups Muter (rgthree)",
|
|
||||||
"pos": {
|
|
||||||
"0": -142,
|
|
||||||
"1": 36
|
|
||||||
},
|
|
||||||
"size": {
|
|
||||||
"0": 226.8000030517578,
|
|
||||||
"1": 130
|
|
||||||
},
|
|
||||||
"flags": {},
|
|
||||||
"order": 3,
|
|
||||||
"mode": 0,
|
|
||||||
"inputs": [],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"name": "OPT_CONNECTION",
|
|
||||||
"type": "*",
|
|
||||||
"links": null
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"matchColors": "",
|
|
||||||
"matchTitle": "",
|
|
||||||
"showNav": true,
|
|
||||||
"sort": "position",
|
|
||||||
"customSortAlphabet": "",
|
|
||||||
"toggleRestriction": "default"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": [
|
|
||||||
[
|
|
||||||
4,
|
|
||||||
6,
|
|
||||||
0,
|
|
||||||
5,
|
|
||||||
0,
|
|
||||||
"IMAGE"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
5,
|
|
||||||
14,
|
|
||||||
0,
|
|
||||||
15,
|
|
||||||
0,
|
|
||||||
"IMAGE"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
6,
|
|
||||||
9,
|
|
||||||
0,
|
|
||||||
20,
|
|
||||||
0,
|
|
||||||
"IMAGE"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"groups": [
|
|
||||||
{
|
|
||||||
"title": "Flux Pro",
|
|
||||||
"bounding": [
|
|
||||||
102,
|
|
||||||
440,
|
|
||||||
872,
|
|
||||||
376
|
|
||||||
],
|
|
||||||
"color": "#8A8",
|
|
||||||
"font_size": 24,
|
|
||||||
"flags": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Flux Dev",
|
|
||||||
"bounding": [
|
|
||||||
103,
|
|
||||||
11,
|
|
||||||
871,
|
|
||||||
407
|
|
||||||
],
|
|
||||||
"color": "#A88",
|
|
||||||
"font_size": 24,
|
|
||||||
"flags": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Flux Pro 1.1",
|
|
||||||
"bounding": [
|
|
||||||
100,
|
|
||||||
834,
|
|
||||||
879,
|
|
||||||
373
|
|
||||||
],
|
|
||||||
"color": "#444",
|
|
||||||
"font_size": 24,
|
|
||||||
"flags": {}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"config": {},
|
|
||||||
"extra": {
|
|
||||||
"ds": {
|
|
||||||
"scale": 0.8264462809917354,
|
|
||||||
"offset": [
|
|
||||||
442.1956952821454,
|
|
||||||
170.1053343944367
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"version": 0.4
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user