thai commited on
Commit
4143208
Β·
1 Parent(s): a5c2bfd

Initial commit of Lo-fi album app

Browse files
Files changed (3) hide show
  1. app.py +10 -0
  2. lofi_mix.py +712 -0
  3. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from lofi_mix import create_gradio_interface
3
+
4
+ def main():
5
+ interface = create_gradio_interface()
6
+ interface.launch()
7
+
8
+ if __name__ == "__main__":
9
+ main()
10
+
lofi_mix.py ADDED
@@ -0,0 +1,712 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional, Any, Dict, TypedDict, Annotated
2
+ from pydantic import BaseModel, Field
3
+ import pandas as pd
4
+ import csv
5
+ import io
6
+ import os
7
+ # from dataclasses import dataclass
8
+ # from enum import Enum
9
+
10
+ from langgraph.graph import StateGraph, START, END
11
+ from langgraph.graph.message import add_messages
12
+ from langchain_google_genai import ChatGoogleGenerativeAI
13
+ # from langchain_openai import ChatOpenAI
14
+ from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
15
+ import gradio as gr
16
+
17
+ # Enhanced State Models
18
+ class StyleData(BaseModel):
19
+ """Enhanced style data with additional context for better song generation"""
20
+ genre: List[str] = Field(description="Primary musical genres (e.g. lo-fi hip hop, jazzhop, chillhop)")
21
+ mood: List[str] = Field(description="Emotional tones (e.g. nostalgic, dreamy, melancholic, peaceful)")
22
+ instruments: List[str] = Field(description="Key instruments (e.g. piano, vinyl crackle, guitar, synth, drums)")
23
+ concept: str = Field(description="Artistic or thematic concept summary")
24
+ bpm_range: str = Field(description="Tempo range (e.g. 65-80 BPM)")
25
+ audio_texture: str = Field(description="Sound characteristics (e.g. warm, grainy, cassette-like)")
26
+ culture_or_season: Optional[str] = Field(description="Cultural reference or seasonal theme")
27
+
28
+ # New fields for better context
29
+ time_of_day: Optional[str] = Field(description="Preferred listening time (e.g. late night, morning, sunset)")
30
+ activity_context: Optional[str] = Field(description="Activity context (e.g. studying, relaxing, working)")
31
+ reference_artists: List[str] = Field(default=[], description="Similar artists or style references")
32
+
33
+ class SunoPrompt(BaseModel):
34
+ """Enhanced Suno prompt with better structure"""
35
+ song_name: str = Field(description="Catchy, descriptive title reflecting theme/vibe")
36
+ genre: str = Field(description="Specific genre/sub-genre")
37
+ song_prompt: str = Field(description="Detailed prompt for SunoAI including style, mood, instruments, atmosphere")
38
+ # tags: List[str] = Field(description="Relevant tags for discoverability")
39
+ # duration_hint: Optional[str] = Field(description="Suggested duration (e.g. '2-3 minutes')")
40
+
41
+ class Album(BaseModel):
42
+ """Collection of songs forming a cohesive album"""
43
+ album_name: str = Field(description="Overall album/playlist name")
44
+ theme: str = Field(description="Unifying theme across all tracks")
45
+ songs: List[SunoPrompt] = Field(description="List of song prompts")
46
+ track_count: int = Field(description="Total number of tracks")
47
+
48
+ class YouTubeContent(BaseModel):
49
+ """YouTube-optimized content for each song"""
50
+ cover_image_prompt: str = Field(description="AI image generation prompt for cover art")
51
+ title: str = Field(description="YouTube-optimized title with keywords")
52
+ description: str = Field(description="Detailed description with tags and timestamps")
53
+ tags: List[str] = Field(description="YouTube tags for discoverability")
54
+ thumbnail_elements: List[str] = Field(description="Key visual elements for thumbnail")
55
+
56
+ class State(TypedDict):
57
+ """Enhanced state with better tracking and error handling"""
58
+ user_input: str
59
+ processing_stage: str # Track current processing stage
60
+ style_data: Optional[StyleData]
61
+ album: Optional[Album]
62
+ number_of_song: int
63
+ youtube_content: List[YouTubeContent]
64
+ messages: Annotated[List[Any], add_messages]
65
+ errors: List[str] # Track any errors
66
+ metadata: Dict[str, Any] # Additional metadata
67
+
68
+ # Agent Implementation
69
+ class LoFiSongGenerator:
70
+ def __init__(self, api_key: str = None):
71
+ self.api_key = api_key
72
+ self.llm = None
73
+ self.graph = None
74
+ if api_key:
75
+ self._initialize_llm()
76
+
77
+ def _initialize_llm(self):
78
+ """Initialize the LLM with the provided API key"""
79
+ self.llm = ChatGoogleGenerativeAI(
80
+ model="gemini-2.5-flash-preview-04-17",
81
+ temperature=0.7,
82
+ google_api_key=self.api_key
83
+ )
84
+ self.graph = self._build_graph()
85
+
86
+ def set_api_key(self, api_key: str):
87
+ """Set a new API key and reinitialize the LLM"""
88
+ self.api_key = api_key
89
+ self._initialize_llm()
90
+
91
+ def _build_graph(self) -> StateGraph:
92
+ """Build the LangGraph workflow"""
93
+ workflow = StateGraph(State)
94
+
95
+ # Add nodes
96
+ workflow.add_node("extract_style", self.extract_style_agent)
97
+ workflow.add_node("generate_songs", self.song_write_agent)
98
+ workflow.add_node("create_youtube_content", self.cover_title_agent)
99
+ workflow.add_node("compile_output", self.compile_output)
100
+
101
+ # Add edges
102
+ workflow.add_edge(START, "extract_style")
103
+ workflow.add_edge("extract_style", "generate_songs")
104
+ workflow.add_edge("generate_songs", "create_youtube_content")
105
+ workflow.add_edge("create_youtube_content", "compile_output")
106
+ workflow.add_edge("compile_output", END)
107
+
108
+ return workflow.compile()
109
+
110
+ def extract_style_agent(self, state: State) -> State:
111
+ """Agent 1: Extract style information from user input"""
112
+ try:
113
+ state["processing_stage"] = "Extracting style information..."
114
+
115
+ system_prompt = """You are a music style analysis expert specializing in lo-fi and chill music genres.
116
+ Analyze the user's input and extract detailed style information that will guide song generation.
117
+
118
+ You MUST respond with a valid JSON object containing the following fields:
119
+ {
120
+ "genre": ["list of genres like lo-fi hip hop, jazzhop, chillhop"],
121
+ "mood": ["list of moods like nostalgic, dreamy, melancholic"],
122
+ "instruments": ["list of instruments like piano, vinyl, guitar, synth"],
123
+ "concept": "short description of the artistic concept",
124
+ "bpm_range": "tempo range like 65-80 BPM",
125
+ "audio_texture": "sound characteristics like warm, grainy, cassette-like",
126
+ "culture_or_season": "cultural or seasonal theme (optional)",
127
+ "time_of_day": "preferred listening time (optional)",
128
+ "activity_context": "activity context like studying, relaxing (optional)",
129
+ "reference_artists": ["list of similar artists or references"]
130
+ }
131
+
132
+ Analyze the user's request and extract all relevant musical style information.
133
+ Be creative and detailed in your analysis but ensure valid JSON format."""
134
+
135
+ user_message = f"Analyze this music style request and return JSON: {state['user_input']}"
136
+
137
+ messages = [
138
+ SystemMessage(content=system_prompt),
139
+ HumanMessage(content=user_message)
140
+ ]
141
+
142
+ response = self.llm.invoke(messages)
143
+
144
+ # Parse the actual LLM response
145
+ style_data = self._parse_style_response(response.content)
146
+
147
+ state["style_data"] = style_data
148
+ state["messages"].append(AIMessage(content=f"Style analysis complete: {style_data.concept}"))
149
+
150
+ except Exception as e:
151
+ state["errors"].append(f"Style extraction error: {str(e)}")
152
+
153
+ return state
154
+
155
+ def song_write_agent(self, state: State) -> State:
156
+ """Agent 2: Generate song prompts based on style data"""
157
+ try:
158
+ state["processing_stage"] = "Generating song prompts..."
159
+
160
+ if not state.get("style_data"):
161
+ raise ValueError("No style data available")
162
+
163
+ style_data = state["style_data"]
164
+
165
+ system_prompt = f"""You are a creative music producer specializing in lo-fi beats and chill music.
166
+ Generate {state["number_of_song"]} unique song prompts that form a cohesive album based on the provided style data.
167
+
168
+ You MUST respond with a valid JSON object in this format:
169
+ {{
170
+ "album_name": "Name of the album/playlist",
171
+ "theme": "Unifying theme description",
172
+ "track_count": {state["number_of_song"]},
173
+ "songs": [
174
+ {{
175
+ "song_name": "Title of the song",
176
+ "genre": "Specific genre",
177
+ "song_prompt": "Detailed Suno AI prompt with instruments, mood, atmosphere"
178
+ }}
179
+ ]
180
+ }}
181
+
182
+ Each song should:
183
+ - Have a unique but related theme
184
+ - Songs must be ordered by semantic proximity to the input style or mood.
185
+ - Describe the mood and atmosphere clearly without labeling them as 'Genre:', 'Mood:', or other headers
186
+ - Avoid repetition
187
+ - Be optimized for SunoAI generation
188
+ - Vary slightly in tempo and energy while maintaining cohesion
189
+
190
+ """
191
+
192
+ style_summary = f"""
193
+ Genre: {', '.join(style_data.genre)}
194
+ Mood: {', '.join(style_data.mood)}
195
+ Instruments: {', '.join(style_data.instruments)}
196
+ Concept: {style_data.concept}
197
+ BPM: {style_data.bpm_range}
198
+ Texture: {style_data.audio_texture}
199
+ Context: {style_data.culture_or_season or 'General'}
200
+ Time: {style_data.time_of_day or 'Anytime'}
201
+ Activity: {style_data.activity_context or 'General listening'}
202
+ """
203
+
204
+ user_message = f"Generate a cohesive lo-fi album JSON based on this style analysis:\n{style_summary}"
205
+
206
+ messages = [
207
+ SystemMessage(content=system_prompt),
208
+ HumanMessage(content=user_message)
209
+ ]
210
+
211
+ response = self.llm.invoke(messages)
212
+
213
+ # Parse the actual LLM response
214
+ album = self._parse_album_response(response.content)
215
+
216
+ state["album"] = album
217
+ state["messages"].append(AIMessage(content=f"Generated {len(album.songs)} songs for album: {album.album_name}"))
218
+
219
+ except Exception as e:
220
+ state["errors"].append(f"Song generation error: {str(e)}")
221
+
222
+ return state
223
+
224
+ def cover_title_agent(self, state: State) -> State:
225
+ """Agent 3: Generate YouTube titles, descriptions, and cover art prompts"""
226
+ try:
227
+ state["processing_stage"] = "Creating YouTube content..."
228
+
229
+ if not state.get("album"):
230
+ raise ValueError("No album data available")
231
+
232
+ album = state["album"]
233
+
234
+ system_prompt = """You are a YouTube content optimization expert specializing in music content.
235
+ For the provided album, create YouTube content for that album.
236
+
237
+ You MUST respond with a valid JSON array in this format:
238
+ [
239
+ {
240
+ "cover_image_prompt": "Detailed AI image generation prompt for cover art",
241
+ "title": "SEO-optimized YouTube title with keywords",
242
+ "description": "Detailed description with hashtags and context",
243
+ "tags": ["youtube", "tags", "for", "discoverability"],
244
+ "thumbnail_elements": ["key", "visual", "elements"]
245
+ }
246
+ ]
247
+
248
+ You should:
249
+ 1. SEO-optimized title that includes relevant keywords
250
+ 2. Detailed description with hashtags and timestamps
251
+ 3. AI image generation prompt for cover art
252
+ 4. List of YouTube tags for discoverability
253
+ 5. Thumbnail design elements
254
+ 6. Think 3 prompts difference
255
+
256
+ Focus on lo-fi, chill, study music, and relaxation keywords.
257
+ Make content discoverable but authentic."""
258
+
259
+ album_info = f"""
260
+ Album: {album.album_name}
261
+ Theme: {album.theme}
262
+ Songs: {[{'name': song.song_name, 'genre': song.genre} for song in album.songs]}
263
+ """
264
+
265
+ user_message = f"Create YouTube content JSON array for this album:\n{album_info}"
266
+
267
+ messages = [
268
+ SystemMessage(content=system_prompt),
269
+ HumanMessage(content=user_message)
270
+ ]
271
+
272
+ response = self.llm.invoke(messages)
273
+
274
+ # Parse the actual LLM response
275
+ youtube_content = self._parse_youtube_response(response.content)
276
+
277
+ state["youtube_content"] = youtube_content
278
+ state["messages"].append(AIMessage(content=f"Created YouTube content for {len(youtube_content)} songs"))
279
+
280
+ except Exception as e:
281
+ state["errors"].append(f"YouTube content generation error: {str(e)}")
282
+
283
+ return state
284
+
285
+ def compile_output(self, state: State) -> State:
286
+ """Compile final output into two separate CSV files"""
287
+ try:
288
+ state["processing_stage"] = "Compiling final output..."
289
+
290
+ if not state.get("album") or not state.get("youtube_content"):
291
+ raise ValueError("Missing required data for compilation")
292
+
293
+ album = state["album"]
294
+ youtube_content = state["youtube_content"]
295
+
296
+ # Create Album/Song CSV data
297
+ album_data = []
298
+ for i, song in enumerate(album.songs):
299
+ album_data.append({
300
+ "track_number": i + 1,
301
+ "song_name": song.song_name,
302
+ "genre": song.genre,
303
+ "suno_prompt": song.song_prompt
304
+ })
305
+
306
+ # Create YouTube CSV data
307
+ youtube_text_output = ""
308
+ for i, yt in enumerate(youtube_content, 1):
309
+ youtube_text_output += f"ID {i}:\n"
310
+ youtube_text_output += f"Title: {yt.title}\n"
311
+ youtube_text_output += f"Description: {yt.description}\n"
312
+ youtube_text_output += f"Tags: {', '.join(yt.tags)}\n"
313
+ youtube_text_output += f"Cover Image Prompt: {yt.cover_image_prompt}\n"
314
+ youtube_text_output += f"Thumbnail Elements: {', '.join(yt.thumbnail_elements)}\n"
315
+ youtube_text_output += "\n"
316
+ youtube_text_output += "-----------------------------------------------------------"
317
+ youtube_text_output += "\n"
318
+
319
+ # Get the directory where the app is running
320
+ app_dir = os.path.dirname(os.path.abspath(__file__))
321
+
322
+ # Sanitize album name for filename
323
+ import re
324
+ sanitized_album_name = re.sub(r'[<>:"/\\|?*]', '_', album.album_name)
325
+ sanitized_album_name = sanitized_album_name.strip()
326
+
327
+ # Define file paths with proper extension
328
+ album_filename = f"{sanitized_album_name}_songs.csv"
329
+ youtube_filename = f"{sanitized_album_name}_youtube.txt"
330
+
331
+ album_filepath = os.path.join(app_dir, album_filename)
332
+ youtube_filepath = os.path.join(app_dir, youtube_filename)
333
+
334
+ # Create StringIO objects for CSV content
335
+ album_csv_content = io.StringIO()
336
+
337
+ # Write Album CSV content to StringIO
338
+ if album_data:
339
+ album_fieldnames = album_data[0].keys()
340
+ album_writer = csv.DictWriter(album_csv_content, fieldnames=album_fieldnames)
341
+ album_writer.writeheader()
342
+ album_writer.writerows(album_data)
343
+
344
+ # Write to file
345
+ with open(album_filepath, 'w', newline='', encoding='utf-8') as csvfile:
346
+ csvfile.write(album_csv_content.getvalue())
347
+
348
+ # Write YouTube text output to file
349
+ if youtube_text_output:
350
+ with open(youtube_filepath, 'w', encoding='utf-8') as txtfile:
351
+ txtfile.write(youtube_text_output)
352
+
353
+ # Store both CSV outputs and file paths in state
354
+ state["metadata"]["album_csv"] = album_csv_content.getvalue()
355
+ state["metadata"]["youtube_txt"] = youtube_text_output
356
+ state["metadata"]["album_csv_file"] = album_filepath
357
+ state["metadata"]["youtube_txt_file"] = youtube_filepath
358
+ state["metadata"]["total_songs"] = len(album_data)
359
+ state["metadata"]["album_name"] = album.album_name
360
+ state["processing_stage"] = "Complete!"
361
+
362
+ except Exception as e:
363
+ state["errors"].append(f"Compilation error: {str(e)}")
364
+ # Clear file paths if there was an error
365
+ state["metadata"]["album_csv_file"] = None
366
+ state["metadata"]["youtube_txt_file"] = None
367
+
368
+ return state
369
+
370
+ # Helper methods for parsing LLM responses
371
+ def _parse_style_response(self, response_content: str) -> StyleData:
372
+ """Parse LLM JSON response into StyleData object"""
373
+ import json
374
+ import re
375
+
376
+ try:
377
+ # Extract JSON from response (handle cases where LLM adds extra text)
378
+ json_match = re.search(r'\{.*\}', response_content, re.DOTALL)
379
+ if json_match:
380
+ json_str = json_match.group()
381
+ data = json.loads(json_str)
382
+
383
+ return StyleData(
384
+ genre=data.get("genre", []),
385
+ mood=data.get("mood", []),
386
+ instruments=data.get("instruments", []),
387
+ concept=data.get("concept", "Lo-fi music"),
388
+ bpm_range=data.get("bpm_range"),
389
+ audio_texture=data.get("audio_texture"),
390
+ culture_or_season=data.get("culture_or_season"),
391
+ time_of_day=data.get("time_of_day"),
392
+ activity_context=data.get("activity_context"),
393
+ reference_artists=data.get("reference_artists", [])
394
+ )
395
+ else:
396
+ raise ValueError("No JSON found in response")
397
+
398
+ except (json.JSONDecodeError, ValueError) as e:
399
+ # Fallback: try to extract information from natural language
400
+ return self._fallback_parse_style(response_content)
401
+
402
+ def _fallback_parse_style(self, content: str) -> StyleData:
403
+ """Fallback parser when JSON parsing fails"""
404
+ # Simple keyword extraction as fallback
405
+ content_lower = content.lower()
406
+
407
+ genres = []
408
+ if "lo-fi" in content_lower or "lofi" in content_lower:
409
+ genres.append("lo-fi hip hop")
410
+ if "jazz" in content_lower:
411
+ genres.append("jazzhop")
412
+ if "chill" in content_lower:
413
+ genres.append("chillhop")
414
+ if not genres:
415
+ genres = ["lo-fi hip hop"]
416
+
417
+ moods = []
418
+ for mood in ["nostalgic", "dreamy", "peaceful", "melancholic", "relaxing", "cozy"]:
419
+ if mood in content_lower:
420
+ moods.append(mood)
421
+ if not moods:
422
+ moods = ["relaxing"]
423
+
424
+ instruments = []
425
+ for instrument in ["piano", "guitar", "vinyl", "drums", "synth", "saxophone"]:
426
+ if instrument in content_lower:
427
+ instruments.append(instrument)
428
+ if not instruments:
429
+ instruments = ["piano", "soft drums"]
430
+
431
+ return StyleData(
432
+ genre=genres,
433
+ mood=moods,
434
+ instruments=instruments,
435
+ concept=f"Musical style based on: {content[:100]}...",
436
+ bpm_range="slow beat BPM",
437
+ audio_texture="warm and atmospheric",
438
+ culture_or_season=None,
439
+ time_of_day=None,
440
+ activity_context=None,
441
+ reference_artists=[]
442
+ )
443
+
444
+ def _parse_album_response(self, response_content: str) -> Album:
445
+ """Parse LLM JSON response into Album object"""
446
+ import json
447
+ import re
448
+
449
+ try:
450
+ # Extract JSON from response
451
+ json_match = re.search(r'\{.*\}', response_content, re.DOTALL)
452
+ if json_match:
453
+ json_str = json_match.group()
454
+ data = json.loads(json_str)
455
+
456
+ songs = []
457
+ for song_data in data.get("songs", []):
458
+ song = SunoPrompt(
459
+ song_name=song_data.get("song_name", "Untitled"),
460
+ genre=song_data.get("genre"),
461
+ song_prompt=song_data.get("song_prompt"),
462
+ # tags=song_data.get("tags", ["lofi", "chill"]),
463
+ # duration_hint=song_data.get("duration_hint", "2-3 minutes")
464
+ )
465
+ songs.append(song)
466
+
467
+ return Album(
468
+ album_name=data.get("album_name", "Lo-Fi Collection"),
469
+ theme=data.get("theme", "Relaxing music"),
470
+ songs=songs,
471
+ track_count=len(songs)
472
+ )
473
+ else:
474
+ raise ValueError("No JSON found in response")
475
+
476
+ except (json.JSONDecodeError, ValueError):
477
+ # Fallback: create a basic album
478
+ return self._fallback_parse_album(response_content)
479
+
480
+ def _fallback_parse_album(self, content: str) -> Album:
481
+ """Fallback parser when JSON parsing fails"""
482
+ # Create basic songs from content
483
+ songs = [
484
+ SunoPrompt(
485
+ song_name=f"Lo-Fi Track {i+1}",
486
+ genre="lo-fi hip hop",
487
+ song_prompt=f"A gentle lo-fi track with piano and soft drums, {content[:50]}...",
488
+ # tags=["lofi", "chill", "relax"],
489
+ # duration_hint="2-3 minutes"
490
+ )
491
+ for i in range(self.state["number_of_song"])
492
+ ]
493
+
494
+ return Album(
495
+ album_name="Generated Lo-Fi Album",
496
+ theme="Relaxing lo-fi music collection",
497
+ songs=songs,
498
+ track_count=self.state["number_of_song"]
499
+ )
500
+
501
+ def _parse_youtube_response(self, response_content: str) -> List[YouTubeContent]:
502
+ """Parse LLM JSON response into list of YouTubeContent objects"""
503
+ import json
504
+ import re
505
+
506
+ try:
507
+ # Extract JSON array from response
508
+ json_match = re.search(r'\[.*\]', response_content, re.DOTALL)
509
+ if json_match:
510
+ json_str = json_match.group()
511
+ data = json.loads(json_str)
512
+
513
+ youtube_content = []
514
+ for item in data:
515
+ yt_content = YouTubeContent(
516
+ cover_image_prompt=item.get("cover_image_prompt", "Lo-fi aesthetic artwork"),
517
+ title=item.get("title", "Lo-Fi Music"),
518
+ description=item.get("description", "Relaxing lo-fi beats"),
519
+ tags=item.get("tags", ["lofi", "chill"]),
520
+ thumbnail_elements=item.get("thumbnail_elements", ["music", "aesthetic"])
521
+ )
522
+ youtube_content.append(yt_content)
523
+
524
+ return youtube_content
525
+ else:
526
+ raise ValueError("No JSON array found in response")
527
+
528
+ except (json.JSONDecodeError, ValueError):
529
+ # Fallback: return basic YouTube content
530
+ return self._fallback_parse_youtube(response_content)
531
+
532
+ def _fallback_parse_youtube(self, content: str) -> List[YouTubeContent]:
533
+ """Fallback parser when JSON parsing fails"""
534
+ # Create basic YouTube content
535
+ return [
536
+ YouTubeContent(
537
+ cover_image_prompt="Aesthetic lo-fi scene with warm lighting and vintage elements",
538
+ title="Lo-Fi Hip Hop Beats | Study & Relaxation Music",
539
+ description="🎡 Relaxing lo-fi hip hop beats perfect for studying, working, or chilling out.\n\n#lofi #chillhop #studymusic #relaxation",
540
+ tags=["lofi", "lo-fi hip hop", "chill beats", "study music", "relaxation"],
541
+ thumbnail_elements=["warm colors", "vintage aesthetic", "musical notes"]
542
+ )
543
+ for _ in range(5) # Default 5 songs
544
+ ]
545
+
546
+ def process(self, user_input: str, number_of_song: int) -> State:
547
+ """Main processing function"""
548
+ initial_state = State(
549
+ user_input=user_input,
550
+ processing_stage="Starting...",
551
+ style_data=None,
552
+ album=None,
553
+ number_of_song=number_of_song,
554
+ youtube_content=[],
555
+ messages=[],
556
+ errors=[],
557
+ metadata={}
558
+ )
559
+
560
+ # Run the graph
561
+ final_state = self.graph.invoke(initial_state)
562
+ return final_state
563
+
564
+ # Gradio Interface
565
+ def create_gradio_interface():
566
+ generator = LoFiSongGenerator()
567
+
568
+ def process_request(user_input, number_of_song, api_key):
569
+ if not api_key.strip():
570
+ return pd.DataFrame(), "", "Please enter your Gemini API key.", None, None
571
+
572
+ if not user_input.strip():
573
+ return pd.DataFrame(), "", "Please enter a style or mood description.", None, None
574
+
575
+ try:
576
+ # Set the API key before processing
577
+ generator.set_api_key(api_key)
578
+
579
+ final_state = generator.process(user_input, number_of_song)
580
+
581
+ # Get CSV outputs from metadata
582
+ album_csv = final_state.get("metadata", {}).get("album_csv", "No album data generated")
583
+ youtube_txt = final_state.get("metadata", {}).get("youtube_txt", "No YouTube data generated")
584
+
585
+ # Get additional info for status
586
+ album_name = final_state.get("metadata", {}).get("album_name", "Unknown")
587
+ total_songs = final_state.get("metadata", {}).get("total_songs", 0)
588
+ status = final_state.get("processing_stage", "Unknown")
589
+
590
+ # Get file paths
591
+ album_csv_file = final_state.get("metadata", {}).get('album_csv_file')
592
+ youtube_txt_file = final_state.get("metadata", {}).get('youtube_txt_file')
593
+
594
+ # Format status with more details
595
+ detailed_status = f"Status: {status}\n"
596
+ detailed_status += f"Album: {album_name}\n"
597
+ detailed_status += f"Total Songs Generated: {total_songs}\n"
598
+
599
+ # Add file paths if available
600
+ if album_csv_file:
601
+ detailed_status += f"Album CSV saved to: {album_csv_file}\n"
602
+ if youtube_txt_file:
603
+ detailed_status += f"YouTube CSV saved to: {youtube_txt_file}\n"
604
+
605
+ # Add any errors
606
+ if final_state.get("errors"):
607
+ detailed_status += f"\nErrors encountered: {', '.join(final_state['errors'])}"
608
+
609
+ # Convert CSV string to DataFrame for Gradio Dataframe component
610
+ import pandas as pd
611
+ import io
612
+
613
+ # Parse the CSV string into a DataFrame
614
+ df = pd.read_csv(io.StringIO(album_csv))
615
+
616
+ # Return the DataFrame and other outputs
617
+ return df, youtube_txt, detailed_status, album_csv_file, youtube_txt_file
618
+
619
+ except Exception as e:
620
+ return pd.DataFrame(), "", f"Error: {str(e)}", None, None
621
+
622
+ # Create Gradio interface
623
+ def make_interface():
624
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
625
+ gr.Markdown("# 🎡 Lo-Fi Song Generator")
626
+ gr.Markdown("Generate Suno lo-fi prompts with YouTube-ready content using AI agents!")
627
+
628
+ with gr.Column():
629
+ api_key = gr.Textbox(
630
+ label="Gemini API Key",
631
+ placeholder="Enter your Gemini API key here",
632
+ type="password",
633
+ info="Get your API key from https://makersuite.google.com/app/apikey"
634
+ )
635
+ user_input = gr.Textbox(
636
+ label="Style or Mood Input",
637
+ placeholder="Enter the style, mood, or theme you want (e.g., 'nostalgic Japanese summer evening', 'cozy winter study session', 'dreamy midnight vibes')",
638
+ lines=3
639
+ )
640
+ num_songs = gr.Slider(minimum=1, maximum=30, value=3, step=1, label="Number of Songs")
641
+
642
+ generate_btn = gr.Button("Generate Lo-fi Album")
643
+
644
+ gr.Markdown("### 🌟 Examples")
645
+ gr.Examples(
646
+ examples=[
647
+ ["Nostalgic Japanese summer evening with cicadas and gentle rain"],
648
+ ["Cozy winter study session with warm piano and crackling fireplace"],
649
+ ["Dreamy midnight cityscape with neon lights and soft jazz"],
650
+ ["Autumn morning coffee shop vibes with acoustic guitar"],
651
+ ["Peaceful forest walk with birds chirping and gentle breeze"]
652
+ ],
653
+ inputs=user_input
654
+ )
655
+
656
+ gr.Markdown("### 🎼 Album Content")
657
+ album_output = gr.Dataframe(
658
+ label="Album/Songs Output",
659
+ headers=["Track", "Title", "Genre", "Suno AI Prompt"],
660
+ row_count=15,
661
+ col_count=(4, "fixed"),
662
+ interactive=False,
663
+ wrap=True
664
+ )
665
+
666
+ gr.Markdown("### πŸ“Ί YouTube Metadata")
667
+ youtube_output = gr.Textbox(
668
+ label="YouTube Content Output",
669
+ lines=15,
670
+ max_lines=25,
671
+ info="Contains YouTube titles, descriptions, tags, and cover art prompts"
672
+ )
673
+
674
+ gr.Markdown("### βš™οΈ Status & Logs")
675
+ status_output = gr.Textbox(
676
+ label="Processing Status & Details",
677
+ lines=5,
678
+ max_lines=15,
679
+ info="Shows processing status, file paths, and any errors"
680
+ )
681
+
682
+ with gr.Row():
683
+ csv_output = gr.File(
684
+ label="Download Album CSV",
685
+ file_types=[".csv"],
686
+ type="filepath",
687
+ interactive=False
688
+ )
689
+ txt_output = gr.File(
690
+ label="Download YouTube TXT",
691
+ file_types=[".txt"],
692
+ type="filepath",
693
+ interactive=False
694
+ )
695
+
696
+ generate_btn.click(
697
+ fn=process_request,
698
+ inputs=[user_input, num_songs, api_key],
699
+ outputs=[album_output, youtube_output, status_output, csv_output, txt_output]
700
+ )
701
+
702
+ return demo
703
+
704
+ interface = make_interface()
705
+
706
+ return interface
707
+
708
+ # Main execution
709
+ if __name__ == "__main__":
710
+ # Create and launch the Gradio interface
711
+ interface = create_gradio_interface()
712
+ interface.launch()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ langgraph>=0.0.15
2
+ langchain-google-genai>=0.0.5
3
+ langchain-core>=0.1.10
4
+ pandas>=2.0.0
5
+ gradio>=4.0.0
6
+ pydantic>=2.0.0