diff --git a/README.md b/README.md index c0155c4..08121e0 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,14 @@ SATA Utility Node Suite is a collection of custom nodes for [ComfyUI](https://github.com/comfyanonymous/ComfyUI) designed to enhance your image processing and workflow experience. It includes: -- **Upscale Machine:** High-quality model-based image upscaling with aspect-ratio preserving rescale. -- **Prompt Machine:** Easy prompt selection from a CSV for positive/negative prompt workflows. +- **Upscale Machine:** High-quality model-based image upscaling with Frequency-Split SR and Blue Noise injection. +- **Prompt Machine (Six-Slot):** Professional structured prompting with JSON output support. +- **Prompt Style Machine:** Easy prompt selection from a CSV for positive/negative prompt workflows. - **Touchpad Pan & Zoom:** Seamless navigation for large images in the ComfyUI preview panel. -- **Resolution Machine:** Quick selection of model-specific image resolutions for popular models. -- **Save Image w/Metadata:** saving images with meta data and with different file formats. -- **Prompt Autocomplete:** Text widget with autocompletion, random selection, and preview capabilities. -- **Latent Machine:** Advanced noise generation for creating empty latents initialized with specific noise patterns instead of standard Gaussian noise. +- **Resolution Machine:** Quick selection of model-specific image resolutions with intelligent Megapixel scaling. +- **Save Image w/Metadata:** Saving images with metadata and with different file formats. +- **Prompt Autocomplete:** Text widget with highly optimized subsequence fuzzy autocompletion. +- **Latent Machine:** Advanced noise generation (Power-Law, Plasma, Blue, Offset) supporting both 4-channel and 16-channel modern architectures. - **Preview Machine:** Privacy-focused image preview node with auto-hide and hover-to-reveal functionality. --- @@ -28,16 +29,23 @@ It includes: ### 🖼️ Upscale Machine - **Model-based Upscaling:** Uses custom or pre-trained models from the `upscale_models` folder. +- **Frequency-Split SR:** Separates image into base colors (low frequencies) and sharp edges (high frequencies). The neural network only upscales edges, while a bicubic algorithm scales colors, eliminating "plastic/waxy" looks and edge halos. +- **Blue Noise Injection:** Adds high-frequency micro-textures (`inject_noise_for_realism`) during upscaling. Automatically activates when using a chained model to fix the "smoothness" problem in low-step/distilled models like SDXL Turbo. - **Aspect Ratio Preserving Rescale:** Output size is always original size × rescale factor. - **CPU-based Rescaling:** Efficient resizing using OpenCV. -- **Supersampling:** Optional detail preservation for downscaling/fractional resizing. -- **Rounding Modulus:** Ensures output dimensions align with model/UI constraints. +- **Rounding Modulus:** Ensures output dimensions seamlessly align with UNet architectural constraints. -### 💬 Prompt Machine +### 💬 Prompt Machine (Six-Slot Framework) + +- **Structured Prompting:** Dedicated input boxes for `subject`, `style`, `lighting`, `composition`, `mood`, and `technical` details to prevent "word salad". +- **JSON Output Mode:** Toggle `json_output` to compile the slots into a strict JSON string, perfect for piping into LLM rewriting nodes or API pipelines. +- **Autocomplete Ready:** Fully integrated with the autocomplete system—just type `#` in any slot to trigger the smart tag popup! +- **Natural Language & Comma Separated:** Dynamically formats outputs for FLUX (Natural Language) or SDXL (Comma Separated). + +### 💬 Prompt Style Machine (Legacy) - **CSV-based Prompt Selection:** Select prompt sets from a CSV file (`prompts.csv`). -- **Positive/Negative Output:** Outputs both positive and negative prompts as strings. -- **Easy Integration:** Designed for use in prompt-driven workflows. +- **Clean Output:** Outputs the positive prompt string with a live, enlarged text preview inside the node. ### 🖱️ Touchpad Pan & Zoom @@ -46,51 +54,43 @@ It includes: ### 📏 Resolution Machine -- **Model-aware Resolution Selection:** Choose from a list of recommended resolutions for each supported model. -- **Auto-populate Width/Height:** Selecting a resolution automatically fills in the correct width and height. -- **Manual Override:** You can manually set width/height, which clears the preset selection. -- **Supports Many Models:** Includes presets for SD1.5, SDXL, Illustrious XL, Pony XL, Flux.1 Dev, Qwen-Image, Wan2.2-S2V, SD3.5 Large, Lumina-Image 2.0, and more. -- **UI Integration:** Intuitive dropdowns for model and resolution selection in the ComfyUI interface. +- **Intelligent Megapixel Scaling:** Uses a target Megapixel slider to automatically calculate the exact width/height required to hit the target while perfectly preserving your chosen aspect ratio. Rounds to the nearest multiple of 8. +- **Model-aware Resolution Selection:** Choose from a list of recommended resolutions for each supported model (SD1.5, SDXL, Flux.1 & Flux.2, SD3 & SD3.5, Ideogram 4, Lumina, Qwen-Image, Z-Image Turbo, Hunyuan-DiT, and Video Models like Wan2.2, HunyuanVideo, Mochi-1, LTX-Video). --- ### 💾 Save Image w/Metadata -- **Single path+filename input:** Provide a single required `path_and_filename` string (examples: `comfy`, `Test/comfy`, `SDXL/Test/comfy`). The last path segment is used as the filename; preceding segments become folders under the ComfyUI output directory. +- **Single path+filename input:** Provide a single required `path_and_filename` string. - **Supported formats:** PNG, JPEG (saved as .jpg), WEBP (choose with the `extension` dropdown). - **Automatic folder creation:** Any missing folders in the provided path are created automatically. - **Metadata embedding:** PNG files receive tEXt entries (parameters/prompt). JPEG/WEBP receive EXIF UserComment where possible. -- **Placeholder resolution:** Filenames and paths accept `%...%` placeholders which are resolved from the node's `prompt` dictionary (best-effort). Example: if `prompt` contains `{"title": "MyTitle"}`, using `%title%/img` will save under `MyTitle/img.png`. -- **Collision-safe saving:** If a file already exists the node will append `_0001`, `_0002`, ... to the base filename to avoid overwriting. +- **Placeholder resolution:** Filenames and paths accept `%...%` placeholders which are resolved from the node's `prompt` dictionary. +- **Collision-safe saving:** Automatically appends numerical suffixes to avoid overwriting. ### ✍️ Prompt Autocomplete -- **Autocompletion:** Type `#` (configurable) to trigger a popup with suggestions from CSV/JSON files in the `prompt` folder. +- **Subsequence Fuzzy Matching:** Ultra-fast, highly optimized fuzzy search with rank-based scoring. +- **Autocompletion:** Type `#` (configurable) to trigger a popup with suggestions from CSV/JSON files in the `prompt` folder. Supports strict 100-item render limits to prevent UI lag. - **Random Selection:** Select `🎲 Random` to insert a random item from a category. -- **Preview:** View full text of long snippets before inserting. - **Global Mode:** Optional setting to enable autocompletion on ALL text widgets in ComfyUI. -- **Custom Data:** Add your own `.csv` or `.json` files to the `prompt` folder to extend the library. ### 🌌 Latent Machine -- **Advanced Noise Generation:** Create empty latents initialized with specific noise patterns instead of standard Gaussian noise. -- **Noise Types:** +- **Advanced Noise Generation:** Initialize latents with specific noise patterns: - **Gaussian (White):** Standard noise, good for sharp details. - - **Pink (1/f):** Balanced noise, great for photorealism and nature. - - **Brown (1/f²):** Smoother noise, ideal for backgrounds and anime. - - **Perlin:** Cloud-like structure, perfect for landscapes and fluids. - - **Plasma:** Very smooth, abstract blobs for sci-fi and alien terrains. -- **Model Support:** Select between **4-channel** (SD1.5, SDXL, etc.) and **16-channel** (Flux, SD3) latents. + - **Blue (High-Frequency):** Enhances detail and textures. + - **Pink (1/f) / Brown (1/f²):** Photorealism, nature, anime backgrounds. + - **Perlin / Plasma:** Fluid landscapes, sci-fi, abstract terrains. +- **High-Contrast Offset Noise:** Toggle `high_contrast` to inject a `0.1` mean shift to the starting noise, allowing the model to generate pure blacks (pitch-black nights) and blown-out whites (snowstorms). +- **Auto 16-Channel Detection:** Automatically provisions the correct latent depth for modern architectures (Flux, SD3, Lumina, Z-Image). - **Reproducibility:** Full seed support for consistent noise generation. ### 👁️ Preview Machine - **Privacy-Focused Preview:** Image preview node that hides content by default. -- **Hover to Reveal:** Simply hover over the node to temporarily reveal the image. -- **Auto-Hide:** Image automatically hides again when you move the mouse away. +- **Hover to Reveal / Auto-Hide:** Simply hover over the node to temporarily reveal the image. - **Global Hide Mode:** Optional setting to hide ALL preview nodes in the workflow (Settings → SATA Utility: Global Hide Previews). -- **Save Machine Integration:** The Save Machine node also includes a `hide_preview` toggle (default: hidden) with the same hover-to-reveal behavior. - ## 📦 Installation diff --git a/__init__.py b/__init__.py index 060d28f..e88ffd6 100644 --- a/__init__.py +++ b/__init__.py @@ -1,5 +1,5 @@ # Import all the node classes from the nodes directory -from .nodes.prompt_machine import Prompt_Machine +from .nodes.prompt_machine import Prompt_Machine, Prompt_Style_Machine from .nodes.resolution_machine import Resolution_Machine from .nodes.save_machine import Save_Machine from .nodes.upscale_machine import Upscale_Machine @@ -10,6 +10,7 @@ from .nodes.preview_machine import Preview_Machine # A dictionary that maps class names to class objects NODE_CLASS_MAPPINGS = { "Prompt_Machine": Prompt_Machine, + "Prompt_Style_Machine": Prompt_Style_Machine, "Resolution_Machine": Resolution_Machine, "Save_Machine": Save_Machine, "Upscale_Machine": Upscale_Machine, @@ -21,6 +22,7 @@ NODE_CLASS_MAPPINGS = { # A dictionary that maps display names to class names NODE_DISPLAY_NAME_MAPPINGS = { "Prompt_Machine": "Prompt Machine", + "Prompt_Style_Machine": "Prompt Style Machine", "Resolution_Machine": "Resolution Machine", "Save_Machine": "Save Machine", "Upscale_Machine": "Upscale Machine", diff --git a/asset/resolutions.json b/asset/resolutions.json index ca0c2c3..ae84469 100644 --- a/asset/resolutions.json +++ b/asset/resolutions.json @@ -313,31 +313,51 @@ } }, "models": { - "SD 1.5": [ + "SD 1.5 Family": [ "512", "768" ], - "SDXL_Illustrious_Pony": [ + "SDXL Family (Pony, Juggernaut)": [ "768", "1024" ], - "Flux": [ + "FLUX Family (FLUX.1, FLUX.2)": [ "1024", "1280", "1536" ], + "SD3 Family (SD3, SD3.5)": [ + "1024", + "1280", + "1536" + ], + "Z-Image Family": [ + "1024", + "1280", + "1536" + ], + "Hunyuan-DiT": [ + "1024", + "1280" + ], "Qwen-Image": [ "1024", - "1280", - "1536" + "1280" ], - "Wan2.2": [ - "Video" + "Lumina": [ + "1024", + "1280" ], - "Z-Image Turbo": [ + "Ideogram 4": [ "1024", "1280", "1536" + ], + "AuraFlow": [ + "1024" + ], + "Video Models (Wan, Hunyuan, Mochi, LTX)": [ + "Video" ] } } \ No newline at end of file diff --git a/asset/styles.csv b/asset/styles.csv index 6368d10..d9c681f 100644 --- a/asset/styles.csv +++ b/asset/styles.csv @@ -47,3 +47,309 @@ Velvet,"{prompt} in smooth velvet fabric, gorgeous exquisite cloth, highly detai "Wool, Fine knit","{prompt} in wool cloth, highly detailed, gorgeous, fine knit cashmere wool fabric","anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted", "Wool, Heavy knit","{prompt} in chunky heavy wool yarn, highly detailed, heavy knit large wool fabric, sailor's wool","anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted", "Wool, Cable knit","{prompt} in chunky heavy cable knit wool yarn, highly detailed, cable knit large wool fabric","anime, graphic, text, painting, crayon, graphite, glitch, cartoon, ugly, deformed, noisy, blurry, distorted", +80s Dark Fantasy Photo,"YOUR CONTEXT: +You are a photographer working on the set of a dark fantasy film reminiscent of the 1980s (think *Excalibur*, *Legend*, or *Conan the Barbarian*). +Your photograph showcases {$spicy-content-with} a heavily stylized composition with practical effects, matte paintings, and the dramatic lighting of that era. The image is shot with soft focus. Lighting is theatrical with saturated colors: deep blues, fiery oranges, and sickly greens. Emphasis is placed on creating a sense of scale and grandeur through forced perspective and miniature work. Post-processing involves adding subtle film grain, bloom effects around light sources, and a slight color shift. +YOUR PHOTOGRAPHY: +{prompt}",, +Anime,"YOUR CONTEXT: +You are a Japanese anime artist. +Your Japanese animation includes {$spicy-content-with} an ominous atmosphere, clean lines, and dramatic facial expressions. The artwork uses cool and dark tones along with dramatic lighting and shadows that emphasize the central figure and create a somber, intense mood. +YOUR DRAWING: +{prompt}",, +Casual Photo,"YOUR CONTEXT: +You are an amateur documentary photographer taking low quality photos. +Your photographs exhibit {$spicy-content-with} sharp backgrounds, unpolished realism with natural lighting, and candid friendship‑level moments that feel immediate and authentic. +YOUR PHOTO: +{prompt}",, +Classic Film Photo,"YOUR CONTEXT: +You are a professional cinematographer of iconic classic mid‑century cinema (1950s‑1960s Hollywood golden age). +Your photographs are on 35mm film with grain, displaying {$spicy-content-with} a warm color temperature, and an anamorphic lens that creates imperfections and blurry edges. +YOUR PHOTO: +{prompt}",, +Comic 1,"YOUR CONTEXT: +You are an American artist. +Your illustration has a comic book cover style, with vibrant colors and outlined shapes, featuring {$spicy-content-with} realistic and epic details in the style of modern . +YOUR ILLUSTRATION: +{prompt}",, +Comic 2,"YOUR CONTEXT: +You are an Italian artist. +Your illustration has a European comic book style, featuring {$spicy-content-with} black hand-drawn outlines, solid colors, realistic anatomical proportions, and cross-hatching for shading. +YOUR ILLUSTRATION: +{prompt}",, +Comic 3,"YOUR CONTEXT: +You are a European artist creating fiction worlds. +Your image is a classic ligne claire futurist illustration, featuring {$spicy-content-with} exceptionally clean lines that define each element with the utmost precision. The composition highlights an intense atmosphere with complex technology. The color palette is vibrant, with flat colors and intricate lines and design. +YOUR IMAGE: +{prompt}",, +Comic Book Cover,"YOUR CONTEXT: +You are a celebrated comic book cover artist, renowned for hyper-realistic action and monumental heroics. +Your cover art is a Dynamic Heroic Realism illustration, characterized by {$spicy-content-with} intricate anatomical accuracy, meticulously rendered textures (fabric, metal, skin), and an explosive sense of motion. The composition often features dramatic foreshortening and a heroic, powerful pose for the central figure, surrounded by dynamic environmental destruction or energy effects. Lighting is grand and directional, sculpting forms with sharp highlights and deep shadows, ensuring every detail contributes to an overwhelming sense of epic scale and immediate impact. +YOUR ILLUSTRATION: +{prompt}",, +Cottagecore Pastoral Photo,"YOUR CONTEXT: +You are a lifestyle photographer capturing the ""Cottagecore"" aesthetic, celebrating a nostalgic, rural life. +Your photograph showcases {$spicy-content-with} a soft-focus, romantic composition bathed in warm, hazy afternoon light. The color palette is soft and organic, featuring sage green, dried rose, lavender, and warm ochre. The atmosphere is cozy and rustic, filled with natural textures like linen, dried flowers, and weathered wood in a peaceful countryside setting. +YOUR PHOTOGRAPHY: +{prompt}",, +Cyberpunk Rain Photo,"YOUR CONTEXT: +You are a cinematic photographer specializing in the Neon Noir aesthetic of a futuristic night city. +Your photograph showcases {$spicy-content-with} high-contrast lighting with deep shadows and glowing neon accents. The color palette is dominated by electric cyan, hot magenta, and deep violet. The atmosphere is moody and atmospheric, featuring wet surfaces, rain reflections, and hazy mist. +YOUR PHOTOGRAPH: +{prompt}",, +Digital Cyberpunk,"YOUR CONTEXT: +You are a digital artist working in a dystopian, technologically advanced future. +Your illustration is a Cyberpunk Noir scene, characterized by {$spicy-content-with} harsh, dramatic lighting that casts long, sharp shadows, reflecting the grimy underbelly of a futuristic scene. It employs a limited color palette dominated by deep blues, neon greens, and stark oranges, giving a mysterious aesthetic. +YOUR ILLUSTRATION: +{prompt}",, +Drone PhotoPhoto,"YOUR CONTEXT: +You are an aerial photographer who enjoys using wide-angle drone cams. +Your photographs exhibit {$spicy-content-with} panoramic scenes captured from afar, high up with elevated perspectives and intense colors. +YOUR PHOTO: +{prompt}",, +Electric Blue Outline,"YOUR CONTEXT: +Your image showcases {$spicy-content-with} a digital cyberpunk draw, characterized by vibrant and saturated colors with solid backgrounds and blue outlines. +YOUR IMAGE: +{prompt}",, +Epic Greg,"YOUR CONTEXT: +You are a master digital painter of heroic sagas. +Your illustration is a grand-scale digital oil painting that includes {$spicy-content-with} characterized by masterful, sweeping brushwork and thick impasto textures. It features dramatic, cinematic lighting with intense chiaroscuro, where light sources like embers or magical energy pierce through atmospheric haze, smoke, and swirling clouds. The composition is heroic and classically inspired, using a rich, moody palette of deep earth tones accented by vibrant, glowing highlights for a legendary and immersive feel. +YOUR PAINTING: +{prompt}",, +Flash 90s PhotoPhoto,"YOUR CONTEXT: +You are an amateur photographer, taking photos with point‑and‑shoot cam-quality, in the early 1990s. +Your photographs exhibit {$spicy-content-with} a rough, low-resolution quality, washed-out colors, including a harsh built-in flash, grainy texture, and overexposed highlights, capturing a casual atmosphere that evokes nostalgia. +YOUR PHOTO: +{prompt}",, +Folk-Art Mosaic,"YOUR CONTEXT: +You are a traditional artisan celebrating the vibrant colors and symbolic imagery of folk art. +Your artwork is a Folk Art Mosaic, composed of {$spicy-content-with} small, tessellated pieces of vibrant, solid colors, forming bold patterns and stylized representations of everyday life or mythical creatures. The aesthetic is naive and handcrafted, with a rich texture and a joyful, earthy color palette. +YOUR ARTWORK: +{prompt}",, +Glass Encased Advertising,"YOUR CONTEXT: +An eye-level, realistic photo of a glass-encased advertising poster (mupi) located on a clean city sidewalk. There are subtle reflections of nearby buildings and trees on the glass surface. The lighting is natural overcast daylight. +WHAT'S BEHIND THE REFLECTIVE GLASS: +A faded and sun-faded, low quality printed advertisement, showing: {prompt}",, +Gothic Engraving,"YOUR CONTEXT: +You are an artist inspired by the macabre aesthetics of 19th-century gothic literature and medieval manuscripts. +Your illustration is a Neo-Gothic engraving, executed with intricate linework. It features {$spicy-content-with} dramatic chiaroscuro and a sense of brooding mystery. The color palette is muted, favoring deep indigos, charcoal grays, and touches of crimson, evoking a sense of antique dread. +YOUR ILLUSTRATION: +{prompt}",, +High-Key Fashion PhotoPhoto,"YOUR CONTEXT: +You are a fashion photographer who creates ultra-bright photos for luxury productions. +Your photograph showcases {$spicy-content-with} a high‑key composition shot flooded with soft studio lighting. The exposure is over‑lit with white background, eliminating shadows and delivering a crisp, polished look. Colors are saturated yet controlled (vivid reds, cobalt blues, and crisp whites) while the photo has immaculate details thanks to high‑resolution capture and minimal post‑processing to maintain the airy, glamorous aesthetic. +YOUR PHOTOGRAPH: +{prompt}",, +Ink Draw,"YOUR CONTEXT: +You are an ink drawing artist. +Your illustration is a realistic ink drawing featuring {$spicy-content-with} fine cross-hatching and stippling techniques. These methods create deep shadows and highlights using only black lines. The image includes large areas of white space and Imagine the colors as all shades of gray. +The colors mentioned are actually shades of gray. +YOUR ILLUSTRATION: +{prompt}",, +Ink Frenzy,"YOUR CONTEXT: +You are an underground street-art comic illustrator. +Your image is a high-energy, chaotic ink drawing featuring {$spicy-content-with} aggressive brush strokes, intentional ink splatters, and ""controlled mess"" textures. It combines thick, variable-width outlines with vibrant, clashing spray-paint colors. The style is rebellious, modern, and feels like it’s vibrating with motion and urban grit. +YOUR IMAGE: +{prompt}",, +Kawaii Pop Photo,"YOUR CONTEXT: +You are a fashion photographer specializing in the bright, playful aesthetic of Kawaii Pop. +Your photograph showcases {$spicy-content-with} a high-key composition flooded with vibrant, saturated colors. The exposure is bright and cheerful, with a focus on pastel pinks, electric blues, and sunny yellows. Scene is styled in over-the-top Kawaii fashion with playful poses. The background is simple and colorful, with elements like balloons, confetti, or cartoonish graphics. +YOUR PHOTOGRAPHY: +{prompt}",, +Light Painting Photo,"YOUR CONTEXT: +You are an experimental photographer who enjoys creating light painting images with long exposures and LED sources. +Your photographs exhibit {$spicy-content-with} luminous trails, vibrant colors, and elements that are strikingly illuminated by the long exposure. +YOUR PHOTO: +{prompt}",, +Light and Airy PhotoPhoto,"YOUR CONTEXT: +You are a photographer who loves bright, ethereal, and romantic images. +Your photograph is bathed in soft, diffused light, creating a bright and airy aesthetic. Colors are pastel and delicate, with a focus on whites, creams, and light blues. The image features {$spicy-content-with} a shallow depth of field, blurring the background and drawing attention to the subject. Post-processing emphasizes highlights and reduces shadows, resulting in a clean, luminous look. +YOUR PHOTOGRAPH: +{prompt}",, +LomographyPhoto,"YOUR CONTEXT: +You are an adventurous experimental photographer exploring the world through a lomographic lens. +Your photographs exhibit {$spicy-content-with} film grain, colorful lens flares, soft focus with motion blur, and analog filter effects, trying to capture emotions. +YOUR PHOTO: +{prompt}",, +Low-Poly Render,"YOUR CONTEXT: +Your unity engine render shows {$spicy-content-with} a low-poly 3d platformer level made from low-poly meshes and low-quality textures. +YOUR UNITY ENGINE RENDER: +{prompt}",, +LowRes Pixel Art,"YOUR CONTEXT: +You are a pixel artist. +Your illustration is a low-resolution pixel art featuring {$spicy-content-with} chunky pixels and vibrant intense colors, arcade game style. +YOUR ILLUSTRATION: +{prompt}",, +Manga,"YOUR CONTEXT: +You are a Japanese manga artist. +Your illustration features {$spicy-content-with} black and white manga-style that includes manga screentone patterns with high-contrast black and white drawing, in a violent atmosphere. Always imagine colors as shades of gray. +YOUR ILLUSTRATION: +{prompt}",, +Minimalist Digital Illustration,"YOUR CONTEXT: +You are a cover artist known for powerful, minimalist designs. +Your cover art is a bold graphic iconography piece, featuring {$spicy-content-with} stark, impactful silhouettes and clean, sharp lines that define forms with absolute clarity. The color palette is vibrant, employing large fields of solid color with subtle gradients. Most importantly, the proportions are exaggerated, and the perspective is extremely distorted, creating a sense of vertigo and action. +YOUR ILLUSTRATION: +{prompt}",, +Minimalist PhotoPhoto,"YOUR CONTEXT: +You are a photographer who prefers serene and tranquil images. +Your photographs exhibit {$spicy-content-with} high contrast and clean, minimalist compositions that emphasize empty space to highlight negative space. +YOUR PHOTO: +{prompt}",, +Modern Impressionism,"YOUR CONTEXT: +Your image showcases {$spicy-content-with} a loose and highly expressive brushstroke, characterized by a rich texture created through visible layers of paint and quick, broad strokes reminiscent of modern Impressionism. This technique employing a limited yet impactful color palette with strong contrasts of light and shadow and a focus on warm and cool tones to create a dynamic and energetic feel. +YOUR IMAGE: +{prompt}",, +Modern Ukiyo-e Print,"YOUR CONTEXT: +You are a master of traditional Japanese woodblock printing adapted to a modern narrative. +Your illustration features {$spicy-content-with} bold, flowing outlines and flat areas of color with subtle wood-grain textures. The composition uses dramatic perspective typical of ""Ukiyo-e"" art, with stylized waves, clouds, or smoke, and a color palette of deep indigo, vermillion, and ochre on an aged washi paper background. +YOUR DRAWING: +{prompt}",, +Noir PhotoPhoto,"YOUR CONTEXT: +You are a cinematographer who works in dark movies. +Your photographs exhibit {$spicy-content-with} intense side lighting and gobo‑crafted patterns to sculpt deep, sharply defined shadows, along with a muted Ektachrome palette that evokes film noir. +YOUR PHOTO: +{prompt}",, +Nostalgic 90s Photo,"YOUR CONTEXT: +You are a street photographer capturing the raw, rebellious energy of the 1990s underground scene. +Your photograph showcases {$spicy-content-with} a low-fidelity, grainy analog film aesthetic. The exposure features harsh camera flash, slight motion blur, and light leaks. The colors are slightly de-saturated with warm skin tones and heavy shadows, giving it a candid, ""snapshot"" feel. +YOUR PHOTOGRAPHY: +{prompt}",, +Orthochromatic Spirit PhotoPhoto,"YOUR CONTEXT: +You are a photographer inspired by the look of early photographic processes, specifically orthochromatic and colorized films. +Your photograph showcases {$spicy-content-with} a scene with a high-contrast, blue-sensitive aesthetic. Reds appear very dark, while blues and greens are bright. The image is characterized by a stark, graphic quality. Post-processing focuses on enhancing the contrast and emphasizing the tonal separation. +YOUR PHOTOGRAPH: +{prompt}",, +Paper-Cut Diorama,"YOUR CONTEXT: +You are a conceptual artist creating a tactile, handcrafted world. +Your illustration is a digital ""paper-cut"" style including {$spicy-content-with} distinct layered shapes with subtle drop shadows to create depth. Each element looks like it was cut from different textures of colored construction paper. The style is geometric, clean, and vibrant, making the comic look like a 3D physical diorama. +YOUR IMAGE: +{prompt}",, +Phone Photo,"YOUR CONTEXT: +Your photographs has android phone cam-quality. +Your photographs exhibit {$spicy-content-with} surprising compositions, sharp complex backgrounds, natural lighting, and candid moments that feel immediate and authentic. +Your photographs are actual gritty candid photographic background. +YOUR PHOTO: +{prompt}",, +Pop PhotoPhoto,"YOUR CONTEXT: +You are a contemporary pop artist known for your vibrant, oversaturated palettes and energetic, graphic compositions. +Your photographs exhibit {$spicy-content-with} exaggerated colors and solid backgrounds using Ben-Day dots, achieving an unconventional and clearly pop art effect. +YOUR PHOTO: +{prompt}",, +Pop-Art,"YOUR CONTEXT: +You are a bold artist. +Your illustration features {$spicy-content-with} Pop Art style, characterized by bold vibrant colors. The colors are executed using the pointillist technique, where dots of color come together to create the overall picture. Dazzling patterns and comic style characters. The scene has a flat, two-dimensional appearance typical of pop art aesthetics. +YOUR ILLUSTRATION: +{prompt}",, +Portra Film PhotoPhoto,"YOUR CONTEXT: +You are a photographer who appreciates the classic aesthetic of Kodak Portra film. +Your photograph emulates that look, known for its soft colors, fine grain, and natural skin tones. The image features {$spicy-content-with} a subtle warmth, with a focus on accurate color rendition and a gentle, diffused glow. Highlights are smooth and creamy, while shadows retain detail. Minimal post-processing is applied to preserve the organic, film-like quality. +YOUR PHOTOGRAPH: +{prompt}",, +Post-Processed Artistry,"YOUR CONTEXT: +You are a digital artist. +Your image is a masterpiece featuring {$spicy-content-with} heavy chromatic aberration, and digital intense color. The aesthetic is ultra-modern and fragmented, using a high-contrast palette of neon purples and cyans, creating a sense of a digital reality. +YOUR IMAGE: +{prompt}",, +Production PhotoPhoto,"YOUR CONTEXT: +You are a Hollywood filmmaker making a high-budget film. +Your photographs exhibit {$spicy-content-with} atmospheric composition that emphasize intense emotions using selective focus, and warm and cold colors with high quality studio lighting. +YOUR PHOTO: +{prompt}",, +Quiet Luxury Photo,"YOUR CONTEXT: +You are an editorial fashion photographer specializing in the Old Money aesthetic. +Your photograph showcases {$spicy-content-with} a sophisticated, clean composition using natural, soft morning light. The color palette is muted and neutral, focusing on beige, cream, navy blue, and forest green. The scene feels expensive and timeless, set in a luxury location. +YOUR PHOTOGRAPH: +{prompt}",, +Retro 80s ,"YOUR CONTEXT: +Your illustration is made with brightly colored bold lines and touches of cracked paint, while having {$spicy-content-with} a 1980s graphic design aesthetic. +YOUR ILLUSTRATION: +{prompt}",, +Risograph Print,"YOUR CONTEXT: +You are an underground indie press printer. +Your illustration is a tactile Risograph-style print, featuring {$spicy-content-with} vibrant fluorescent ink and a heavy grainy texture. It uses a punchy palette of neon pink, intense yellow, and deep teal. +YOUR MULTI SUBJECT PRINT: +{prompt}",, +Shadow Fantasy Illustration,"YOUR CONTEXT: +You are a dark fantasy comic book artist. +Your drawing is a high-contrast illustration characterized by {$spicy-content-with} massive areas of solid black ink and minimal white highlights. Inspired by ""Mignola-esque"" aesthetics, it uses chunky, angular silhouettes and geometric shadows to define form rather than internal lines. The atmosphere is ancient, heavy, and supernatural. +YOUR DRAWING: +{prompt}",, +Simple 3D Render,"YOUR CONTEXT: +You are a 3d artist. +Your render is a high-gloss, illustration featuring {$spicy-content-with} ultra-reflective surfaces. The style is dark with dark backgrounds and iridescent highlights. +YOUR RENDER: +{prompt}",, +Spotlight Stage Photo,"YOUR CONTEXT: +You are a professional stage and theater photographer capturing dramatic performances. +Your photographs exhibit {$spicy-content-with} a single, high-intensity overhead spotlight that creates a sharp circular pool of light, dramatic chiaroscuro contrast that isolates the subject against a pitch-black background, and visible volumetric light beams with subtle dust motes for an authentic theatrical atmosphere. +YOUR PHOTO: +{prompt}",, +Street Documentary PhotoPhoto,"YOUR CONTEXT: +You are a documentary photographer chronicling urban life. +Your photograph showcases {$spicy-content-with} candid moments in natural street environments. The image employs a natural color balance, modest contrast, and a shallow depth of field to isolate subjects within bustling scenes. Emphasis is placed on authentic emotion, body language, and the interplay of ambient city light (neon signs, street lamps). Minimal post‑processing preserves the raw, honest feel—only slight exposure adjustments and noise reduction are applied to maintain the immediacy of the moment. +YOUR PHOTOGRAPH: +{prompt}",, +Studio Anime,"YOUR CONTEXT: +You are a Japanese anime artist. +Your Japanese animation includes {$spicy-content-with} warm artwork inspired by Studio Ghibli, with detailed characters and a whimsical aesthetic, full of emotion. +YOUR DRAWING: +{prompt}",, +Synthwave Photo,"YOUR CONTEXT: +You are a photographer inspired by the retro-futuristic visuals of the 1980s. +Your photograph showcases {$spicy-content-with} Synthwave aesthetic, characterized by neon colors and geometric shapes, it includes vibrant pinks, purples, and blues, with a glowing effect. Grids, scanlines, and a retro-futuristic atmosphere. The photograph feels like a scene from a classic 80s sci-fi film. +Real people against a computer-generated background. +YOUR PHOTOGRAPH: +{prompt}",, +Teal and Orange PhotoPhoto,"YOUR CONTEXT: +You are a color specialist photographer. +Your photograph showcases {$spicy-content-with} a satured teal and orange colors. The image features cool teal tones in the shadows and highlights, balanced by warm orange tones in the midtones and skin tones, creating a vibrant photography with high contrast and vivid colors. +YOUR PHOTOGRAPH: +{prompt}",, +Tilt Shift / Toy PhotoPhoto,"YOUR CONTEXT: +You are a photographer who enjoys using tilt‑shift lenses to turn real‑world scenes into miniature toy models. +Your photographs exhibit {$spicy-content-with} crisp, high‑resolution details captured in an exaggerated perspective, with a very narrow focus plane, and a dreamy, toy‑like atmosphere that creates the classic “miniature” illusion. +YOUR PHOTO: +{prompt}",, +Unsettling,"YOUR CONTEXT: +You are an illustrator of dark and disturbing themes. +Your illustration includes {$spicy-content-with} stippling textures, dramatic raking side lighting, and a dread-filled atmosphere that is oppressive and claustrophobic. High contrast emphasizes form and shadow, employing cinematic chiaroscuro for depth. +YOUR DRAWING: +{prompt}",, +Vintage Comic,"YOUR CONTEXT: +You are a 1950s artist. +Your illustration in a worn sheet of paper features {$spicy-content-with} a vintage 1950s comic book style made with hand-drawn lines and painted in solid white, red, blue, and black. +YOUR ILLUSTRATION: +{prompt}",, +Vintage Illustration,"YOUR CONTEXT: +You are a sci-fi pulp magazine illustrator from the 1940s. +Your drawing is a lush, painted-style comic cover including {$spicy-content-with} dramatic, heroic lighting and a vivid ""Technicolor"" palette of saturated oranges, deep teals, and bright yellows. It features soft airbrushed textures, muscular anatomy, and a grainy film-stock finish that evokes a sense of retro-futuristic adventure. +YOUR DRAWING: +{prompt}",, +Vintage PhotoPhoto,"YOUR CONTEXT: +You are an 80s photographer who enjoys informal shots. +Your worn vintage photographs exhibit {$spicy-content-with} a minimalist, amateurish composition, warm desaturated tones and a soft focus that creates a cozy atmosphere. +YOUR PHOTO: +{prompt}",, +Vintage VGA Monitor,"YOUR CONTEXT: +You have an old VGA CRT monitor sitting on a wooden desk, and a window is reflected on its screen. The monitor's casing has a raised black inscription in the upper left corner that reads ""VGA"". The screen has a slight curve and sweep lines typical of CRT displays. +WHAT'S INSIDE YOUR MONITOR'S SCREEN: +A low-resolution, 2D pixel art platform videogame form early 1990s, featuring: {prompt}",, +Vintage Polaroid Photo,"YOUR CONTEXT: +You are a nostalgic photographer who embraces instant film’s tactile charm. +Your Polaroid photograph is using vintage‑style film. The photograph features the iconic square format including {$spicy-content-with} a soft focus, a subtle vignetting, and the characteristic pastel color shift of instant film. Showing candid moments or intimate still‑lifes. The photograph retains the authentic border, emulsion texture, and occasional “film‑like” imperfections (light leaks, scratches). +YOUR PHOTOGRAPH: +{prompt}",, +Whimsical Watercolor,"YOUR CONTEXT: +You are an illustrator specializing in gentle, enchanting narratives for children's books. +Your drawing is a whimsical watercolor image, featuring {$spicy-content-with} loose, fluid watercolor washes, delicate linework, and playful, anthropomorphic elements within a lush natural setting. The style is soft and inviting, with a vibrant yet gentle color palette that captures the magic of innocence. +YOUR DRAWING: +{prompt}",, +Wide Angle / Peephole,"YOUR CONTEXT: +Your peephole shows {$spicy-content-with} a shot from a high angle, inside a fisheye lens distortion glass circle, highly distorted with a wide angle. +YOUR PEEPHOLE: +{prompt}",, +Witchcore Photo,"YOUR CONTEXT: +You are a photographer drawn to the mystical and earthy aesthetic of Witchcore. +Your photograph showcases {$spicy-content-with} a moody, atmospheric scene steeped in natural elements. The image employs a natural color balance, with a focus on deep greens, browns, and blacks. Emphasis is placed on textures: moss, wood, crystals, and flowing fabrics. The photograph has a raw, organic feel, with mystical ambiance. +YOUR PHOTOGRAPHY: +{prompt}",, diff --git a/js/prompt_autocomplete.js b/js/prompt_autocomplete.js index e99bef9..6f38e2a 100644 --- a/js/prompt_autocomplete.js +++ b/js/prompt_autocomplete.js @@ -11,6 +11,31 @@ function getSettingValue(id, defaultValue) { return defaultValue; } +function fuzzyMatch(text, query) { + const textLower = text.toLowerCase(); + const queryLower = query.toLowerCase(); + if (!queryLower) return { match: true, score: 0 }; + + let queryIdx = 0; + let score = 0; + let lastMatchIdx = -1; + + for (let i = 0; i < textLower.length; i++) { + if (textLower[i] === queryLower[queryIdx]) { + if (lastMatchIdx !== -1) { + score += (i - lastMatchIdx - 1); + } + lastMatchIdx = i; + queryIdx++; + if (queryIdx === queryLower.length) { + score += (textLower.length - i - 1) * 0.1; + return { match: true, score: score }; + } + } + } + return { match: false, score: Infinity }; +} + // Register extension app.registerExtension({ name: "SATA_UtilityNode.PromptAutocomplete", @@ -31,7 +56,7 @@ app.registerExtension({ ], async initAutocompleteForNode(node) { - const isTargetNode = node.comfyClass === "PromptAutocomplete"; + const isTargetNode = node.comfyClass === "PromptAutocomplete" || node.comfyClass === "Prompt_Machine"; const isGlobal = getSettingValue("SATA_UtilityNode.PromptAutocomplete.Global", false); if (!isTargetNode && !isGlobal) return; @@ -308,14 +333,22 @@ app.registerExtension({ updateCategories(query) { const files = Object.keys(this.snippetsCache || {}); - // Filter by query - // We display stripped names (no extension) - this.filteredItems = files.map(f => ({ - type: "category", - value: f, - display: f.replace(/\.(csv|json)$/, "") - })).filter(item => item.display.toLowerCase().includes(query.toLowerCase())); + const matches = []; + files.forEach(f => { + const display = f.replace(/\.(csv|json)$/, ""); + const res = fuzzyMatch(display, query); + if (res.match) { + matches.push({ + type: "category", + value: f, + display: display, + score: res.score + }); + } + }); + matches.sort((a, b) => a.score - b.score); + this.filteredItems = matches; this.selectedIndex = 0; this.renderPopup(); }, @@ -324,13 +357,20 @@ app.registerExtension({ if (!this.currentCategory) return; const items = this.snippetsCache[this.currentCategory] || []; - // Filter - let matches = items.filter(i => i.toLowerCase().includes(query.toLowerCase())); + const matches = []; + items.forEach(i => { + const res = fuzzyMatch(i, query); + if (res.match) { + matches.push({ value: i, score: res.score }); + } + }); + matches.sort((a, b) => a.score - b.score); + const sortedItems = matches.map(m => m.value).slice(0, 100); // Add "Random" option at top this.filteredItems = [ { type: "random", value: "RANDOM", display: "🎲 Random" }, - ...matches.map(i => ({ type: "item", value: i, display: i })) + ...sortedItems.map(i => ({ type: "item", value: i, display: i })) ]; this.selectedIndex = 0; diff --git a/js/prompt_machine.js b/js/prompt_machine.js index c168491..b430333 100644 --- a/js/prompt_machine.js +++ b/js/prompt_machine.js @@ -4,7 +4,7 @@ import { app } from "../../../scripts/app.js"; app.registerExtension({ name: "SATA_UtilityNode.PromptMachineFrontend", async nodeCreated(node) { - if (node.comfyClass !== "Prompt_Machine") return; + if (node.comfyClass !== "Prompt_Style_Machine") return; // Widgets const csvWidget = node.widgets.find(w => w.name === "csv_file"); @@ -76,11 +76,11 @@ app.registerExtension({ padding: "8px", background: "rgba(0,0,0,0.4)", borderRadius: "4px", - fontSize: "12px", - lineHeight: "1.4", + fontSize: "14px", + lineHeight: "1.5", color: "#ddd", wordBreak: "break-word", - maxHeight: "300px", + maxHeight: "400px", overflowY: "auto" }); @@ -97,9 +97,7 @@ app.registerExtension({ const resp = await fetch(`/sata/prompt_machine/get?csv=${encodeURIComponent(csv)}&name=${encodeURIComponent(name)}`); const data = await resp.json(); let html = ""; - if (data.positive) html += `Positive: ${data.positive}

`; - if (data.negative) html += `Negative: ${data.negative}

`; - if (data.note) html += `Note: ${data.note}`; + if (data.positive) html += `${data.positive}`; previewEl.innerHTML = html || "No preview available"; diff --git a/js/resolution_machine.js b/js/resolution_machine.js index 74c9bfa..8b10ea6 100644 --- a/js/resolution_machine.js +++ b/js/resolution_machine.js @@ -4,14 +4,14 @@ app.registerExtension({ name: "SATA_UtilityNode.Resolution_Machine", async nodeCreated(node) { - if (node.comfyClass !== "Resolution_Machine") return; + if (node.comfyClass !== "Resolution_Machine" && node.comfyClass !== "Latent_Machine") return; // Find widgets const modelWidget = node.widgets.find(w => w.name === "model"); const dimensionWidget = node.widgets.find(w => w.name === "dimension"); const resolutionWidget = node.widgets.find(w => w.name === "resolution"); - const widthWidget = node.widgets.find(w => w.name === "custom_width"); - const heightWidget = node.widgets.find(w => w.name === "custom_height"); + const widthWidget = node.widgets.find(w => w.name === "width") || node.widgets.find(w => w.name === "custom_width"); + const heightWidget = node.widgets.find(w => w.name === "height") || node.widgets.find(w => w.name === "custom_height"); if (!modelWidget || !dimensionWidget || !resolutionWidget || !widthWidget || !heightWidget) { console.warn("[Resolution_Machine] Missing widgets, check backend definition."); @@ -51,20 +51,11 @@ app.registerExtension({ }); } - // Always add Custom? Or only via button? The plan implies button adds it or it is a fallback. - // But if user wants to select custom again after selecting a preset? - // "Custom" option should probably be always available or injected. - // Let's keep it clean: presets only. Button forces custom mode. - // BUT if we want to get out of custom mode, we select a preset. - // If we are in custom mode, "Custom" should be in the list? - - if (available.length === 0) { + // Always include "Custom" option so that switching model/dimension dropdowns doesn't kick the user out of custom mode + if (!available.includes("Custom")) { available.push("Custom"); } - // Preserve "Custom" if it was already there/selected? - // Better to re-build list cleanly. - resolutionWidget.options.values = available; // Pick safe default if current value is not valid @@ -164,6 +155,7 @@ app.registerExtension({ } resolutionWidget.value = "Custom"; + checkCustomMode(); app.graph.change(); }); diff --git a/js/touchpad_pan_zoom.js b/js/touchpad_pan_zoom.js index b788b6a..e508bdb 100644 --- a/js/touchpad_pan_zoom.js +++ b/js/touchpad_pan_zoom.js @@ -1,6 +1,6 @@ import { app } from "../../../scripts/app.js"; -const SETTING_ID = "SATA.EnableTouchpadPanZoom"; +const SETTING_ID = "SATA_UtilityNode.EnableTouchpadPanZoom"; app.registerExtension({ name: "SATA_UtilityNode.TouchpadPanZoom", diff --git a/nodes/latent_machine.py b/nodes/latent_machine.py index 8e50305..058a9cc 100644 --- a/nodes/latent_machine.py +++ b/nodes/latent_machine.py @@ -1,44 +1,70 @@ +import json +import os import torch import torch.fft import random import comfy.model_management +NODE_NAME = "Latent_Machine" + +# Path to JSON config (root of SATA_UtilityNode folder) +CONFIG_PATH = os.path.join( + os.path.dirname(os.path.dirname(__file__)), "asset", "resolutions.json" +) + +def load_config(): + if not os.path.exists(CONFIG_PATH): + raise FileNotFoundError(f"[{NODE_NAME}] resolutions.json not found at {CONFIG_PATH}") + with open(CONFIG_PATH, "r", encoding="utf-8-sig") as f: + return json.load(f) + + class Latent_Machine: """ Creates an empty latent initialized with Power-Law (1/f) noise, Perlin-like noise, or Plasma noise. Supports 4-channel (SD1.5/SDXL) and 16-channel (Flux/SD3) latents. + Supports recommended model resolutions out of the box. """ def __init__(self): pass @classmethod - def INPUT_TYPES(s): + def INPUT_TYPES(cls): + config = load_config() + + # all models + models = list(config["models"].keys()) + default_model = models[0] if models else "Unknown" + + # Collect ALL possible resolutions to pass validation + all_resolutions = set(["Custom"]) + if "resolutions" in config: + for bucket in config["resolutions"].values(): + for dim in bucket.values(): + all_resolutions.update(dim.keys()) + + resolutions_list = sorted(list(all_resolutions)) + return { "required": { + "model": (models, {"default": default_model}), + "dimension": (["Square", "Portrait", "Landscape"],), + "resolution": (resolutions_list,), "width": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}), "height": ("INT", {"default": 512, "min": 64, "max": 8192, "step": 8}), "batch_size": ("INT", {"default": 1, "min": 1, "max": 64}), - "model_type": ([ - "SD1.5", - "SDXL", - "PicsArt", - "Kolors", - "Auraflow", - "Flux", - "Qwen", - "SD3", - "Lumina" - ],), "noise_type": ([ "Gaussian (White): Sharp Architecture, Text, intricate mechanics", "Pink (1/f): Photorealism, Portraits, Nature", "Brown (1/f²): Anime, Digital Art, Bokeh/Backgrounds", + "Blue (High-Frequency): High-Detail, Textures, low-step/distilled models", "Perlin : Fantasy landscapes, Fluids, Hair/Fabric", "Plasma : Sci-Fi, Abstract, Alien terrain" ],), "intensity": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.1}), "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "high_contrast": ("BOOLEAN", {"default": False}), } } @@ -47,44 +73,93 @@ class Latent_Machine: FUNCTION = "generate_noise" CATEGORY = "SATA_UtilityNode" - def generate_noise(self, width, height, batch_size, model_type, noise_type, intensity, seed): + def get_resolution_dimensions(self, model, dimension, resolution, width, height): + try: + config = load_config() + except Exception: + return (width, height) + + if model not in config.get("models", {}): + return (width, height) + + if resolution == "Custom": + return (width, height) + + buckets = config["models"][model] + + resolution_data = None + for bucket in buckets: + if bucket in config.get("resolutions", {}): + if dimension in config["resolutions"][bucket]: + if resolution in config["resolutions"][bucket][dimension]: + resolution_data = config["resolutions"][bucket][dimension][resolution] + break + + if not resolution_data: + return (width, height) + + return (resolution_data["width"], resolution_data["height"]) + + def generate_noise(self, model="SD 1.5", dimension="Square", resolution="Custom", width=512, height=512, batch_size=1, noise_type="Gaussian (White): Sharp Architecture, Text, intricate mechanics", intensity=1.0, seed=0, high_contrast=False): + # Check if this is an old node layout (shifted arguments due to ComfyUI's positional widget serialization) + if isinstance(model, int): + actual_width = model + actual_height = dimension + actual_batch_size = resolution + actual_model = width # The 4th argument 'width' holds the old 'model_type' (str) + actual_noise_type = height + actual_intensity = batch_size + actual_seed = noise_type + actual_high_contrast = False + else: + actual_model = model + actual_width, actual_height = self.get_resolution_dimensions(model, dimension, resolution, width, height) + actual_batch_size = batch_size + actual_noise_type = noise_type + actual_intensity = intensity + actual_seed = seed + actual_high_contrast = high_contrast + self.device = comfy.model_management.get_torch_device() # Set seed for reproducibility - torch.manual_seed(seed) - random.seed(seed) + torch.manual_seed(actual_seed) + random.seed(actual_seed) - # Determine channels based on model type - sixteen_channel_models = ["Flux", "Qwen", "SD3", "Lumina"] - - if model_type in sixteen_channel_models: + # Determine channels based on model + model_lower = actual_model.lower() + if any(k in model_lower for k in ["flux", "qwen", "sd3", "lumina", "wan", "auraflow", "z-image", "mochi", "ltx", "hunyuanvideo"]): c = 16 else: c = 4 # Latent dimensions (compressed by 8) - h = height // 8 - w = width // 8 + h = actual_height // 8 + w = actual_width // 8 # Generate base noise - if "Gaussian" in noise_type: - noise = torch.randn((batch_size, c, h, w), device=self.device) + if "Gaussian" in actual_noise_type: + noise = torch.randn((actual_batch_size, c, h, w), device=self.device) - elif "Perlin" in noise_type: + elif "Perlin" in actual_noise_type: # Multi-Octave Value Noise Approximation - noise = self.generate_perlin_approx(batch_size, c, h, w) + noise = self.generate_perlin_approx(actual_batch_size, c, h, w) + + elif "Blue" in actual_noise_type: + # FFT-based Blue Noise (High-Frequency) + noise = self.generate_blue_noise(actual_batch_size, c, h, w) else: # FFT-based Power Law Noise (Pink, Brown, Plasma) - if "Pink" in noise_type: + if "Pink" in actual_noise_type: alpha = 1.0 - elif "Brown" in noise_type: + elif "Brown" in actual_noise_type: alpha = 2.0 - elif "Plasma" in noise_type: + elif "Plasma" in actual_noise_type: alpha = 3.0 # Very smooth else: alpha = 0.0 # Fallback to white - noise = self.generate_power_law_noise(batch_size, c, h, w, alpha) + noise = self.generate_power_law_noise(actual_batch_size, c, h, w, alpha) # Normalize standard deviation to match expected latent variance (approx 1.0 for standard Gaussian) # This ensures intensity works consistently across different noise types @@ -92,15 +167,19 @@ class Latent_Machine: if current_std > 1e-6: noise = noise / current_std + # Apply offset noise for high contrast + if actual_high_contrast: + noise = noise + 0.1 + # Apply intensity - noise = noise * intensity + noise = noise * actual_intensity # Prepare Image output for visualization/masking # Slice the first 3 channels and normalize to [0, 1] img_tensor = noise[:, :3, :, :] if img_tensor.shape[1] < 3: # If for some weird reason it's less than 3 channels, pad it - padding = torch.zeros((batch_size, 3 - img_tensor.shape[1], h, w), device=self.device) + padding = torch.zeros((actual_batch_size, 3 - img_tensor.shape[1], h, w), device=self.device) img_tensor = torch.cat([img_tensor, padding], dim=1) # Normalize min-max @@ -177,3 +256,31 @@ class Latent_Machine: noise += torch.randn((batch_size, c, h, w), device=self.device) * 0.05 return noise + + def generate_blue_noise(self, batch_size, c, h, w, beta=1.5): + # Generate White Noise (Standard Gaussian) + white_noise = torch.randn((batch_size, c, h, w), device=self.device) + + # Convert to Frequency Domain (FFT) + fft_noise = torch.fft.fft2(white_noise) + + # Create Frequency Grid + y = torch.fft.fftfreq(h, device=self.device) + x = torch.fft.fftfreq(w, device=self.device) + dy, dx = torch.meshgrid(y, x, indexing='ij') + + # Calculate distance from center (frequency magnitude) + frequency_magnitude = torch.sqrt(dy**2 + dx**2) + + # Apply High-Pass Scaling: Amplitude ~ f^(beta/2) + scale = frequency_magnitude ** (beta / 2.0) + + # Zero out DC component + scale[0, 0] = 0 + + fft_structured = fft_noise * scale + + # Convert back to Spatial Domain (Inverse FFT) + blue_noise = torch.fft.ifft2(fft_structured).real + + return blue_noise diff --git a/nodes/prompt_machine.py b/nodes/prompt_machine.py index 6bc2a41..6989b03 100644 --- a/nodes/prompt_machine.py +++ b/nodes/prompt_machine.py @@ -87,7 +87,7 @@ def read_prompt_row(csv_file, name): return ("", "", "") -class Prompt_Machine: +class Prompt_Style_Machine: """ NOTE: name is a free STRING (not a static dropdown) to avoid 'Value not in list' validation errors. The frontend helper will still provide dropdown-like selection and write the chosen name into this STRING. @@ -113,8 +113,8 @@ class Prompt_Machine: "optional": {} } - RETURN_TYPES = ("STRING", "STRING") - RETURN_NAMES = ("positive", "negative") + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("prompt",) FUNCTION = "get_prompts" CATEGORY = "SATA_UtilityNode" @@ -122,7 +122,52 @@ class Prompt_Machine: pos, neg, note = read_prompt_row(csv_file, name) - return (pos, neg) + return (pos,) + + +class Prompt_Machine: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "subject": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}), + "style": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}), + "lighting": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}), + "composition": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}), + "mood": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}), + "technical": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": True}), + "json_output": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("prompt",) + FUNCTION = "get_prompt" + CATEGORY = "SATA_UtilityNode" + + def get_prompt(self, subject, style, lighting, composition, mood, technical, json_output): + if json_output: + import json + data = { + "subject": subject.strip(), + "style": style.strip(), + "lighting": lighting.strip(), + "composition": composition.strip(), + "mood": mood.strip(), + "technical": technical.strip() + } + # Remove empty fields to keep JSON clean + data = {k: v for k, v in data.items() if v} + return (json.dumps(data, indent=2),) + else: + parts = [] + if subject.strip(): parts.append(subject.strip()) + if style.strip(): parts.append(style.strip()) + if lighting.strip(): parts.append(lighting.strip()) + if composition.strip(): parts.append(composition.strip()) + if mood.strip(): parts.append(mood.strip()) + if technical.strip(): parts.append(technical.strip()) + return (", ".join(parts),) # ---------------- REST API ---------------- diff --git a/nodes/resolution_machine.py b/nodes/resolution_machine.py index 1913560..7071afd 100644 --- a/nodes/resolution_machine.py +++ b/nodes/resolution_machine.py @@ -44,6 +44,7 @@ class Resolution_Machine: "resolution": (resolutions_list,), "custom_width": ("INT", {"default": 512, "min": 1, "max": 8192}), "custom_height": ("INT", {"default": 512, "min": 1, "max": 8192}), + "megapixel": (["None", "0.5MP", "1.0MP", "1.5MP", "2.0MP", "4.0MP"], {"default": "None"}), }, } @@ -52,37 +53,55 @@ class Resolution_Machine: FUNCTION = "get_resolution" CATEGORY = "SATA_UtilityNode" - def get_resolution(self, model, dimension, resolution, custom_width, custom_height): + def get_resolution(self, model, dimension, resolution, custom_width, custom_height, megapixel="None"): config = load_config() if model not in config["models"]: raise ValueError(f"[{NODE_NAME}] Unknown model: {model}") if resolution == "Custom": - return (custom_width, custom_height) + w, h = custom_width, custom_height + else: + # Search for resolution string in the specific bucket/dimension + # Iterate over tiers to find the bucket that has this model? + # Actually config["models"][model] gives list of buckets. + buckets = config["models"][model] + + resolution_data = None + for bucket in buckets: + # Check if this bucket has the selected dimension + if bucket in config["resolutions"]: + if dimension in config["resolutions"][bucket]: + if resolution in config["resolutions"][bucket][dimension]: + resolution_data = config["resolutions"][bucket][dimension][resolution] + break + + if not resolution_data: + # Fallback: maybe the user switched model and the resolution is now invalid, or it is custom manual + # If obscure mismatch, default to custom values provided + print(f"[{NODE_NAME}] Warning: resolution '{resolution}' not found for model '{model}' dim '{dimension}', using Custom") + w, h = custom_width, custom_height + else: + w, h = resolution_data["width"], resolution_data["height"] - # Search for resolution string in the specific bucket/dimension - # Iterate over tiers to find the bucket that has this model? - # Actually config["models"][model] gives list of buckets. - buckets = config["models"][model] - - resolution_data = None - for bucket in buckets: - # Check if this bucket has the selected dimension - if bucket in config["resolutions"]: - if dimension in config["resolutions"][bucket]: - if resolution in config["resolutions"][bucket][dimension]: - resolution_data = config["resolutions"][bucket][dimension][resolution] - break - - if not resolution_data: - # Fallback: maybe the user switched model and the resolution is now invalid, or it is custom manual - # If "Custom" was passed, we handled it above. - # If obscure mismatch, default to custom values provided - print(f"[{NODE_NAME}] Warning: resolution '{resolution}' not found for model '{model}' dim '{dimension}', using Custom") - return (custom_width, custom_height) + if megapixel != "None": + mp_map = { + "0.5MP": 500000, + "1.0MP": 1000000, + "1.5MP": 1500000, + "2.0MP": 2000000, + "4.0MP": 4000000, + } + if megapixel in mp_map: + P = mp_map[megapixel] + R = float(w) / float(h) + import math + w_new = int(round(math.sqrt(P * R) / 8.0) * 8.0) + h_new = int(round(math.sqrt(P / R) / 8.0) * 8.0) + w = max(8, w_new) + h = max(8, h_new) - return (resolution_data["width"], resolution_data["height"]) + return (w, h) # --- REST endpoint for frontend config fetch --- diff --git a/nodes/save_machine.py b/nodes/save_machine.py index 264b9a9..d76367e 100644 --- a/nodes/save_machine.py +++ b/nodes/save_machine.py @@ -135,12 +135,6 @@ class Save_Machine: "path_and_filename": ("STRING", {"default": "%time", "tooltip": "Supports %date, %time, and custom prompt %placeholders%"}), "extension": ((['png', 'jpeg', 'webp']),), }, - "optional": { - "custom_string": ("STRING", {"default": ''}), - "lossless_webp": ("BOOLEAN", {"default": True}), - "quality_jpeg_or_webp": ("INT", {"default": 100, "min": 1, "max": 100}), - "hide_preview": ("BOOLEAN", {"default": True, "label_on": "Hidden", "label_off": "Visible"}), - }, "hidden": { "prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO" @@ -153,8 +147,7 @@ class Save_Machine: OUTPUT_NODE = True CATEGORY = "SATA_UtilityNode" - def save_files(self, images, path_and_filename, extension, custom_string=None, - quality_jpeg_or_webp=100, lossless_webp=True, hide_preview=True, prompt=None, extra_pnginfo=None): + def save_files(self, images, path_and_filename, extension, prompt=None, extra_pnginfo=None): # Resolve placeholders first (uses prompt) try: resolved = resolve_placeholders(str(path_and_filename), prompt, extra_pnginfo) @@ -180,8 +173,6 @@ class Save_Machine: os.makedirs(output_path, exist_ok=True) comment_parts = [] - if custom_string: - comment_parts.append(handle_whitespace(custom_string)) if prompt is not None: try: prompt_text = json.dumps(prompt) @@ -199,8 +190,9 @@ class Save_Machine: # Best-effort: if resolution fails, continue with original templates pass + # We always use quality=100 and lossless_webp=True filenames = self.save_images(images, output_path, filename, comment, extension, - quality_jpeg_or_webp, lossless_webp, prompt, extra_pnginfo) + 100, True, prompt, extra_pnginfo) subfolder = os.path.normpath(path) ui_images = [ {"filename": fn, "subfolder": subfolder if subfolder != '.' else '', "type": 'output'} for fn in filenames ] diff --git a/nodes/upscale_machine.py b/nodes/upscale_machine.py index 07bf417..4d9d71a 100644 --- a/nodes/upscale_machine.py +++ b/nodes/upscale_machine.py @@ -8,6 +8,40 @@ from comfy import model_management from spandrel import ModelLoader +def generate_blue_noise(batch_size, c, h, w, device, beta=1.5): + # Generate White Noise (Standard Gaussian) + white_noise = torch.randn((batch_size, c, h, w), device=device) + + # Convert to Frequency Domain (FFT) + fft_noise = torch.fft.fft2(white_noise) + + # Create Frequency Grid + y = torch.fft.fftfreq(h, device=device) + x = torch.fft.fftfreq(w, device=device) + dy, dx = torch.meshgrid(y, x, indexing='ij') + + # Calculate distance from center (frequency magnitude) + frequency_magnitude = torch.sqrt(dy**2 + dx**2) + + # Apply High-Pass Scaling: Amplitude ~ f^(beta/2) + scale = frequency_magnitude ** (beta / 2.0) + + # Zero out DC component + scale[0, 0] = 0 + + fft_structured = fft_noise * scale + + # Convert back to Spatial Domain (Inverse FFT) + blue_noise = torch.fft.ifft2(fft_structured).real + + # Normalize standard deviation to ~1.0 + std = blue_noise.std() + if std > 1e-6: + blue_noise = blue_noise / std + + return blue_noise + + def resize_tensor_opencv(tensor_chw, target_width, target_height, supersample='true', factor=2.0): """ CPU-based resizing using OpenCV for 'rescale' mode. @@ -55,8 +89,7 @@ class Upscale_Machine: "upscale_model": (folder_paths.get_filename_list("upscale_models"), {"default": None}), "chained_model": (["None"] + folder_paths.get_filename_list("upscale_models"), {"default": "None"}), "rescale_factor": ("FLOAT", {"default": 2.0, "min": 0.01, "max": 16.0, "step": 0.01}), - "supersample": (["true", "false"],), - "rounding_modulus": ("INT", {"default": 8, "min": 1, "max": 1024, "step": 1}), + "frequency_split": ("BOOLEAN", {"default": True}), } } @@ -135,7 +168,7 @@ class Upscale_Machine: return max(modulus, int(round(value / modulus)) * modulus) def upscale(self, image, upscale_model, chained_model="None", rounding_modulus=8, supersample='true', - rescale_factor=2.0): + rescale_factor=2.0, frequency_split=True): if image.ndim != 4: raise ValueError("Expected IMAGE tensor with 4 dims (B,H,W,C).") @@ -213,4 +246,54 @@ class Upscale_Machine: out_bchw = torch.cat(batch_out, dim=0) images_out = out_bchw.movedim(1, -1).contiguous() + # Apply Frequency-Split SR if enabled + if frequency_split and upscale_model: + import torchvision.transforms.functional as TF + + # Upscale original image to target resolution using basic bicubic + batch_base = [] + for i in range(image.shape[0]): + chw = image[i].movedim(-1, 0) + chw = torch.clamp(chw, 0.0, 1.0) + resized_chw = resize_tensor_opencv( + chw, target_width=target_width, target_height=target_height, + supersample='false', factor=rescale_factor + ) + if resized_chw.shape[0] == 1: + resized_chw = resized_chw.repeat(3, 1, 1) + elif resized_chw.shape[0] > 3: + resized_chw = resized_chw[:3, :, :] + batch_base.append(resized_chw.unsqueeze(0)) + + bicubic_out = torch.cat(batch_base, dim=0).movedim(1, -1).contiguous() + + def blur_bhwc(tensor_bhwc): + tensor_bchw = tensor_bhwc.movedim(-1, 1) + h, w = tensor_bchw.shape[-2:] + k_y = min(15, h if h % 2 != 0 else h - 1) + k_x = min(15, w if w % 2 != 0 else w - 1) + k_y = max(1, k_y) + k_x = max(1, k_x) + if k_y >= 3 and k_x >= 3: + blurred = TF.gaussian_blur(tensor_bchw, [k_y, k_x], [3.0, 3.0]) + else: + blurred = tensor_bchw + return blurred.movedim(1, -1) + + sr_blurred = blur_bhwc(images_out) + sr_high_freq = images_out - sr_blurred + + bicubic_blurred = blur_bhwc(bicubic_out) + images_out = bicubic_blurred + sr_high_freq + images_out = torch.clamp(images_out, 0.0, 1.0) + + if chained_model and chained_model != "None": + inject_noise_for_realism = 0.15 + B, H, W, C = images_out.shape + device = images_out.device + noise = generate_blue_noise(B, C, H, W, device) # shape (B, C, H, W) + noise = noise.movedim(1, -1) # shape (B, H, W, C) + images_out = images_out + noise * inject_noise_for_realism + images_out = torch.clamp(images_out, 0.0, 1.0) + return (images_out,) \ No newline at end of file