From 5c72e54df8a51248e6ad652e2084707a5956fbed Mon Sep 17 00:00:00 2001 From: pk Date: Tue, 10 Mar 2026 10:07:32 +0100 Subject: [PATCH] add covers generators --- __init__.py | 8 ++-- nodes/api/AQ_Cover.py | 66 ++++++++++++++++++++++++++ nodes/api/AQ_Gemini.py | 104 ++++++++++++++++++++++++++++++++--------- prompts/cover.md | 37 +++++++++++++++ prompts/song.md | 48 +++++++++++++++++++ 5 files changed, 237 insertions(+), 26 deletions(-) create mode 100644 nodes/api/AQ_Cover.py create mode 100644 prompts/cover.md create mode 100644 prompts/song.md diff --git a/__init__.py b/__init__.py index a15e993..b7b0f5c 100755 --- a/__init__.py +++ b/__init__.py @@ -10,6 +10,7 @@ from .nodes.api.AQ_Gemini import ( AQ_Parse_JSON_to_acestep ) from .nodes.api.AQ_SendImageToAPI import AQ_SendImageToAPI +from .nodes.api.AQ_Cover import AQ_CoverPrompt from .nodes.clip.AQ_CLIPSetLastLayer import AQ_CLIPSetLastLayer from .nodes.numbers.AQ_Numbers import AQ_Increment, AQ_Random @@ -43,7 +44,8 @@ NODE_CLASS_MAPPINGS = { "AQ_BatchAverageImage": AQ_BatchAverageImage, "AQ_ColorMatchImage": AQ_ColorMatchImage, "AQ_TextEncodeQwenImageEdit": AQ_TextEncodeQwenImageEdit, - "AQ_TextEncodeQwenImageEditPlus": AQ_TextEncodeQwenImageEditPlus + "AQ_TextEncodeQwenImageEditPlus": AQ_TextEncodeQwenImageEditPlus, + "AQ_CoverPrompt": AQ_CoverPrompt, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -69,8 +71,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "AQ_BatchAverageImage": "AQ_BatchAverageImage", "AQ_ColorMatchImage": "AQ_ColorMatchImage", "AQ_TextEncodeQwenImageEdit": "AQ_TextEncodeQwenImageEdit", - "AQ_TextEncodeQwenImageEditPlus": "AQ_TextEncodeQwenImageEditPlus" - + "AQ_TextEncodeQwenImageEditPlus": "AQ_TextEncodeQwenImageEditPlus", + "AQ_CoverPrompt": "AQ_CoverPrompt", } diff --git a/nodes/api/AQ_Cover.py b/nodes/api/AQ_Cover.py new file mode 100644 index 0000000..619c459 --- /dev/null +++ b/nodes/api/AQ_Cover.py @@ -0,0 +1,66 @@ +import os + + +class AQ_CoverPrompt: + ASPECT_RATIO_NOTES = { + "16:9 YouTube": "optimized for 16:9 widescreen YouTube thumbnail format (1280x720)", + "1:1 Square": "optimized for 1:1 square format (1080x1080)", + "4:5 Portrait": "optimized for 4:5 portrait format (1080x1350)", + } + + NEGATIVE_PROMPT = ( + "blur, blurry, out of focus, low resolution, low quality, pixelated, " + "watermark, logo, signature, text errors, misspelled text, distorted letters, " + "distorted anatomy, disfigured, bad proportions, extra limbs, missing limbs, " + "bad lighting, flat lighting, overexposed, underexposed, washed out, " + "generic stock photo, clip art, cartoon (unless style selected), " + "oversaturated, noise, grain, compression artifacts" + ) + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "cover_prompt": ("STRING", {"multiline": True, "default": ""}), + "title": ("STRING", {"multiline": False, "default": ""}), + "tagline": ("STRING", {"multiline": False, "default": ""}), + "headline": ("STRING", {"multiline": False, "default": ""}), + "tags": ("STRING", {"multiline": False, "default": ""}), + "keyscale": ("STRING", {"multiline": False, "default": "C major"}), + "style": (["cinematic photo", "digital illustration", "oil painting", "neon art", "minimalist", "watercolor", "3D render"], {"default": "cinematic photo"}), + "aspect_ratio": (["16:9 YouTube", "1:1 Square", "4:5 Portrait"], {"default": "16:9 YouTube"}), + }, + "optional": { + "template_override": ("STRING", {"multiline": True, "default": ""}), + } + } + + RETURN_TYPES = ("STRING", "STRING") + RETURN_NAMES = ("prompt", "negative_prompt") + FUNCTION = "compose" + CATEGORY = "Aquasite/LLM" + + def compose(self, cover_prompt, title, tagline, headline, tags, keyscale, style, aspect_ratio, template_override=""): + if template_override and template_override.strip(): + template = template_override + else: + template_path = os.path.join( + os.path.dirname(__file__), "..", "..", "prompts", "cover.md" + ) + with open(template_path, "r", encoding="utf-8") as f: + template = f.read() + + aspect_ratio_note = self.ASPECT_RATIO_NOTES.get(aspect_ratio, aspect_ratio) + + prompt = template.format( + style=style, + aspect_ratio_note=aspect_ratio_note, + coverImage=cover_prompt, + title=title, + tagline=tagline, + headline=headline, + tags=tags, + keyscale=keyscale, + ) + + return (prompt, self.NEGATIVE_PROMPT) diff --git a/nodes/api/AQ_Gemini.py b/nodes/api/AQ_Gemini.py index f6b446f..df9ce98 100755 --- a/nodes/api/AQ_Gemini.py +++ b/nodes/api/AQ_Gemini.py @@ -320,6 +320,9 @@ class AQ_Gemini_acstep15: "COMBO", "STRING", "STRING", + "STRING", + "STRING", + "STRING", ) RETURN_NAMES = ( "response_json", @@ -332,6 +335,9 @@ class AQ_Gemini_acstep15: "language", "keyscale", "coverImage", + "title", + "tagline", + "headline", "instruction", ) FUNCTION = "generate" @@ -344,7 +350,7 @@ class AQ_Gemini_acstep15: "- Uses Google Gemini API." ) if not gemini_api_key: - return ("", "", "", seed, 120, 120.0, "4", "en", "C major", "", instruction) + return ("", "", "", seed, 120, 120.0, "4", "en", "C major", "", "", "", "", instruction) json_schema = { "type": "object", @@ -358,7 +364,10 @@ class AQ_Gemini_acstep15: "durationSeconds", "timesignature", "language", - "coverImage" + "coverImage", + "title", + "tagline", + "headline" ], "properties": { "description": {"type": "string"}, @@ -370,7 +379,10 @@ class AQ_Gemini_acstep15: "durationSeconds": {"type": "number"}, "timesignature": {"type": "string"}, "language": {"type": "string"}, - "coverImage": {"type": "string", "description": "Prompt for generating an album cover image for this song."} + "coverImage": {"type": "string", "description": "150-250 word rich art-direction prompt for an album cover image."}, + "title": {"type": "string", "description": "Song title, 1-6 words."}, + "tagline": {"type": "string", "description": "Catchy subtitle, 5-12 words, for YouTube thumbnail."}, + "headline": {"type": "string", "description": "Bold thumbnail primary text, 3-8 words, under 30 chars."} } } @@ -473,6 +485,9 @@ class AQ_Gemini_acstep15: language = str(json_response.get("language", "en")) keyscale = str(json_response.get("keyscale", "C major")) coverImage = str(json_response.get("coverImage", "")) + title = str(json_response.get("title", "")) + tagline = str(json_response.get("tagline", "")) + headline = str(json_response.get("headline", "")) response_json = json.dumps({ "description": json_response.get("description", ""), @@ -484,10 +499,13 @@ class AQ_Gemini_acstep15: "durationSeconds": duration, "timesignature": timesignature, "language": language, - "coverImage": coverImage + "coverImage": coverImage, + "title": title, + "tagline": tagline, + "headline": headline }) - return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, instruction) + return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, instruction) except Exception as e: print(f"Error in Gemini API: {str(e)}") raise e @@ -592,6 +610,9 @@ class AQ_OpenAI_acstep15: "COMBO", "STRING", "STRING", + "STRING", + "STRING", + "STRING", ) RETURN_NAMES = ( "response_json", @@ -604,6 +625,9 @@ class AQ_OpenAI_acstep15: "language", "keyscale", "coverImage", + "title", + "tagline", + "headline", "instruction", ) FUNCTION = "generate" @@ -618,10 +642,10 @@ class AQ_OpenAI_acstep15: "- verbosity: none|low|medium|high (none omits).\n" "- reasoning_effort: none|minimal|low|medium|high (none omits).\n" "- response_json contains the raw JSON output (or error JSON).\n" - "- tags/lyrics/bpm/duration/timesignature/language/keyscale/coverImage are parsed from JSON." + "- tags/lyrics/bpm/duration/timesignature/language/keyscale/coverImage/title/tagline/headline are parsed from JSON." ) if not openai_api_key: - return ("", "", "", seed, 120, 120.0, "4", "en", "C major", "", instruction) + return ("", "", "", seed, 120, 120.0, "4", "en", "C major", "", "", "", "", instruction) schema = { "type": "object", @@ -666,7 +690,19 @@ class AQ_OpenAI_acstep15: }, "coverImage": { "type": "string", - "description": "Artistic prompt for generating an album cover visual for this song." + "description": "150-250 word rich art-direction prompt for an album cover image." + }, + "title": { + "type": "string", + "description": "Song title, 1-6 words." + }, + "tagline": { + "type": "string", + "description": "Catchy subtitle, 5-12 words, for YouTube thumbnail." + }, + "headline": { + "type": "string", + "description": "Bold thumbnail primary text, 3-8 words, under 30 chars." } }, "required": [ @@ -678,7 +714,10 @@ class AQ_OpenAI_acstep15: "durationSeconds", "timesignature", "language", - "coverImage" + "coverImage", + "title", + "tagline", + "headline" ], "additionalProperties": False } @@ -790,6 +829,9 @@ class AQ_OpenAI_acstep15: language = str(json_response.get("language", "en")) keyscale = str(json_response.get("keyscale", "C major")) coverImage = str(json_response.get("coverImage", "")) + title = str(json_response.get("title", "")) + tagline = str(json_response.get("tagline", "")) + headline = str(json_response.get("headline", "")) response_json = json.dumps({ "tags": tags, @@ -800,10 +842,13 @@ class AQ_OpenAI_acstep15: "durationSeconds": duration, "timesignature": timesignature, "language": language, - "coverImage": coverImage + "coverImage": coverImage, + "title": title, + "tagline": tagline, + "headline": headline }) - return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, instruction) + return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, instruction) except Exception as e: print(f"Error in OpenAI API: {str(e)}") raise e @@ -851,10 +896,10 @@ class AQ_OpenAI_Compatible_acstep15(AQ_OpenAI_acstep15): "- Allows custom API endpoint." ) if not openai_api_key: - return ("", "", "", seed, 120, 120.0, "4", "en", "C major", "", instruction) + return ("", "", "", seed, 120, 120.0, "4", "en", "C major", "", "", "", "", instruction) model_name = custom_model if model == "custom" and custom_model else model - + messages = [] if system_message: messages.append({"role": "system", "content": system_message}) @@ -895,9 +940,12 @@ class AQ_OpenAI_Compatible_acstep15(AQ_OpenAI_acstep15): "durationSeconds": {"type": "integer", "minimum": 1}, "timesignature": {"type": "string", "enum": ["2", "3", "4", "6"]}, "language": {"type": "string", "pattern": "^[a-z]{2}$", "minLength": 2, "maxLength": 2}, - "coverImage": {"type": "string", "description": "Artistic prompt for generating an album cover visual for this song."} + "coverImage": {"type": "string", "description": "150-250 word rich art-direction prompt for an album cover image."}, + "title": {"type": "string", "description": "Song title, 1-6 words."}, + "tagline": {"type": "string", "description": "Catchy subtitle, 5-12 words, for YouTube thumbnail."}, + "headline": {"type": "string", "description": "Bold thumbnail primary text, 3-8 words, under 30 chars."} }, - "required": ["tags", "lyrics", "seed", "bpm", "keyscale", "durationSeconds", "timesignature", "language", "coverImage"], + "required": ["tags", "lyrics", "seed", "bpm", "keyscale", "durationSeconds", "timesignature", "language", "coverImage", "title", "tagline", "headline"], "additionalProperties": False } @@ -958,6 +1006,9 @@ class AQ_OpenAI_Compatible_acstep15(AQ_OpenAI_acstep15): language = str(json_response.get("language", "en")) keyscale = str(json_response.get("keyscale", "C major")) coverImage = str(json_response.get("coverImage", "")) + title = str(json_response.get("title", "")) + tagline = str(json_response.get("tagline", "")) + headline = str(json_response.get("headline", "")) response_json = json.dumps({ "tags": tags, @@ -968,10 +1019,13 @@ class AQ_OpenAI_Compatible_acstep15(AQ_OpenAI_acstep15): "durationSeconds": duration, "timesignature": timesignature, "language": language, - "coverImage": coverImage + "coverImage": coverImage, + "title": title, + "tagline": tagline, + "headline": headline }) - return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, instruction) + return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, instruction) except Exception as e: print(f"Error in API: {str(e)}") raise e @@ -996,6 +1050,9 @@ class AQ_Parse_JSON_to_acestep: "COMBO", "COMBO", "STRING", + "STRING", + "STRING", + "STRING", ) RETURN_NAMES = ( "response_json", @@ -1008,6 +1065,9 @@ class AQ_Parse_JSON_to_acestep: "language", "keyscale", "coverImage", + "title", + "tagline", + "headline", ) FUNCTION = "parse" CATEGORY = "Aquasite/LLM" @@ -1039,15 +1099,13 @@ class AQ_Parse_JSON_to_acestep: language = str(data.get("language", "en")) keyscale = str(data.get("keyscale", "C major")) coverImage = str(data.get("coverImage", "")) - - # Ensure it's a valid COMBO value for ComfyUI if needed, - # but usually RETURN_TYPES "COMBO" means it returns one of the values. - # We'll just return the strings as they are. + title = str(data.get("title", "")) + tagline = str(data.get("tagline", "")) + headline = str(data.get("headline", "")) - return (json_text, tags, lyrics, seed, bpm, duration, timesignature, language, keyscale, coverImage) + return (json_text, tags, lyrics, seed, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline) except Exception as e: print(f"Error parsing JSON in AQ_Parse_JSON_to_acestep: {str(e)}") - # Fail the node if parsing fails raise e def _coerce_int(self, value, default=0): diff --git a/prompts/cover.md b/prompts/cover.md new file mode 100644 index 0000000..5513fa9 --- /dev/null +++ b/prompts/cover.md @@ -0,0 +1,37 @@ +Create a stunning album/single cover image with the following specifications: + +**Style:** {style} +**Format:** {aspect_ratio_note} + +--- + +**Art Direction:** +{coverImage} + +--- + +**Genre & Mood Context:** +Music style: {tags} +Key & scale: {keyscale} +Let the color palette, lighting, and visual atmosphere reflect this musical character. + +--- + +**Typography — integrate the following text into the image design:** + +- **Title** (bold, highly legible, prominent placement): {title} +- **Tagline** (smaller subtitle beneath or near the title): {tagline} +- **Headline** (largest, most dominant text element — this is the first thing the viewer reads): {headline} + +Text must be sharp, high-contrast, and readable at thumbnail size (even at 320px wide). +Use a typeface and color that complement the visual style. Avoid script fonts that are hard to read at small sizes. + +--- + +**Technical Requirements:** +- Sharp focus, high resolution feel +- High contrast — readable as a small YouTube thumbnail +- No watermarks, logos, or signatures +- No blurry or low-resolution textures +- No generic stock-photo feel +- Composition should work at 16:9, 1:1, and 4:5 crops if possible diff --git a/prompts/song.md b/prompts/song.md new file mode 100644 index 0000000..5e0271b --- /dev/null +++ b/prompts/song.md @@ -0,0 +1,48 @@ +# Song Generation System Prompt + +You are a creative music production assistant. Given a user's song concept, generate a complete, cohesive set of music metadata fields. All fields must be thematically consistent with each other. + +## Required Output Fields + +Return ONLY valid JSON with the following fields: + +- **description** — brief summary of the song concept (1-3 sentences) +- **tags** — style prompt: genre, instruments, mood, tempo, production style (comma-separated keywords or descriptive phrase) +- **lyrics** — complete song lyrics with structure tags: `[intro]`, `[verse]`, `[chorus]`, `[bridge]`, `[outro]`, `[instrumental]`; use `\n` for line breaks +- **seed** — integer seed for reproducibility +- **bpm** — tempo in beats per minute (integer, 1–300) +- **keyscale** — musical key and scale, e.g. "C major", "A minor" +- **durationSeconds** — target duration in seconds (integer, 30–600) +- **timesignature** — beats per measure: "2", "3", "4", or "6" +- **language** — ISO 639-1 two-letter code of primary lyric language (e.g. "en", "es", "fr") +- **coverImage** — 150–250 word rich art-direction prompt for an album cover image; describe composition, foreground/background elements, color palette, lighting setup, visual style (photography, illustration, painting, etc.), mood/atmosphere, and any symbolic imagery. Be specific and evocative. +- **title** — the song's name, 1–6 words, memorable and genre-appropriate +- **tagline** — catchy subtitle, 5–12 words, suitable for a YouTube thumbnail subtitle line +- **headline** — bold primary text for a YouTube thumbnail, 3–8 words, ideally under 30 characters, all-caps acceptable; this is the dominant visual text + +## Consistency Guidelines + +- `title`, `tagline`, and `headline` must reflect the same emotional core as the lyrics and tags +- `coverImage` must reference the song's mood, setting, and genre implied by `tags` +- `keyscale` should match the emotional tone: major for uplifting/happy, minor for emotional/dark +- `bpm` should align with the energy described in `tags` + +## Example (structure only) + +```json +{ + "description": "...", + "tags": "...", + "lyrics": "[verse]\n...\n[chorus]\n...", + "seed": 42, + "bpm": 120, + "keyscale": "G major", + "durationSeconds": 180, + "timesignature": "4", + "language": "en", + "coverImage": "Wide cinematic shot of a lone figure standing on a rain-soaked city rooftop at dusk...", + "title": "Neon Rain", + "tagline": "When the city lights blur your tears away", + "headline": "NEON RAIN" +} +```