File size: 6,894 Bytes
957256e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
"""
Music Service
High-level service for music generation operations.
Abstracts all API complexity from the UI layer.
"""
from typing import Callable, Optional, List
from dataclasses import dataclass, field
from ..api.client import StackNetClient, MediaAction
@dataclass
class MusicClip:
"""Generated music clip."""
title: str
audio_url: str
audio_path: Optional[str] = None
duration: Optional[str] = None
image_url: Optional[str] = None
video_url: Optional[str] = None
tags: List[str] = field(default_factory=list)
@dataclass
class StemResult:
"""Extracted audio stems."""
vocals_path: Optional[str] = None
drums_path: Optional[str] = None
bass_path: Optional[str] = None
other_path: Optional[str] = None
class MusicService:
"""
Service for music generation and manipulation.
Provides clean interfaces for:
- Text-to-music generation
- Cover song creation
- Stem extraction
"""
def __init__(self, client: Optional[StackNetClient] = None):
self.client = client or StackNetClient()
async def generate_music(
self,
prompt: str,
title: Optional[str] = None,
tags: Optional[str] = None,
lyrics: Optional[str] = None,
instrumental: bool = False,
on_progress: Optional[Callable[[float, str], None]] = None
) -> List[MusicClip]:
"""
Generate original music from a text prompt.
Args:
prompt: Description of desired music
title: Optional song title
tags: Optional genre/style tags (comma-separated)
lyrics: Optional lyrics (ignored if instrumental=True)
instrumental: Generate instrumental only
on_progress: Callback for progress updates
Returns:
List of generated MusicClip objects
"""
options = {}
if tags:
options["tags"] = tags
if title:
options["title"] = title
if instrumental:
options["make_instrumental"] = True
if lyrics and not instrumental:
options["lyrics"] = lyrics
result = await self.client.submit_media_task(
action=MediaAction.GENERATE_MUSIC,
prompt=prompt,
options=options if options else None,
on_progress=on_progress
)
if not result.success:
raise Exception(result.error or "Music generation failed")
return self._parse_music_result(result.data)
async def create_cover(
self,
audio_url: str,
style_prompt: str,
title: Optional[str] = None,
tags: Optional[str] = None,
on_progress: Optional[Callable[[float, str], None]] = None
) -> List[MusicClip]:
"""
Create a cover version of audio.
Args:
audio_url: URL to source audio
style_prompt: Style/voice direction for the cover
title: Optional title for the cover
tags: Optional genre/style tags
on_progress: Progress callback
Returns:
List of generated cover clips
"""
options = {}
if tags:
options["tags"] = tags
if title:
options["title"] = title
result = await self.client.submit_media_task(
action=MediaAction.CREATE_COVER,
audio_url=audio_url,
prompt=style_prompt,
options=options if options else None,
on_progress=on_progress
)
if not result.success:
raise Exception(result.error or "Cover creation failed")
return self._parse_music_result(result.data)
async def extract_stems(
self,
audio_url: str,
on_progress: Optional[Callable[[float, str], None]] = None
) -> StemResult:
"""
Extract stems (vocals, drums, bass, other) from audio.
Args:
audio_url: URL to source audio
on_progress: Progress callback
Returns:
StemResult with paths to each stem
"""
result = await self.client.submit_media_task(
action=MediaAction.EXTRACT_STEMS,
audio_url=audio_url,
on_progress=on_progress
)
if not result.success:
raise Exception(result.error or "Stem extraction failed")
stems_data = result.data.get("stems", result.data)
stem_result = StemResult()
# Download each stem if URL provided
if stems_data.get("vocals"):
stem_result.vocals_path = await self.client.download_file(
stems_data["vocals"], "vocals.mp3"
)
if stems_data.get("drums"):
stem_result.drums_path = await self.client.download_file(
stems_data["drums"], "drums.mp3"
)
if stems_data.get("bass"):
stem_result.bass_path = await self.client.download_file(
stems_data["bass"], "bass.mp3"
)
if stems_data.get("other"):
stem_result.other_path = await self.client.download_file(
stems_data["other"], "other.mp3"
)
return stem_result
def _parse_music_result(self, data: dict) -> List[MusicClip]:
"""Parse API response into MusicClip objects."""
clips = []
# Handle various response formats
raw_clips = data.get("clips", [])
# If no clips array, treat the data itself as a single clip
if not raw_clips:
if data.get("audio_url") or data.get("audioUrl"):
raw_clips = [data]
elif data.get("url"):
raw_clips = [{"audio_url": data["url"], "title": data.get("title", "Generated")}]
for clip_data in raw_clips:
audio_url = clip_data.get("audio_url") or clip_data.get("audioUrl") or clip_data.get("url")
if audio_url:
clips.append(MusicClip(
title=clip_data.get("title", "Generated Music"),
audio_url=audio_url,
duration=clip_data.get("duration"),
image_url=clip_data.get("image_url") or clip_data.get("imageUrl"),
video_url=clip_data.get("video_url") or clip_data.get("videoUrl"),
tags=clip_data.get("tags", [])
))
return clips
async def download_clip(self, clip: MusicClip) -> str:
"""Download a clip's audio to local file."""
if clip.audio_path:
return clip.audio_path
filename = f"{clip.title.replace(' ', '_')[:30]}.mp3"
clip.audio_path = await self.client.download_file(clip.audio_url, filename)
return clip.audio_path
def cleanup(self):
"""Clean up temporary files."""
self.client.cleanup()
|