Bobby Collins Claude Opus 4.6 commited on
Commit
26c6924
Β·
1 Parent(s): f526dd6

v1.4.0: ElectronHub support, Free Mode, SDXL cover art generation

Browse files

- ElectronHub API support with auto-detection from key prefix
- Free Mode: generate prompts without an API key using free models
with random selection and cascade fallback on failure
- SDXL cover art image generation via ElectronHub (free for everyone)
- 2-column cover art layout: generated image + text prompt
- Robust JSON parser handles free model quirks (literal newlines,
preamble text, markdown fences)
- 90s per-model timeout for free mode cascade
- No .env fallback for premium models β€” users must enter their own key
- ElectronHub model name translation (auto strips provider prefix)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (5) hide show
  1. .env.example +4 -0
  2. CHANGELOG.md +12 -0
  3. Launch Suno Prompter.bat +1 -2
  4. app.py +348 -55
  5. requirements.txt +1 -0
.env.example CHANGED
@@ -1,3 +1,7 @@
1
  # OpenRouter API Key
2
  # Get yours at https://openrouter.ai/keys
3
  OPENROUTER_API_KEY=sk-or-v1-your-key-here
 
 
 
 
 
1
  # OpenRouter API Key
2
  # Get yours at https://openrouter.ai/keys
3
  OPENROUTER_API_KEY=sk-or-v1-your-key-here
4
+
5
+ # ElectronHub API Key (used for Free Mode and SDXL image generation)
6
+ # For HF Spaces, set as a Spaces secret named ELECTRONHUB_API_KEY
7
+ ELECTRONHUB_API_KEY=ek-your-key-here
CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
  # Changelog
2
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ## [1.3.2] - 2026-02-24
4
 
5
  ### Added
 
1
  # Changelog
2
 
3
+ ## [1.4.0] - 2026-02-25
4
+
5
+ ### Added
6
+ - **ElectronHub API support** β€” The app now works with both OpenRouter and ElectronHub API keys. Provider is auto-detected from the key prefix (`sk-or-` routes to OpenRouter, `ek-` routes to ElectronHub). No manual selection needed.
7
+ - **Free Mode** β€” New "Free Mode" checkbox lets users generate prompts without any API key. Uses a hardcoded ElectronHub key with free models (glm-4.5-air, qwen3-coder, llama-4-maverick, claude-3-haiku, kimi-k2.5, gemini-2.5-flash). Randomly selects a model and cascades through the rest on failure. Grays out the API key and model dropdown when active.
8
+ - **SDXL cover art image generation** β€” The cover art section is now split into two columns: the left column generates an actual image using SDXL via ElectronHub's image generation endpoint, and the right column shows the text prompt (with copy button). Image generation is free for everyone using the hardcoded ElectronHub key. If image generation fails, text outputs still appear normally.
9
+ - **`requests` dependency** added to requirements.txt for image generation API calls.
10
+
11
+ ### Changed
12
+ - **API Key field** β€” Label updated to "API Key (OpenRouter or ElectronHub)" with placeholder showing both key formats.
13
+ - **Intro text** β€” Now mentions both providers and Free Mode availability.
14
+
15
  ## [1.3.2] - 2026-02-24
16
 
17
  ### Added
Launch Suno Prompter.bat CHANGED
@@ -1,4 +1,3 @@
1
  @echo off
2
  cd /d "%~dp0"
3
- venv\Scripts\python.exe app.py
4
- pause
 
1
  @echo off
2
  cd /d "%~dp0"
3
+ start "Suno Prompt Generator" /min venv\Scripts\python.exe app.py
 
app.py CHANGED
@@ -1,12 +1,17 @@
1
  """
2
  Suno Prompting App
3
- Converts natural language song ideas into structured Suno AI prompts via OpenRouter API.
 
4
  """
5
 
6
  import json
7
  import os
 
 
 
8
 
9
  import gradio as gr
 
10
  from dotenv import load_dotenv
11
  from openai import OpenAI
12
 
@@ -25,6 +30,22 @@ MODELS = {
25
  "Custom": "custom",
26
  }
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  THEME = gr.themes.Base(
29
  primary_hue=gr.themes.colors.orange,
30
  secondary_hue=gr.themes.colors.neutral,
@@ -60,60 +81,234 @@ THEME = gr.themes.Base(
60
  # CORE LOGIC
61
  # ─────────────────────────────────────────────
62
 
63
- def _get_client(api_key: str = ""):
64
- """Create OpenAI client. Uses provided key, falls back to env var."""
65
- key = api_key.strip() or os.getenv("OPENROUTER_API_KEY", "")
66
- if not key:
67
- raise ValueError("No API key provided. Enter your OpenRouter API key above.")
68
- return OpenAI(
69
- base_url="https://openrouter.ai/api/v1",
70
- api_key=key,
71
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- def generate_prompt(api_key: str, song_idea: str, model_choice: str, custom_model: str, weirdness: int):
75
- """Call OpenRouter and parse the structured response."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  if not song_idea.strip():
77
- return "", "Please enter a song idea.", "", "", ""
78
 
79
  # Resolve model ID
80
- if model_choice == "Custom":
 
 
81
  model_id = custom_model.strip()
82
  if not model_id:
83
- return "", "Please enter a custom model ID.", "", "", ""
84
  else:
85
  model_id = MODELS.get(model_choice, "google/gemini-3-flash-preview")
86
 
87
  system_prompt = build_system_prompt(weirdness)
88
 
89
  try:
90
- client = _get_client(api_key)
91
  except ValueError as e:
92
- return "", str(e), "", "", ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
 
94
  try:
95
- response = client.chat.completions.create(
96
- model=model_id,
97
- messages=[
98
- {"role": "system", "content": system_prompt},
99
- {"role": "user", "content": song_idea},
100
- ],
101
- temperature=0.9,
102
- max_tokens=4096,
103
- )
104
-
105
- raw = response.choices[0].message.content.strip()
106
-
107
- # Strip markdown code fences if present
108
- if raw.startswith("```"):
109
- lines = raw.split("\n")
110
- if lines[-1].strip() == "```":
111
- lines = lines[1:-1]
112
- else:
113
- lines = lines[1:]
114
- raw = "\n".join(lines)
115
-
116
- data = json.loads(raw)
117
 
118
  song_title = data.get("song_title", "Untitled")
119
  style_prompt = data.get("style_prompt", "")
@@ -125,9 +320,12 @@ def generate_prompt(api_key: str, song_idea: str, model_choice: str, custom_mode
125
  si_reason = data.get("style_influence_reasoning", "")
126
  settings = f"Weirdness: {w}/100\n{w_reason}\n\nStyle Influence: {si}/100\n{si_reason}"
127
 
128
- cover_art = data.get("cover_art_prompt", "")
129
 
130
- return song_title, style_prompt, lyrics, settings, cover_art
 
 
 
131
 
132
  except json.JSONDecodeError:
133
  return (
@@ -135,10 +333,11 @@ def generate_prompt(api_key: str, song_idea: str, model_choice: str, custom_mode
135
  f"[JSON parse error - raw response below]\n\n{raw}",
136
  "",
137
  "",
 
138
  "",
139
  )
140
  except Exception as e:
141
- return "", f"Error: {e}", "", "", ""
142
 
143
 
144
  def toggle_custom_visibility(choice):
@@ -146,6 +345,14 @@ def toggle_custom_visibility(choice):
146
  return gr.update(visible=(choice == "Custom"))
147
 
148
 
 
 
 
 
 
 
 
 
149
  # ─────────────────────────────────────────────
150
  # APP BUILDER
151
  # ─────────────────────────────────────────────
@@ -156,18 +363,25 @@ def create_app():
156
  gr.Markdown(
157
  "# Suno Prompt Generator <span style='font-size:0.45em; font-weight:normal; color:#999;'>by AnimalMonk</span>\n"
158
  "This is meant to spark new ideas or get you a starting point. Take what it gives you and make it goldensome!\n\n"
159
- "You need an [OpenRouter API key](https://openrouter.ai/keys) to use this app. "
160
- "Your key is sent directly to OpenRouter and is never stored."
 
 
161
  )
162
 
163
  with gr.Accordion("API Key", open=True):
164
  api_key_input = gr.Textbox(
165
- label="OpenRouter API Key",
166
- placeholder="sk-or-v1-...",
167
  type="password",
168
  lines=1,
169
  )
170
 
 
 
 
 
 
171
  with gr.Row():
172
  model_dropdown = gr.Dropdown(
173
  choices=list(MODELS.keys()),
@@ -231,12 +445,22 @@ def create_app():
231
 
232
  gr.Markdown("---")
233
 
234
- cover_art_output = gr.Textbox(
235
- label="Cover Art Image Prompt (paste into Grok or image generator)",
236
- lines=6,
237
- buttons=["copy"],
238
- interactive=False,
239
- )
 
 
 
 
 
 
 
 
 
 
240
 
241
  # Events
242
  model_dropdown.change(
@@ -245,8 +469,14 @@ def create_app():
245
  outputs=custom_model_input,
246
  )
247
 
248
- outputs = [title_output, style_output, lyrics_output, settings_output, cover_art_output]
249
- inputs = [api_key_input, song_input, model_dropdown, custom_model_input, weirdness_slider]
 
 
 
 
 
 
250
 
251
  generate_btn.click(
252
  fn=generate_prompt,
@@ -271,5 +501,68 @@ def create_app():
271
  demo, _theme = create_app()
272
 
273
  if __name__ == "__main__":
 
 
 
 
 
274
  load_dotenv() # Load .env for local dev
275
- demo.launch(inbrowser=True, theme=_theme)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  Suno Prompting App
3
+ Converts natural language song ideas into structured Suno AI prompts.
4
+ Supports OpenRouter and ElectronHub APIs with auto-detection.
5
  """
6
 
7
  import json
8
  import os
9
+ import random
10
+ import re
11
+ import tempfile
12
 
13
  import gradio as gr
14
+ import requests
15
  from dotenv import load_dotenv
16
  from openai import OpenAI
17
 
 
30
  "Custom": "custom",
31
  }
32
 
33
+ FREE_MODELS = [
34
+ "glm-4.5-air",
35
+ "qwen3-coder-480b-a35b-instruct:free",
36
+ "llama-4-maverick-17b-128e-instruct",
37
+ "claude-3-haiku-20240307",
38
+ "kimi-k2.5:free",
39
+ "gemini-2.5-flash",
40
+ ]
41
+
42
+ # ElectronHub uses different model IDs for some models.
43
+ # Most just drop the provider prefix, but these two need explicit overrides.
44
+ ELECTRONHUB_MODEL_OVERRIDES = {
45
+ "anthropic/claude-sonnet-4.6": "claude-sonnet-4-6",
46
+ "x-ai/grok-4": "grok-4-0709",
47
+ }
48
+
49
  THEME = gr.themes.Base(
50
  primary_hue=gr.themes.colors.orange,
51
  secondary_hue=gr.themes.colors.neutral,
 
81
  # CORE LOGIC
82
  # ─────────────────────────────────────────────
83
 
84
+ def _get_client(api_key: str = "", free_mode: bool = False):
85
+ """Create OpenAI-compatible client. Auto-detects provider from key prefix.
86
+
87
+ Free mode uses the hardcoded ElectronHub key.
88
+ Otherwise: ek-* -> ElectronHub, everything else -> OpenRouter.
89
+ """
90
+ if free_mode:
91
+ key = os.getenv("ELECTRONHUB_API_KEY", "")
92
+ if not key:
93
+ raise ValueError(
94
+ "Free Mode is unavailable. Enter your own API key and uncheck Free Mode."
95
+ )
96
+ base_url = "https://api.electronhub.ai/v1"
97
+ else:
98
+ key = api_key.strip()
99
+ if not key:
100
+ raise ValueError("No API key provided. Enter your OpenRouter or ElectronHub API key above.")
101
+ # Auto-detect provider from key prefix
102
+ if key.startswith("ek-"):
103
+ base_url = "https://api.electronhub.ai/v1"
104
+ else:
105
+ base_url = "https://openrouter.ai/api/v1"
106
+
107
+ return OpenAI(base_url=base_url, api_key=key, timeout=90.0)
108
+
109
+
110
+ def _resolve_model_id(model_id: str) -> str:
111
+ """Translate OpenRouter model ID to ElectronHub format.
112
+
113
+ Most models just drop the provider prefix (google/, anthropic/, etc.).
114
+ A few need explicit overrides (different naming conventions).
115
+ """
116
+ if model_id in ELECTRONHUB_MODEL_OVERRIDES:
117
+ return ELECTRONHUB_MODEL_OVERRIDES[model_id]
118
+ if "/" in model_id:
119
+ return model_id.split("/", 1)[1]
120
+ return model_id
121
+
122
+
123
+ def _fix_json_newlines(text: str) -> str:
124
+ """Replace literal newlines inside JSON string values with \\n.
125
+
126
+ Free models often put real line breaks in string fields (especially lyrics)
127
+ instead of \\n escape sequences, which makes json.loads() fail.
128
+ """
129
+ result = []
130
+ in_string = False
131
+ escape_next = False
132
+ for char in text:
133
+ if escape_next:
134
+ result.append(char)
135
+ escape_next = False
136
+ continue
137
+ if char == '\\':
138
+ result.append(char)
139
+ escape_next = True
140
+ continue
141
+ if char == '"':
142
+ in_string = not in_string
143
+ result.append(char)
144
+ continue
145
+ if char == '\n' and in_string:
146
+ result.append('\\n')
147
+ continue
148
+ result.append(char)
149
+ return ''.join(result)
150
+
151
+
152
+ def _extract_json(raw: str) -> dict:
153
+ """Extract JSON object from model response, even if wrapped in extra text.
154
+
155
+ Handles: clean JSON, markdown code fences, preamble/trailing text,
156
+ and literal newlines inside string values (common with free models).
157
+ """
158
+ # Strip markdown code fences
159
+ cleaned = re.sub(r'^```(?:json)?\s*\n?', '', raw.strip(), flags=re.MULTILINE)
160
+ cleaned = re.sub(r'\n?```\s*$', '', cleaned.strip(), flags=re.MULTILINE)
161
+
162
+ # Try parsing cleaned text directly
163
+ try:
164
+ return json.loads(cleaned)
165
+ except json.JSONDecodeError:
166
+ pass
167
+
168
+ # Fix literal newlines inside string values, then try again
169
+ try:
170
+ return json.loads(_fix_json_newlines(cleaned))
171
+ except json.JSONDecodeError:
172
+ pass
173
 
174
+ # Find the first { ... } block (handles preamble/trailing text)
175
+ match = re.search(r'\{[\s\S]*\}', cleaned)
176
+ if match:
177
+ block = match.group()
178
+ try:
179
+ return json.loads(block)
180
+ except json.JSONDecodeError:
181
+ pass
182
+ try:
183
+ return json.loads(_fix_json_newlines(block))
184
+ except json.JSONDecodeError:
185
+ pass
186
 
187
+ raise json.JSONDecodeError("No valid JSON found in response", raw, 0)
188
+
189
+
190
+ def _generate_cover_image(prompt: str):
191
+ """Generate cover art via ElectronHub SDXL. Returns file path or None on failure."""
192
+ if not prompt:
193
+ return None
194
+
195
+ eh_key = os.getenv("ELECTRONHUB_API_KEY", "")
196
+ if not eh_key:
197
+ return None # Silently skip β€” image gen is a bonus, not critical
198
+
199
+ try:
200
+ resp = requests.post(
201
+ "https://api.electronhub.ai/v1/images/generations",
202
+ headers={
203
+ "Authorization": f"Bearer {eh_key}",
204
+ "Content-Type": "application/json",
205
+ },
206
+ json={
207
+ "model": "sdxl",
208
+ "prompt": prompt,
209
+ "n": 1,
210
+ "size": "1024x1024",
211
+ },
212
+ timeout=60,
213
+ )
214
+ resp.raise_for_status()
215
+ data = resp.json()
216
+ image_url = data["data"][0]["url"]
217
+
218
+ # Download image to temp file for Gradio
219
+ img_resp = requests.get(image_url, timeout=30)
220
+ img_resp.raise_for_status()
221
+
222
+ tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
223
+ tmp.write(img_resp.content)
224
+ tmp.close()
225
+ return tmp.name
226
+
227
+ except Exception:
228
+ return None # Image gen failure should never block the main output
229
+
230
+
231
+ def generate_prompt(
232
+ api_key: str,
233
+ song_idea: str,
234
+ model_choice: str,
235
+ custom_model: str,
236
+ weirdness: int,
237
+ free_mode: bool,
238
+ ):
239
+ """Call LLM API, parse structured response, and generate cover art image."""
240
+ # 6 outputs: title, style, lyrics, settings, cover_art_image, cover_art_text
241
  if not song_idea.strip():
242
+ return "", "Please enter a song idea.", "", "", None, ""
243
 
244
  # Resolve model ID
245
+ if free_mode:
246
+ model_id = None # Set during cascade
247
+ elif model_choice == "Custom":
248
  model_id = custom_model.strip()
249
  if not model_id:
250
+ return "", "Please enter a custom model ID.", "", "", None, ""
251
  else:
252
  model_id = MODELS.get(model_choice, "google/gemini-3-flash-preview")
253
 
254
  system_prompt = build_system_prompt(weirdness)
255
 
256
  try:
257
+ client = _get_client(api_key, free_mode=free_mode)
258
  except ValueError as e:
259
+ return "", str(e), "", "", None, ""
260
+
261
+ # Translate model ID for ElectronHub (different naming convention)
262
+ if not free_mode and model_id:
263
+ key = api_key.strip()
264
+ if key.startswith("ek-"):
265
+ model_id = _resolve_model_id(model_id)
266
+
267
+ # --- LLM call ---
268
+ raw = None
269
+ if free_mode:
270
+ # Shuffle and cascade through free models until one works
271
+ models_to_try = FREE_MODELS[:]
272
+ random.shuffle(models_to_try)
273
+ last_error = None
274
+ for model_id in models_to_try:
275
+ try:
276
+ response = client.chat.completions.create(
277
+ model=model_id,
278
+ messages=[
279
+ {"role": "system", "content": system_prompt},
280
+ {"role": "user", "content": song_idea},
281
+ ],
282
+ temperature=0.9,
283
+ max_tokens=4096,
284
+ )
285
+ raw = response.choices[0].message.content.strip()
286
+ print(f"Free Mode: {model_id} succeeded")
287
+ break
288
+ except Exception as e:
289
+ last_error = e
290
+ print(f"Free Mode: {model_id} failed ({e}), trying next...")
291
+ continue
292
+ if raw is None:
293
+ return "", f"All free models failed. Last error: {last_error}", "", "", None, ""
294
+ else:
295
+ try:
296
+ response = client.chat.completions.create(
297
+ model=model_id,
298
+ messages=[
299
+ {"role": "system", "content": system_prompt},
300
+ {"role": "user", "content": song_idea},
301
+ ],
302
+ temperature=0.9,
303
+ max_tokens=4096,
304
+ )
305
+ raw = response.choices[0].message.content.strip()
306
+ except Exception as e:
307
+ return "", f"Error: {e}", "", "", None, ""
308
 
309
+ # --- Parse JSON response ---
310
  try:
311
+ data = _extract_json(raw)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
  song_title = data.get("song_title", "Untitled")
314
  style_prompt = data.get("style_prompt", "")
 
320
  si_reason = data.get("style_influence_reasoning", "")
321
  settings = f"Weirdness: {w}/100\n{w_reason}\n\nStyle Influence: {si}/100\n{si_reason}"
322
 
323
+ cover_art_text = data.get("cover_art_prompt", "")
324
 
325
+ # Generate cover art image (failure is silent, never blocks text outputs)
326
+ cover_art_image = _generate_cover_image(cover_art_text)
327
+
328
+ return song_title, style_prompt, lyrics, settings, cover_art_image, cover_art_text
329
 
330
  except json.JSONDecodeError:
331
  return (
 
333
  f"[JSON parse error - raw response below]\n\n{raw}",
334
  "",
335
  "",
336
+ None,
337
  "",
338
  )
339
  except Exception as e:
340
+ return "", f"Error: {e}", "", "", None, ""
341
 
342
 
343
  def toggle_custom_visibility(choice):
 
345
  return gr.update(visible=(choice == "Custom"))
346
 
347
 
348
+ def toggle_free_mode(free_mode: bool):
349
+ """When Free Mode is checked, gray out API key and model dropdown."""
350
+ return (
351
+ gr.update(interactive=not free_mode), # api_key_input
352
+ gr.update(interactive=not free_mode), # model_dropdown
353
+ )
354
+
355
+
356
  # ─────────────────────────────────────────────
357
  # APP BUILDER
358
  # ─────────────────────────────────────────────
 
363
  gr.Markdown(
364
  "# Suno Prompt Generator <span style='font-size:0.45em; font-weight:normal; color:#999;'>by AnimalMonk</span>\n"
365
  "This is meant to spark new ideas or get you a starting point. Take what it gives you and make it goldensome!\n\n"
366
+ "Works with [OpenRouter](https://openrouter.ai/keys) and "
367
+ "[ElectronHub](https://api.electronhub.ai) API keys. "
368
+ "Your key is sent directly to the provider and is never stored. "
369
+ "Or check **Free Mode** below to try it without any key!"
370
  )
371
 
372
  with gr.Accordion("API Key", open=True):
373
  api_key_input = gr.Textbox(
374
+ label="API Key (OpenRouter or ElectronHub)",
375
+ placeholder="sk-or-v1-... or ek-...",
376
  type="password",
377
  lines=1,
378
  )
379
 
380
+ free_mode_checkbox = gr.Checkbox(
381
+ label="Free Mode",
382
+ value=False,
383
+ )
384
+
385
  with gr.Row():
386
  model_dropdown = gr.Dropdown(
387
  choices=list(MODELS.keys()),
 
445
 
446
  gr.Markdown("---")
447
 
448
+ gr.Markdown("### Cover Art")
449
+ with gr.Row():
450
+ with gr.Column(scale=1):
451
+ cover_art_image = gr.Image(
452
+ label="Generated Cover Art",
453
+ type="filepath",
454
+ interactive=False,
455
+ height=512,
456
+ )
457
+ with gr.Column(scale=1):
458
+ cover_art_output = gr.Textbox(
459
+ label="Cover Art Image Prompt (paste into Grok or image generator)",
460
+ lines=6,
461
+ buttons=["copy"],
462
+ interactive=False,
463
+ )
464
 
465
  # Events
466
  model_dropdown.change(
 
469
  outputs=custom_model_input,
470
  )
471
 
472
+ free_mode_checkbox.change(
473
+ fn=toggle_free_mode,
474
+ inputs=free_mode_checkbox,
475
+ outputs=[api_key_input, model_dropdown],
476
+ )
477
+
478
+ outputs = [title_output, style_output, lyrics_output, settings_output, cover_art_image, cover_art_output]
479
+ inputs = [api_key_input, song_input, model_dropdown, custom_model_input, weirdness_slider, free_mode_checkbox]
480
 
481
  generate_btn.click(
482
  fn=generate_prompt,
 
501
  demo, _theme = create_app()
502
 
503
  if __name__ == "__main__":
504
+ import sys
505
+ import time
506
+ import threading
507
+ import webbrowser
508
+
509
  load_dotenv() # Load .env for local dev
510
+
511
+ _server_url = "http://127.0.0.1:7860"
512
+ _shutdown = threading.Event()
513
+
514
+ # ── System tray icon ──
515
+ def _start_tray():
516
+ try:
517
+ import pystray
518
+ from PIL import Image, ImageDraw
519
+
520
+ # Create a small orange icon with "S" on it
521
+ img = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
522
+ draw = ImageDraw.Draw(img)
523
+ draw.rounded_rectangle([4, 4, 60, 60], radius=12, fill="#FF8C00")
524
+ draw.text((20, 12), "S", fill="white")
525
+
526
+ def on_open(icon, item):
527
+ webbrowser.open(_server_url)
528
+
529
+ def on_quit(icon, item):
530
+ print("πŸ›‘ Quit from system tray.")
531
+ _shutdown.set()
532
+ icon.stop()
533
+
534
+ icon = pystray.Icon(
535
+ "suno_prompt_gen",
536
+ img,
537
+ "Suno Prompt Generator",
538
+ menu=pystray.Menu(
539
+ pystray.MenuItem("Open in Browser", on_open, default=True),
540
+ pystray.MenuItem("Quit", on_quit),
541
+ ),
542
+ )
543
+ icon.run()
544
+ except Exception as e:
545
+ print(f"⚠️ System tray unavailable: {e}")
546
+
547
+ tray_thread = threading.Thread(target=_start_tray, daemon=True)
548
+ tray_thread.start()
549
+
550
+ # ── Server loop ──
551
+ first_launch = True
552
+ while not _shutdown.is_set():
553
+ try:
554
+ print(f"{'πŸš€ Launching' if first_launch else 'πŸ”„ Relaunching'} Suno Prompt Generator...")
555
+ demo, _theme = create_app()
556
+ demo.launch(inbrowser=first_launch, theme=_theme, quiet=False)
557
+ # If launch() returns, the server stopped
558
+ if not _shutdown.is_set():
559
+ print("⚠️ Gradio server stopped. Restarting in 3s...")
560
+ except Exception as e:
561
+ if not _shutdown.is_set():
562
+ print(f"❌ Gradio crashed: {e}. Restarting in 3s...")
563
+ first_launch = False
564
+ if not _shutdown.is_set():
565
+ time.sleep(3)
566
+
567
+ print("πŸ‘‹ Suno Prompt Generator shut down.")
568
+ sys.exit(0)
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  gradio
2
  openai
3
  python-dotenv
 
4
 
5
  # Build-time only (not needed at runtime):
6
  # pip install pyinstaller
 
1
  gradio
2
  openai
3
  python-dotenv
4
+ requests
5
 
6
  # Build-time only (not needed at runtime):
7
  # pip install pyinstaller