From 534349298c7b0a4fb986aeeb2e49491929bd2d7d Mon Sep 17 00:00:00 2001 From: pk Date: Tue, 10 Mar 2026 14:16:27 +0100 Subject: [PATCH] add video node --- nodes/api/AQ_Gemini.py | 47 +++++++++++++++++++++++----------- nodes/api/AQ_VideoFromImage.py | 21 ++++++++++----- prompts/song.md | 8 +++++- 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/nodes/api/AQ_Gemini.py b/nodes/api/AQ_Gemini.py index 811ec04..1b2e6dd 100755 --- a/nodes/api/AQ_Gemini.py +++ b/nodes/api/AQ_Gemini.py @@ -325,6 +325,7 @@ class AQ_Gemini_acstep15: "STRING", "STRING", "STRING", + "STRING", ) RETURN_NAMES = ( "response_json", @@ -342,6 +343,7 @@ class AQ_Gemini_acstep15: "headline", "yt_description", "yt_tags", + "yt_title", "instruction", ) FUNCTION = "generate" @@ -354,7 +356,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", @@ -373,7 +375,8 @@ class AQ_Gemini_acstep15: "tagline", "headline", "yt_description", - "yt_tags" + "yt_tags", + "yt_title" ], "properties": { "description": {"type": "string"}, @@ -390,7 +393,8 @@ class AQ_Gemini_acstep15: "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."}, "yt_description": {"type": "string", "description": "Full YouTube video description: 2-4 paragraphs covering mood, story, genre, and a lyrics/credits section."}, - "yt_tags": {"type": "string", "description": "Comma-separated YouTube search tags, 10-20 keywords covering genre, mood, instruments, era, and use-case (e.g. study music, workout)."} + "yt_tags": {"type": "string", "description": "Comma-separated YouTube search tags, 10-20 keywords covering genre, mood, instruments, era, and use-case (e.g. study music, workout)."}, + "yt_title": {"type": "string", "description": "SEO-optimised YouTube video title. Use a keyword-rich format with a pipe or colon separator, e.g. 'Focus Music | Minimalist Ambient Drone for Deep Work' or 'Deep Sleep Music | Gentle Soundscapes for Insomnia Relief'. Include the primary use-case, mood, and one or two distinctive sonic elements. 60-100 characters."} } } @@ -498,6 +502,7 @@ class AQ_Gemini_acstep15: headline = str(json_response.get("headline", "")) yt_description = str(json_response.get("yt_description", "")) yt_tags = str(json_response.get("yt_tags", "")) + yt_title = str(json_response.get("yt_title", "")) response_json = json.dumps({ "description": json_response.get("description", ""), @@ -514,10 +519,11 @@ class AQ_Gemini_acstep15: "tagline": tagline, "headline": headline, "yt_description": yt_description, - "yt_tags": yt_tags + "yt_tags": yt_tags, + "yt_title": yt_title }) - return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, yt_description, yt_tags, instruction) + return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, yt_description, yt_tags, yt_title, instruction) except Exception as e: print(f"Error in Gemini API: {str(e)}") raise e @@ -627,6 +633,7 @@ class AQ_OpenAI_acstep15: "STRING", "STRING", "STRING", + "STRING", ) RETURN_NAMES = ( "response_json", @@ -644,6 +651,7 @@ class AQ_OpenAI_acstep15: "headline", "yt_description", "yt_tags", + "yt_title", "instruction", ) FUNCTION = "generate" @@ -661,7 +669,7 @@ class AQ_OpenAI_acstep15: "- tags/lyrics/bpm/duration/timesignature/language/keyscale/coverImage/title/tagline/headline/yt_description/yt_tags 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", @@ -743,7 +751,8 @@ class AQ_OpenAI_acstep15: "tagline", "headline", "yt_description", - "yt_tags" + "yt_tags", + "yt_title" ], "additionalProperties": False } @@ -860,6 +869,7 @@ class AQ_OpenAI_acstep15: headline = str(json_response.get("headline", "")) yt_description = str(json_response.get("yt_description", "")) yt_tags = str(json_response.get("yt_tags", "")) + yt_title = str(json_response.get("yt_title", "")) response_json = json.dumps({ "tags": tags, @@ -875,10 +885,11 @@ class AQ_OpenAI_acstep15: "tagline": tagline, "headline": headline, "yt_description": yt_description, - "yt_tags": yt_tags + "yt_tags": yt_tags, + "yt_title": yt_title }) - return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, yt_description, yt_tags, instruction) + return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, yt_description, yt_tags, yt_title, instruction) except Exception as e: print(f"Error in OpenAI API: {str(e)}") raise e @@ -926,7 +937,7 @@ 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 @@ -975,9 +986,10 @@ class AQ_OpenAI_Compatible_acstep15(AQ_OpenAI_acstep15): "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."}, "yt_description": {"type": "string", "description": "Full YouTube video description: 2-4 paragraphs covering mood, story, genre, and a lyrics/credits section."}, - "yt_tags": {"type": "string", "description": "Comma-separated YouTube search tags, 10-20 keywords covering genre, mood, instruments, era, and use-case (e.g. study music, workout)."} + "yt_tags": {"type": "string", "description": "Comma-separated YouTube search tags, 10-20 keywords covering genre, mood, instruments, era, and use-case (e.g. study music, workout)."}, + "yt_title": {"type": "string", "description": "SEO-optimised YouTube video title. Use a keyword-rich format with a pipe or colon separator, e.g. 'Focus Music | Minimalist Ambient Drone for Deep Work' or 'Deep Sleep Music | Gentle Soundscapes for Insomnia Relief'. Include the primary use-case, mood, and one or two distinctive sonic elements. 60-100 characters."} }, - "required": ["tags", "lyrics", "seed", "bpm", "keyscale", "durationSeconds", "timesignature", "language", "coverImage", "title", "tagline", "headline", "yt_description", "yt_tags"], + "required": ["tags", "lyrics", "seed", "bpm", "keyscale", "durationSeconds", "timesignature", "language", "coverImage", "title", "tagline", "headline", "yt_description", "yt_tags", "yt_title"], "additionalProperties": False } @@ -1043,6 +1055,7 @@ class AQ_OpenAI_Compatible_acstep15(AQ_OpenAI_acstep15): headline = str(json_response.get("headline", "")) yt_description = str(json_response.get("yt_description", "")) yt_tags = str(json_response.get("yt_tags", "")) + yt_title = str(json_response.get("yt_title", "")) response_json = json.dumps({ "tags": tags, @@ -1058,10 +1071,11 @@ class AQ_OpenAI_Compatible_acstep15(AQ_OpenAI_acstep15): "tagline": tagline, "headline": headline, "yt_description": yt_description, - "yt_tags": yt_tags + "yt_tags": yt_tags, + "yt_title": yt_title }) - return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, yt_description, yt_tags, instruction) + return (response_json, tags, lyrics, seed_value, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, yt_description, yt_tags, yt_title, instruction) except Exception as e: print(f"Error in API: {str(e)}") raise e @@ -1091,6 +1105,7 @@ class AQ_Parse_JSON_to_acestep: "STRING", "STRING", "STRING", + "STRING", ) RETURN_NAMES = ( "response_json", @@ -1108,6 +1123,7 @@ class AQ_Parse_JSON_to_acestep: "headline", "yt_description", "yt_tags", + "yt_title", ) FUNCTION = "parse" CATEGORY = "Aquasite/LLM" @@ -1144,8 +1160,9 @@ class AQ_Parse_JSON_to_acestep: headline = str(data.get("headline", "")) yt_description = str(data.get("yt_description", "")) yt_tags = str(data.get("yt_tags", "")) + yt_title = str(data.get("yt_title", "")) - return (json_text, tags, lyrics, seed, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, yt_description, yt_tags) + return (json_text, tags, lyrics, seed, bpm, duration, timesignature, language, keyscale, coverImage, title, tagline, headline, yt_description, yt_tags, yt_title) except Exception as e: print(f"Error parsing JSON in AQ_Parse_JSON_to_acestep: {str(e)}") raise e diff --git a/nodes/api/AQ_VideoFromImage.py b/nodes/api/AQ_VideoFromImage.py index cb0e774..561ee98 100644 --- a/nodes/api/AQ_VideoFromImage.py +++ b/nodes/api/AQ_VideoFromImage.py @@ -105,7 +105,7 @@ class AQ_StillImageToVideo: # ------------------------------------------------------------------ def _encode(self, encoder_choice, img_path, wav_path, output_path, fps, meta_args): - """Run ffmpeg, trying GPU first when encoder is 'auto'.""" + """Run ffmpeg. Named GPU encoders fall back to CPU on failure.""" _GPU_CODECS = { "nvidia (h264_nvenc)": self._nvenc_args, @@ -119,22 +119,29 @@ class AQ_StillImageToVideo: self._run(cmd) elif encoder_choice in _GPU_CODECS: - codec_args = _GPU_CODECS[encoder_choice]() + # Try the requested GPU encoder; fall back to CPU on failure cmd = self._build_cmd(img_path, wav_path, output_path, fps, - codec_args, meta_args) + _GPU_CODECS[encoder_choice](), meta_args) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=6000) + if result.returncode == 0: + return + last_line = result.stderr.splitlines()[-1] if result.stderr else "" + print(f"[AQ_StillImageToVideo] {encoder_choice} failed: {last_line}") + print("[AQ_StillImageToVideo] Falling back to CPU (libx264 ultrafast)") + cmd = self._build_cmd(img_path, wav_path, output_path, fps, + self._cpu_args(), meta_args) self._run(cmd) - else: # auto: try GPU codecs in order, fall back to CPU + else: # auto: try all GPU codecs in order, fall back to CPU for codec_fn in (self._nvenc_args, self._amf_args, self._qsv_args): cmd = self._build_cmd(img_path, wav_path, output_path, fps, codec_fn(), meta_args) result = subprocess.run(cmd, capture_output=True, text=True, timeout=6000) if result.returncode == 0: return - print(f"[AQ_StillImageToVideo] GPU encoder failed, trying next: " - f"{result.stderr.splitlines()[-1] if result.stderr else ''}") + last_line = result.stderr.splitlines()[-1] if result.stderr else "" + print(f"[AQ_StillImageToVideo] GPU encoder failed, trying next: {last_line}") - # CPU fallback print("[AQ_StillImageToVideo] Falling back to CPU (libx264 ultrafast)") cmd = self._build_cmd(img_path, wav_path, output_path, fps, self._cpu_args(), meta_args) diff --git a/prompts/song.md b/prompts/song.md index 73986f8..32cba3d 100644 --- a/prompts/song.md +++ b/prompts/song.md @@ -21,6 +21,11 @@ Return ONLY valid JSON with the following fields: - **headline** — bold primary text for a YouTube thumbnail, 3–8 words, ideally under 30 characters, all-caps acceptable; this is the dominant visual text - **yt_description** — full YouTube video description, 2–4 paragraphs: open with the mood/story, describe the sound and instrumentation, note key/BPM/genre for music discovery, close with a call-to-action or credits placeholder - **yt_tags** — comma-separated YouTube search keywords, 10–20 tags; mix genre, mood, instruments, era, tempo descriptor, and use-case terms (e.g. "study music", "workout playlist", "relaxing beats") +- **yt_title** — SEO-optimised YouTube video title, 60–100 characters. Use a keyword-rich format with `|` or `:` as separator. Lead with the primary use-case or mood category, follow with the specific sonic identity. Examples by style: + - Focus/Study: `Focus Music | Minimalist Ambient Drone for Deep Work (No Distractions)` + - Sleep: `Deep Sleep Music | Gentle Ambient Soundscapes for Insomnia Relief` + - Meditation: `Pure Relaxation | Calming Piano Melodies for Inner Peace` + - General: `[Mood/Use-case] | [Instrument/Style] for [Activity/Feeling]` ## Consistency Guidelines @@ -47,6 +52,7 @@ Return ONLY valid JSON with the following fields: "tagline": "When the city lights blur your tears away", "headline": "NEON RAIN", "yt_description": "A melancholic synth-pop journey through rain-soaked city streets...\n\nFeaturing lush synthesizers, driving bass, and evocative vocals in G minor at 118 BPM.\n\n🎵 Stream & save | 💬 Share your thoughts below", - "yt_tags": "synth-pop, melancholic music, city vibes, night drive music, rainy day playlist, 80s inspired, synthwave, emotional music, study music, chill beats" + "yt_tags": "synth-pop, melancholic music, city vibes, night drive music, rainy day playlist, 80s inspired, synthwave, emotional music, study music, chill beats", + "yt_title": "Neon Rain | Melancholic Synth-Pop for Late Night City Drives" } ```