mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-04-14 12:32:31 +08:00
Merge branch 'master' into add-coderabbit-config
This commit is contained in:
commit
b11c0b6807
44
blueprints/.glsl/Brightness_and_Contrast_1.frag
Normal file
44
blueprints/.glsl/Brightness_and_Contrast_1.frag
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform float u_float0; // Brightness slider -100..100
|
||||||
|
uniform float u_float1; // Contrast slider -100..100
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
const float MID_GRAY = 0.18; // 18% reflectance
|
||||||
|
|
||||||
|
// sRGB gamma 2.2 approximation
|
||||||
|
vec3 srgbToLinear(vec3 c) {
|
||||||
|
return pow(max(c, 0.0), vec3(2.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 linearToSrgb(vec3 c) {
|
||||||
|
return pow(max(c, 0.0), vec3(1.0/2.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
float mapBrightness(float b) {
|
||||||
|
return clamp(b / 100.0, -1.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float mapContrast(float c) {
|
||||||
|
return clamp(c / 100.0 + 1.0, 0.0, 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 orig = texture(u_image0, v_texCoord);
|
||||||
|
|
||||||
|
float brightness = mapBrightness(u_float0);
|
||||||
|
float contrast = mapContrast(u_float1);
|
||||||
|
|
||||||
|
vec3 lin = srgbToLinear(orig.rgb);
|
||||||
|
|
||||||
|
lin = (lin - MID_GRAY) * contrast + brightness + MID_GRAY;
|
||||||
|
|
||||||
|
// Convert back to sRGB
|
||||||
|
vec3 result = linearToSrgb(clamp(lin, 0.0, 1.0));
|
||||||
|
|
||||||
|
fragColor = vec4(result, orig.a);
|
||||||
|
}
|
||||||
72
blueprints/.glsl/Chromatic_Aberration_16.frag
Normal file
72
blueprints/.glsl/Chromatic_Aberration_16.frag
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform int u_int0; // Mode
|
||||||
|
uniform float u_float0; // Amount (0 to 100)
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
const int MODE_LINEAR = 0;
|
||||||
|
const int MODE_RADIAL = 1;
|
||||||
|
const int MODE_BARREL = 2;
|
||||||
|
const int MODE_SWIRL = 3;
|
||||||
|
const int MODE_DIAGONAL = 4;
|
||||||
|
|
||||||
|
const float AMOUNT_SCALE = 0.0005;
|
||||||
|
const float RADIAL_MULT = 4.0;
|
||||||
|
const float BARREL_MULT = 8.0;
|
||||||
|
const float INV_SQRT2 = 0.70710678118;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 uv = v_texCoord;
|
||||||
|
vec4 original = texture(u_image0, uv);
|
||||||
|
|
||||||
|
float amount = u_float0 * AMOUNT_SCALE;
|
||||||
|
|
||||||
|
if (amount < 0.000001) {
|
||||||
|
fragColor = original;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aspect-corrected coordinates for circular effects
|
||||||
|
float aspect = u_resolution.x / u_resolution.y;
|
||||||
|
vec2 centered = uv - 0.5;
|
||||||
|
vec2 corrected = vec2(centered.x * aspect, centered.y);
|
||||||
|
float r = length(corrected);
|
||||||
|
vec2 dir = r > 0.0001 ? corrected / r : vec2(0.0);
|
||||||
|
vec2 offset = vec2(0.0);
|
||||||
|
|
||||||
|
if (u_int0 == MODE_LINEAR) {
|
||||||
|
// Horizontal shift (no aspect correction needed)
|
||||||
|
offset = vec2(amount, 0.0);
|
||||||
|
}
|
||||||
|
else if (u_int0 == MODE_RADIAL) {
|
||||||
|
// Outward from center, stronger at edges
|
||||||
|
offset = dir * r * amount * RADIAL_MULT;
|
||||||
|
offset.x /= aspect; // Convert back to UV space
|
||||||
|
}
|
||||||
|
else if (u_int0 == MODE_BARREL) {
|
||||||
|
// Lens distortion simulation (r² falloff)
|
||||||
|
offset = dir * r * r * amount * BARREL_MULT;
|
||||||
|
offset.x /= aspect; // Convert back to UV space
|
||||||
|
}
|
||||||
|
else if (u_int0 == MODE_SWIRL) {
|
||||||
|
// Perpendicular to radial (rotational aberration)
|
||||||
|
vec2 perp = vec2(-dir.y, dir.x);
|
||||||
|
offset = perp * r * amount * RADIAL_MULT;
|
||||||
|
offset.x /= aspect; // Convert back to UV space
|
||||||
|
}
|
||||||
|
else if (u_int0 == MODE_DIAGONAL) {
|
||||||
|
// 45° offset (no aspect correction needed)
|
||||||
|
offset = vec2(amount, amount) * INV_SQRT2;
|
||||||
|
}
|
||||||
|
|
||||||
|
float red = texture(u_image0, uv + offset).r;
|
||||||
|
float green = original.g;
|
||||||
|
float blue = texture(u_image0, uv - offset).b;
|
||||||
|
|
||||||
|
fragColor = vec4(red, green, blue, original.a);
|
||||||
|
}
|
||||||
78
blueprints/.glsl/Color_Adjustment_15.frag
Normal file
78
blueprints/.glsl/Color_Adjustment_15.frag
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform float u_float0; // temperature (-100 to 100)
|
||||||
|
uniform float u_float1; // tint (-100 to 100)
|
||||||
|
uniform float u_float2; // vibrance (-100 to 100)
|
||||||
|
uniform float u_float3; // saturation (-100 to 100)
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
const float INPUT_SCALE = 0.01;
|
||||||
|
const float TEMP_TINT_PRIMARY = 0.3;
|
||||||
|
const float TEMP_TINT_SECONDARY = 0.15;
|
||||||
|
const float VIBRANCE_BOOST = 2.0;
|
||||||
|
const float SATURATION_BOOST = 2.0;
|
||||||
|
const float SKIN_PROTECTION = 0.5;
|
||||||
|
const float EPSILON = 0.001;
|
||||||
|
const vec3 LUMA_WEIGHTS = vec3(0.299, 0.587, 0.114);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 tex = texture(u_image0, v_texCoord);
|
||||||
|
vec3 color = tex.rgb;
|
||||||
|
|
||||||
|
// Scale inputs: -100/100 → -1/1
|
||||||
|
float temperature = u_float0 * INPUT_SCALE;
|
||||||
|
float tint = u_float1 * INPUT_SCALE;
|
||||||
|
float vibrance = u_float2 * INPUT_SCALE;
|
||||||
|
float saturation = u_float3 * INPUT_SCALE;
|
||||||
|
|
||||||
|
// Temperature (warm/cool): positive = warm, negative = cool
|
||||||
|
color.r += temperature * TEMP_TINT_PRIMARY;
|
||||||
|
color.b -= temperature * TEMP_TINT_PRIMARY;
|
||||||
|
|
||||||
|
// Tint (green/magenta): positive = green, negative = magenta
|
||||||
|
color.g += tint * TEMP_TINT_PRIMARY;
|
||||||
|
color.r -= tint * TEMP_TINT_SECONDARY;
|
||||||
|
color.b -= tint * TEMP_TINT_SECONDARY;
|
||||||
|
|
||||||
|
// Single clamp after temperature/tint
|
||||||
|
color = clamp(color, 0.0, 1.0);
|
||||||
|
|
||||||
|
// Vibrance with skin protection
|
||||||
|
if (vibrance != 0.0) {
|
||||||
|
float maxC = max(color.r, max(color.g, color.b));
|
||||||
|
float minC = min(color.r, min(color.g, color.b));
|
||||||
|
float sat = maxC - minC;
|
||||||
|
float gray = dot(color, LUMA_WEIGHTS);
|
||||||
|
|
||||||
|
if (vibrance < 0.0) {
|
||||||
|
// Desaturate: -100 → gray
|
||||||
|
color = mix(vec3(gray), color, 1.0 + vibrance);
|
||||||
|
} else {
|
||||||
|
// Boost less saturated colors more
|
||||||
|
float vibranceAmt = vibrance * (1.0 - sat);
|
||||||
|
|
||||||
|
// Branchless skin tone protection
|
||||||
|
float isWarmTone = step(color.b, color.g) * step(color.g, color.r);
|
||||||
|
float warmth = (color.r - color.b) / max(maxC, EPSILON);
|
||||||
|
float skinTone = isWarmTone * warmth * sat * (1.0 - sat);
|
||||||
|
vibranceAmt *= (1.0 - skinTone * SKIN_PROTECTION);
|
||||||
|
|
||||||
|
color = mix(vec3(gray), color, 1.0 + vibranceAmt * VIBRANCE_BOOST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saturation
|
||||||
|
if (saturation != 0.0) {
|
||||||
|
float gray = dot(color, LUMA_WEIGHTS);
|
||||||
|
float satMix = saturation < 0.0
|
||||||
|
? 1.0 + saturation // -100 → gray
|
||||||
|
: 1.0 + saturation * SATURATION_BOOST; // +100 → 3x boost
|
||||||
|
color = mix(vec3(gray), color, satMix);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragColor = vec4(clamp(color, 0.0, 1.0), tex.a);
|
||||||
|
}
|
||||||
94
blueprints/.glsl/Edge-Preserving_Blur_128.frag
Normal file
94
blueprints/.glsl/Edge-Preserving_Blur_128.frag
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform float u_float0; // Blur radius (0–20, default ~5)
|
||||||
|
uniform float u_float1; // Edge threshold (0–100, default ~30)
|
||||||
|
uniform int u_int0; // Step size (0/1 = every pixel, 2+ = skip pixels)
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
const int MAX_RADIUS = 20;
|
||||||
|
const float EPSILON = 0.0001;
|
||||||
|
|
||||||
|
// Perceptual luminance
|
||||||
|
float getLuminance(vec3 rgb) {
|
||||||
|
return dot(rgb, vec3(0.299, 0.587, 0.114));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 bilateralFilter(vec2 uv, vec2 texelSize, int radius,
|
||||||
|
float sigmaSpatial, float sigmaColor)
|
||||||
|
{
|
||||||
|
vec4 center = texture(u_image0, uv);
|
||||||
|
vec3 centerRGB = center.rgb;
|
||||||
|
|
||||||
|
float invSpatial2 = -0.5 / (sigmaSpatial * sigmaSpatial);
|
||||||
|
float invColor2 = -0.5 / (sigmaColor * sigmaColor + EPSILON);
|
||||||
|
|
||||||
|
vec3 sumRGB = vec3(0.0);
|
||||||
|
float sumWeight = 0.0;
|
||||||
|
|
||||||
|
int step = max(u_int0, 1);
|
||||||
|
float radius2 = float(radius * radius);
|
||||||
|
|
||||||
|
for (int dy = -MAX_RADIUS; dy <= MAX_RADIUS; dy++) {
|
||||||
|
if (dy < -radius || dy > radius) continue;
|
||||||
|
if (abs(dy) % step != 0) continue;
|
||||||
|
|
||||||
|
for (int dx = -MAX_RADIUS; dx <= MAX_RADIUS; dx++) {
|
||||||
|
if (dx < -radius || dx > radius) continue;
|
||||||
|
if (abs(dx) % step != 0) continue;
|
||||||
|
|
||||||
|
vec2 offset = vec2(float(dx), float(dy));
|
||||||
|
float dist2 = dot(offset, offset);
|
||||||
|
if (dist2 > radius2) continue;
|
||||||
|
|
||||||
|
vec3 sampleRGB = texture(u_image0, uv + offset * texelSize).rgb;
|
||||||
|
|
||||||
|
// Spatial Gaussian
|
||||||
|
float spatialWeight = exp(dist2 * invSpatial2);
|
||||||
|
|
||||||
|
// Perceptual color distance (weighted RGB)
|
||||||
|
vec3 diff = sampleRGB - centerRGB;
|
||||||
|
float colorDist = dot(diff * diff, vec3(0.299, 0.587, 0.114));
|
||||||
|
float colorWeight = exp(colorDist * invColor2);
|
||||||
|
|
||||||
|
float w = spatialWeight * colorWeight;
|
||||||
|
sumRGB += sampleRGB * w;
|
||||||
|
sumWeight += w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 resultRGB = sumRGB / max(sumWeight, EPSILON);
|
||||||
|
return vec4(resultRGB, center.a); // preserve center alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 texelSize = 1.0 / vec2(textureSize(u_image0, 0));
|
||||||
|
|
||||||
|
float radiusF = clamp(u_float0, 0.0, float(MAX_RADIUS));
|
||||||
|
int radius = int(radiusF + 0.5);
|
||||||
|
|
||||||
|
if (radius == 0) {
|
||||||
|
fragColor = texture(u_image0, v_texCoord);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edge threshold → color sigma
|
||||||
|
// Squared curve for better low-end control
|
||||||
|
float t = clamp(u_float1, 0.0, 100.0) / 100.0;
|
||||||
|
t *= t;
|
||||||
|
float sigmaColor = mix(0.01, 0.5, t);
|
||||||
|
|
||||||
|
// Spatial sigma tied to radius
|
||||||
|
float sigmaSpatial = max(radiusF * 0.75, 0.5);
|
||||||
|
|
||||||
|
fragColor = bilateralFilter(
|
||||||
|
v_texCoord,
|
||||||
|
texelSize,
|
||||||
|
radius,
|
||||||
|
sigmaSpatial,
|
||||||
|
sigmaColor
|
||||||
|
);
|
||||||
|
}
|
||||||
124
blueprints/.glsl/Film_Grain_15.frag
Normal file
124
blueprints/.glsl/Film_Grain_15.frag
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform float u_float0; // grain amount [0.0 – 1.0] typical: 0.2–0.8
|
||||||
|
uniform float u_float1; // grain size [0.3 – 3.0] lower = finer grain
|
||||||
|
uniform float u_float2; // color amount [0.0 – 1.0] 0 = monochrome, 1 = RGB grain
|
||||||
|
uniform float u_float3; // luminance bias [0.0 – 1.0] 0 = uniform, 1 = shadows only
|
||||||
|
uniform int u_int0; // noise mode [0 or 1] 0 = smooth, 1 = grainy
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
layout(location = 0) out vec4 fragColor0;
|
||||||
|
|
||||||
|
// High-quality integer hash (pcg-like)
|
||||||
|
uint pcg(uint v) {
|
||||||
|
uint state = v * 747796405u + 2891336453u;
|
||||||
|
uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
|
||||||
|
return (word >> 22u) ^ word;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2D -> 1D hash input
|
||||||
|
uint hash2d(uvec2 p) {
|
||||||
|
return pcg(p.x + pcg(p.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash to float [0, 1]
|
||||||
|
float hashf(uvec2 p) {
|
||||||
|
return float(hash2d(p)) / float(0xffffffffu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash to float with offset (for RGB channels)
|
||||||
|
float hashf(uvec2 p, uint offset) {
|
||||||
|
return float(pcg(hash2d(p) + offset)) / float(0xffffffffu);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert uniform [0,1] to roughly Gaussian distribution
|
||||||
|
// Using simple approximation: average of multiple samples
|
||||||
|
float toGaussian(uvec2 p) {
|
||||||
|
float sum = hashf(p, 0u) + hashf(p, 1u) + hashf(p, 2u) + hashf(p, 3u);
|
||||||
|
return (sum - 2.0) * 0.7; // Centered, scaled
|
||||||
|
}
|
||||||
|
|
||||||
|
float toGaussian(uvec2 p, uint offset) {
|
||||||
|
float sum = hashf(p, offset) + hashf(p, offset + 1u)
|
||||||
|
+ hashf(p, offset + 2u) + hashf(p, offset + 3u);
|
||||||
|
return (sum - 2.0) * 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smooth noise with better interpolation
|
||||||
|
float smoothNoise(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
|
||||||
|
// Quintic interpolation (less banding than cubic)
|
||||||
|
f = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
||||||
|
|
||||||
|
uvec2 ui = uvec2(i);
|
||||||
|
float a = toGaussian(ui);
|
||||||
|
float b = toGaussian(ui + uvec2(1u, 0u));
|
||||||
|
float c = toGaussian(ui + uvec2(0u, 1u));
|
||||||
|
float d = toGaussian(ui + uvec2(1u, 1u));
|
||||||
|
|
||||||
|
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
float smoothNoise(vec2 p, uint offset) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
|
||||||
|
f = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
||||||
|
|
||||||
|
uvec2 ui = uvec2(i);
|
||||||
|
float a = toGaussian(ui, offset);
|
||||||
|
float b = toGaussian(ui + uvec2(1u, 0u), offset);
|
||||||
|
float c = toGaussian(ui + uvec2(0u, 1u), offset);
|
||||||
|
float d = toGaussian(ui + uvec2(1u, 1u), offset);
|
||||||
|
|
||||||
|
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 color = texture(u_image0, v_texCoord);
|
||||||
|
|
||||||
|
// Luminance (Rec.709)
|
||||||
|
float luma = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
|
||||||
|
// Grain UV (resolution-independent)
|
||||||
|
vec2 grainUV = v_texCoord * u_resolution / max(u_float1, 0.01);
|
||||||
|
uvec2 grainPixel = uvec2(grainUV);
|
||||||
|
|
||||||
|
float g;
|
||||||
|
vec3 grainRGB;
|
||||||
|
|
||||||
|
if (u_int0 == 1) {
|
||||||
|
// Grainy mode: pure hash noise (no interpolation = no banding)
|
||||||
|
g = toGaussian(grainPixel);
|
||||||
|
grainRGB = vec3(
|
||||||
|
toGaussian(grainPixel, 100u),
|
||||||
|
toGaussian(grainPixel, 200u),
|
||||||
|
toGaussian(grainPixel, 300u)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Smooth mode: interpolated with quintic curve
|
||||||
|
g = smoothNoise(grainUV);
|
||||||
|
grainRGB = vec3(
|
||||||
|
smoothNoise(grainUV, 100u),
|
||||||
|
smoothNoise(grainUV, 200u),
|
||||||
|
smoothNoise(grainUV, 300u)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Luminance weighting (less grain in highlights)
|
||||||
|
float lumWeight = mix(1.0, 1.0 - luma, clamp(u_float3, 0.0, 1.0));
|
||||||
|
|
||||||
|
// Strength
|
||||||
|
float strength = u_float0 * 0.15;
|
||||||
|
|
||||||
|
// Color vs monochrome grain
|
||||||
|
vec3 grainColor = mix(vec3(g), grainRGB, clamp(u_float2, 0.0, 1.0));
|
||||||
|
|
||||||
|
color.rgb += grainColor * strength * lumWeight;
|
||||||
|
fragColor0 = vec4(clamp(color.rgb, 0.0, 1.0), color.a);
|
||||||
|
}
|
||||||
133
blueprints/.glsl/Glow_30.frag
Normal file
133
blueprints/.glsl/Glow_30.frag
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform int u_int0; // Blend mode
|
||||||
|
uniform int u_int1; // Color tint
|
||||||
|
uniform float u_float0; // Intensity
|
||||||
|
uniform float u_float1; // Radius
|
||||||
|
uniform float u_float2; // Threshold
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
const int BLEND_ADD = 0;
|
||||||
|
const int BLEND_SCREEN = 1;
|
||||||
|
const int BLEND_SOFT = 2;
|
||||||
|
const int BLEND_OVERLAY = 3;
|
||||||
|
const int BLEND_LIGHTEN = 4;
|
||||||
|
|
||||||
|
const float GOLDEN_ANGLE = 2.39996323;
|
||||||
|
const int MAX_SAMPLES = 48;
|
||||||
|
const vec3 LUMA = vec3(0.299, 0.587, 0.114);
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
p = fract(p * vec2(123.34, 456.21));
|
||||||
|
p += dot(p, p + 45.32);
|
||||||
|
return fract(p.x * p.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hexToRgb(int h) {
|
||||||
|
return vec3(
|
||||||
|
float((h >> 16) & 255),
|
||||||
|
float((h >> 8) & 255),
|
||||||
|
float(h & 255)
|
||||||
|
) * (1.0 / 255.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 blend(vec3 base, vec3 glow, int mode) {
|
||||||
|
if (mode == BLEND_SCREEN) {
|
||||||
|
return 1.0 - (1.0 - base) * (1.0 - glow);
|
||||||
|
}
|
||||||
|
if (mode == BLEND_SOFT) {
|
||||||
|
return mix(
|
||||||
|
base - (1.0 - 2.0 * glow) * base * (1.0 - base),
|
||||||
|
base + (2.0 * glow - 1.0) * (sqrt(base) - base),
|
||||||
|
step(0.5, glow)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mode == BLEND_OVERLAY) {
|
||||||
|
return mix(
|
||||||
|
2.0 * base * glow,
|
||||||
|
1.0 - 2.0 * (1.0 - base) * (1.0 - glow),
|
||||||
|
step(0.5, base)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mode == BLEND_LIGHTEN) {
|
||||||
|
return max(base, glow);
|
||||||
|
}
|
||||||
|
return base + glow;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 original = texture(u_image0, v_texCoord);
|
||||||
|
|
||||||
|
float intensity = u_float0 * 0.05;
|
||||||
|
float radius = u_float1 * u_float1 * 0.012;
|
||||||
|
|
||||||
|
if (intensity < 0.001 || radius < 0.1) {
|
||||||
|
fragColor = original;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float threshold = 1.0 - u_float2 * 0.01;
|
||||||
|
float t0 = threshold - 0.15;
|
||||||
|
float t1 = threshold + 0.15;
|
||||||
|
|
||||||
|
vec2 texelSize = 1.0 / u_resolution;
|
||||||
|
float radius2 = radius * radius;
|
||||||
|
|
||||||
|
float sampleScale = clamp(radius * 0.75, 0.35, 1.0);
|
||||||
|
int samples = int(float(MAX_SAMPLES) * sampleScale);
|
||||||
|
|
||||||
|
float noise = hash(gl_FragCoord.xy);
|
||||||
|
float angleOffset = noise * GOLDEN_ANGLE;
|
||||||
|
float radiusJitter = 0.85 + noise * 0.3;
|
||||||
|
|
||||||
|
float ca = cos(GOLDEN_ANGLE);
|
||||||
|
float sa = sin(GOLDEN_ANGLE);
|
||||||
|
vec2 dir = vec2(cos(angleOffset), sin(angleOffset));
|
||||||
|
|
||||||
|
vec3 glow = vec3(0.0);
|
||||||
|
float totalWeight = 0.0;
|
||||||
|
|
||||||
|
// Center tap
|
||||||
|
float centerMask = smoothstep(t0, t1, dot(original.rgb, LUMA));
|
||||||
|
glow += original.rgb * centerMask * 2.0;
|
||||||
|
totalWeight += 2.0;
|
||||||
|
|
||||||
|
for (int i = 1; i < MAX_SAMPLES; i++) {
|
||||||
|
if (i >= samples) break;
|
||||||
|
|
||||||
|
float fi = float(i);
|
||||||
|
float dist = sqrt(fi / float(samples)) * radius * radiusJitter;
|
||||||
|
|
||||||
|
vec2 offset = dir * dist * texelSize;
|
||||||
|
vec3 c = texture(u_image0, v_texCoord + offset).rgb;
|
||||||
|
float mask = smoothstep(t0, t1, dot(c, LUMA));
|
||||||
|
|
||||||
|
float w = 1.0 - (dist * dist) / (radius2 * 1.5);
|
||||||
|
w = max(w, 0.0);
|
||||||
|
w *= w;
|
||||||
|
|
||||||
|
glow += c * mask * w;
|
||||||
|
totalWeight += w;
|
||||||
|
|
||||||
|
dir = vec2(
|
||||||
|
dir.x * ca - dir.y * sa,
|
||||||
|
dir.x * sa + dir.y * ca
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
glow *= intensity / max(totalWeight, 0.001);
|
||||||
|
|
||||||
|
if (u_int1 > 0) {
|
||||||
|
glow *= hexToRgb(u_int1);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 result = blend(original.rgb, glow, u_int0);
|
||||||
|
result += (noise - 0.5) * (1.0 / 255.0);
|
||||||
|
|
||||||
|
fragColor = vec4(clamp(result, 0.0, 1.0), original.a);
|
||||||
|
}
|
||||||
222
blueprints/.glsl/Hue_and_Saturation_1.frag
Normal file
222
blueprints/.glsl/Hue_and_Saturation_1.frag
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform int u_int0; // Mode: 0=Master, 1=Reds, 2=Yellows, 3=Greens, 4=Cyans, 5=Blues, 6=Magentas, 7=Colorize
|
||||||
|
uniform int u_int1; // Color Space: 0=HSL, 1=HSB/HSV
|
||||||
|
uniform float u_float0; // Hue (-180 to 180)
|
||||||
|
uniform float u_float1; // Saturation (-100 to 100)
|
||||||
|
uniform float u_float2; // Lightness/Brightness (-100 to 100)
|
||||||
|
uniform float u_float3; // Overlap (0 to 100) - feathering between adjacent color ranges
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
// Color range modes
|
||||||
|
const int MODE_MASTER = 0;
|
||||||
|
const int MODE_RED = 1;
|
||||||
|
const int MODE_YELLOW = 2;
|
||||||
|
const int MODE_GREEN = 3;
|
||||||
|
const int MODE_CYAN = 4;
|
||||||
|
const int MODE_BLUE = 5;
|
||||||
|
const int MODE_MAGENTA = 6;
|
||||||
|
const int MODE_COLORIZE = 7;
|
||||||
|
|
||||||
|
// Color space modes
|
||||||
|
const int COLORSPACE_HSL = 0;
|
||||||
|
const int COLORSPACE_HSB = 1;
|
||||||
|
|
||||||
|
const float EPSILON = 0.0001;
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// RGB <-> HSL Conversions
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
vec3 rgb2hsl(vec3 c) {
|
||||||
|
float maxC = max(max(c.r, c.g), c.b);
|
||||||
|
float minC = min(min(c.r, c.g), c.b);
|
||||||
|
float delta = maxC - minC;
|
||||||
|
|
||||||
|
float h = 0.0;
|
||||||
|
float s = 0.0;
|
||||||
|
float l = (maxC + minC) * 0.5;
|
||||||
|
|
||||||
|
if (delta > EPSILON) {
|
||||||
|
s = l < 0.5
|
||||||
|
? delta / (maxC + minC)
|
||||||
|
: delta / (2.0 - maxC - minC);
|
||||||
|
|
||||||
|
if (maxC == c.r) {
|
||||||
|
h = (c.g - c.b) / delta + (c.g < c.b ? 6.0 : 0.0);
|
||||||
|
} else if (maxC == c.g) {
|
||||||
|
h = (c.b - c.r) / delta + 2.0;
|
||||||
|
} else {
|
||||||
|
h = (c.r - c.g) / delta + 4.0;
|
||||||
|
}
|
||||||
|
h /= 6.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec3(h, s, l);
|
||||||
|
}
|
||||||
|
|
||||||
|
float hue2rgb(float p, float q, float t) {
|
||||||
|
t = fract(t);
|
||||||
|
if (t < 1.0/6.0) return p + (q - p) * 6.0 * t;
|
||||||
|
if (t < 0.5) return q;
|
||||||
|
if (t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0;
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hsl2rgb(vec3 hsl) {
|
||||||
|
if (hsl.y < EPSILON) return vec3(hsl.z);
|
||||||
|
|
||||||
|
float q = hsl.z < 0.5
|
||||||
|
? hsl.z * (1.0 + hsl.y)
|
||||||
|
: hsl.z + hsl.y - hsl.z * hsl.y;
|
||||||
|
float p = 2.0 * hsl.z - q;
|
||||||
|
|
||||||
|
return vec3(
|
||||||
|
hue2rgb(p, q, hsl.x + 1.0/3.0),
|
||||||
|
hue2rgb(p, q, hsl.x),
|
||||||
|
hue2rgb(p, q, hsl.x - 1.0/3.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 rgb2hsb(vec3 c) {
|
||||||
|
float maxC = max(max(c.r, c.g), c.b);
|
||||||
|
float minC = min(min(c.r, c.g), c.b);
|
||||||
|
float delta = maxC - minC;
|
||||||
|
|
||||||
|
float h = 0.0;
|
||||||
|
float s = (maxC > EPSILON) ? delta / maxC : 0.0;
|
||||||
|
float b = maxC;
|
||||||
|
|
||||||
|
if (delta > EPSILON) {
|
||||||
|
if (maxC == c.r) {
|
||||||
|
h = (c.g - c.b) / delta + (c.g < c.b ? 6.0 : 0.0);
|
||||||
|
} else if (maxC == c.g) {
|
||||||
|
h = (c.b - c.r) / delta + 2.0;
|
||||||
|
} else {
|
||||||
|
h = (c.r - c.g) / delta + 4.0;
|
||||||
|
}
|
||||||
|
h /= 6.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec3(h, s, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hsb2rgb(vec3 hsb) {
|
||||||
|
vec3 rgb = clamp(abs(mod(hsb.x * 6.0 + vec3(0.0, 4.0, 2.0), 6.0) - 3.0) - 1.0, 0.0, 1.0);
|
||||||
|
return hsb.z * mix(vec3(1.0), rgb, hsb.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Color Range Weight Calculation
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
float hueDistance(float a, float b) {
|
||||||
|
float d = abs(a - b);
|
||||||
|
return min(d, 1.0 - d);
|
||||||
|
}
|
||||||
|
|
||||||
|
float getHueWeight(float hue, float center, float overlap) {
|
||||||
|
float baseWidth = 1.0 / 6.0;
|
||||||
|
float feather = baseWidth * overlap;
|
||||||
|
|
||||||
|
float d = hueDistance(hue, center);
|
||||||
|
|
||||||
|
float inner = baseWidth * 0.5;
|
||||||
|
float outer = inner + feather;
|
||||||
|
|
||||||
|
return 1.0 - smoothstep(inner, outer, d);
|
||||||
|
}
|
||||||
|
|
||||||
|
float getModeWeight(float hue, int mode, float overlap) {
|
||||||
|
if (mode == MODE_MASTER || mode == MODE_COLORIZE) return 1.0;
|
||||||
|
|
||||||
|
if (mode == MODE_RED) {
|
||||||
|
return max(
|
||||||
|
getHueWeight(hue, 0.0, overlap),
|
||||||
|
getHueWeight(hue, 1.0, overlap)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
float center = float(mode - 1) / 6.0;
|
||||||
|
return getHueWeight(hue, center, overlap);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Adjustment Functions
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
float adjustLightness(float l, float amount) {
|
||||||
|
return amount > 0.0
|
||||||
|
? l + (1.0 - l) * amount
|
||||||
|
: l + l * amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
float adjustBrightness(float b, float amount) {
|
||||||
|
return clamp(b + amount, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float adjustSaturation(float s, float amount) {
|
||||||
|
return amount > 0.0
|
||||||
|
? s + (1.0 - s) * amount
|
||||||
|
: s + s * amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 colorize(vec3 rgb, float hue, float sat, float light) {
|
||||||
|
float lum = dot(rgb, vec3(0.299, 0.587, 0.114));
|
||||||
|
float l = adjustLightness(lum, light);
|
||||||
|
|
||||||
|
vec3 hsl = vec3(fract(hue), clamp(sat, 0.0, 1.0), clamp(l, 0.0, 1.0));
|
||||||
|
return hsl2rgb(hsl);
|
||||||
|
}
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// Main
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 original = texture(u_image0, v_texCoord);
|
||||||
|
|
||||||
|
float hueShift = u_float0 / 360.0; // -180..180 -> -0.5..0.5
|
||||||
|
float satAmount = u_float1 / 100.0; // -100..100 -> -1..1
|
||||||
|
float lightAmount= u_float2 / 100.0; // -100..100 -> -1..1
|
||||||
|
float overlap = u_float3 / 100.0; // 0..100 -> 0..1
|
||||||
|
|
||||||
|
vec3 result;
|
||||||
|
|
||||||
|
if (u_int0 == MODE_COLORIZE) {
|
||||||
|
result = colorize(original.rgb, hueShift, satAmount, lightAmount);
|
||||||
|
fragColor = vec4(result, original.a);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 hsx = (u_int1 == COLORSPACE_HSL)
|
||||||
|
? rgb2hsl(original.rgb)
|
||||||
|
: rgb2hsb(original.rgb);
|
||||||
|
|
||||||
|
float weight = getModeWeight(hsx.x, u_int0, overlap);
|
||||||
|
|
||||||
|
if (u_int0 != MODE_MASTER && hsx.y < EPSILON) {
|
||||||
|
weight = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weight > EPSILON) {
|
||||||
|
float h = fract(hsx.x + hueShift * weight);
|
||||||
|
float s = clamp(adjustSaturation(hsx.y, satAmount * weight), 0.0, 1.0);
|
||||||
|
float v = (u_int1 == COLORSPACE_HSL)
|
||||||
|
? clamp(adjustLightness(hsx.z, lightAmount * weight), 0.0, 1.0)
|
||||||
|
: clamp(adjustBrightness(hsx.z, lightAmount * weight), 0.0, 1.0);
|
||||||
|
|
||||||
|
vec3 adjusted = vec3(h, s, v);
|
||||||
|
result = (u_int1 == COLORSPACE_HSL)
|
||||||
|
? hsl2rgb(adjusted)
|
||||||
|
: hsb2rgb(adjusted);
|
||||||
|
} else {
|
||||||
|
result = original.rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragColor = vec4(result, original.a);
|
||||||
|
}
|
||||||
111
blueprints/.glsl/Image_Blur_1.frag
Normal file
111
blueprints/.glsl/Image_Blur_1.frag
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
#version 300 es
|
||||||
|
#pragma passes 2
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
// Blur type constants
|
||||||
|
const int BLUR_GAUSSIAN = 0;
|
||||||
|
const int BLUR_BOX = 1;
|
||||||
|
const int BLUR_RADIAL = 2;
|
||||||
|
|
||||||
|
// Radial blur config
|
||||||
|
const int RADIAL_SAMPLES = 12;
|
||||||
|
const float RADIAL_STRENGTH = 0.0003;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform int u_int0; // Blur type (BLUR_GAUSSIAN, BLUR_BOX, BLUR_RADIAL)
|
||||||
|
uniform float u_float0; // Blur radius/amount
|
||||||
|
uniform int u_pass; // Pass index (0 = horizontal, 1 = vertical)
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
layout(location = 0) out vec4 fragColor0;
|
||||||
|
|
||||||
|
float gaussian(float x, float sigma) {
|
||||||
|
return exp(-(x * x) / (2.0 * sigma * sigma));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 texelSize = 1.0 / u_resolution;
|
||||||
|
float radius = max(u_float0, 0.0);
|
||||||
|
|
||||||
|
// Radial (angular) blur - single pass, doesn't use separable
|
||||||
|
if (u_int0 == BLUR_RADIAL) {
|
||||||
|
// Only execute on first pass
|
||||||
|
if (u_pass > 0) {
|
||||||
|
fragColor0 = texture(u_image0, v_texCoord);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 center = vec2(0.5);
|
||||||
|
vec2 dir = v_texCoord - center;
|
||||||
|
float dist = length(dir);
|
||||||
|
|
||||||
|
if (dist < 1e-4) {
|
||||||
|
fragColor0 = texture(u_image0, v_texCoord);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec4 sum = vec4(0.0);
|
||||||
|
float totalWeight = 0.0;
|
||||||
|
float angleStep = radius * RADIAL_STRENGTH;
|
||||||
|
|
||||||
|
dir /= dist;
|
||||||
|
|
||||||
|
float cosStep = cos(angleStep);
|
||||||
|
float sinStep = sin(angleStep);
|
||||||
|
|
||||||
|
float negAngle = -float(RADIAL_SAMPLES) * angleStep;
|
||||||
|
vec2 rotDir = vec2(
|
||||||
|
dir.x * cos(negAngle) - dir.y * sin(negAngle),
|
||||||
|
dir.x * sin(negAngle) + dir.y * cos(negAngle)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (int i = -RADIAL_SAMPLES; i <= RADIAL_SAMPLES; i++) {
|
||||||
|
vec2 uv = center + rotDir * dist;
|
||||||
|
float w = 1.0 - abs(float(i)) / float(RADIAL_SAMPLES);
|
||||||
|
sum += texture(u_image0, uv) * w;
|
||||||
|
totalWeight += w;
|
||||||
|
|
||||||
|
rotDir = vec2(
|
||||||
|
rotDir.x * cosStep - rotDir.y * sinStep,
|
||||||
|
rotDir.x * sinStep + rotDir.y * cosStep
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragColor0 = sum / max(totalWeight, 0.001);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separable Gaussian / Box blur
|
||||||
|
int samples = int(ceil(radius));
|
||||||
|
|
||||||
|
if (samples == 0) {
|
||||||
|
fragColor0 = texture(u_image0, v_texCoord);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direction: pass 0 = horizontal, pass 1 = vertical
|
||||||
|
vec2 dir = (u_pass == 0) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
|
||||||
|
|
||||||
|
vec4 color = vec4(0.0);
|
||||||
|
float totalWeight = 0.0;
|
||||||
|
float sigma = radius / 2.0;
|
||||||
|
|
||||||
|
for (int i = -samples; i <= samples; i++) {
|
||||||
|
vec2 offset = dir * float(i) * texelSize;
|
||||||
|
vec4 sample_color = texture(u_image0, v_texCoord + offset);
|
||||||
|
|
||||||
|
float weight;
|
||||||
|
if (u_int0 == BLUR_GAUSSIAN) {
|
||||||
|
weight = gaussian(float(i), sigma);
|
||||||
|
} else {
|
||||||
|
// BLUR_BOX
|
||||||
|
weight = 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
color += sample_color * weight;
|
||||||
|
totalWeight += weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragColor0 = color / totalWeight;
|
||||||
|
}
|
||||||
19
blueprints/.glsl/Image_Channels_23.frag
Normal file
19
blueprints/.glsl/Image_Channels_23.frag
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
layout(location = 0) out vec4 fragColor0;
|
||||||
|
layout(location = 1) out vec4 fragColor1;
|
||||||
|
layout(location = 2) out vec4 fragColor2;
|
||||||
|
layout(location = 3) out vec4 fragColor3;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 color = texture(u_image0, v_texCoord);
|
||||||
|
// Output each channel as grayscale to separate render targets
|
||||||
|
fragColor0 = vec4(vec3(color.r), 1.0); // Red channel
|
||||||
|
fragColor1 = vec4(vec3(color.g), 1.0); // Green channel
|
||||||
|
fragColor2 = vec4(vec3(color.b), 1.0); // Blue channel
|
||||||
|
fragColor3 = vec4(vec3(color.a), 1.0); // Alpha channel
|
||||||
|
}
|
||||||
71
blueprints/.glsl/Image_Levels_1.frag
Normal file
71
blueprints/.glsl/Image_Levels_1.frag
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
// Levels Adjustment
|
||||||
|
// u_int0: channel (0=RGB, 1=R, 2=G, 3=B) default: 0
|
||||||
|
// u_float0: input black (0-255) default: 0
|
||||||
|
// u_float1: input white (0-255) default: 255
|
||||||
|
// u_float2: gamma (0.01-9.99) default: 1.0
|
||||||
|
// u_float3: output black (0-255) default: 0
|
||||||
|
// u_float4: output white (0-255) default: 255
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform int u_int0;
|
||||||
|
uniform float u_float0;
|
||||||
|
uniform float u_float1;
|
||||||
|
uniform float u_float2;
|
||||||
|
uniform float u_float3;
|
||||||
|
uniform float u_float4;
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
vec3 applyLevels(vec3 color, float inBlack, float inWhite, float gamma, float outBlack, float outWhite) {
|
||||||
|
float inRange = max(inWhite - inBlack, 0.0001);
|
||||||
|
vec3 result = clamp((color - inBlack) / inRange, 0.0, 1.0);
|
||||||
|
result = pow(result, vec3(1.0 / gamma));
|
||||||
|
result = mix(vec3(outBlack), vec3(outWhite), result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
float applySingleChannel(float value, float inBlack, float inWhite, float gamma, float outBlack, float outWhite) {
|
||||||
|
float inRange = max(inWhite - inBlack, 0.0001);
|
||||||
|
float result = clamp((value - inBlack) / inRange, 0.0, 1.0);
|
||||||
|
result = pow(result, 1.0 / gamma);
|
||||||
|
result = mix(outBlack, outWhite, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 texColor = texture(u_image0, v_texCoord);
|
||||||
|
vec3 color = texColor.rgb;
|
||||||
|
|
||||||
|
float inBlack = u_float0 / 255.0;
|
||||||
|
float inWhite = u_float1 / 255.0;
|
||||||
|
float gamma = u_float2;
|
||||||
|
float outBlack = u_float3 / 255.0;
|
||||||
|
float outWhite = u_float4 / 255.0;
|
||||||
|
|
||||||
|
vec3 result;
|
||||||
|
|
||||||
|
if (u_int0 == 0) {
|
||||||
|
result = applyLevels(color, inBlack, inWhite, gamma, outBlack, outWhite);
|
||||||
|
}
|
||||||
|
else if (u_int0 == 1) {
|
||||||
|
result = color;
|
||||||
|
result.r = applySingleChannel(color.r, inBlack, inWhite, gamma, outBlack, outWhite);
|
||||||
|
}
|
||||||
|
else if (u_int0 == 2) {
|
||||||
|
result = color;
|
||||||
|
result.g = applySingleChannel(color.g, inBlack, inWhite, gamma, outBlack, outWhite);
|
||||||
|
}
|
||||||
|
else if (u_int0 == 3) {
|
||||||
|
result = color;
|
||||||
|
result.b = applySingleChannel(color.b, inBlack, inWhite, gamma, outBlack, outWhite);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragColor = vec4(result, texColor.a);
|
||||||
|
}
|
||||||
28
blueprints/.glsl/README.md
Normal file
28
blueprints/.glsl/README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# GLSL Shader Sources
|
||||||
|
|
||||||
|
This folder contains the GLSL fragment shaders extracted from blueprint JSON files for easier editing and version control.
|
||||||
|
|
||||||
|
## File Naming Convention
|
||||||
|
|
||||||
|
`{Blueprint_Name}_{node_id}.frag`
|
||||||
|
|
||||||
|
- **Blueprint_Name**: The JSON filename with spaces/special chars replaced by underscores
|
||||||
|
- **node_id**: The GLSLShader node ID within the subgraph
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Extract shaders from blueprint JSONs to this folder
|
||||||
|
python update_blueprints.py extract
|
||||||
|
|
||||||
|
# Patch edited shaders back into blueprint JSONs
|
||||||
|
python update_blueprints.py patch
|
||||||
|
```
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Run `extract` to pull current shaders from JSONs
|
||||||
|
2. Edit `.frag` files
|
||||||
|
3. Run `patch` to update the blueprint JSONs
|
||||||
|
4. Test
|
||||||
|
5. Commit both `.frag` files and updated JSONs
|
||||||
28
blueprints/.glsl/Sharpen_23.frag
Normal file
28
blueprints/.glsl/Sharpen_23.frag
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform float u_float0; // strength [0.0 – 2.0] typical: 0.3–1.0
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
layout(location = 0) out vec4 fragColor0;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 texel = 1.0 / u_resolution;
|
||||||
|
|
||||||
|
// Sample center and neighbors
|
||||||
|
vec4 center = texture(u_image0, v_texCoord);
|
||||||
|
vec4 top = texture(u_image0, v_texCoord + vec2( 0.0, -texel.y));
|
||||||
|
vec4 bottom = texture(u_image0, v_texCoord + vec2( 0.0, texel.y));
|
||||||
|
vec4 left = texture(u_image0, v_texCoord + vec2(-texel.x, 0.0));
|
||||||
|
vec4 right = texture(u_image0, v_texCoord + vec2( texel.x, 0.0));
|
||||||
|
|
||||||
|
// Edge enhancement (Laplacian)
|
||||||
|
vec4 edges = center * 4.0 - top - bottom - left - right;
|
||||||
|
|
||||||
|
// Add edges back scaled by strength
|
||||||
|
vec4 sharpened = center + edges * u_float0;
|
||||||
|
|
||||||
|
fragColor0 = vec4(clamp(sharpened.rgb, 0.0, 1.0), center.a);
|
||||||
|
}
|
||||||
61
blueprints/.glsl/Unsharp_Mask_26.frag
Normal file
61
blueprints/.glsl/Unsharp_Mask_26.frag
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform float u_float0; // amount [0.0 - 3.0] typical: 0.5-1.5
|
||||||
|
uniform float u_float1; // radius [0.5 - 10.0] blur radius in pixels
|
||||||
|
uniform float u_float2; // threshold [0.0 - 0.1] min difference to sharpen
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
layout(location = 0) out vec4 fragColor0;
|
||||||
|
|
||||||
|
float gaussian(float x, float sigma) {
|
||||||
|
return exp(-(x * x) / (2.0 * sigma * sigma));
|
||||||
|
}
|
||||||
|
|
||||||
|
float getLuminance(vec3 color) {
|
||||||
|
return dot(color, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 texel = 1.0 / u_resolution;
|
||||||
|
float radius = max(u_float1, 0.5);
|
||||||
|
float amount = u_float0;
|
||||||
|
float threshold = u_float2;
|
||||||
|
|
||||||
|
vec4 original = texture(u_image0, v_texCoord);
|
||||||
|
|
||||||
|
// Gaussian blur for the "unsharp" mask
|
||||||
|
int samples = int(ceil(radius));
|
||||||
|
float sigma = radius / 2.0;
|
||||||
|
|
||||||
|
vec4 blurred = vec4(0.0);
|
||||||
|
float totalWeight = 0.0;
|
||||||
|
|
||||||
|
for (int x = -samples; x <= samples; x++) {
|
||||||
|
for (int y = -samples; y <= samples; y++) {
|
||||||
|
vec2 offset = vec2(float(x), float(y)) * texel;
|
||||||
|
vec4 sample_color = texture(u_image0, v_texCoord + offset);
|
||||||
|
|
||||||
|
float dist = length(vec2(float(x), float(y)));
|
||||||
|
float weight = gaussian(dist, sigma);
|
||||||
|
blurred += sample_color * weight;
|
||||||
|
totalWeight += weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blurred /= totalWeight;
|
||||||
|
|
||||||
|
// Unsharp mask = original - blurred
|
||||||
|
vec3 mask = original.rgb - blurred.rgb;
|
||||||
|
|
||||||
|
// Luminance-based threshold with smooth falloff
|
||||||
|
float lumaDelta = abs(getLuminance(original.rgb) - getLuminance(blurred.rgb));
|
||||||
|
float thresholdScale = smoothstep(0.0, threshold, lumaDelta);
|
||||||
|
mask *= thresholdScale;
|
||||||
|
|
||||||
|
// Sharpen: original + mask * amount
|
||||||
|
vec3 sharpened = original.rgb + mask * amount;
|
||||||
|
|
||||||
|
fragColor0 = vec4(clamp(sharpened, 0.0, 1.0), original.a);
|
||||||
|
}
|
||||||
159
blueprints/.glsl/update_blueprints.py
Normal file
159
blueprints/.glsl/update_blueprints.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Shader Blueprint Updater
|
||||||
|
|
||||||
|
Syncs GLSL shader files between this folder and blueprint JSON files.
|
||||||
|
|
||||||
|
File naming convention:
|
||||||
|
{Blueprint Name}_{node_id}.frag
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python update_blueprints.py extract # Extract shaders from JSONs to here
|
||||||
|
python update_blueprints.py patch # Patch shaders back into JSONs
|
||||||
|
python update_blueprints.py # Same as patch (default)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
GLSL_DIR = Path(__file__).parent
|
||||||
|
BLUEPRINTS_DIR = GLSL_DIR.parent
|
||||||
|
|
||||||
|
|
||||||
|
def get_blueprint_files():
|
||||||
|
"""Get all blueprint JSON files."""
|
||||||
|
return sorted(BLUEPRINTS_DIR.glob("*.json"))
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_filename(name):
|
||||||
|
"""Convert blueprint name to safe filename."""
|
||||||
|
return re.sub(r'[^\w\-]', '_', name)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_shaders():
|
||||||
|
"""Extract all shaders from blueprint JSONs to this folder."""
|
||||||
|
extracted = 0
|
||||||
|
for json_path in get_blueprint_files():
|
||||||
|
blueprint_name = json_path.stem
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(json_path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError) as e:
|
||||||
|
logger.warning("Skipping %s: %s", json_path.name, e)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find GLSLShader nodes in subgraphs
|
||||||
|
for subgraph in data.get('definitions', {}).get('subgraphs', []):
|
||||||
|
for node in subgraph.get('nodes', []):
|
||||||
|
if node.get('type') == 'GLSLShader':
|
||||||
|
node_id = node.get('id')
|
||||||
|
widgets = node.get('widgets_values', [])
|
||||||
|
|
||||||
|
# Find shader code (first string that looks like GLSL)
|
||||||
|
for widget in widgets:
|
||||||
|
if isinstance(widget, str) and widget.startswith('#version'):
|
||||||
|
safe_name = sanitize_filename(blueprint_name)
|
||||||
|
frag_name = f"{safe_name}_{node_id}.frag"
|
||||||
|
frag_path = GLSL_DIR / frag_name
|
||||||
|
|
||||||
|
with open(frag_path, 'w') as f:
|
||||||
|
f.write(widget)
|
||||||
|
|
||||||
|
logger.info(" Extracted: %s", frag_name)
|
||||||
|
extracted += 1
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info("\nExtracted %d shader(s)", extracted)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_shaders():
|
||||||
|
"""Patch shaders from this folder back into blueprint JSONs."""
|
||||||
|
# Build lookup: blueprint_name -> [(node_id, shader_code), ...]
|
||||||
|
shader_updates = {}
|
||||||
|
|
||||||
|
for frag_path in sorted(GLSL_DIR.glob("*.frag")):
|
||||||
|
# Parse filename: {blueprint_name}_{node_id}.frag
|
||||||
|
parts = frag_path.stem.rsplit('_', 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
logger.warning("Skipping %s: invalid filename format", frag_path.name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
blueprint_name, node_id_str = parts
|
||||||
|
|
||||||
|
try:
|
||||||
|
node_id = int(node_id_str)
|
||||||
|
except ValueError:
|
||||||
|
logger.warning("Skipping %s: invalid node_id", frag_path.name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(frag_path, 'r') as f:
|
||||||
|
shader_code = f.read()
|
||||||
|
|
||||||
|
if blueprint_name not in shader_updates:
|
||||||
|
shader_updates[blueprint_name] = []
|
||||||
|
shader_updates[blueprint_name].append((node_id, shader_code))
|
||||||
|
|
||||||
|
# Apply updates to JSON files
|
||||||
|
patched = 0
|
||||||
|
for json_path in get_blueprint_files():
|
||||||
|
blueprint_name = sanitize_filename(json_path.stem)
|
||||||
|
|
||||||
|
if blueprint_name not in shader_updates:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(json_path, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
except (json.JSONDecodeError, IOError) as e:
|
||||||
|
logger.error("Error reading %s: %s", json_path.name, e)
|
||||||
|
continue
|
||||||
|
|
||||||
|
modified = False
|
||||||
|
for node_id, shader_code in shader_updates[blueprint_name]:
|
||||||
|
# Find the node and update
|
||||||
|
for subgraph in data.get('definitions', {}).get('subgraphs', []):
|
||||||
|
for node in subgraph.get('nodes', []):
|
||||||
|
if node.get('id') == node_id and node.get('type') == 'GLSLShader':
|
||||||
|
widgets = node.get('widgets_values', [])
|
||||||
|
if len(widgets) > 0 and widgets[0] != shader_code:
|
||||||
|
widgets[0] = shader_code
|
||||||
|
modified = True
|
||||||
|
logger.info(" Patched: %s (node %d)", json_path.name, node_id)
|
||||||
|
patched += 1
|
||||||
|
|
||||||
|
if modified:
|
||||||
|
with open(json_path, 'w') as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
if patched == 0:
|
||||||
|
logger.info("No changes to apply.")
|
||||||
|
else:
|
||||||
|
logger.info("\nPatched %d shader(s)", patched)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
command = "patch"
|
||||||
|
else:
|
||||||
|
command = sys.argv[1].lower()
|
||||||
|
|
||||||
|
if command == "extract":
|
||||||
|
logger.info("Extracting shaders from blueprints...")
|
||||||
|
extract_shaders()
|
||||||
|
elif command in ("patch", "update", "apply"):
|
||||||
|
logger.info("Patching shaders into blueprints...")
|
||||||
|
patch_shaders()
|
||||||
|
else:
|
||||||
|
logger.info(__doc__)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
blueprints/Brightness and Contrast.json
Normal file
1
blueprints/Brightness and Contrast.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"revision":0,"last_node_id":140,"last_link_id":0,"nodes":[{"id":140,"type":"916dff42-6166-4d45-b028-04eaf69fbb35","pos":[500,1440],"size":[250,178],"flags":{},"order":2,"mode":0,"inputs":[{"label":"image","localized_name":"images.image0","name":"images.image0","type":"IMAGE","link":null}],"outputs":[{"label":"IMAGE","localized_name":"IMAGE0","name":"IMAGE0","type":"IMAGE","links":[]}],"properties":{"proxyWidgets":[["4","value"],["5","value"]]},"widgets_values":[],"title":"Brightness and Contrast"}],"links":[],"version":0.4,"definitions":{"subgraphs":[{"id":"916dff42-6166-4d45-b028-04eaf69fbb35","version":1,"state":{"lastGroupId":0,"lastNodeId":143,"lastLinkId":118,"lastRerouteId":0},"revision":0,"config":{},"name":"Brightness and Contrast","inputNode":{"id":-10,"bounding":[360,-176,120,60]},"outputNode":{"id":-20,"bounding":[1410,-176,120,60]},"inputs":[{"id":"a5aae7ea-b511-4045-b5da-94101e269cd7","name":"images.image0","type":"IMAGE","linkIds":[117],"localized_name":"images.image0","label":"image","pos":[460,-156]}],"outputs":[{"id":"30b72604-69b3-4944-b253-a9099bbd73a9","name":"IMAGE0","type":"IMAGE","linkIds":[118],"localized_name":"IMAGE0","label":"IMAGE","pos":[1430,-156]}],"widgets":[],"nodes":[{"id":4,"type":"PrimitiveFloat","pos":[540,-280],"size":[270,58],"flags":{},"order":0,"mode":0,"inputs":[{"label":"brightness","localized_name":"value","name":"value","type":"FLOAT","widget":{"name":"value"},"link":null}],"outputs":[{"localized_name":"FLOAT","name":"FLOAT","type":"FLOAT","links":[115]}],"properties":{"Node name for S&R":"PrimitiveFloat","min":0,"max":100,"precision":1,"step":1},"widgets_values":[50]},{"id":5,"type":"PrimitiveFloat","pos":[540,-170],"size":[270,58],"flags":{},"order":1,"mode":0,"inputs":[{"label":"contrast","localized_name":"value","name":"value","type":"FLOAT","widget":{"name":"value"},"link":null}],"outputs":[{"localized_name":"FLOAT","name":"FLOAT","type":"FLOAT","links":[116]}],"properties":{"Node name for S&R":"PrimitiveFloat","min":0,"max":100,"precision":1,"step":1},"widgets_values":[0]},{"id":143,"type":"GLSLShader","pos":[840,-280],"size":[400,212],"flags":{},"order":2,"mode":0,"inputs":[{"label":"image0","localized_name":"images.image0","name":"images.image0","type":"IMAGE","link":117},{"label":"image1","localized_name":"images.image1","name":"images.image1","shape":7,"type":"IMAGE","link":null},{"label":"u_float0","localized_name":"floats.u_float0","name":"floats.u_float0","shape":7,"type":"FLOAT","link":115},{"label":"u_float1","localized_name":"floats.u_float1","name":"floats.u_float1","shape":7,"type":"FLOAT","link":116},{"label":"u_float2","localized_name":"floats.u_float2","name":"floats.u_float2","shape":7,"type":"FLOAT","link":null},{"label":"u_int0","localized_name":"ints.u_int0","name":"ints.u_int0","shape":7,"type":"INT","link":null},{"localized_name":"fragment_shader","name":"fragment_shader","type":"STRING","widget":{"name":"fragment_shader"},"link":null},{"localized_name":"size_mode","name":"size_mode","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"size_mode"},"link":null}],"outputs":[{"localized_name":"IMAGE0","name":"IMAGE0","type":"IMAGE","links":[118]},{"localized_name":"IMAGE1","name":"IMAGE1","type":"IMAGE","links":null},{"localized_name":"IMAGE2","name":"IMAGE2","type":"IMAGE","links":null},{"localized_name":"IMAGE3","name":"IMAGE3","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"GLSLShader"},"widgets_values":["#version 300 es\nprecision highp float;\n\nuniform sampler2D u_image0;\nuniform float u_float0; // Brightness slider -100..100\nuniform float u_float1; // Contrast slider -100..100\n\nin vec2 v_texCoord;\nout vec4 fragColor;\n\nconst float MID_GRAY = 0.18; // 18% reflectance\n\n// sRGB gamma 2.2 approximation\nvec3 srgbToLinear(vec3 c) {\n return pow(max(c, 0.0), vec3(2.2));\n}\n\nvec3 linearToSrgb(vec3 c) {\n return pow(max(c, 0.0), vec3(1.0/2.2));\n}\n\nfloat mapBrightness(float b) {\n return clamp(b / 100.0, -1.0, 1.0);\n}\n\nfloat mapContrast(float c) {\n return clamp(c / 100.0 + 1.0, 0.0, 2.0);\n}\n\nvoid main() {\n vec4 orig = texture(u_image0, v_texCoord);\n\n float brightness = mapBrightness(u_float0);\n float contrast = mapContrast(u_float1);\n\n vec3 lin = srgbToLinear(orig.rgb);\n\n lin = (lin - MID_GRAY) * contrast + brightness + MID_GRAY;\n\n // Convert back to sRGB\n vec3 result = linearToSrgb(clamp(lin, 0.0, 1.0));\n\n fragColor = vec4(result, orig.a);\n}\n","from_input"]}],"groups":[],"links":[{"id":115,"origin_id":4,"origin_slot":0,"target_id":143,"target_slot":2,"type":"FLOAT"},{"id":116,"origin_id":5,"origin_slot":0,"target_id":143,"target_slot":3,"type":"FLOAT"},{"id":117,"origin_id":-10,"origin_slot":0,"target_id":143,"target_slot":0,"type":"IMAGE"},{"id":118,"origin_id":143,"origin_slot":0,"target_id":-20,"target_slot":0,"type":"IMAGE"}],"extra":{"workflowRendererVersion":"LG"}}]},"extra":{}}
|
||||||
1
blueprints/Chromatic Aberration.json
Normal file
1
blueprints/Chromatic Aberration.json
Normal file
File diff suppressed because one or more lines are too long
1
blueprints/Color Adjustment.json
Normal file
1
blueprints/Color Adjustment.json
Normal file
File diff suppressed because one or more lines are too long
1
blueprints/Edge-Preserving Blur.json
Normal file
1
blueprints/Edge-Preserving Blur.json
Normal file
File diff suppressed because one or more lines are too long
1
blueprints/Film Grain.json
Normal file
1
blueprints/Film Grain.json
Normal file
File diff suppressed because one or more lines are too long
1
blueprints/Glow.json
Normal file
1
blueprints/Glow.json
Normal file
File diff suppressed because one or more lines are too long
1
blueprints/Hue and Saturation.json
Normal file
1
blueprints/Hue and Saturation.json
Normal file
File diff suppressed because one or more lines are too long
1
blueprints/Image Blur.json
Normal file
1
blueprints/Image Blur.json
Normal file
File diff suppressed because one or more lines are too long
1
blueprints/Image Channels.json
Normal file
1
blueprints/Image Channels.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"revision": 0, "last_node_id": 29, "last_link_id": 0, "nodes": [{"id": 29, "type": "4c9d6ea4-b912-40e5-8766-6793a9758c53", "pos": [1970, -230], "size": [180, 86], "flags": {}, "order": 5, "mode": 0, "inputs": [{"label": "image", "localized_name": "images.image0", "name": "images.image0", "type": "IMAGE", "link": null}], "outputs": [{"label": "R", "localized_name": "IMAGE0", "name": "IMAGE0", "type": "IMAGE", "links": []}, {"label": "G", "localized_name": "IMAGE1", "name": "IMAGE1", "type": "IMAGE", "links": []}, {"label": "B", "localized_name": "IMAGE2", "name": "IMAGE2", "type": "IMAGE", "links": []}, {"label": "A", "localized_name": "IMAGE3", "name": "IMAGE3", "type": "IMAGE", "links": []}], "title": "Image Channels", "properties": {"proxyWidgets": []}, "widgets_values": []}], "links": [], "version": 0.4, "definitions": {"subgraphs": [{"id": "4c9d6ea4-b912-40e5-8766-6793a9758c53", "version": 1, "state": {"lastGroupId": 0, "lastNodeId": 28, "lastLinkId": 39, "lastRerouteId": 0}, "revision": 0, "config": {}, "name": "Image Channels", "inputNode": {"id": -10, "bounding": [1820, -185, 120, 60]}, "outputNode": {"id": -20, "bounding": [2460, -215, 120, 120]}, "inputs": [{"id": "3522932b-2d86-4a1f-a02a-cb29f3a9d7fe", "name": "images.image0", "type": "IMAGE", "linkIds": [39], "localized_name": "images.image0", "label": "image", "pos": [1920, -165]}], "outputs": [{"id": "605cb9c3-b065-4d9b-81d2-3ec331889b2b", "name": "IMAGE0", "type": "IMAGE", "linkIds": [26], "localized_name": "IMAGE0", "label": "R", "pos": [2480, -195]}, {"id": "fb44a77e-0522-43e9-9527-82e7465b3596", "name": "IMAGE1", "type": "IMAGE", "linkIds": [27], "localized_name": "IMAGE1", "label": "G", "pos": [2480, -175]}, {"id": "81460ee6-0131-402a-874f-6bf3001fc4ff", "name": "IMAGE2", "type": "IMAGE", "linkIds": [28], "localized_name": "IMAGE2", "label": "B", "pos": [2480, -155]}, {"id": "ae690246-80d4-4951-b1d9-9306d8a77417", "name": "IMAGE3", "type": "IMAGE", "linkIds": [29], "localized_name": "IMAGE3", "label": "A", "pos": [2480, -135]}], "widgets": [], "nodes": [{"id": 23, "type": "GLSLShader", "pos": [2000, -330], "size": [400, 172], "flags": {}, "order": 0, "mode": 0, "inputs": [{"label": "image", "localized_name": "images.image0", "name": "images.image0", "type": "IMAGE", "link": 39}, {"localized_name": "fragment_shader", "name": "fragment_shader", "type": "STRING", "widget": {"name": "fragment_shader"}, "link": null}, {"localized_name": "size_mode", "name": "size_mode", "type": "COMFY_DYNAMICCOMBO_V3", "widget": {"name": "size_mode"}, "link": null}, {"label": "image1", "localized_name": "images.image1", "name": "images.image1", "shape": 7, "type": "IMAGE", "link": null}], "outputs": [{"label": "R", "localized_name": "IMAGE0", "name": "IMAGE0", "type": "IMAGE", "links": [26]}, {"label": "G", "localized_name": "IMAGE1", "name": "IMAGE1", "type": "IMAGE", "links": [27]}, {"label": "B", "localized_name": "IMAGE2", "name": "IMAGE2", "type": "IMAGE", "links": [28]}, {"label": "A", "localized_name": "IMAGE3", "name": "IMAGE3", "type": "IMAGE", "links": [29]}], "properties": {"Node name for S&R": "GLSLShader"}, "widgets_values": ["#version 300 es\nprecision highp float;\n\nuniform sampler2D u_image0;\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\nlayout(location = 1) out vec4 fragColor1;\nlayout(location = 2) out vec4 fragColor2;\nlayout(location = 3) out vec4 fragColor3;\n\nvoid main() {\n vec4 color = texture(u_image0, v_texCoord);\n // Output each channel as grayscale to separate render targets\n fragColor0 = vec4(vec3(color.r), 1.0); // Red channel\n fragColor1 = vec4(vec3(color.g), 1.0); // Green channel\n fragColor2 = vec4(vec3(color.b), 1.0); // Blue channel\n fragColor3 = vec4(vec3(color.a), 1.0); // Alpha channel\n}\n", "from_input"]}], "groups": [], "links": [{"id": 39, "origin_id": -10, "origin_slot": 0, "target_id": 23, "target_slot": 0, "type": "IMAGE"}, {"id": 26, "origin_id": 23, "origin_slot": 0, "target_id": -20, "target_slot": 0, "type": "IMAGE"}, {"id": 27, "origin_id": 23, "origin_slot": 1, "target_id": -20, "target_slot": 1, "type": "IMAGE"}, {"id": 28, "origin_id": 23, "origin_slot": 2, "target_id": -20, "target_slot": 2, "type": "IMAGE"}, {"id": 29, "origin_id": 23, "origin_slot": 3, "target_id": -20, "target_slot": 3, "type": "IMAGE"}], "extra": {"workflowRendererVersion": "LG"}}]}}
|
||||||
1
blueprints/Image Levels.json
Normal file
1
blueprints/Image Levels.json
Normal file
File diff suppressed because one or more lines are too long
1
blueprints/Sharpen.json
Normal file
1
blueprints/Sharpen.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"revision":0,"last_node_id":25,"last_link_id":0,"nodes":[{"id":25,"type":"621ba4e2-22a8-482d-a369-023753198b7b","pos":[4610,-790],"size":[230,58],"flags":{},"order":4,"mode":0,"inputs":[{"label":"image","localized_name":"images.image0","name":"images.image0","type":"IMAGE","link":null}],"outputs":[{"label":"IMAGE","localized_name":"IMAGE0","name":"IMAGE0","type":"IMAGE","links":[]}],"title":"Sharpen","properties":{"proxyWidgets":[["24","value"]]},"widgets_values":[]}],"links":[],"version":0.4,"definitions":{"subgraphs":[{"id":"621ba4e2-22a8-482d-a369-023753198b7b","version":1,"state":{"lastGroupId":0,"lastNodeId":24,"lastLinkId":36,"lastRerouteId":0},"revision":0,"config":{},"name":"Sharpen","inputNode":{"id":-10,"bounding":[4090,-825,120,60]},"outputNode":{"id":-20,"bounding":[5150,-825,120,60]},"inputs":[{"id":"37011fb7-14b7-4e0e-b1a0-6a02e8da1fd7","name":"images.image0","type":"IMAGE","linkIds":[34],"localized_name":"images.image0","label":"image","pos":[4190,-805]}],"outputs":[{"id":"e9182b3f-635c-4cd4-a152-4b4be17ae4b9","name":"IMAGE0","type":"IMAGE","linkIds":[35],"localized_name":"IMAGE0","label":"IMAGE","pos":[5170,-805]}],"widgets":[],"nodes":[{"id":24,"type":"PrimitiveFloat","pos":[4280,-1240],"size":[270,58],"flags":{},"order":0,"mode":0,"inputs":[{"label":"strength","localized_name":"value","name":"value","type":"FLOAT","widget":{"name":"value"},"link":null}],"outputs":[{"localized_name":"FLOAT","name":"FLOAT","type":"FLOAT","links":[36]}],"properties":{"Node name for S&R":"PrimitiveFloat","min":0,"max":3,"precision":2,"step":0.05},"widgets_values":[0.5]},{"id":23,"type":"GLSLShader","pos":[4570,-1240],"size":[370,192],"flags":{},"order":1,"mode":0,"inputs":[{"label":"image0","localized_name":"images.image0","name":"images.image0","type":"IMAGE","link":34},{"label":"image1","localized_name":"images.image1","name":"images.image1","shape":7,"type":"IMAGE","link":null},{"label":"u_float0","localized_name":"floats.u_float0","name":"floats.u_float0","shape":7,"type":"FLOAT","link":36},{"label":"u_float1","localized_name":"floats.u_float1","name":"floats.u_float1","shape":7,"type":"FLOAT","link":null},{"label":"u_int0","localized_name":"ints.u_int0","name":"ints.u_int0","shape":7,"type":"INT","link":null},{"localized_name":"fragment_shader","name":"fragment_shader","type":"STRING","widget":{"name":"fragment_shader"},"link":null},{"localized_name":"size_mode","name":"size_mode","type":"COMFY_DYNAMICCOMBO_V3","widget":{"name":"size_mode"},"link":null}],"outputs":[{"localized_name":"IMAGE0","name":"IMAGE0","type":"IMAGE","links":[35]},{"localized_name":"IMAGE1","name":"IMAGE1","type":"IMAGE","links":null},{"localized_name":"IMAGE2","name":"IMAGE2","type":"IMAGE","links":null},{"localized_name":"IMAGE3","name":"IMAGE3","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"GLSLShader"},"widgets_values":["#version 300 es\nprecision highp float;\n\nuniform sampler2D u_image0;\nuniform vec2 u_resolution;\nuniform float u_float0; // strength [0.0 – 2.0] typical: 0.3–1.0\n\nin vec2 v_texCoord;\nlayout(location = 0) out vec4 fragColor0;\n\nvoid main() {\n vec2 texel = 1.0 / u_resolution;\n \n // Sample center and neighbors\n vec4 center = texture(u_image0, v_texCoord);\n vec4 top = texture(u_image0, v_texCoord + vec2( 0.0, -texel.y));\n vec4 bottom = texture(u_image0, v_texCoord + vec2( 0.0, texel.y));\n vec4 left = texture(u_image0, v_texCoord + vec2(-texel.x, 0.0));\n vec4 right = texture(u_image0, v_texCoord + vec2( texel.x, 0.0));\n \n // Edge enhancement (Laplacian)\n vec4 edges = center * 4.0 - top - bottom - left - right;\n \n // Add edges back scaled by strength\n vec4 sharpened = center + edges * u_float0;\n \n fragColor0 = vec4(clamp(sharpened.rgb, 0.0, 1.0), center.a);\n}","from_input"]}],"groups":[],"links":[{"id":36,"origin_id":24,"origin_slot":0,"target_id":23,"target_slot":2,"type":"FLOAT"},{"id":34,"origin_id":-10,"origin_slot":0,"target_id":23,"target_slot":0,"type":"IMAGE"},{"id":35,"origin_id":23,"origin_slot":0,"target_id":-20,"target_slot":0,"type":"IMAGE"}],"extra":{"workflowRendererVersion":"LG"}}]}}
|
||||||
1
blueprints/Unsharp Mask.json
Normal file
1
blueprints/Unsharp Mask.json
Normal file
File diff suppressed because one or more lines are too long
@ -426,10 +426,8 @@ class CLIP:
|
|||||||
def generate(self, tokens, do_sample=True, max_length=256, temperature=1.0, top_k=50, top_p=0.95, min_p=0.0, repetition_penalty=1.0, seed=None):
|
def generate(self, tokens, do_sample=True, max_length=256, temperature=1.0, top_k=50, top_p=0.95, min_p=0.0, repetition_penalty=1.0, seed=None):
|
||||||
self.cond_stage_model.reset_clip_options()
|
self.cond_stage_model.reset_clip_options()
|
||||||
|
|
||||||
if self.layer_idx is not None:
|
|
||||||
self.cond_stage_model.set_clip_options({"layer": self.layer_idx})
|
|
||||||
|
|
||||||
self.load_model()
|
self.load_model()
|
||||||
|
self.cond_stage_model.set_clip_options({"layer": None})
|
||||||
self.cond_stage_model.set_clip_options({"execution_device": self.patcher.load_device})
|
self.cond_stage_model.set_clip_options({"execution_device": self.patcher.load_device})
|
||||||
return self.cond_stage_model.generate(tokens, do_sample=do_sample, max_length=max_length, temperature=temperature, top_k=top_k, top_p=top_p, min_p=min_p, repetition_penalty=repetition_penalty, seed=seed)
|
return self.cond_stage_model.generate(tokens, do_sample=do_sample, max_length=max_length, temperature=temperature, top_k=top_k, top_p=top_p, min_p=min_p, repetition_penalty=repetition_penalty, seed=seed)
|
||||||
|
|
||||||
|
|||||||
@ -308,14 +308,14 @@ class SDClipModel(torch.nn.Module, ClipTokenWeightEncoder):
|
|||||||
def load_sd(self, sd):
|
def load_sd(self, sd):
|
||||||
return self.transformer.load_state_dict(sd, strict=False, assign=getattr(self, "can_assign_sd", False))
|
return self.transformer.load_state_dict(sd, strict=False, assign=getattr(self, "can_assign_sd", False))
|
||||||
|
|
||||||
def generate(self, tokens, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed, stop_tokens=[]):
|
def generate(self, tokens, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed):
|
||||||
if isinstance(tokens, dict):
|
if isinstance(tokens, dict):
|
||||||
tokens_only = next(iter(tokens.values())) # todo: get this better?
|
tokens_only = next(iter(tokens.values())) # todo: get this better?
|
||||||
else:
|
else:
|
||||||
tokens_only = tokens
|
tokens_only = tokens
|
||||||
tokens_only = [[t[0] for t in b] for b in tokens_only]
|
tokens_only = [[t[0] for t in b] for b in tokens_only]
|
||||||
embeds = self.process_tokens(tokens_only, device=self.execution_device)[0]
|
embeds = self.process_tokens(tokens_only, device=self.execution_device)[0]
|
||||||
return self.transformer.generate(embeds, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed, stop_tokens)
|
return self.transformer.generate(embeds, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed)
|
||||||
|
|
||||||
def parse_parentheses(string):
|
def parse_parentheses(string):
|
||||||
result = []
|
result = []
|
||||||
@ -573,6 +573,8 @@ class SDTokenizer:
|
|||||||
min_length = tokenizer_options.get("{}_min_length".format(self.embedding_key), self.min_length)
|
min_length = tokenizer_options.get("{}_min_length".format(self.embedding_key), self.min_length)
|
||||||
min_padding = tokenizer_options.get("{}_min_padding".format(self.embedding_key), self.min_padding)
|
min_padding = tokenizer_options.get("{}_min_padding".format(self.embedding_key), self.min_padding)
|
||||||
|
|
||||||
|
min_length = kwargs.get("min_length", min_length)
|
||||||
|
|
||||||
text = escape_important(text)
|
text = escape_important(text)
|
||||||
if kwargs.get("disable_weights", self.disable_weights):
|
if kwargs.get("disable_weights", self.disable_weights):
|
||||||
parsed_weights = [(text, 1.0)]
|
parsed_weights = [(text, 1.0)]
|
||||||
|
|||||||
@ -33,6 +33,8 @@ class AnimaTokenizer:
|
|||||||
def state_dict(self):
|
def state_dict(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def decode(self, token_ids, **kwargs):
|
||||||
|
return self.qwen3_06b.decode(token_ids, **kwargs)
|
||||||
|
|
||||||
class Qwen3_06BModel(sd1_clip.SDClipModel):
|
class Qwen3_06BModel(sd1_clip.SDClipModel):
|
||||||
def __init__(self, device="cpu", layer="last", layer_idx=None, dtype=None, attention_mask=True, model_options={}):
|
def __init__(self, device="cpu", layer="last", layer_idx=None, dtype=None, attention_mask=True, model_options={}):
|
||||||
|
|||||||
@ -105,6 +105,7 @@ class Qwen3_06BConfig:
|
|||||||
rope_scale = None
|
rope_scale = None
|
||||||
final_norm: bool = True
|
final_norm: bool = True
|
||||||
lm_head: bool = False
|
lm_head: bool = False
|
||||||
|
stop_tokens = [151643, 151645]
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Qwen3_06B_ACE15_Config:
|
class Qwen3_06B_ACE15_Config:
|
||||||
@ -128,6 +129,7 @@ class Qwen3_06B_ACE15_Config:
|
|||||||
rope_scale = None
|
rope_scale = None
|
||||||
final_norm: bool = True
|
final_norm: bool = True
|
||||||
lm_head: bool = False
|
lm_head: bool = False
|
||||||
|
stop_tokens = [151643, 151645]
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Qwen3_2B_ACE15_lm_Config:
|
class Qwen3_2B_ACE15_lm_Config:
|
||||||
@ -151,6 +153,7 @@ class Qwen3_2B_ACE15_lm_Config:
|
|||||||
rope_scale = None
|
rope_scale = None
|
||||||
final_norm: bool = True
|
final_norm: bool = True
|
||||||
lm_head: bool = False
|
lm_head: bool = False
|
||||||
|
stop_tokens = [151643, 151645]
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Qwen3_4B_ACE15_lm_Config:
|
class Qwen3_4B_ACE15_lm_Config:
|
||||||
@ -174,6 +177,7 @@ class Qwen3_4B_ACE15_lm_Config:
|
|||||||
rope_scale = None
|
rope_scale = None
|
||||||
final_norm: bool = True
|
final_norm: bool = True
|
||||||
lm_head: bool = False
|
lm_head: bool = False
|
||||||
|
stop_tokens = [151643, 151645]
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Qwen3_4BConfig:
|
class Qwen3_4BConfig:
|
||||||
@ -197,6 +201,7 @@ class Qwen3_4BConfig:
|
|||||||
rope_scale = None
|
rope_scale = None
|
||||||
final_norm: bool = True
|
final_norm: bool = True
|
||||||
lm_head: bool = False
|
lm_head: bool = False
|
||||||
|
stop_tokens = [151643, 151645]
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Qwen3_8BConfig:
|
class Qwen3_8BConfig:
|
||||||
@ -220,6 +225,7 @@ class Qwen3_8BConfig:
|
|||||||
rope_scale = None
|
rope_scale = None
|
||||||
final_norm: bool = True
|
final_norm: bool = True
|
||||||
lm_head: bool = False
|
lm_head: bool = False
|
||||||
|
stop_tokens = [151643, 151645]
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Ovis25_2BConfig:
|
class Ovis25_2BConfig:
|
||||||
@ -290,6 +296,7 @@ class Gemma2_2B_Config:
|
|||||||
rope_scale = None
|
rope_scale = None
|
||||||
final_norm: bool = True
|
final_norm: bool = True
|
||||||
lm_head: bool = False
|
lm_head: bool = False
|
||||||
|
stop_tokens = [1]
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Gemma3_4B_Config:
|
class Gemma3_4B_Config:
|
||||||
@ -314,6 +321,7 @@ class Gemma3_4B_Config:
|
|||||||
rope_scale = [8.0, 1.0]
|
rope_scale = [8.0, 1.0]
|
||||||
final_norm: bool = True
|
final_norm: bool = True
|
||||||
lm_head: bool = False
|
lm_head: bool = False
|
||||||
|
stop_tokens = [1, 106]
|
||||||
|
|
||||||
GEMMA3_VISION_CONFIG = {"num_channels": 3, "hidden_act": "gelu_pytorch_tanh", "hidden_size": 1152, "image_size": 896, "intermediate_size": 4304, "model_type": "siglip_vision_model", "num_attention_heads": 16, "num_hidden_layers": 27, "patch_size": 14}
|
GEMMA3_VISION_CONFIG = {"num_channels": 3, "hidden_act": "gelu_pytorch_tanh", "hidden_size": 1152, "image_size": 896, "intermediate_size": 4304, "model_type": "siglip_vision_model", "num_attention_heads": 16, "num_hidden_layers": 27, "patch_size": 14}
|
||||||
|
|
||||||
@ -347,6 +355,7 @@ class Gemma3_12B_Config:
|
|||||||
lm_head: bool = False
|
lm_head: bool = False
|
||||||
vision_config = GEMMA3_VISION_CONFIG
|
vision_config = GEMMA3_VISION_CONFIG
|
||||||
mm_tokens_per_image = 256
|
mm_tokens_per_image = 256
|
||||||
|
stop_tokens = [1, 106]
|
||||||
|
|
||||||
class RMSNorm(nn.Module):
|
class RMSNorm(nn.Module):
|
||||||
def __init__(self, dim: int, eps: float = 1e-5, add=False, device=None, dtype=None):
|
def __init__(self, dim: int, eps: float = 1e-5, add=False, device=None, dtype=None):
|
||||||
@ -803,10 +812,13 @@ class BaseGenerate:
|
|||||||
comfy.ops.uncast_bias_weight(module, weight, None, offload_stream)
|
comfy.ops.uncast_bias_weight(module, weight, None, offload_stream)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
def generate(self, embeds=None, do_sample=True, max_length=256, temperature=1.0, top_k=50, top_p=0.9, min_p=0.0, repetition_penalty=1.0, seed=42, stop_tokens=[], initial_tokens=[], execution_dtype=None, min_tokens=0):
|
def generate(self, embeds=None, do_sample=True, max_length=256, temperature=1.0, top_k=50, top_p=0.9, min_p=0.0, repetition_penalty=1.0, seed=42, stop_tokens=None, initial_tokens=[], execution_dtype=None, min_tokens=0):
|
||||||
device = embeds.device
|
device = embeds.device
|
||||||
model_config = self.model.config
|
model_config = self.model.config
|
||||||
|
|
||||||
|
if stop_tokens is None:
|
||||||
|
stop_tokens = self.model.config.stop_tokens
|
||||||
|
|
||||||
if execution_dtype is None:
|
if execution_dtype is None:
|
||||||
if comfy.model_management.should_use_bf16(device):
|
if comfy.model_management.should_use_bf16(device):
|
||||||
execution_dtype = torch.bfloat16
|
execution_dtype = torch.bfloat16
|
||||||
@ -925,7 +937,7 @@ class Qwen25_3B(BaseLlama, torch.nn.Module):
|
|||||||
self.model = Llama2_(config, device=device, dtype=dtype, ops=operations)
|
self.model = Llama2_(config, device=device, dtype=dtype, ops=operations)
|
||||||
self.dtype = dtype
|
self.dtype = dtype
|
||||||
|
|
||||||
class Qwen3_06B(BaseLlama, BaseQwen3, torch.nn.Module):
|
class Qwen3_06B(BaseLlama, BaseQwen3, BaseGenerate, torch.nn.Module):
|
||||||
def __init__(self, config_dict, dtype, device, operations):
|
def __init__(self, config_dict, dtype, device, operations):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
config = Qwen3_06BConfig(**config_dict)
|
config = Qwen3_06BConfig(**config_dict)
|
||||||
@ -952,7 +964,7 @@ class Qwen3_2B_ACE15_lm(BaseLlama, BaseQwen3, torch.nn.Module):
|
|||||||
self.model = Llama2_(config, device=device, dtype=dtype, ops=operations)
|
self.model = Llama2_(config, device=device, dtype=dtype, ops=operations)
|
||||||
self.dtype = dtype
|
self.dtype = dtype
|
||||||
|
|
||||||
class Qwen3_4B(BaseLlama, BaseQwen3, torch.nn.Module):
|
class Qwen3_4B(BaseLlama, BaseQwen3, BaseGenerate, torch.nn.Module):
|
||||||
def __init__(self, config_dict, dtype, device, operations):
|
def __init__(self, config_dict, dtype, device, operations):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
config = Qwen3_4BConfig(**config_dict)
|
config = Qwen3_4BConfig(**config_dict)
|
||||||
@ -970,7 +982,7 @@ class Qwen3_4B_ACE15_lm(BaseLlama, BaseQwen3, torch.nn.Module):
|
|||||||
self.model = Llama2_(config, device=device, dtype=dtype, ops=operations)
|
self.model = Llama2_(config, device=device, dtype=dtype, ops=operations)
|
||||||
self.dtype = dtype
|
self.dtype = dtype
|
||||||
|
|
||||||
class Qwen3_8B(BaseLlama, BaseQwen3, torch.nn.Module):
|
class Qwen3_8B(BaseLlama, BaseQwen3, BaseGenerate, torch.nn.Module):
|
||||||
def __init__(self, config_dict, dtype, device, operations):
|
def __init__(self, config_dict, dtype, device, operations):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
config = Qwen3_8BConfig(**config_dict)
|
config = Qwen3_8BConfig(**config_dict)
|
||||||
@ -1034,7 +1046,7 @@ class Qwen25_7BVLI(BaseLlama, BaseGenerate, torch.nn.Module):
|
|||||||
|
|
||||||
return super().forward(x, attention_mask=attention_mask, embeds=embeds, num_tokens=num_tokens, intermediate_output=intermediate_output, final_layer_norm_intermediate=final_layer_norm_intermediate, dtype=dtype, position_ids=position_ids)
|
return super().forward(x, attention_mask=attention_mask, embeds=embeds, num_tokens=num_tokens, intermediate_output=intermediate_output, final_layer_norm_intermediate=final_layer_norm_intermediate, dtype=dtype, position_ids=position_ids)
|
||||||
|
|
||||||
class Gemma2_2B(BaseLlama, torch.nn.Module):
|
class Gemma2_2B(BaseLlama, BaseGenerate, torch.nn.Module):
|
||||||
def __init__(self, config_dict, dtype, device, operations):
|
def __init__(self, config_dict, dtype, device, operations):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
config = Gemma2_2B_Config(**config_dict)
|
config = Gemma2_2B_Config(**config_dict)
|
||||||
|
|||||||
@ -31,9 +31,6 @@ class Gemma2_2BModel(sd1_clip.SDClipModel):
|
|||||||
def __init__(self, device="cpu", layer="hidden", layer_idx=-2, dtype=None, attention_mask=True, model_options={}):
|
def __init__(self, device="cpu", layer="hidden", layer_idx=-2, dtype=None, attention_mask=True, model_options={}):
|
||||||
super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config={}, dtype=dtype, special_tokens={"start": 2, "pad": 0}, layer_norm_hidden_state=False, model_class=comfy.text_encoders.llama.Gemma2_2B, enable_attention_masks=attention_mask, return_attention_masks=attention_mask, model_options=model_options)
|
super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config={}, dtype=dtype, special_tokens={"start": 2, "pad": 0}, layer_norm_hidden_state=False, model_class=comfy.text_encoders.llama.Gemma2_2B, enable_attention_masks=attention_mask, return_attention_masks=attention_mask, model_options=model_options)
|
||||||
|
|
||||||
def generate(self, embeds, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed):
|
|
||||||
return super().generate(embeds, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed, stop_tokens=[107])
|
|
||||||
|
|
||||||
class Gemma3_4BModel(sd1_clip.SDClipModel):
|
class Gemma3_4BModel(sd1_clip.SDClipModel):
|
||||||
def __init__(self, device="cpu", layer="hidden", layer_idx=-2, dtype=None, attention_mask=True, model_options={}):
|
def __init__(self, device="cpu", layer="hidden", layer_idx=-2, dtype=None, attention_mask=True, model_options={}):
|
||||||
llama_quantization_metadata = model_options.get("llama_quantization_metadata", None)
|
llama_quantization_metadata = model_options.get("llama_quantization_metadata", None)
|
||||||
@ -43,9 +40,6 @@ class Gemma3_4BModel(sd1_clip.SDClipModel):
|
|||||||
|
|
||||||
super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config={}, dtype=dtype, special_tokens={"start": 2, "pad": 0}, layer_norm_hidden_state=False, model_class=comfy.text_encoders.llama.Gemma3_4B, enable_attention_masks=attention_mask, return_attention_masks=attention_mask, model_options=model_options)
|
super().__init__(device=device, layer=layer, layer_idx=layer_idx, textmodel_json_config={}, dtype=dtype, special_tokens={"start": 2, "pad": 0}, layer_norm_hidden_state=False, model_class=comfy.text_encoders.llama.Gemma3_4B, enable_attention_masks=attention_mask, return_attention_masks=attention_mask, model_options=model_options)
|
||||||
|
|
||||||
def generate(self, embeds, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed):
|
|
||||||
return super().generate(embeds, do_sample, max_length, temperature, top_k, top_p, min_p, repetition_penalty, seed, stop_tokens=[106])
|
|
||||||
|
|
||||||
class Gemma3_4B_Vision_Model(sd1_clip.SDClipModel):
|
class Gemma3_4B_Vision_Model(sd1_clip.SDClipModel):
|
||||||
def __init__(self, device="cpu", layer="hidden", layer_idx=-2, dtype=None, attention_mask=True, model_options={}):
|
def __init__(self, device="cpu", layer="hidden", layer_idx=-2, dtype=None, attention_mask=True, model_options={}):
|
||||||
llama_quantization_metadata = model_options.get("llama_quantization_metadata", None)
|
llama_quantization_metadata = model_options.get("llama_quantization_metadata", None)
|
||||||
|
|||||||
88
comfy_api_nodes/apis/elevenlabs.py
Normal file
88
comfy_api_nodes/apis/elevenlabs.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
class SpeechToTextRequest(BaseModel):
|
||||||
|
model_id: str = Field(...)
|
||||||
|
cloud_storage_url: str = Field(...)
|
||||||
|
language_code: str | None = Field(None, description="ISO-639-1 or ISO-639-3 language code")
|
||||||
|
tag_audio_events: bool | None = Field(None, description="Annotate sounds like (laughter) in transcript")
|
||||||
|
num_speakers: int | None = Field(None, description="Max speakers predicted")
|
||||||
|
timestamps_granularity: str = Field(default="word", description="Timing precision: none, word, or character")
|
||||||
|
diarize: bool | None = Field(None, description="Annotate which speaker is talking")
|
||||||
|
diarization_threshold: float | None = Field(None, description="Speaker separation sensitivity")
|
||||||
|
temperature: float | None = Field(None, description="Randomness control")
|
||||||
|
seed: int = Field(..., description="Seed for deterministic sampling")
|
||||||
|
|
||||||
|
|
||||||
|
class SpeechToTextWord(BaseModel):
|
||||||
|
text: str = Field(..., description="The word text")
|
||||||
|
type: str = Field(default="word", description="Type of text element (word, spacing, etc.)")
|
||||||
|
start: float | None = Field(None, description="Start time in seconds (when timestamps enabled)")
|
||||||
|
end: float | None = Field(None, description="End time in seconds (when timestamps enabled)")
|
||||||
|
speaker_id: str | None = Field(None, description="Speaker identifier when diarization is enabled")
|
||||||
|
logprob: float | None = Field(None, description="Log probability of the word")
|
||||||
|
|
||||||
|
|
||||||
|
class SpeechToTextResponse(BaseModel):
|
||||||
|
language_code: str = Field(..., description="Detected or specified language code")
|
||||||
|
language_probability: float | None = Field(None, description="Confidence of language detection")
|
||||||
|
text: str = Field(..., description="Full transcript text")
|
||||||
|
words: list[SpeechToTextWord] | None = Field(None, description="Word-level timing information")
|
||||||
|
|
||||||
|
|
||||||
|
class TextToSpeechVoiceSettings(BaseModel):
|
||||||
|
stability: float | None = Field(None, description="Voice stability")
|
||||||
|
similarity_boost: float | None = Field(None, description="Similarity boost")
|
||||||
|
style: float | None = Field(None, description="Style exaggeration")
|
||||||
|
use_speaker_boost: bool | None = Field(None, description="Boost similarity to original speaker")
|
||||||
|
speed: float | None = Field(None, description="Speech speed")
|
||||||
|
|
||||||
|
|
||||||
|
class TextToSpeechRequest(BaseModel):
|
||||||
|
text: str = Field(..., description="Text to convert to speech")
|
||||||
|
model_id: str = Field(..., description="Model ID for TTS")
|
||||||
|
language_code: str | None = Field(None, description="ISO-639-1 or ISO-639-3 language code")
|
||||||
|
voice_settings: TextToSpeechVoiceSettings | None = Field(None, description="Voice settings")
|
||||||
|
seed: int = Field(..., description="Seed for deterministic sampling")
|
||||||
|
apply_text_normalization: str | None = Field(None, description="Text normalization mode: auto, on, off")
|
||||||
|
|
||||||
|
|
||||||
|
class TextToSoundEffectsRequest(BaseModel):
|
||||||
|
text: str = Field(..., description="Text prompt to convert into a sound effect")
|
||||||
|
duration_seconds: float = Field(..., description="Duration of generated sound in seconds")
|
||||||
|
prompt_influence: float = Field(..., description="How closely generation follows the prompt")
|
||||||
|
loop: bool | None = Field(None, description="Whether to create a smoothly looping sound effect")
|
||||||
|
|
||||||
|
|
||||||
|
class AddVoiceRequest(BaseModel):
|
||||||
|
name: str = Field(..., description="Name that identifies the voice")
|
||||||
|
remove_background_noise: bool = Field(..., description="Remove background noise from voice samples")
|
||||||
|
|
||||||
|
|
||||||
|
class AddVoiceResponse(BaseModel):
|
||||||
|
voice_id: str = Field(..., description="The newly created voice's unique identifier")
|
||||||
|
|
||||||
|
|
||||||
|
class SpeechToSpeechRequest(BaseModel):
|
||||||
|
model_id: str = Field(..., description="Model ID for speech-to-speech")
|
||||||
|
voice_settings: str = Field(..., description="JSON string of voice settings")
|
||||||
|
seed: int = Field(..., description="Seed for deterministic sampling")
|
||||||
|
remove_background_noise: bool = Field(..., description="Remove background noise from input audio")
|
||||||
|
|
||||||
|
|
||||||
|
class DialogueInput(BaseModel):
|
||||||
|
text: str = Field(..., description="Text content to convert to speech")
|
||||||
|
voice_id: str = Field(..., description="Voice identifier for this dialogue segment")
|
||||||
|
|
||||||
|
|
||||||
|
class DialogueSettings(BaseModel):
|
||||||
|
stability: float | None = Field(None, description="Voice stability (0-1)")
|
||||||
|
|
||||||
|
|
||||||
|
class TextToDialogueRequest(BaseModel):
|
||||||
|
inputs: list[DialogueInput] = Field(..., description="List of dialogue segments")
|
||||||
|
model_id: str = Field(..., description="Model ID for dialogue generation")
|
||||||
|
language_code: str | None = Field(None, description="ISO-639-1 language code")
|
||||||
|
settings: DialogueSettings | None = Field(None, description="Voice settings")
|
||||||
|
seed: int | None = Field(None, description="Seed for deterministic sampling")
|
||||||
|
apply_text_normalization: str | None = Field(None, description="Text normalization mode: auto, on, off")
|
||||||
924
comfy_api_nodes/nodes_elevenlabs.py
Normal file
924
comfy_api_nodes/nodes_elevenlabs.py
Normal file
@ -0,0 +1,924 @@
|
|||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from comfy_api.latest import IO, ComfyExtension, Input
|
||||||
|
from comfy_api_nodes.apis.elevenlabs import (
|
||||||
|
AddVoiceRequest,
|
||||||
|
AddVoiceResponse,
|
||||||
|
DialogueInput,
|
||||||
|
DialogueSettings,
|
||||||
|
SpeechToSpeechRequest,
|
||||||
|
SpeechToTextRequest,
|
||||||
|
SpeechToTextResponse,
|
||||||
|
TextToDialogueRequest,
|
||||||
|
TextToSoundEffectsRequest,
|
||||||
|
TextToSpeechRequest,
|
||||||
|
TextToSpeechVoiceSettings,
|
||||||
|
)
|
||||||
|
from comfy_api_nodes.util import (
|
||||||
|
ApiEndpoint,
|
||||||
|
audio_bytes_to_audio_input,
|
||||||
|
audio_ndarray_to_bytesio,
|
||||||
|
audio_tensor_to_contiguous_ndarray,
|
||||||
|
sync_op,
|
||||||
|
sync_op_raw,
|
||||||
|
upload_audio_to_comfyapi,
|
||||||
|
validate_string,
|
||||||
|
)
|
||||||
|
|
||||||
|
ELEVENLABS_MUSIC_SECTIONS = "ELEVENLABS_MUSIC_SECTIONS" # Custom type for music sections
|
||||||
|
ELEVENLABS_COMPOSITION_PLAN = "ELEVENLABS_COMPOSITION_PLAN" # Custom type for composition plan
|
||||||
|
ELEVENLABS_VOICE = "ELEVENLABS_VOICE" # Custom type for voice selection
|
||||||
|
|
||||||
|
# Predefined ElevenLabs voices: (voice_id, display_name, gender, accent)
|
||||||
|
ELEVENLABS_VOICES = [
|
||||||
|
("CwhRBWXzGAHq8TQ4Fs17", "Roger", "male", "american"),
|
||||||
|
("EXAVITQu4vr4xnSDxMaL", "Sarah", "female", "american"),
|
||||||
|
("FGY2WhTYpPnrIDTdsKH5", "Laura", "female", "american"),
|
||||||
|
("IKne3meq5aSn9XLyUdCD", "Charlie", "male", "australian"),
|
||||||
|
("JBFqnCBsd6RMkjVDRZzb", "George", "male", "british"),
|
||||||
|
("N2lVS1w4EtoT3dr4eOWO", "Callum", "male", "american"),
|
||||||
|
("SAz9YHcvj6GT2YYXdXww", "River", "neutral", "american"),
|
||||||
|
("SOYHLrjzK2X1ezoPC6cr", "Harry", "male", "american"),
|
||||||
|
("TX3LPaxmHKxFdv7VOQHJ", "Liam", "male", "american"),
|
||||||
|
("Xb7hH8MSUJpSbSDYk0k2", "Alice", "female", "british"),
|
||||||
|
("XrExE9yKIg1WjnnlVkGX", "Matilda", "female", "american"),
|
||||||
|
("bIHbv24MWmeRgasZH58o", "Will", "male", "american"),
|
||||||
|
("cgSgspJ2msm6clMCkdW9", "Jessica", "female", "american"),
|
||||||
|
("cjVigY5qzO86Huf0OWal", "Eric", "male", "american"),
|
||||||
|
("hpp4J3VqNfWAUOO0d1Us", "Bella", "female", "american"),
|
||||||
|
("iP95p4xoKVk53GoZ742B", "Chris", "male", "american"),
|
||||||
|
("nPczCjzI2devNBz1zQrb", "Brian", "male", "american"),
|
||||||
|
("onwK4e9ZLuTAKqWW03F9", "Daniel", "male", "british"),
|
||||||
|
("pFZP5JQG7iQjIQuC4Bku", "Lily", "female", "british"),
|
||||||
|
("pNInz6obpgDQGcFmaJgB", "Adam", "male", "american"),
|
||||||
|
("pqHfZKP75CvOlQylNhV4", "Bill", "male", "american"),
|
||||||
|
]
|
||||||
|
|
||||||
|
ELEVENLABS_VOICE_OPTIONS = [f"{name} ({gender}, {accent})" for _, name, gender, accent in ELEVENLABS_VOICES]
|
||||||
|
ELEVENLABS_VOICE_MAP = {
|
||||||
|
f"{name} ({gender}, {accent})": voice_id for voice_id, name, gender, accent in ELEVENLABS_VOICES
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsSpeechToText(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> IO.Schema:
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="ElevenLabsSpeechToText",
|
||||||
|
display_name="ElevenLabs Speech to Text",
|
||||||
|
category="api node/audio/ElevenLabs",
|
||||||
|
description="Transcribe audio to text. "
|
||||||
|
"Supports automatic language detection, speaker diarization, and audio event tagging.",
|
||||||
|
inputs=[
|
||||||
|
IO.Audio.Input(
|
||||||
|
"audio",
|
||||||
|
tooltip="Audio to transcribe.",
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"model",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"scribe_v2",
|
||||||
|
[
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"tag_audio_events",
|
||||||
|
default=False,
|
||||||
|
tooltip="Annotate sounds like (laughter), (music), etc. in transcript.",
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"diarize",
|
||||||
|
default=False,
|
||||||
|
tooltip="Annotate which speaker is talking.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"diarization_threshold",
|
||||||
|
default=0.22,
|
||||||
|
min=0.1,
|
||||||
|
max=0.4,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Speaker separation sensitivity. "
|
||||||
|
"Lower values are more sensitive to speaker changes.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"temperature",
|
||||||
|
default=0.0,
|
||||||
|
min=0.0,
|
||||||
|
max=2.0,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Randomness control. "
|
||||||
|
"0.0 uses model default. Higher values increase randomness.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"timestamps_granularity",
|
||||||
|
options=["word", "character", "none"],
|
||||||
|
default="word",
|
||||||
|
tooltip="Timing precision for transcript words.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
tooltip="Model to use for transcription.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"language_code",
|
||||||
|
default="",
|
||||||
|
tooltip="ISO-639-1 or ISO-639-3 language code (e.g., 'en', 'es', 'fra'). "
|
||||||
|
"Leave empty for automatic detection.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"num_speakers",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=32,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Maximum number of speakers to predict. Set to 0 for automatic detection.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=1,
|
||||||
|
min=0,
|
||||||
|
max=2147483647,
|
||||||
|
tooltip="Seed for reproducibility (determinism not guaranteed).",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.String.Output(display_name="text"),
|
||||||
|
IO.String.Output(display_name="language_code"),
|
||||||
|
IO.String.Output(display_name="words_json"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.0073,"format":{"approximate":true,"suffix":"/minute"}}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
audio: Input.Audio,
|
||||||
|
model: dict,
|
||||||
|
language_code: str,
|
||||||
|
num_speakers: int,
|
||||||
|
seed: int,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
if model["diarize"] and num_speakers:
|
||||||
|
raise ValueError(
|
||||||
|
"Number of speakers cannot be specified when diarization is enabled. "
|
||||||
|
"Either disable diarization or set num_speakers to 0."
|
||||||
|
)
|
||||||
|
request = SpeechToTextRequest(
|
||||||
|
model_id=model["model"],
|
||||||
|
cloud_storage_url=await upload_audio_to_comfyapi(
|
||||||
|
cls, audio, container_format="mp4", codec_name="aac", mime_type="audio/mp4"
|
||||||
|
),
|
||||||
|
language_code=language_code if language_code.strip() else None,
|
||||||
|
tag_audio_events=model["tag_audio_events"],
|
||||||
|
num_speakers=num_speakers if num_speakers > 0 else None,
|
||||||
|
timestamps_granularity=model["timestamps_granularity"],
|
||||||
|
diarize=model["diarize"],
|
||||||
|
diarization_threshold=model["diarization_threshold"] if model["diarize"] else None,
|
||||||
|
seed=seed,
|
||||||
|
temperature=model["temperature"],
|
||||||
|
)
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/elevenlabs/v1/speech-to-text", method="POST"),
|
||||||
|
response_model=SpeechToTextResponse,
|
||||||
|
data=request,
|
||||||
|
content_type="multipart/form-data",
|
||||||
|
)
|
||||||
|
words_json = json.dumps(
|
||||||
|
[w.model_dump(exclude_none=True) for w in response.words] if response.words else [],
|
||||||
|
indent=2,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(response.text, response.language_code, words_json)
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsVoiceSelector(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> IO.Schema:
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="ElevenLabsVoiceSelector",
|
||||||
|
display_name="ElevenLabs Voice Selector",
|
||||||
|
category="api node/audio/ElevenLabs",
|
||||||
|
description="Select a predefined ElevenLabs voice for text-to-speech generation.",
|
||||||
|
inputs=[
|
||||||
|
IO.Combo.Input(
|
||||||
|
"voice",
|
||||||
|
options=ELEVENLABS_VOICE_OPTIONS,
|
||||||
|
tooltip="Choose a voice from the predefined ElevenLabs voices.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Custom(ELEVENLABS_VOICE).Output(display_name="voice"),
|
||||||
|
],
|
||||||
|
is_api_node=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(cls, voice: str) -> IO.NodeOutput:
|
||||||
|
voice_id = ELEVENLABS_VOICE_MAP.get(voice)
|
||||||
|
if not voice_id:
|
||||||
|
raise ValueError(f"Unknown voice: {voice}")
|
||||||
|
return IO.NodeOutput(voice_id)
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsTextToSpeech(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> IO.Schema:
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="ElevenLabsTextToSpeech",
|
||||||
|
display_name="ElevenLabs Text to Speech",
|
||||||
|
category="api node/audio/ElevenLabs",
|
||||||
|
description="Convert text to speech.",
|
||||||
|
inputs=[
|
||||||
|
IO.Custom(ELEVENLABS_VOICE).Input(
|
||||||
|
"voice",
|
||||||
|
tooltip="Voice to use for speech synthesis. Connect from Voice Selector or Instant Voice Clone.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"text",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
tooltip="The text to convert to speech.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"stability",
|
||||||
|
default=0.5,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Voice stability. Lower values give broader emotional range, "
|
||||||
|
"higher values produce more consistent but potentially monotonous speech.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"apply_text_normalization",
|
||||||
|
options=["auto", "on", "off"],
|
||||||
|
tooltip="Text normalization mode. 'auto' lets the system decide, "
|
||||||
|
"'on' always applies normalization, 'off' skips it.",
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"model",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"eleven_multilingual_v2",
|
||||||
|
[
|
||||||
|
IO.Float.Input(
|
||||||
|
"speed",
|
||||||
|
default=1.0,
|
||||||
|
min=0.7,
|
||||||
|
max=1.3,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Speech speed. 1.0 is normal, <1.0 slower, >1.0 faster.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"similarity_boost",
|
||||||
|
default=0.75,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Similarity boost. Higher values make the voice more similar to the original.",
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"use_speaker_boost",
|
||||||
|
default=False,
|
||||||
|
tooltip="Boost similarity to the original speaker voice.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"style",
|
||||||
|
default=0.0,
|
||||||
|
min=0.0,
|
||||||
|
max=0.2,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Style exaggeration. Higher values increase stylistic expression "
|
||||||
|
"but may reduce stability.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"eleven_v3",
|
||||||
|
[
|
||||||
|
IO.Float.Input(
|
||||||
|
"speed",
|
||||||
|
default=1.0,
|
||||||
|
min=0.7,
|
||||||
|
max=1.3,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Speech speed. 1.0 is normal, <1.0 slower, >1.0 faster.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"similarity_boost",
|
||||||
|
default=0.75,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Similarity boost. Higher values make the voice more similar to the original.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
tooltip="Model to use for text-to-speech.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"language_code",
|
||||||
|
default="",
|
||||||
|
tooltip="ISO-639-1 or ISO-639-3 language code (e.g., 'en', 'es', 'fra'). "
|
||||||
|
"Leave empty for automatic detection.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=1,
|
||||||
|
min=0,
|
||||||
|
max=2147483647,
|
||||||
|
tooltip="Seed for reproducibility (determinism not guaranteed).",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"output_format",
|
||||||
|
options=["mp3_44100_192", "opus_48000_192"],
|
||||||
|
tooltip="Audio output format.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Audio.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.24,"format":{"approximate":true,"suffix":"/1K chars"}}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
voice: str,
|
||||||
|
text: str,
|
||||||
|
stability: float,
|
||||||
|
apply_text_normalization: str,
|
||||||
|
model: dict,
|
||||||
|
language_code: str,
|
||||||
|
seed: int,
|
||||||
|
output_format: str,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
validate_string(text, min_length=1)
|
||||||
|
request = TextToSpeechRequest(
|
||||||
|
text=text,
|
||||||
|
model_id=model["model"],
|
||||||
|
language_code=language_code if language_code.strip() else None,
|
||||||
|
voice_settings=TextToSpeechVoiceSettings(
|
||||||
|
stability=stability,
|
||||||
|
similarity_boost=model["similarity_boost"],
|
||||||
|
speed=model["speed"],
|
||||||
|
use_speaker_boost=model.get("use_speaker_boost", None),
|
||||||
|
style=model.get("style", None),
|
||||||
|
),
|
||||||
|
seed=seed,
|
||||||
|
apply_text_normalization=apply_text_normalization,
|
||||||
|
)
|
||||||
|
response = await sync_op_raw(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(
|
||||||
|
path=f"/proxy/elevenlabs/v1/text-to-speech/{voice}",
|
||||||
|
method="POST",
|
||||||
|
query_params={"output_format": output_format},
|
||||||
|
),
|
||||||
|
data=request,
|
||||||
|
as_binary=True,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(audio_bytes_to_audio_input(response))
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsAudioIsolation(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> IO.Schema:
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="ElevenLabsAudioIsolation",
|
||||||
|
display_name="ElevenLabs Voice Isolation",
|
||||||
|
category="api node/audio/ElevenLabs",
|
||||||
|
description="Remove background noise from audio, isolating vocals or speech.",
|
||||||
|
inputs=[
|
||||||
|
IO.Audio.Input(
|
||||||
|
"audio",
|
||||||
|
tooltip="Audio to process for background noise removal.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Audio.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.24,"format":{"approximate":true,"suffix":"/minute"}}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
audio: Input.Audio,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
audio_data_np = audio_tensor_to_contiguous_ndarray(audio["waveform"])
|
||||||
|
audio_bytes_io = audio_ndarray_to_bytesio(audio_data_np, audio["sample_rate"], "mp4", "aac")
|
||||||
|
response = await sync_op_raw(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/elevenlabs/v1/audio-isolation", method="POST"),
|
||||||
|
files={"audio": ("audio.mp4", audio_bytes_io, "audio/mp4")},
|
||||||
|
content_type="multipart/form-data",
|
||||||
|
as_binary=True,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(audio_bytes_to_audio_input(response))
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsTextToSoundEffects(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> IO.Schema:
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="ElevenLabsTextToSoundEffects",
|
||||||
|
display_name="ElevenLabs Text to Sound Effects",
|
||||||
|
category="api node/audio/ElevenLabs",
|
||||||
|
description="Generate sound effects from text descriptions.",
|
||||||
|
inputs=[
|
||||||
|
IO.String.Input(
|
||||||
|
"text",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
tooltip="Text description of the sound effect to generate.",
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"model",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"eleven_sfx_v2",
|
||||||
|
[
|
||||||
|
IO.Float.Input(
|
||||||
|
"duration",
|
||||||
|
default=5.0,
|
||||||
|
min=0.5,
|
||||||
|
max=30.0,
|
||||||
|
step=0.1,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Duration of generated sound in seconds.",
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"loop",
|
||||||
|
default=False,
|
||||||
|
tooltip="Create a smoothly looping sound effect.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"prompt_influence",
|
||||||
|
default=0.3,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="How closely generation follows the prompt. "
|
||||||
|
"Higher values make the sound follow the text more closely.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
tooltip="Model to use for sound effect generation.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"output_format",
|
||||||
|
options=["mp3_44100_192", "opus_48000_192"],
|
||||||
|
tooltip="Audio output format.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Audio.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.14,"format":{"approximate":true,"suffix":"/minute"}}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
text: str,
|
||||||
|
model: dict,
|
||||||
|
output_format: str,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
validate_string(text, min_length=1)
|
||||||
|
response = await sync_op_raw(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(
|
||||||
|
path="/proxy/elevenlabs/v1/sound-generation",
|
||||||
|
method="POST",
|
||||||
|
query_params={"output_format": output_format},
|
||||||
|
),
|
||||||
|
data=TextToSoundEffectsRequest(
|
||||||
|
text=text,
|
||||||
|
duration_seconds=model["duration"],
|
||||||
|
prompt_influence=model["prompt_influence"],
|
||||||
|
loop=model.get("loop", None),
|
||||||
|
),
|
||||||
|
as_binary=True,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(audio_bytes_to_audio_input(response))
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsInstantVoiceClone(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> IO.Schema:
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="ElevenLabsInstantVoiceClone",
|
||||||
|
display_name="ElevenLabs Instant Voice Clone",
|
||||||
|
category="api node/audio/ElevenLabs",
|
||||||
|
description="Create a cloned voice from audio samples. "
|
||||||
|
"Provide 1-8 audio recordings of the voice to clone.",
|
||||||
|
inputs=[
|
||||||
|
IO.Autogrow.Input(
|
||||||
|
"files",
|
||||||
|
template=IO.Autogrow.TemplatePrefix(
|
||||||
|
IO.Audio.Input("audio"),
|
||||||
|
prefix="audio",
|
||||||
|
min=1,
|
||||||
|
max=8,
|
||||||
|
),
|
||||||
|
tooltip="Audio recordings for voice cloning.",
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"remove_background_noise",
|
||||||
|
default=False,
|
||||||
|
tooltip="Remove background noise from voice samples using audio isolation.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Custom(ELEVENLABS_VOICE).Output(display_name="voice"),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(expr="""{"type":"usd","usd":0.15}"""),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
files: IO.Autogrow.Type,
|
||||||
|
remove_background_noise: bool,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
file_tuples: list[tuple[str, tuple[str, bytes, str]]] = []
|
||||||
|
for key in files:
|
||||||
|
audio = files[key]
|
||||||
|
sample_rate: int = audio["sample_rate"]
|
||||||
|
waveform = audio["waveform"]
|
||||||
|
audio_data_np = audio_tensor_to_contiguous_ndarray(waveform)
|
||||||
|
audio_bytes_io = audio_ndarray_to_bytesio(audio_data_np, sample_rate, "mp4", "aac")
|
||||||
|
file_tuples.append(("files", (f"{key}.mp4", audio_bytes_io.getvalue(), "audio/mp4")))
|
||||||
|
|
||||||
|
response = await sync_op(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(path="/proxy/elevenlabs/v1/voices/add", method="POST"),
|
||||||
|
response_model=AddVoiceResponse,
|
||||||
|
data=AddVoiceRequest(
|
||||||
|
name=str(uuid.uuid4()),
|
||||||
|
remove_background_noise=remove_background_noise,
|
||||||
|
),
|
||||||
|
files=file_tuples,
|
||||||
|
content_type="multipart/form-data",
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(response.voice_id)
|
||||||
|
|
||||||
|
|
||||||
|
ELEVENLABS_STS_VOICE_SETTINGS = [
|
||||||
|
IO.Float.Input(
|
||||||
|
"speed",
|
||||||
|
default=1.0,
|
||||||
|
min=0.7,
|
||||||
|
max=1.3,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Speech speed. 1.0 is normal, <1.0 slower, >1.0 faster.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"similarity_boost",
|
||||||
|
default=0.75,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Similarity boost. Higher values make the voice more similar to the original.",
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"use_speaker_boost",
|
||||||
|
default=False,
|
||||||
|
tooltip="Boost similarity to the original speaker voice.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"style",
|
||||||
|
default=0.0,
|
||||||
|
min=0.0,
|
||||||
|
max=0.2,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Style exaggeration. Higher values increase stylistic expression but may reduce stability.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsSpeechToSpeech(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> IO.Schema:
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="ElevenLabsSpeechToSpeech",
|
||||||
|
display_name="ElevenLabs Speech to Speech",
|
||||||
|
category="api node/audio/ElevenLabs",
|
||||||
|
description="Transform speech from one voice to another while preserving the original content and emotion.",
|
||||||
|
inputs=[
|
||||||
|
IO.Custom(ELEVENLABS_VOICE).Input(
|
||||||
|
"voice",
|
||||||
|
tooltip="Target voice for the transformation. "
|
||||||
|
"Connect from Voice Selector or Instant Voice Clone.",
|
||||||
|
),
|
||||||
|
IO.Audio.Input(
|
||||||
|
"audio",
|
||||||
|
tooltip="Source audio to transform.",
|
||||||
|
),
|
||||||
|
IO.Float.Input(
|
||||||
|
"stability",
|
||||||
|
default=0.5,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.01,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Voice stability. Lower values give broader emotional range, "
|
||||||
|
"higher values produce more consistent but potentially monotonous speech.",
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"model",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"eleven_multilingual_sts_v2",
|
||||||
|
ELEVENLABS_STS_VOICE_SETTINGS,
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Option(
|
||||||
|
"eleven_english_sts_v2",
|
||||||
|
ELEVENLABS_STS_VOICE_SETTINGS,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
tooltip="Model to use for speech-to-speech transformation.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"output_format",
|
||||||
|
options=["mp3_44100_192", "opus_48000_192"],
|
||||||
|
tooltip="Audio output format.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=0,
|
||||||
|
min=0,
|
||||||
|
max=4294967295,
|
||||||
|
tooltip="Seed for reproducibility.",
|
||||||
|
),
|
||||||
|
IO.Boolean.Input(
|
||||||
|
"remove_background_noise",
|
||||||
|
default=False,
|
||||||
|
tooltip="Remove background noise from input audio using audio isolation.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Audio.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.24,"format":{"approximate":true,"suffix":"/minute"}}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
voice: str,
|
||||||
|
audio: Input.Audio,
|
||||||
|
stability: float,
|
||||||
|
model: dict,
|
||||||
|
output_format: str,
|
||||||
|
seed: int,
|
||||||
|
remove_background_noise: bool,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
audio_data_np = audio_tensor_to_contiguous_ndarray(audio["waveform"])
|
||||||
|
audio_bytes_io = audio_ndarray_to_bytesio(audio_data_np, audio["sample_rate"], "mp4", "aac")
|
||||||
|
voice_settings = TextToSpeechVoiceSettings(
|
||||||
|
stability=stability,
|
||||||
|
similarity_boost=model["similarity_boost"],
|
||||||
|
style=model["style"],
|
||||||
|
use_speaker_boost=model["use_speaker_boost"],
|
||||||
|
speed=model["speed"],
|
||||||
|
)
|
||||||
|
response = await sync_op_raw(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(
|
||||||
|
path=f"/proxy/elevenlabs/v1/speech-to-speech/{voice}",
|
||||||
|
method="POST",
|
||||||
|
query_params={"output_format": output_format},
|
||||||
|
),
|
||||||
|
data=SpeechToSpeechRequest(
|
||||||
|
model_id=model["model"],
|
||||||
|
voice_settings=voice_settings.model_dump_json(exclude_none=True),
|
||||||
|
seed=seed,
|
||||||
|
remove_background_noise=remove_background_noise,
|
||||||
|
),
|
||||||
|
files={"audio": ("audio.mp4", audio_bytes_io.getvalue(), "audio/mp4")},
|
||||||
|
content_type="multipart/form-data",
|
||||||
|
as_binary=True,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(audio_bytes_to_audio_input(response))
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_dialogue_inputs(count: int) -> list:
|
||||||
|
"""Generate input widgets for a given number of dialogue entries."""
|
||||||
|
inputs = []
|
||||||
|
for i in range(1, count + 1):
|
||||||
|
inputs.extend(
|
||||||
|
[
|
||||||
|
IO.String.Input(
|
||||||
|
f"text{i}",
|
||||||
|
multiline=True,
|
||||||
|
default="",
|
||||||
|
tooltip=f"Text content for dialogue entry {i}.",
|
||||||
|
),
|
||||||
|
IO.Custom(ELEVENLABS_VOICE).Input(
|
||||||
|
f"voice{i}",
|
||||||
|
tooltip=f"Voice for dialogue entry {i}. Connect from Voice Selector or Instant Voice Clone.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return inputs
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsTextToDialogue(IO.ComfyNode):
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> IO.Schema:
|
||||||
|
return IO.Schema(
|
||||||
|
node_id="ElevenLabsTextToDialogue",
|
||||||
|
display_name="ElevenLabs Text to Dialogue",
|
||||||
|
category="api node/audio/ElevenLabs",
|
||||||
|
description="Generate multi-speaker dialogue from text. Each dialogue entry has its own text and voice.",
|
||||||
|
inputs=[
|
||||||
|
IO.Float.Input(
|
||||||
|
"stability",
|
||||||
|
default=0.5,
|
||||||
|
min=0.0,
|
||||||
|
max=1.0,
|
||||||
|
step=0.5,
|
||||||
|
display_mode=IO.NumberDisplay.slider,
|
||||||
|
tooltip="Voice stability. Lower values give broader emotional range, "
|
||||||
|
"higher values produce more consistent but potentially monotonous speech.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"apply_text_normalization",
|
||||||
|
options=["auto", "on", "off"],
|
||||||
|
tooltip="Text normalization mode. 'auto' lets the system decide, "
|
||||||
|
"'on' always applies normalization, 'off' skips it.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"model",
|
||||||
|
options=["eleven_v3"],
|
||||||
|
tooltip="Model to use for dialogue generation.",
|
||||||
|
),
|
||||||
|
IO.DynamicCombo.Input(
|
||||||
|
"inputs",
|
||||||
|
options=[
|
||||||
|
IO.DynamicCombo.Option("1", _generate_dialogue_inputs(1)),
|
||||||
|
IO.DynamicCombo.Option("2", _generate_dialogue_inputs(2)),
|
||||||
|
IO.DynamicCombo.Option("3", _generate_dialogue_inputs(3)),
|
||||||
|
IO.DynamicCombo.Option("4", _generate_dialogue_inputs(4)),
|
||||||
|
IO.DynamicCombo.Option("5", _generate_dialogue_inputs(5)),
|
||||||
|
IO.DynamicCombo.Option("6", _generate_dialogue_inputs(6)),
|
||||||
|
IO.DynamicCombo.Option("7", _generate_dialogue_inputs(7)),
|
||||||
|
IO.DynamicCombo.Option("8", _generate_dialogue_inputs(8)),
|
||||||
|
IO.DynamicCombo.Option("9", _generate_dialogue_inputs(9)),
|
||||||
|
IO.DynamicCombo.Option("10", _generate_dialogue_inputs(10)),
|
||||||
|
],
|
||||||
|
tooltip="Number of dialogue entries.",
|
||||||
|
),
|
||||||
|
IO.String.Input(
|
||||||
|
"language_code",
|
||||||
|
default="",
|
||||||
|
tooltip="ISO-639-1 or ISO-639-3 language code (e.g., 'en', 'es', 'fra'). "
|
||||||
|
"Leave empty for automatic detection.",
|
||||||
|
),
|
||||||
|
IO.Int.Input(
|
||||||
|
"seed",
|
||||||
|
default=1,
|
||||||
|
min=0,
|
||||||
|
max=4294967295,
|
||||||
|
tooltip="Seed for reproducibility.",
|
||||||
|
),
|
||||||
|
IO.Combo.Input(
|
||||||
|
"output_format",
|
||||||
|
options=["mp3_44100_192", "opus_48000_192"],
|
||||||
|
tooltip="Audio output format.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
IO.Audio.Output(),
|
||||||
|
],
|
||||||
|
hidden=[
|
||||||
|
IO.Hidden.auth_token_comfy_org,
|
||||||
|
IO.Hidden.api_key_comfy_org,
|
||||||
|
IO.Hidden.unique_id,
|
||||||
|
],
|
||||||
|
is_api_node=True,
|
||||||
|
price_badge=IO.PriceBadge(
|
||||||
|
expr="""{"type":"usd","usd":0.24,"format":{"approximate":true,"suffix":"/1K chars"}}""",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def execute(
|
||||||
|
cls,
|
||||||
|
stability: float,
|
||||||
|
apply_text_normalization: str,
|
||||||
|
model: str,
|
||||||
|
inputs: dict,
|
||||||
|
language_code: str,
|
||||||
|
seed: int,
|
||||||
|
output_format: str,
|
||||||
|
) -> IO.NodeOutput:
|
||||||
|
num_entries = int(inputs["inputs"])
|
||||||
|
dialogue_inputs: list[DialogueInput] = []
|
||||||
|
for i in range(1, num_entries + 1):
|
||||||
|
text = inputs[f"text{i}"]
|
||||||
|
voice_id = inputs[f"voice{i}"]
|
||||||
|
validate_string(text, min_length=1)
|
||||||
|
dialogue_inputs.append(DialogueInput(text=text, voice_id=voice_id))
|
||||||
|
request = TextToDialogueRequest(
|
||||||
|
inputs=dialogue_inputs,
|
||||||
|
model_id=model,
|
||||||
|
language_code=language_code if language_code.strip() else None,
|
||||||
|
settings=DialogueSettings(stability=stability),
|
||||||
|
seed=seed,
|
||||||
|
apply_text_normalization=apply_text_normalization,
|
||||||
|
)
|
||||||
|
response = await sync_op_raw(
|
||||||
|
cls,
|
||||||
|
ApiEndpoint(
|
||||||
|
path="/proxy/elevenlabs/v1/text-to-dialogue",
|
||||||
|
method="POST",
|
||||||
|
query_params={"output_format": output_format},
|
||||||
|
),
|
||||||
|
data=request,
|
||||||
|
as_binary=True,
|
||||||
|
)
|
||||||
|
return IO.NodeOutput(audio_bytes_to_audio_input(response))
|
||||||
|
|
||||||
|
|
||||||
|
class ElevenLabsExtension(ComfyExtension):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[IO.ComfyNode]]:
|
||||||
|
return [
|
||||||
|
ElevenLabsSpeechToText,
|
||||||
|
ElevenLabsVoiceSelector,
|
||||||
|
ElevenLabsTextToSpeech,
|
||||||
|
ElevenLabsAudioIsolation,
|
||||||
|
ElevenLabsTextToSoundEffects,
|
||||||
|
ElevenLabsInstantVoiceClone,
|
||||||
|
ElevenLabsSpeechToSpeech,
|
||||||
|
ElevenLabsTextToDialogue,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> ElevenLabsExtension:
|
||||||
|
return ElevenLabsExtension()
|
||||||
@ -9,6 +9,8 @@ from .client import (
|
|||||||
from .conversions import (
|
from .conversions import (
|
||||||
audio_bytes_to_audio_input,
|
audio_bytes_to_audio_input,
|
||||||
audio_input_to_mp3,
|
audio_input_to_mp3,
|
||||||
|
audio_ndarray_to_bytesio,
|
||||||
|
audio_tensor_to_contiguous_ndarray,
|
||||||
audio_to_base64_string,
|
audio_to_base64_string,
|
||||||
bytesio_to_image_tensor,
|
bytesio_to_image_tensor,
|
||||||
convert_mask_to_image,
|
convert_mask_to_image,
|
||||||
@ -78,6 +80,8 @@ __all__ = [
|
|||||||
# Conversions
|
# Conversions
|
||||||
"audio_bytes_to_audio_input",
|
"audio_bytes_to_audio_input",
|
||||||
"audio_input_to_mp3",
|
"audio_input_to_mp3",
|
||||||
|
"audio_ndarray_to_bytesio",
|
||||||
|
"audio_tensor_to_contiguous_ndarray",
|
||||||
"audio_to_base64_string",
|
"audio_to_base64_string",
|
||||||
"bytesio_to_image_tensor",
|
"bytesio_to_image_tensor",
|
||||||
"convert_mask_to_image",
|
"convert_mask_to_image",
|
||||||
|
|||||||
895
comfy_extras/nodes_glsl.py
Normal file
895
comfy_extras/nodes_glsl.py
Normal file
@ -0,0 +1,895 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
import ctypes.util
|
||||||
|
import importlib.util
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
|
||||||
|
import nodes
|
||||||
|
from comfy_api.latest import ComfyExtension, io, ui
|
||||||
|
from typing_extensions import override
|
||||||
|
from utils.install_util import get_missing_requirements_message
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_opengl_availability():
|
||||||
|
"""Early check for OpenGL availability. Raises RuntimeError if unlikely to work."""
|
||||||
|
logger.debug("_check_opengl_availability: starting")
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
# Check Python packages (using find_spec to avoid importing)
|
||||||
|
logger.debug("_check_opengl_availability: checking for glfw package")
|
||||||
|
if importlib.util.find_spec("glfw") is None:
|
||||||
|
missing.append("glfw")
|
||||||
|
|
||||||
|
logger.debug("_check_opengl_availability: checking for OpenGL package")
|
||||||
|
if importlib.util.find_spec("OpenGL") is None:
|
||||||
|
missing.append("PyOpenGL")
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"OpenGL dependencies not available.\n{get_missing_requirements_message()}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# On Linux without display, check if headless backends are available
|
||||||
|
logger.debug(f"_check_opengl_availability: platform={sys.platform}")
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
has_display = os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")
|
||||||
|
logger.debug(f"_check_opengl_availability: has_display={bool(has_display)}")
|
||||||
|
if not has_display:
|
||||||
|
# Check for EGL or OSMesa libraries
|
||||||
|
logger.debug("_check_opengl_availability: checking for EGL library")
|
||||||
|
has_egl = ctypes.util.find_library("EGL")
|
||||||
|
logger.debug("_check_opengl_availability: checking for OSMesa library")
|
||||||
|
has_osmesa = ctypes.util.find_library("OSMesa")
|
||||||
|
|
||||||
|
# Error disabled for CI as it fails this check
|
||||||
|
# if not has_egl and not has_osmesa:
|
||||||
|
# raise RuntimeError(
|
||||||
|
# "GLSL Shader node: No display and no headless backend (EGL/OSMesa) found.\n"
|
||||||
|
# "See error below for installation instructions."
|
||||||
|
# )
|
||||||
|
logger.debug(f"Headless mode: EGL={'yes' if has_egl else 'no'}, OSMesa={'yes' if has_osmesa else 'no'}")
|
||||||
|
|
||||||
|
logger.debug("_check_opengl_availability: completed")
|
||||||
|
|
||||||
|
|
||||||
|
# Run early check at import time
|
||||||
|
logger.debug("nodes_glsl: running _check_opengl_availability at import time")
|
||||||
|
_check_opengl_availability()
|
||||||
|
|
||||||
|
# OpenGL modules - initialized lazily when context is created
|
||||||
|
gl = None
|
||||||
|
glfw = None
|
||||||
|
EGL = None
|
||||||
|
|
||||||
|
|
||||||
|
def _import_opengl():
|
||||||
|
"""Import OpenGL module. Called after context is created."""
|
||||||
|
global gl
|
||||||
|
if gl is None:
|
||||||
|
logger.debug("_import_opengl: importing OpenGL.GL")
|
||||||
|
import OpenGL.GL as _gl
|
||||||
|
gl = _gl
|
||||||
|
logger.debug("_import_opengl: import completed")
|
||||||
|
return gl
|
||||||
|
|
||||||
|
|
||||||
|
class SizeModeInput(TypedDict):
|
||||||
|
size_mode: str
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
|
||||||
|
|
||||||
|
MAX_IMAGES = 5 # u_image0-4
|
||||||
|
MAX_UNIFORMS = 5 # u_float0-4, u_int0-4
|
||||||
|
MAX_OUTPUTS = 4 # fragColor0-3 (MRT)
|
||||||
|
|
||||||
|
# Vertex shader using gl_VertexID trick - no VBO needed.
|
||||||
|
# Draws a single triangle that covers the entire screen:
|
||||||
|
#
|
||||||
|
# (-1,3)
|
||||||
|
# /|
|
||||||
|
# / | <- visible area is the unit square from (-1,-1) to (1,1)
|
||||||
|
# / | parts outside get clipped away
|
||||||
|
# (-1,-1)---(3,-1)
|
||||||
|
#
|
||||||
|
# v_texCoord is computed from clip space: * 0.5 + 0.5 maps (-1,1) -> (0,1)
|
||||||
|
VERTEX_SHADER = """#version 330 core
|
||||||
|
out vec2 v_texCoord;
|
||||||
|
void main() {
|
||||||
|
vec2 verts[3] = vec2[](vec2(-1, -1), vec2(3, -1), vec2(-1, 3));
|
||||||
|
v_texCoord = verts[gl_VertexID] * 0.5 + 0.5;
|
||||||
|
gl_Position = vec4(verts[gl_VertexID], 0, 1);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_FRAGMENT_SHADER = """#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
|
||||||
|
uniform sampler2D u_image0;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
|
||||||
|
in vec2 v_texCoord;
|
||||||
|
layout(location = 0) out vec4 fragColor0;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
fragColor0 = texture(u_image0, v_texCoord);
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_es_to_desktop(source: str) -> str:
|
||||||
|
"""Convert GLSL ES (WebGL) shader source to desktop GLSL 330 core."""
|
||||||
|
# Remove any existing #version directive
|
||||||
|
source = re.sub(r"#version\s+\d+(\s+es)?\s*\n?", "", source, flags=re.IGNORECASE)
|
||||||
|
# Remove precision qualifiers (not needed in desktop GLSL)
|
||||||
|
source = re.sub(r"precision\s+(lowp|mediump|highp)\s+\w+\s*;\s*\n?", "", source)
|
||||||
|
# Prepend desktop GLSL version
|
||||||
|
return "#version 330 core\n" + source
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_output_count(source: str) -> int:
|
||||||
|
"""Detect how many fragColor outputs are used in the shader.
|
||||||
|
|
||||||
|
Returns the count of outputs needed (1 to MAX_OUTPUTS).
|
||||||
|
"""
|
||||||
|
matches = re.findall(r"fragColor(\d+)", source)
|
||||||
|
if not matches:
|
||||||
|
return 1 # Default to 1 output if none found
|
||||||
|
max_index = max(int(m) for m in matches)
|
||||||
|
return min(max_index + 1, MAX_OUTPUTS)
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_pass_count(source: str) -> int:
|
||||||
|
"""Detect multi-pass rendering from #pragma passes N directive.
|
||||||
|
|
||||||
|
Returns the number of passes (1 if not specified).
|
||||||
|
"""
|
||||||
|
match = re.search(r'#pragma\s+passes\s+(\d+)', source)
|
||||||
|
if match:
|
||||||
|
return max(1, int(match.group(1)))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def _init_glfw():
|
||||||
|
"""Initialize GLFW. Returns (window, glfw_module). Raises RuntimeError on failure."""
|
||||||
|
logger.debug("_init_glfw: starting")
|
||||||
|
# On macOS, glfw.init() must be called from main thread or it hangs forever
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
logger.debug("_init_glfw: skipping on macOS")
|
||||||
|
raise RuntimeError("GLFW backend not supported on macOS")
|
||||||
|
|
||||||
|
logger.debug("_init_glfw: importing glfw module")
|
||||||
|
import glfw as _glfw
|
||||||
|
|
||||||
|
logger.debug("_init_glfw: calling glfw.init()")
|
||||||
|
if not _glfw.init():
|
||||||
|
raise RuntimeError("glfw.init() failed")
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug("_init_glfw: setting window hints")
|
||||||
|
_glfw.window_hint(_glfw.VISIBLE, _glfw.FALSE)
|
||||||
|
_glfw.window_hint(_glfw.CONTEXT_VERSION_MAJOR, 3)
|
||||||
|
_glfw.window_hint(_glfw.CONTEXT_VERSION_MINOR, 3)
|
||||||
|
_glfw.window_hint(_glfw.OPENGL_PROFILE, _glfw.OPENGL_CORE_PROFILE)
|
||||||
|
|
||||||
|
logger.debug("_init_glfw: calling create_window()")
|
||||||
|
window = _glfw.create_window(64, 64, "ComfyUI GLSL", None, None)
|
||||||
|
if not window:
|
||||||
|
raise RuntimeError("glfw.create_window() failed")
|
||||||
|
|
||||||
|
logger.debug("_init_glfw: calling make_context_current()")
|
||||||
|
_glfw.make_context_current(window)
|
||||||
|
logger.debug("_init_glfw: completed successfully")
|
||||||
|
return window, _glfw
|
||||||
|
except Exception:
|
||||||
|
logger.debug("_init_glfw: failed, terminating glfw")
|
||||||
|
_glfw.terminate()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _init_egl():
|
||||||
|
"""Initialize EGL for headless rendering. Returns (display, context, surface, EGL_module). Raises RuntimeError on failure."""
|
||||||
|
logger.debug("_init_egl: starting")
|
||||||
|
from OpenGL import EGL as _EGL
|
||||||
|
from OpenGL.EGL import (
|
||||||
|
eglGetDisplay, eglInitialize, eglChooseConfig, eglCreateContext,
|
||||||
|
eglMakeCurrent, eglCreatePbufferSurface, eglBindAPI,
|
||||||
|
eglTerminate, eglDestroyContext, eglDestroySurface,
|
||||||
|
EGL_DEFAULT_DISPLAY, EGL_NO_CONTEXT, EGL_NONE,
|
||||||
|
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
||||||
|
EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_BLUE_SIZE, EGL_ALPHA_SIZE, EGL_DEPTH_SIZE,
|
||||||
|
EGL_WIDTH, EGL_HEIGHT, EGL_OPENGL_API,
|
||||||
|
)
|
||||||
|
logger.debug("_init_egl: imports completed")
|
||||||
|
|
||||||
|
display = None
|
||||||
|
context = None
|
||||||
|
surface = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug("_init_egl: calling eglGetDisplay()")
|
||||||
|
display = eglGetDisplay(EGL_DEFAULT_DISPLAY)
|
||||||
|
if display == _EGL.EGL_NO_DISPLAY:
|
||||||
|
raise RuntimeError("eglGetDisplay() failed")
|
||||||
|
|
||||||
|
logger.debug("_init_egl: calling eglInitialize()")
|
||||||
|
major, minor = _EGL.EGLint(), _EGL.EGLint()
|
||||||
|
if not eglInitialize(display, major, minor):
|
||||||
|
display = None # Not initialized, don't terminate
|
||||||
|
raise RuntimeError("eglInitialize() failed")
|
||||||
|
logger.debug(f"_init_egl: EGL version {major.value}.{minor.value}")
|
||||||
|
|
||||||
|
config_attribs = [
|
||||||
|
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
|
||||||
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
||||||
|
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,
|
||||||
|
EGL_DEPTH_SIZE, 0, EGL_NONE
|
||||||
|
]
|
||||||
|
configs = (_EGL.EGLConfig * 1)()
|
||||||
|
num_configs = _EGL.EGLint()
|
||||||
|
if not eglChooseConfig(display, config_attribs, configs, 1, num_configs) or num_configs.value == 0:
|
||||||
|
raise RuntimeError("eglChooseConfig() failed")
|
||||||
|
config = configs[0]
|
||||||
|
logger.debug(f"_init_egl: config chosen, num_configs={num_configs.value}")
|
||||||
|
|
||||||
|
if not eglBindAPI(EGL_OPENGL_API):
|
||||||
|
raise RuntimeError("eglBindAPI() failed")
|
||||||
|
|
||||||
|
logger.debug("_init_egl: calling eglCreateContext()")
|
||||||
|
context_attribs = [
|
||||||
|
_EGL.EGL_CONTEXT_MAJOR_VERSION, 3,
|
||||||
|
_EGL.EGL_CONTEXT_MINOR_VERSION, 3,
|
||||||
|
_EGL.EGL_CONTEXT_OPENGL_PROFILE_MASK, _EGL.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
|
||||||
|
EGL_NONE
|
||||||
|
]
|
||||||
|
context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attribs)
|
||||||
|
if context == EGL_NO_CONTEXT:
|
||||||
|
raise RuntimeError("eglCreateContext() failed")
|
||||||
|
|
||||||
|
logger.debug("_init_egl: calling eglCreatePbufferSurface()")
|
||||||
|
pbuffer_attribs = [EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE]
|
||||||
|
surface = eglCreatePbufferSurface(display, config, pbuffer_attribs)
|
||||||
|
if surface == _EGL.EGL_NO_SURFACE:
|
||||||
|
raise RuntimeError("eglCreatePbufferSurface() failed")
|
||||||
|
|
||||||
|
logger.debug("_init_egl: calling eglMakeCurrent()")
|
||||||
|
if not eglMakeCurrent(display, surface, surface, context):
|
||||||
|
raise RuntimeError("eglMakeCurrent() failed")
|
||||||
|
|
||||||
|
logger.debug("_init_egl: completed successfully")
|
||||||
|
return display, context, surface, _EGL
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
logger.debug("_init_egl: failed, cleaning up")
|
||||||
|
# Clean up any resources on failure
|
||||||
|
if surface is not None:
|
||||||
|
eglDestroySurface(display, surface)
|
||||||
|
if context is not None:
|
||||||
|
eglDestroyContext(display, context)
|
||||||
|
if display is not None:
|
||||||
|
eglTerminate(display)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _init_osmesa():
|
||||||
|
"""Initialize OSMesa for software rendering. Returns (context, buffer). Raises RuntimeError on failure."""
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
logger.debug("_init_osmesa: starting")
|
||||||
|
os.environ["PYOPENGL_PLATFORM"] = "osmesa"
|
||||||
|
|
||||||
|
logger.debug("_init_osmesa: importing OpenGL.osmesa")
|
||||||
|
from OpenGL import GL as _gl
|
||||||
|
from OpenGL.osmesa import (
|
||||||
|
OSMesaCreateContextExt, OSMesaMakeCurrent, OSMesaDestroyContext,
|
||||||
|
OSMESA_RGBA,
|
||||||
|
)
|
||||||
|
logger.debug("_init_osmesa: imports completed")
|
||||||
|
|
||||||
|
ctx = OSMesaCreateContextExt(OSMESA_RGBA, 24, 0, 0, None)
|
||||||
|
if not ctx:
|
||||||
|
raise RuntimeError("OSMesaCreateContextExt() failed")
|
||||||
|
|
||||||
|
width, height = 64, 64
|
||||||
|
buffer = (ctypes.c_ubyte * (width * height * 4))()
|
||||||
|
|
||||||
|
logger.debug("_init_osmesa: calling OSMesaMakeCurrent()")
|
||||||
|
if not OSMesaMakeCurrent(ctx, buffer, _gl.GL_UNSIGNED_BYTE, width, height):
|
||||||
|
OSMesaDestroyContext(ctx)
|
||||||
|
raise RuntimeError("OSMesaMakeCurrent() failed")
|
||||||
|
|
||||||
|
logger.debug("_init_osmesa: completed successfully")
|
||||||
|
return ctx, buffer
|
||||||
|
|
||||||
|
|
||||||
|
class GLContext:
|
||||||
|
"""Manages OpenGL context and resources for shader execution.
|
||||||
|
|
||||||
|
Tries backends in order: GLFW (desktop) → EGL (headless GPU) → OSMesa (software).
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
_initialized = False
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if GLContext._initialized:
|
||||||
|
logger.debug("GLContext.__init__: already initialized, skipping")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug("GLContext.__init__: starting initialization")
|
||||||
|
|
||||||
|
global glfw, EGL
|
||||||
|
|
||||||
|
import time
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
self._backend = None
|
||||||
|
self._window = None
|
||||||
|
self._egl_display = None
|
||||||
|
self._egl_context = None
|
||||||
|
self._egl_surface = None
|
||||||
|
self._osmesa_ctx = None
|
||||||
|
self._osmesa_buffer = None
|
||||||
|
self._vao = None
|
||||||
|
|
||||||
|
# Try backends in order: GLFW → EGL → OSMesa
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
logger.debug("GLContext.__init__: trying GLFW backend")
|
||||||
|
try:
|
||||||
|
self._window, glfw = _init_glfw()
|
||||||
|
self._backend = "glfw"
|
||||||
|
logger.debug("GLContext.__init__: GLFW backend succeeded")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"GLContext.__init__: GLFW backend failed: {e}")
|
||||||
|
errors.append(("GLFW", e))
|
||||||
|
|
||||||
|
if self._backend is None:
|
||||||
|
logger.debug("GLContext.__init__: trying EGL backend")
|
||||||
|
try:
|
||||||
|
self._egl_display, self._egl_context, self._egl_surface, EGL = _init_egl()
|
||||||
|
self._backend = "egl"
|
||||||
|
logger.debug("GLContext.__init__: EGL backend succeeded")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"GLContext.__init__: EGL backend failed: {e}")
|
||||||
|
errors.append(("EGL", e))
|
||||||
|
|
||||||
|
if self._backend is None:
|
||||||
|
logger.debug("GLContext.__init__: trying OSMesa backend")
|
||||||
|
try:
|
||||||
|
self._osmesa_ctx, self._osmesa_buffer = _init_osmesa()
|
||||||
|
self._backend = "osmesa"
|
||||||
|
logger.debug("GLContext.__init__: OSMesa backend succeeded")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"GLContext.__init__: OSMesa backend failed: {e}")
|
||||||
|
errors.append(("OSMesa", e))
|
||||||
|
|
||||||
|
if self._backend is None:
|
||||||
|
if sys.platform == "win32":
|
||||||
|
platform_help = (
|
||||||
|
"Windows: Ensure GPU drivers are installed and display is available.\n"
|
||||||
|
" CPU-only/headless mode is not supported on Windows."
|
||||||
|
)
|
||||||
|
elif sys.platform == "darwin":
|
||||||
|
platform_help = (
|
||||||
|
"macOS: GLFW is not supported.\n"
|
||||||
|
" Install OSMesa via Homebrew: brew install mesa\n"
|
||||||
|
" Then: pip install PyOpenGL PyOpenGL-accelerate"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
platform_help = (
|
||||||
|
"Linux: Install one of these backends:\n"
|
||||||
|
" Desktop: sudo apt install libgl1-mesa-glx libglfw3\n"
|
||||||
|
" Headless with GPU: sudo apt install libegl1-mesa libgl1-mesa-dri\n"
|
||||||
|
" Headless (CPU): sudo apt install libosmesa6"
|
||||||
|
)
|
||||||
|
|
||||||
|
error_details = "\n".join(f" {name}: {err}" for name, err in errors)
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to create OpenGL context.\n\n"
|
||||||
|
f"Backend errors:\n{error_details}\n\n"
|
||||||
|
f"{platform_help}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Now import OpenGL.GL (after context is current)
|
||||||
|
logger.debug("GLContext.__init__: importing OpenGL.GL")
|
||||||
|
_import_opengl()
|
||||||
|
|
||||||
|
# Create VAO (required for core profile, but OSMesa may use compat profile)
|
||||||
|
logger.debug("GLContext.__init__: creating VAO")
|
||||||
|
try:
|
||||||
|
vao = gl.glGenVertexArrays(1)
|
||||||
|
gl.glBindVertexArray(vao)
|
||||||
|
self._vao = vao # Only store after successful bind
|
||||||
|
logger.debug("GLContext.__init__: VAO created successfully")
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"GLContext.__init__: VAO creation failed (may be expected for OSMesa): {e}")
|
||||||
|
# OSMesa with older Mesa may not support VAOs
|
||||||
|
# Clean up if we created but couldn't bind
|
||||||
|
if vao:
|
||||||
|
try:
|
||||||
|
gl.glDeleteVertexArrays(1, [vao])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
elapsed = (time.perf_counter() - start) * 1000
|
||||||
|
|
||||||
|
# Log device info
|
||||||
|
renderer = gl.glGetString(gl.GL_RENDERER)
|
||||||
|
vendor = gl.glGetString(gl.GL_VENDOR)
|
||||||
|
version = gl.glGetString(gl.GL_VERSION)
|
||||||
|
renderer = renderer.decode() if renderer else "Unknown"
|
||||||
|
vendor = vendor.decode() if vendor else "Unknown"
|
||||||
|
version = version.decode() if version else "Unknown"
|
||||||
|
|
||||||
|
GLContext._initialized = True
|
||||||
|
logger.info(f"GLSL context initialized in {elapsed:.1f}ms ({self._backend}) - {renderer} ({vendor}), GL {version}")
|
||||||
|
|
||||||
|
def make_current(self):
|
||||||
|
if self._backend == "glfw":
|
||||||
|
glfw.make_context_current(self._window)
|
||||||
|
elif self._backend == "egl":
|
||||||
|
from OpenGL.EGL import eglMakeCurrent
|
||||||
|
eglMakeCurrent(self._egl_display, self._egl_surface, self._egl_surface, self._egl_context)
|
||||||
|
elif self._backend == "osmesa":
|
||||||
|
from OpenGL.osmesa import OSMesaMakeCurrent
|
||||||
|
OSMesaMakeCurrent(self._osmesa_ctx, self._osmesa_buffer, gl.GL_UNSIGNED_BYTE, 64, 64)
|
||||||
|
|
||||||
|
if self._vao is not None:
|
||||||
|
gl.glBindVertexArray(self._vao)
|
||||||
|
|
||||||
|
|
||||||
|
def _compile_shader(source: str, shader_type: int) -> int:
|
||||||
|
"""Compile a shader and return its ID."""
|
||||||
|
shader = gl.glCreateShader(shader_type)
|
||||||
|
gl.glShaderSource(shader, source)
|
||||||
|
gl.glCompileShader(shader)
|
||||||
|
|
||||||
|
if gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
|
||||||
|
error = gl.glGetShaderInfoLog(shader).decode()
|
||||||
|
gl.glDeleteShader(shader)
|
||||||
|
raise RuntimeError(f"Shader compilation failed:\n{error}")
|
||||||
|
|
||||||
|
return shader
|
||||||
|
|
||||||
|
|
||||||
|
def _create_program(vertex_source: str, fragment_source: str) -> int:
|
||||||
|
"""Create and link a shader program."""
|
||||||
|
vertex_shader = _compile_shader(vertex_source, gl.GL_VERTEX_SHADER)
|
||||||
|
try:
|
||||||
|
fragment_shader = _compile_shader(fragment_source, gl.GL_FRAGMENT_SHADER)
|
||||||
|
except RuntimeError:
|
||||||
|
gl.glDeleteShader(vertex_shader)
|
||||||
|
raise
|
||||||
|
|
||||||
|
program = gl.glCreateProgram()
|
||||||
|
gl.glAttachShader(program, vertex_shader)
|
||||||
|
gl.glAttachShader(program, fragment_shader)
|
||||||
|
gl.glLinkProgram(program)
|
||||||
|
|
||||||
|
gl.glDeleteShader(vertex_shader)
|
||||||
|
gl.glDeleteShader(fragment_shader)
|
||||||
|
|
||||||
|
if gl.glGetProgramiv(program, gl.GL_LINK_STATUS) != gl.GL_TRUE:
|
||||||
|
error = gl.glGetProgramInfoLog(program).decode()
|
||||||
|
gl.glDeleteProgram(program)
|
||||||
|
raise RuntimeError(f"Program linking failed:\n{error}")
|
||||||
|
|
||||||
|
return program
|
||||||
|
|
||||||
|
|
||||||
|
def _render_shader_batch(
|
||||||
|
fragment_code: str,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
image_batches: list[list[np.ndarray]],
|
||||||
|
floats: list[float],
|
||||||
|
ints: list[int],
|
||||||
|
) -> list[list[np.ndarray]]:
|
||||||
|
"""
|
||||||
|
Render a fragment shader for multiple batches efficiently.
|
||||||
|
|
||||||
|
Compiles shader once, reuses framebuffer/textures across batches.
|
||||||
|
Supports multi-pass rendering via #pragma passes N directive.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fragment_code: User's fragment shader code
|
||||||
|
width: Output width
|
||||||
|
height: Output height
|
||||||
|
image_batches: List of batches, each batch is a list of input images (H, W, C) float32 [0,1]
|
||||||
|
floats: List of float uniforms
|
||||||
|
ints: List of int uniforms
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of batch outputs, each is a list of output images (H, W, 4) float32 [0,1]
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
|
||||||
|
if not image_batches:
|
||||||
|
return []
|
||||||
|
|
||||||
|
ctx = GLContext()
|
||||||
|
ctx.make_current()
|
||||||
|
|
||||||
|
# Convert from GLSL ES to desktop GLSL 330
|
||||||
|
fragment_source = _convert_es_to_desktop(fragment_code)
|
||||||
|
|
||||||
|
# Detect how many outputs the shader actually uses
|
||||||
|
num_outputs = _detect_output_count(fragment_code)
|
||||||
|
|
||||||
|
# Detect multi-pass rendering
|
||||||
|
num_passes = _detect_pass_count(fragment_code)
|
||||||
|
|
||||||
|
# Track resources for cleanup
|
||||||
|
program = None
|
||||||
|
fbo = None
|
||||||
|
output_textures = []
|
||||||
|
input_textures = []
|
||||||
|
ping_pong_textures = []
|
||||||
|
ping_pong_fbos = []
|
||||||
|
|
||||||
|
num_inputs = len(image_batches[0])
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Compile shaders (once for all batches)
|
||||||
|
try:
|
||||||
|
program = _create_program(VERTEX_SHADER, fragment_source)
|
||||||
|
except RuntimeError:
|
||||||
|
logger.error(f"Fragment shader:\n{fragment_source}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
gl.glUseProgram(program)
|
||||||
|
|
||||||
|
# Create framebuffer with only the needed color attachments
|
||||||
|
fbo = gl.glGenFramebuffers(1)
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
|
||||||
|
|
||||||
|
draw_buffers = []
|
||||||
|
for i in range(num_outputs):
|
||||||
|
tex = gl.glGenTextures(1)
|
||||||
|
output_textures.append(tex)
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
|
||||||
|
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, width, height, 0, gl.GL_RGBA, gl.GL_FLOAT, None)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
|
||||||
|
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0 + i, gl.GL_TEXTURE_2D, tex, 0)
|
||||||
|
draw_buffers.append(gl.GL_COLOR_ATTACHMENT0 + i)
|
||||||
|
|
||||||
|
gl.glDrawBuffers(num_outputs, draw_buffers)
|
||||||
|
|
||||||
|
if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
|
||||||
|
raise RuntimeError("Framebuffer is not complete")
|
||||||
|
|
||||||
|
# Create ping-pong resources for multi-pass rendering
|
||||||
|
if num_passes > 1:
|
||||||
|
for _ in range(2):
|
||||||
|
pp_tex = gl.glGenTextures(1)
|
||||||
|
ping_pong_textures.append(pp_tex)
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, pp_tex)
|
||||||
|
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, width, height, 0, gl.GL_RGBA, gl.GL_FLOAT, None)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
|
||||||
|
|
||||||
|
pp_fbo = gl.glGenFramebuffers(1)
|
||||||
|
ping_pong_fbos.append(pp_fbo)
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, pp_fbo)
|
||||||
|
gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, pp_tex, 0)
|
||||||
|
gl.glDrawBuffers(1, [gl.GL_COLOR_ATTACHMENT0])
|
||||||
|
|
||||||
|
if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE:
|
||||||
|
raise RuntimeError("Ping-pong framebuffer is not complete")
|
||||||
|
|
||||||
|
# Create input textures (reused for all batches)
|
||||||
|
for i in range(num_inputs):
|
||||||
|
tex = gl.glGenTextures(1)
|
||||||
|
input_textures.append(tex)
|
||||||
|
gl.glActiveTexture(gl.GL_TEXTURE0 + i)
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
|
||||||
|
gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
|
||||||
|
|
||||||
|
loc = gl.glGetUniformLocation(program, f"u_image{i}")
|
||||||
|
if loc >= 0:
|
||||||
|
gl.glUniform1i(loc, i)
|
||||||
|
|
||||||
|
# Set static uniforms (once for all batches)
|
||||||
|
loc = gl.glGetUniformLocation(program, "u_resolution")
|
||||||
|
if loc >= 0:
|
||||||
|
gl.glUniform2f(loc, float(width), float(height))
|
||||||
|
|
||||||
|
for i, v in enumerate(floats):
|
||||||
|
loc = gl.glGetUniformLocation(program, f"u_float{i}")
|
||||||
|
if loc >= 0:
|
||||||
|
gl.glUniform1f(loc, v)
|
||||||
|
|
||||||
|
for i, v in enumerate(ints):
|
||||||
|
loc = gl.glGetUniformLocation(program, f"u_int{i}")
|
||||||
|
if loc >= 0:
|
||||||
|
gl.glUniform1i(loc, v)
|
||||||
|
|
||||||
|
# Get u_pass uniform location for multi-pass
|
||||||
|
pass_loc = gl.glGetUniformLocation(program, "u_pass")
|
||||||
|
|
||||||
|
gl.glViewport(0, 0, width, height)
|
||||||
|
gl.glDisable(gl.GL_BLEND) # Ensure no alpha blending - write output directly
|
||||||
|
|
||||||
|
# Process each batch
|
||||||
|
all_batch_outputs = []
|
||||||
|
for images in image_batches:
|
||||||
|
# Update input textures with this batch's images
|
||||||
|
for i, img in enumerate(images):
|
||||||
|
gl.glActiveTexture(gl.GL_TEXTURE0 + i)
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, input_textures[i])
|
||||||
|
|
||||||
|
# Flip vertically for GL coordinates, ensure RGBA
|
||||||
|
h, w, c = img.shape
|
||||||
|
if c == 3:
|
||||||
|
img_upload = np.empty((h, w, 4), dtype=np.float32)
|
||||||
|
img_upload[:, :, :3] = img[::-1, :, :]
|
||||||
|
img_upload[:, :, 3] = 1.0
|
||||||
|
else:
|
||||||
|
img_upload = np.ascontiguousarray(img[::-1, :, :])
|
||||||
|
|
||||||
|
gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, w, h, 0, gl.GL_RGBA, gl.GL_FLOAT, img_upload)
|
||||||
|
|
||||||
|
if num_passes == 1:
|
||||||
|
# Single pass - render directly to output FBO
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
|
||||||
|
if pass_loc >= 0:
|
||||||
|
gl.glUniform1i(pass_loc, 0)
|
||||||
|
gl.glClearColor(0, 0, 0, 0)
|
||||||
|
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
||||||
|
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
|
||||||
|
else:
|
||||||
|
# Multi-pass rendering with ping-pong
|
||||||
|
for p in range(num_passes):
|
||||||
|
is_last_pass = (p == num_passes - 1)
|
||||||
|
|
||||||
|
# Set pass uniform
|
||||||
|
if pass_loc >= 0:
|
||||||
|
gl.glUniform1i(pass_loc, p)
|
||||||
|
|
||||||
|
if is_last_pass:
|
||||||
|
# Last pass renders to the main output FBO
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
|
||||||
|
else:
|
||||||
|
# Intermediate passes render to ping-pong FBO
|
||||||
|
target_fbo = ping_pong_fbos[p % 2]
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, target_fbo)
|
||||||
|
|
||||||
|
# Set input texture for this pass
|
||||||
|
gl.glActiveTexture(gl.GL_TEXTURE0)
|
||||||
|
if p == 0:
|
||||||
|
# First pass reads from original input
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, input_textures[0])
|
||||||
|
else:
|
||||||
|
# Subsequent passes read from previous pass output
|
||||||
|
source_tex = ping_pong_textures[(p - 1) % 2]
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, source_tex)
|
||||||
|
|
||||||
|
gl.glClearColor(0, 0, 0, 0)
|
||||||
|
gl.glClear(gl.GL_COLOR_BUFFER_BIT)
|
||||||
|
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
|
||||||
|
|
||||||
|
# Read back outputs for this batch
|
||||||
|
# (glGetTexImage is synchronous, implicitly waits for rendering)
|
||||||
|
batch_outputs = []
|
||||||
|
for tex in output_textures:
|
||||||
|
gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
|
||||||
|
data = gl.glGetTexImage(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, gl.GL_FLOAT)
|
||||||
|
img = np.frombuffer(data, dtype=np.float32).reshape(height, width, 4)
|
||||||
|
batch_outputs.append(img[::-1, :, :].copy())
|
||||||
|
|
||||||
|
# Pad with black images for unused outputs
|
||||||
|
black_img = np.zeros((height, width, 4), dtype=np.float32)
|
||||||
|
for _ in range(num_outputs, MAX_OUTPUTS):
|
||||||
|
batch_outputs.append(black_img)
|
||||||
|
|
||||||
|
all_batch_outputs.append(batch_outputs)
|
||||||
|
|
||||||
|
elapsed = (time.perf_counter() - start_time) * 1000
|
||||||
|
num_batches = len(image_batches)
|
||||||
|
pass_info = f", {num_passes} passes" if num_passes > 1 else ""
|
||||||
|
logger.info(f"GLSL shader executed in {elapsed:.1f}ms ({num_batches} batch{'es' if num_batches != 1 else ''}, {width}x{height}{pass_info})")
|
||||||
|
|
||||||
|
return all_batch_outputs
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Unbind before deleting
|
||||||
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
||||||
|
gl.glUseProgram(0)
|
||||||
|
|
||||||
|
if input_textures:
|
||||||
|
gl.glDeleteTextures(len(input_textures), input_textures)
|
||||||
|
if output_textures:
|
||||||
|
gl.glDeleteTextures(len(output_textures), output_textures)
|
||||||
|
if ping_pong_textures:
|
||||||
|
gl.glDeleteTextures(len(ping_pong_textures), ping_pong_textures)
|
||||||
|
if fbo is not None:
|
||||||
|
gl.glDeleteFramebuffers(1, [fbo])
|
||||||
|
for pp_fbo in ping_pong_fbos:
|
||||||
|
gl.glDeleteFramebuffers(1, [pp_fbo])
|
||||||
|
if program is not None:
|
||||||
|
gl.glDeleteProgram(program)
|
||||||
|
|
||||||
|
class GLSLShader(io.ComfyNode):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_schema(cls) -> io.Schema:
|
||||||
|
image_template = io.Autogrow.TemplatePrefix(
|
||||||
|
io.Image.Input("image"),
|
||||||
|
prefix="image",
|
||||||
|
min=1,
|
||||||
|
max=MAX_IMAGES,
|
||||||
|
)
|
||||||
|
|
||||||
|
float_template = io.Autogrow.TemplatePrefix(
|
||||||
|
io.Float.Input("float", default=0.0),
|
||||||
|
prefix="u_float",
|
||||||
|
min=0,
|
||||||
|
max=MAX_UNIFORMS,
|
||||||
|
)
|
||||||
|
|
||||||
|
int_template = io.Autogrow.TemplatePrefix(
|
||||||
|
io.Int.Input("int", default=0),
|
||||||
|
prefix="u_int",
|
||||||
|
min=0,
|
||||||
|
max=MAX_UNIFORMS,
|
||||||
|
)
|
||||||
|
|
||||||
|
return io.Schema(
|
||||||
|
node_id="GLSLShader",
|
||||||
|
display_name="GLSL Shader",
|
||||||
|
category="image/shader",
|
||||||
|
description=(
|
||||||
|
"Apply GLSL ES fragment shaders to images. "
|
||||||
|
"u_resolution (vec2) is always available."
|
||||||
|
),
|
||||||
|
inputs=[
|
||||||
|
io.String.Input(
|
||||||
|
"fragment_shader",
|
||||||
|
default=DEFAULT_FRAGMENT_SHADER,
|
||||||
|
multiline=True,
|
||||||
|
tooltip="GLSL fragment shader source code (GLSL ES 3.00 / WebGL 2.0 compatible)",
|
||||||
|
),
|
||||||
|
io.DynamicCombo.Input(
|
||||||
|
"size_mode",
|
||||||
|
options=[
|
||||||
|
io.DynamicCombo.Option("from_input", []),
|
||||||
|
io.DynamicCombo.Option(
|
||||||
|
"custom",
|
||||||
|
[
|
||||||
|
io.Int.Input(
|
||||||
|
"width",
|
||||||
|
default=512,
|
||||||
|
min=1,
|
||||||
|
max=nodes.MAX_RESOLUTION,
|
||||||
|
),
|
||||||
|
io.Int.Input(
|
||||||
|
"height",
|
||||||
|
default=512,
|
||||||
|
min=1,
|
||||||
|
max=nodes.MAX_RESOLUTION,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
tooltip="Output size: 'from_input' uses first input image dimensions, 'custom' allows manual size",
|
||||||
|
),
|
||||||
|
io.Autogrow.Input("images", template=image_template, tooltip=f"Images are available as u_image0-{MAX_IMAGES-1} (sampler2D) in the shader code"),
|
||||||
|
io.Autogrow.Input("floats", template=float_template, tooltip=f"Floats are available as u_float0-{MAX_UNIFORMS-1} in the shader code"),
|
||||||
|
io.Autogrow.Input("ints", template=int_template, tooltip=f"Ints are available as u_int0-{MAX_UNIFORMS-1} in the shader code"),
|
||||||
|
],
|
||||||
|
outputs=[
|
||||||
|
io.Image.Output(display_name="IMAGE0", tooltip="Available via layout(location = 0) out vec4 fragColor0 in the shader code"),
|
||||||
|
io.Image.Output(display_name="IMAGE1", tooltip="Available via layout(location = 1) out vec4 fragColor1 in the shader code"),
|
||||||
|
io.Image.Output(display_name="IMAGE2", tooltip="Available via layout(location = 2) out vec4 fragColor2 in the shader code"),
|
||||||
|
io.Image.Output(display_name="IMAGE3", tooltip="Available via layout(location = 3) out vec4 fragColor3 in the shader code"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def execute(
|
||||||
|
cls,
|
||||||
|
fragment_shader: str,
|
||||||
|
size_mode: SizeModeInput,
|
||||||
|
images: io.Autogrow.Type,
|
||||||
|
floats: io.Autogrow.Type = None,
|
||||||
|
ints: io.Autogrow.Type = None,
|
||||||
|
**kwargs,
|
||||||
|
) -> io.NodeOutput:
|
||||||
|
image_list = [v for v in images.values() if v is not None]
|
||||||
|
float_list = (
|
||||||
|
[v if v is not None else 0.0 for v in floats.values()] if floats else []
|
||||||
|
)
|
||||||
|
int_list = [v if v is not None else 0 for v in ints.values()] if ints else []
|
||||||
|
|
||||||
|
if not image_list:
|
||||||
|
raise ValueError("At least one input image is required")
|
||||||
|
|
||||||
|
# Determine output dimensions
|
||||||
|
if size_mode["size_mode"] == "custom":
|
||||||
|
out_width = size_mode["width"]
|
||||||
|
out_height = size_mode["height"]
|
||||||
|
else:
|
||||||
|
out_height, out_width = image_list[0].shape[1:3]
|
||||||
|
|
||||||
|
batch_size = image_list[0].shape[0]
|
||||||
|
|
||||||
|
# Prepare batches
|
||||||
|
image_batches = []
|
||||||
|
for batch_idx in range(batch_size):
|
||||||
|
batch_images = [img_tensor[batch_idx].cpu().numpy().astype(np.float32) for img_tensor in image_list]
|
||||||
|
image_batches.append(batch_images)
|
||||||
|
|
||||||
|
all_batch_outputs = _render_shader_batch(
|
||||||
|
fragment_shader,
|
||||||
|
out_width,
|
||||||
|
out_height,
|
||||||
|
image_batches,
|
||||||
|
float_list,
|
||||||
|
int_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Collect outputs into tensors
|
||||||
|
all_outputs = [[] for _ in range(MAX_OUTPUTS)]
|
||||||
|
for batch_outputs in all_batch_outputs:
|
||||||
|
for i, out_img in enumerate(batch_outputs):
|
||||||
|
all_outputs[i].append(torch.from_numpy(out_img))
|
||||||
|
|
||||||
|
output_tensors = [torch.stack(all_outputs[i], dim=0) for i in range(MAX_OUTPUTS)]
|
||||||
|
return io.NodeOutput(
|
||||||
|
*output_tensors,
|
||||||
|
ui=cls._build_ui_output(image_list, output_tensors[0]),
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_ui_output(
|
||||||
|
cls, image_list: list[torch.Tensor], output_batch: torch.Tensor
|
||||||
|
) -> dict[str, list]:
|
||||||
|
"""Build UI output with input and output images for client-side shader execution."""
|
||||||
|
combined_inputs = torch.cat(image_list, dim=0)
|
||||||
|
input_images_ui = ui.ImageSaveHelper.save_images(
|
||||||
|
combined_inputs,
|
||||||
|
filename_prefix="GLSLShader_input",
|
||||||
|
folder_type=io.FolderType.temp,
|
||||||
|
cls=None,
|
||||||
|
compress_level=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
output_images_ui = ui.ImageSaveHelper.save_images(
|
||||||
|
output_batch,
|
||||||
|
filename_prefix="GLSLShader_output",
|
||||||
|
folder_type=io.FolderType.temp,
|
||||||
|
cls=None,
|
||||||
|
compress_level=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"input_images": input_images_ui, "images": output_images_ui}
|
||||||
|
|
||||||
|
|
||||||
|
class GLSLExtension(ComfyExtension):
|
||||||
|
@override
|
||||||
|
async def get_node_list(self) -> list[type[io.ComfyNode]]:
|
||||||
|
return [GLSLShader]
|
||||||
|
|
||||||
|
|
||||||
|
async def comfy_entrypoint() -> GLSLExtension:
|
||||||
|
return GLSLExtension()
|
||||||
@ -42,7 +42,7 @@ class TextGenerate(io.ComfyNode):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def execute(cls, clip, prompt, max_length, sampling_mode, image=None) -> io.NodeOutput:
|
def execute(cls, clip, prompt, max_length, sampling_mode, image=None) -> io.NodeOutput:
|
||||||
|
|
||||||
tokens = clip.tokenize(prompt, image=image, skip_template=False)
|
tokens = clip.tokenize(prompt, image=image, skip_template=False, min_length=1)
|
||||||
|
|
||||||
# Get sampling parameters from dynamic combo
|
# Get sampling parameters from dynamic combo
|
||||||
do_sample = sampling_mode.get("sampling_mode") == "on"
|
do_sample = sampling_mode.get("sampling_mode") == "on"
|
||||||
|
|||||||
1
nodes.py
1
nodes.py
@ -2442,6 +2442,7 @@ async def init_builtin_extra_nodes():
|
|||||||
"nodes_wanmove.py",
|
"nodes_wanmove.py",
|
||||||
"nodes_image_compare.py",
|
"nodes_image_compare.py",
|
||||||
"nodes_zimage.py",
|
"nodes_zimage.py",
|
||||||
|
"nodes_glsl.py",
|
||||||
"nodes_lora_debug.py",
|
"nodes_lora_debug.py",
|
||||||
"nodes_textgen.py",
|
"nodes_textgen.py",
|
||||||
"nodes_color.py",
|
"nodes_color.py",
|
||||||
|
|||||||
@ -30,3 +30,6 @@ kornia>=0.7.1
|
|||||||
spandrel
|
spandrel
|
||||||
pydantic~=2.0
|
pydantic~=2.0
|
||||||
pydantic-settings~=2.0
|
pydantic-settings~=2.0
|
||||||
|
PyOpenGL
|
||||||
|
PyOpenGL-accelerate
|
||||||
|
glfw
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user