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()