add covers generators
This commit is contained in:
+5
-3
@@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
+81
-23
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user