hwonder commited on
Commit
957256e
·
0 Parent(s):

Initial StackNet Demo for Hugging Face Spaces

Browse files

- Text to Music generation
- Music to Music (diffusion from reference)
- Stem extraction
- Text to Image
- Image to Image editing
- Text to Video
- Image to Video animation

.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # STACKNET Configuration
2
+ STACKNET_NETWORK_URL=https://geoffnet.magma-rpc.com
3
+ STACKNET_SERVICE_KEY=gn_3a2f41ba3e33428f9dcedf54280e9ad6
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .env
5
+ venv/
6
+ .venv/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .DS_Store
README.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: StackNet 1:1 Preview Playground
3
+ emoji: 🎵
4
+ colorFrom: purple
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 5.0.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # StackNet 1:1 Preview Playground
14
+
15
+ Explore AI-powered media generation with StackNet:
16
+
17
+ - **Text to Music** - Generate original music from descriptions
18
+ - **Music to Music** - Create from reference audio (diffusion)
19
+ - **Extract Stems** - Separate audio into vocals, drums, bass, other
20
+ - **Text to Image** - Generate images from descriptions
21
+ - **Image to Image** - Transform and edit images
22
+ - **Text to Video** - Generate videos from descriptions
23
+ - **Image to Video** - Animate static images
24
+
25
+ ## Usage
26
+
27
+ 1. Enter your StackNet API key in the Settings section
28
+ 2. Select a tab for the type of content you want to create
29
+ 3. Enter your prompt and click generate
30
+
31
+ ## API Key
32
+
33
+ Get your StackNet API key from [stacknet.ai](https://stacknet.ai)
app.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ StackNet Demo
3
+
4
+
5
+ A Gradio-based demo showcasing StackNet's capabilities:
6
+ - Text-to-Music
7
+ - Music-to-Music (Cover Songs, Stem Extraction)
8
+ - Text-to-Image
9
+ - Image-to-Image
10
+ - Text-to-Video
11
+ - Image-to-Video
12
+
13
+ """
14
+
15
+ import gradio as gr
16
+
17
+ from src.ui.tabs import create_all_tabs
18
+ from src.ui.handlers import Handlers
19
+
20
+
21
+ def create_demo() -> gr.Blocks:
22
+ """Create the complete Gradio demo application."""
23
+
24
+ with gr.Blocks(
25
+ title="StackNet Demo",
26
+ ) as demo:
27
+
28
+ gr.Markdown("""
29
+ # StackNet Demo 1:1 Preview
30
+
31
+ """)
32
+
33
+ with gr.Accordion("Settings", open=False):
34
+ api_key = gr.Textbox(
35
+ label="StackNet Key",
36
+ placeholder="Enter your key (e.g., sn_xxxx...)",
37
+ type="password",
38
+ value=""
39
+ )
40
+
41
+ with gr.Tabs():
42
+ tabs = create_all_tabs()
43
+
44
+ # Wire up event handlers (all use api_name=None to hide from API)
45
+
46
+ # Text to Music
47
+ ttm = tabs["text_to_music"]
48
+ ttm["generate_btn"].click(
49
+ fn=Handlers.generate_music,
50
+ inputs=[
51
+ ttm["prompt"],
52
+ ttm["tags"],
53
+ ttm["instrumental"],
54
+ ttm["lyrics"],
55
+ ttm["title"],
56
+ api_key
57
+ ],
58
+ outputs=[ttm["output_audio"], ttm["status"]],
59
+ api_name=None
60
+ )
61
+
62
+ # Music to Music - Cover
63
+ mtm = tabs["music_to_music"]
64
+ mtm["cover_btn"].click(
65
+ fn=Handlers.create_cover,
66
+ inputs=[
67
+ mtm["cover_audio_input"],
68
+ mtm["cover_style_prompt"],
69
+ mtm["cover_tags"],
70
+ mtm["cover_title"],
71
+ api_key
72
+ ],
73
+ outputs=[mtm["cover_output"], mtm["cover_status"]],
74
+ api_name=None
75
+ )
76
+
77
+ # Music to Music - Stems
78
+ mtm["stems_btn"].click(
79
+ fn=Handlers.extract_stems,
80
+ inputs=[mtm["stems_audio_input"], api_key],
81
+ outputs=[
82
+ mtm["vocals_output"],
83
+ mtm["drums_output"],
84
+ mtm["bass_output"],
85
+ mtm["other_output"],
86
+ mtm["stems_status"]
87
+ ],
88
+ api_name=None
89
+ )
90
+
91
+ # Text to Image
92
+ tti = tabs["text_to_image"]
93
+ tti["generate_btn"].click(
94
+ fn=Handlers.generate_image,
95
+ inputs=[
96
+ tti["prompt"],
97
+ tti["style"],
98
+ tti["aspect_ratio"],
99
+ api_key
100
+ ],
101
+ outputs=[tti["output_image"], tti["status"]],
102
+ api_name=None
103
+ )
104
+
105
+ # Image to Image
106
+ iti = tabs["image_to_image"]
107
+ iti["edit_btn"].click(
108
+ fn=Handlers.edit_image,
109
+ inputs=[
110
+ iti["input_image"],
111
+ iti["edit_prompt"],
112
+ iti["strength"],
113
+ api_key
114
+ ],
115
+ outputs=[iti["output_image"], iti["status"]],
116
+ api_name=None
117
+ )
118
+
119
+ # Text to Video
120
+ ttv = tabs["text_to_video"]
121
+ ttv["generate_btn"].click(
122
+ fn=Handlers.generate_video,
123
+ inputs=[
124
+ ttv["prompt"],
125
+ ttv["duration"],
126
+ ttv["style"],
127
+ api_key
128
+ ],
129
+ outputs=[ttv["output_video"], ttv["status"]],
130
+ api_name=None
131
+ )
132
+
133
+ # Image to Video
134
+ itv = tabs["image_to_video"]
135
+ itv["animate_btn"].click(
136
+ fn=Handlers.animate_image,
137
+ inputs=[
138
+ itv["input_image"],
139
+ itv["motion_prompt"],
140
+ itv["duration"],
141
+ api_key
142
+ ],
143
+ outputs=[itv["output_video"], itv["status"]],
144
+ api_name=None
145
+ )
146
+
147
+ return demo
148
+
149
+
150
+ if __name__ == "__main__":
151
+ demo = create_demo()
152
+ demo.launch(
153
+ server_name="0.0.0.0",
154
+ server_port=7860,
155
+ share=False,
156
+ theme=gr.themes.Soft(),
157
+ )
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=4.0.0
2
+ httpx>=0.25.0
3
+ python-dotenv>=1.0.0
src/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # StackNet Demo - Source Package
src/api/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # API Client Package
2
+ from .client import StackNetClient
3
+
4
+ __all__ = ["StackNetClient"]
src/api/client.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ StackNet API Client
3
+
4
+ Handles all communication with the StackNettask network.
5
+ SSE parsing and progress tracking are handled internally.
6
+ """
7
+
8
+ import json
9
+ import tempfile
10
+ import os
11
+ from typing import AsyncGenerator, Optional, Any, Callable
12
+ from dataclasses import dataclass
13
+ from enum import Enum
14
+
15
+ import httpx
16
+
17
+ from ..config import config
18
+
19
+
20
+ class MediaAction(str, Enum):
21
+ """Supported media orchestration actions."""
22
+ GENERATE_MUSIC = "generate_music"
23
+ CREATE_COVER = "create_cover"
24
+ EXTRACT_STEMS = "extract_stems"
25
+ ANALYZE_VISUAL = "analyze_visual"
26
+ DESCRIBE_VIDEO = "describe_video"
27
+ CREATE_COMPOSITE = "create_composite"
28
+
29
+
30
+ @dataclass
31
+ class TaskProgress:
32
+ """Progress update from a running task."""
33
+ progress: float # 0.0 to 1.0
34
+ status: str
35
+ message: str
36
+
37
+
38
+ @dataclass
39
+ class TaskResult:
40
+ """Final result from a completed task."""
41
+ success: bool
42
+ data: dict
43
+ error: Optional[str] = None
44
+
45
+
46
+ class StackNetClient:
47
+ """
48
+ Client for StackNet task network API.
49
+
50
+ All SSE parsing and polling is handled internally.
51
+ Consumers receive clean progress updates and final results.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ base_url: Optional[str] = None,
57
+ api_key: Optional[str] = None,
58
+ timeout: float = 300.0
59
+ ):
60
+ self.base_url = base_url or config.stacknet_url
61
+ self.api_key = api_key or config.stacknet_api_key
62
+ self.timeout = timeout
63
+ self._temp_dir = tempfile.mkdtemp(prefix="stacknet_")
64
+
65
+ async def submit_media_task(
66
+ self,
67
+ action: MediaAction,
68
+ prompt: Optional[str] = None,
69
+ media_url: Optional[str] = None,
70
+ audio_url: Optional[str] = None,
71
+ video_url: Optional[str] = None,
72
+ options: Optional[dict] = None,
73
+ on_progress: Optional[Callable[[float, str], None]] = None
74
+ ) -> TaskResult:
75
+ """
76
+ Submit a media orchestration task and wait for completion.
77
+
78
+ Args:
79
+ action: The media action to perform
80
+ prompt: Text prompt for generation
81
+ media_url: URL for image input
82
+ audio_url: URL for audio input
83
+ video_url: URL for video input
84
+ options: Additional options (tags, title, etc.)
85
+ on_progress: Callback for progress updates (progress: 0-1, message: str)
86
+
87
+ Returns:
88
+ TaskResult with success status and output data
89
+ """
90
+ payload = {
91
+ "type": config.TASK_TYPE_MEDIA,
92
+ "action": action.value,
93
+ "stream": True,
94
+ }
95
+
96
+ if prompt:
97
+ payload["prompt"] = prompt
98
+ if media_url:
99
+ payload["mediaUrl"] = media_url
100
+ if audio_url:
101
+ payload["audioUrl"] = audio_url
102
+ if video_url:
103
+ payload["videoUrl"] = video_url
104
+ if options:
105
+ payload["options"] = options
106
+
107
+ headers = {"Content-Type": "application/json"}
108
+ if self.api_key:
109
+ auth_header = self.api_key if self.api_key.startswith("Bearer ") else f"Bearer {self.api_key}"
110
+ headers["Authorization"] = auth_header
111
+
112
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
113
+ try:
114
+ async with client.stream(
115
+ "POST",
116
+ f"{self.base_url}/tasks",
117
+ json=payload,
118
+ headers=headers
119
+ ) as response:
120
+ if response.status_code != 200:
121
+ error_text = await response.aread()
122
+ return TaskResult(
123
+ success=False,
124
+ data={},
125
+ error=f"API request failed ({response.status_code}): {error_text.decode()[:200]}"
126
+ )
127
+
128
+ return await self._process_sse_stream(response, on_progress)
129
+
130
+ except httpx.TimeoutException:
131
+ return TaskResult(
132
+ success=False,
133
+ data={},
134
+ error="Request timed out. The operation took too long."
135
+ )
136
+ except httpx.RequestError as e:
137
+ return TaskResult(
138
+ success=False,
139
+ data={},
140
+ error=f"Network error: {str(e)}"
141
+ )
142
+
143
+ async def _process_sse_stream(
144
+ self,
145
+ response: httpx.Response,
146
+ on_progress: Optional[Callable[[float, str], None]] = None
147
+ ) -> TaskResult:
148
+ """Process SSE stream and extract final result."""
149
+ buffer = ""
150
+ final_result: Optional[dict] = None
151
+ error_message: Optional[str] = None
152
+
153
+ async for chunk in response.aiter_text():
154
+ buffer += chunk
155
+ lines = buffer.split("\n")
156
+ buffer = lines.pop() # Keep incomplete line
157
+
158
+ for line in lines:
159
+ if not line.startswith("data: "):
160
+ continue
161
+
162
+ raw_data = line[6:].strip()
163
+
164
+ # Skip markers
165
+ if raw_data == "[DONE]" or not raw_data:
166
+ continue
167
+
168
+ try:
169
+ event = json.loads(raw_data)
170
+ event_type = event.get("type", "")
171
+ event_data = event.get("data", event)
172
+
173
+ if event_type == "progress":
174
+ if on_progress:
175
+ progress = self._calculate_progress(event_data)
176
+ message = event_data.get("message", "Processing...")
177
+ on_progress(progress, message)
178
+
179
+ elif event_type == "result":
180
+ final_result = event_data.get("output", event_data)
181
+
182
+ elif event_type == "error":
183
+ error_message = event_data.get("message", "Unknown error occurred")
184
+
185
+ elif event_type == "complete":
186
+ # Task completed successfully
187
+ pass
188
+
189
+ except json.JSONDecodeError:
190
+ continue
191
+
192
+ # Process any remaining buffer
193
+ if buffer.strip() and buffer.startswith("data: "):
194
+ raw_data = buffer[6:].strip()
195
+ if raw_data and raw_data != "[DONE]":
196
+ try:
197
+ event = json.loads(raw_data)
198
+ if event.get("type") == "result":
199
+ final_result = event.get("data", {}).get("output", event.get("data", {}))
200
+ except json.JSONDecodeError:
201
+ pass
202
+
203
+ if error_message:
204
+ return TaskResult(success=False, data={}, error=error_message)
205
+
206
+ if final_result:
207
+ return TaskResult(success=True, data=final_result)
208
+
209
+ return TaskResult(
210
+ success=False,
211
+ data={},
212
+ error="No result received from the API"
213
+ )
214
+
215
+ def _calculate_progress(self, data: dict) -> float:
216
+ """Calculate normalized progress (0.0 to 1.0)."""
217
+ if not data:
218
+ return 0.5
219
+
220
+ status = data.get("status", "")
221
+
222
+ if status == "completed":
223
+ return 1.0
224
+ if status == "polling":
225
+ attempt = data.get("attempt", 1)
226
+ max_attempts = data.get("maxAttempts", 30)
227
+ return 0.2 + (attempt / max_attempts) * 0.6
228
+ if status == "processing":
229
+ return 0.5
230
+ if status == "submitted":
231
+ return 0.1
232
+
233
+ return 0.5
234
+
235
+ async def download_file(self, url: str, filename: Optional[str] = None) -> str:
236
+ """Download a file to the temp directory and return local path."""
237
+ if not filename:
238
+ filename = url.split("/")[-1].split("?")[0]
239
+ if not filename:
240
+ filename = "download"
241
+
242
+ local_path = os.path.join(self._temp_dir, filename)
243
+
244
+ async with httpx.AsyncClient(timeout=60.0) as client:
245
+ response = await client.get(url)
246
+ response.raise_for_status()
247
+
248
+ with open(local_path, "wb") as f:
249
+ f.write(response.content)
250
+
251
+ return local_path
252
+
253
+ def cleanup(self):
254
+ """Clean up temporary files."""
255
+ import shutil
256
+ if os.path.exists(self._temp_dir):
257
+ shutil.rmtree(self._temp_dir, ignore_errors=True)
src/config.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration management for StackNet Demo.
3
+
4
+ Loads settings from environment variables with sensible defaults.
5
+ """
6
+
7
+ import os
8
+ from dataclasses import dataclass
9
+ from dotenv import load_dotenv
10
+
11
+ # Load .env file if present
12
+ load_dotenv()
13
+
14
+
15
+ @dataclass
16
+ class Config:
17
+ """Application configuration."""
18
+
19
+ # StackNet API
20
+ stacknet_url: str = os.getenv("STACKNET_NETWORK_URL", "https://geoffnet.magma-rpc.com")
21
+ stacknet_api_key: str = os.getenv("STACKNET_SERVICE_KEY", "")
22
+
23
+ # Endpoints
24
+ @property
25
+ def tasks_endpoint(self) -> str:
26
+ return f"{self.stacknet_url}/tasks"
27
+
28
+ @property
29
+ def chat_endpoint(self) -> str:
30
+ return f"{self.stacknet_url}/v1/chat/completions"
31
+
32
+ # Timeouts (seconds)
33
+ request_timeout: float = 300.0 # 5 minutes for long operations
34
+
35
+ # Task types
36
+ TASK_TYPE_MEDIA = "media-orchestration"
37
+ TASK_TYPE_MCP = "mcp-tool"
38
+ TASK_TYPE_AI_PROMPT = "ai-prompt"
39
+
40
+
41
+ # Global config instance
42
+ config = Config()
src/services/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ # Services Package
2
+ from .music import MusicService
3
+ from .image import ImageService
4
+ from .video import VideoService
5
+
6
+ __all__ = ["MusicService", "ImageService", "VideoService"]
src/services/image.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Image Service
3
+
4
+ High-level service for image generation and editing.
5
+ Abstracts all API complexity from the UI layer.
6
+ """
7
+
8
+ from typing import Callable, Optional, List
9
+ from dataclasses import dataclass
10
+
11
+ from ..api.client import StackNetClient, MediaAction
12
+
13
+
14
+ @dataclass
15
+ class GeneratedImage:
16
+ """Generated image result."""
17
+ image_url: str
18
+ image_path: Optional[str] = None
19
+ prompt: Optional[str] = None
20
+ width: Optional[int] = None
21
+ height: Optional[int] = None
22
+
23
+
24
+ class ImageService:
25
+ """
26
+ Service for image generation and editing.
27
+
28
+ Provides clean interfaces for:
29
+ - Text-to-image generation
30
+ - Image-to-image editing/transformation
31
+ """
32
+
33
+ def __init__(self, client: Optional[StackNetClient] = None):
34
+ self.client = client or StackNetClient()
35
+
36
+ async def generate_image(
37
+ self,
38
+ prompt: str,
39
+ style: Optional[str] = None,
40
+ aspect_ratio: Optional[str] = None,
41
+ on_progress: Optional[Callable[[float, str], None]] = None
42
+ ) -> List[GeneratedImage]:
43
+ """
44
+ Generate image from a text prompt.
45
+
46
+ Args:
47
+ prompt: Description of desired image
48
+ style: Style preset (Photorealistic, Digital Art, etc.)
49
+ aspect_ratio: Aspect ratio (1:1, 16:9, 9:16, etc.)
50
+ on_progress: Callback for progress updates
51
+
52
+ Returns:
53
+ List of generated images
54
+ """
55
+ full_prompt = prompt
56
+ if style and style != "Photorealistic":
57
+ full_prompt = f"{prompt}, {style.lower()} style"
58
+
59
+ options = {}
60
+ if aspect_ratio:
61
+ options["aspect_ratio"] = aspect_ratio
62
+
63
+ result = await self.client.submit_media_task(
64
+ action=MediaAction.ANALYZE_VISUAL,
65
+ prompt=full_prompt,
66
+ options=options if options else None,
67
+ on_progress=on_progress
68
+ )
69
+
70
+ if not result.success:
71
+ raise Exception(result.error or "Image generation failed")
72
+
73
+ return self._parse_image_result(result.data, prompt)
74
+
75
+ async def edit_image(
76
+ self,
77
+ image_url: str,
78
+ edit_prompt: str,
79
+ strength: float = 0.5,
80
+ on_progress: Optional[Callable[[float, str], None]] = None
81
+ ) -> List[GeneratedImage]:
82
+ """
83
+ Edit/transform an existing image.
84
+
85
+ Args:
86
+ image_url: URL to source image
87
+ edit_prompt: Edit instructions
88
+ strength: Edit strength (0.1 to 1.0)
89
+ on_progress: Progress callback
90
+
91
+ Returns:
92
+ List of edited images
93
+ """
94
+ options = {
95
+ "strength": strength,
96
+ "edit_mode": True
97
+ }
98
+
99
+ result = await self.client.submit_media_task(
100
+ action=MediaAction.ANALYZE_VISUAL,
101
+ media_url=image_url,
102
+ prompt=edit_prompt,
103
+ options=options,
104
+ on_progress=on_progress
105
+ )
106
+
107
+ if not result.success:
108
+ raise Exception(result.error or "Image editing failed")
109
+
110
+ return self._parse_image_result(result.data, edit_prompt)
111
+
112
+ def _parse_image_result(self, data: dict, prompt: str) -> List[GeneratedImage]:
113
+ """Parse API response into GeneratedImage objects."""
114
+ images = []
115
+
116
+ # Handle various response formats
117
+ raw_images = data.get("images", [])
118
+
119
+ if not raw_images:
120
+ # Check for single image URL
121
+ image_url = (
122
+ data.get("image_url") or
123
+ data.get("imageUrl") or
124
+ data.get("url") or
125
+ data.get("content")
126
+ )
127
+ if image_url:
128
+ raw_images = [{"url": image_url}]
129
+
130
+ for img_data in raw_images:
131
+ if isinstance(img_data, str):
132
+ # Raw URL string
133
+ image_url = img_data
134
+ else:
135
+ image_url = (
136
+ img_data.get("url") or
137
+ img_data.get("image_url") or
138
+ img_data.get("imageUrl")
139
+ )
140
+
141
+ if image_url:
142
+ images.append(GeneratedImage(
143
+ image_url=image_url,
144
+ prompt=prompt,
145
+ width=img_data.get("width") if isinstance(img_data, dict) else None,
146
+ height=img_data.get("height") if isinstance(img_data, dict) else None
147
+ ))
148
+
149
+ return images
150
+
151
+ async def download_image(self, image: GeneratedImage) -> str:
152
+ """Download an image to local file."""
153
+ if image.image_path:
154
+ return image.image_path
155
+
156
+ # Determine extension from URL
157
+ url = image.image_url
158
+ if ".png" in url:
159
+ ext = ".png"
160
+ elif ".jpg" in url or ".jpeg" in url:
161
+ ext = ".jpg"
162
+ else:
163
+ ext = ".png"
164
+
165
+ filename = f"image_{hash(url) % 10000}{ext}"
166
+ image.image_path = await self.client.download_file(url, filename)
167
+ return image.image_path
168
+
169
+ def cleanup(self):
170
+ """Clean up temporary files."""
171
+ self.client.cleanup()
src/services/music.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Music Service
3
+
4
+ High-level service for music generation operations.
5
+ Abstracts all API complexity from the UI layer.
6
+ """
7
+
8
+ from typing import Callable, Optional, List
9
+ from dataclasses import dataclass, field
10
+
11
+ from ..api.client import StackNetClient, MediaAction
12
+
13
+
14
+ @dataclass
15
+ class MusicClip:
16
+ """Generated music clip."""
17
+ title: str
18
+ audio_url: str
19
+ audio_path: Optional[str] = None
20
+ duration: Optional[str] = None
21
+ image_url: Optional[str] = None
22
+ video_url: Optional[str] = None
23
+ tags: List[str] = field(default_factory=list)
24
+
25
+
26
+ @dataclass
27
+ class StemResult:
28
+ """Extracted audio stems."""
29
+ vocals_path: Optional[str] = None
30
+ drums_path: Optional[str] = None
31
+ bass_path: Optional[str] = None
32
+ other_path: Optional[str] = None
33
+
34
+
35
+ class MusicService:
36
+ """
37
+ Service for music generation and manipulation.
38
+
39
+ Provides clean interfaces for:
40
+ - Text-to-music generation
41
+ - Cover song creation
42
+ - Stem extraction
43
+ """
44
+
45
+ def __init__(self, client: Optional[StackNetClient] = None):
46
+ self.client = client or StackNetClient()
47
+
48
+ async def generate_music(
49
+ self,
50
+ prompt: str,
51
+ title: Optional[str] = None,
52
+ tags: Optional[str] = None,
53
+ lyrics: Optional[str] = None,
54
+ instrumental: bool = False,
55
+ on_progress: Optional[Callable[[float, str], None]] = None
56
+ ) -> List[MusicClip]:
57
+ """
58
+ Generate original music from a text prompt.
59
+
60
+ Args:
61
+ prompt: Description of desired music
62
+ title: Optional song title
63
+ tags: Optional genre/style tags (comma-separated)
64
+ lyrics: Optional lyrics (ignored if instrumental=True)
65
+ instrumental: Generate instrumental only
66
+ on_progress: Callback for progress updates
67
+
68
+ Returns:
69
+ List of generated MusicClip objects
70
+ """
71
+ options = {}
72
+ if tags:
73
+ options["tags"] = tags
74
+ if title:
75
+ options["title"] = title
76
+ if instrumental:
77
+ options["make_instrumental"] = True
78
+ if lyrics and not instrumental:
79
+ options["lyrics"] = lyrics
80
+
81
+ result = await self.client.submit_media_task(
82
+ action=MediaAction.GENERATE_MUSIC,
83
+ prompt=prompt,
84
+ options=options if options else None,
85
+ on_progress=on_progress
86
+ )
87
+
88
+ if not result.success:
89
+ raise Exception(result.error or "Music generation failed")
90
+
91
+ return self._parse_music_result(result.data)
92
+
93
+ async def create_cover(
94
+ self,
95
+ audio_url: str,
96
+ style_prompt: str,
97
+ title: Optional[str] = None,
98
+ tags: Optional[str] = None,
99
+ on_progress: Optional[Callable[[float, str], None]] = None
100
+ ) -> List[MusicClip]:
101
+ """
102
+ Create a cover version of audio.
103
+
104
+ Args:
105
+ audio_url: URL to source audio
106
+ style_prompt: Style/voice direction for the cover
107
+ title: Optional title for the cover
108
+ tags: Optional genre/style tags
109
+ on_progress: Progress callback
110
+
111
+ Returns:
112
+ List of generated cover clips
113
+ """
114
+ options = {}
115
+ if tags:
116
+ options["tags"] = tags
117
+ if title:
118
+ options["title"] = title
119
+
120
+ result = await self.client.submit_media_task(
121
+ action=MediaAction.CREATE_COVER,
122
+ audio_url=audio_url,
123
+ prompt=style_prompt,
124
+ options=options if options else None,
125
+ on_progress=on_progress
126
+ )
127
+
128
+ if not result.success:
129
+ raise Exception(result.error or "Cover creation failed")
130
+
131
+ return self._parse_music_result(result.data)
132
+
133
+ async def extract_stems(
134
+ self,
135
+ audio_url: str,
136
+ on_progress: Optional[Callable[[float, str], None]] = None
137
+ ) -> StemResult:
138
+ """
139
+ Extract stems (vocals, drums, bass, other) from audio.
140
+
141
+ Args:
142
+ audio_url: URL to source audio
143
+ on_progress: Progress callback
144
+
145
+ Returns:
146
+ StemResult with paths to each stem
147
+ """
148
+ result = await self.client.submit_media_task(
149
+ action=MediaAction.EXTRACT_STEMS,
150
+ audio_url=audio_url,
151
+ on_progress=on_progress
152
+ )
153
+
154
+ if not result.success:
155
+ raise Exception(result.error or "Stem extraction failed")
156
+
157
+ stems_data = result.data.get("stems", result.data)
158
+
159
+ stem_result = StemResult()
160
+
161
+ # Download each stem if URL provided
162
+ if stems_data.get("vocals"):
163
+ stem_result.vocals_path = await self.client.download_file(
164
+ stems_data["vocals"], "vocals.mp3"
165
+ )
166
+ if stems_data.get("drums"):
167
+ stem_result.drums_path = await self.client.download_file(
168
+ stems_data["drums"], "drums.mp3"
169
+ )
170
+ if stems_data.get("bass"):
171
+ stem_result.bass_path = await self.client.download_file(
172
+ stems_data["bass"], "bass.mp3"
173
+ )
174
+ if stems_data.get("other"):
175
+ stem_result.other_path = await self.client.download_file(
176
+ stems_data["other"], "other.mp3"
177
+ )
178
+
179
+ return stem_result
180
+
181
+ def _parse_music_result(self, data: dict) -> List[MusicClip]:
182
+ """Parse API response into MusicClip objects."""
183
+ clips = []
184
+
185
+ # Handle various response formats
186
+ raw_clips = data.get("clips", [])
187
+
188
+ # If no clips array, treat the data itself as a single clip
189
+ if not raw_clips:
190
+ if data.get("audio_url") or data.get("audioUrl"):
191
+ raw_clips = [data]
192
+ elif data.get("url"):
193
+ raw_clips = [{"audio_url": data["url"], "title": data.get("title", "Generated")}]
194
+
195
+ for clip_data in raw_clips:
196
+ audio_url = clip_data.get("audio_url") or clip_data.get("audioUrl") or clip_data.get("url")
197
+ if audio_url:
198
+ clips.append(MusicClip(
199
+ title=clip_data.get("title", "Generated Music"),
200
+ audio_url=audio_url,
201
+ duration=clip_data.get("duration"),
202
+ image_url=clip_data.get("image_url") or clip_data.get("imageUrl"),
203
+ video_url=clip_data.get("video_url") or clip_data.get("videoUrl"),
204
+ tags=clip_data.get("tags", [])
205
+ ))
206
+
207
+ return clips
208
+
209
+ async def download_clip(self, clip: MusicClip) -> str:
210
+ """Download a clip's audio to local file."""
211
+ if clip.audio_path:
212
+ return clip.audio_path
213
+
214
+ filename = f"{clip.title.replace(' ', '_')[:30]}.mp3"
215
+ clip.audio_path = await self.client.download_file(clip.audio_url, filename)
216
+ return clip.audio_path
217
+
218
+ def cleanup(self):
219
+ """Clean up temporary files."""
220
+ self.client.cleanup()
src/services/video.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Video Service
3
+
4
+ High-level service for video generation.
5
+ Abstracts all API complexity from the UI layer.
6
+ """
7
+
8
+ from typing import Callable, Optional, List
9
+ from dataclasses import dataclass
10
+
11
+ from ..api.client import StackNetClient, MediaAction
12
+
13
+
14
+ @dataclass
15
+ class GeneratedVideo:
16
+ """Generated video result."""
17
+ video_url: str
18
+ video_path: Optional[str] = None
19
+ thumbnail_url: Optional[str] = None
20
+ duration: Optional[float] = None
21
+ prompt: Optional[str] = None
22
+
23
+
24
+ class VideoService:
25
+ """
26
+ Service for video generation.
27
+
28
+ Provides clean interfaces for:
29
+ - Text-to-video generation
30
+ - Image-to-video animation
31
+ """
32
+
33
+ def __init__(self, client: Optional[StackNetClient] = None):
34
+ self.client = client or StackNetClient()
35
+
36
+ async def generate_video(
37
+ self,
38
+ prompt: str,
39
+ duration: int = 10,
40
+ style: Optional[str] = None,
41
+ on_progress: Optional[Callable[[float, str], None]] = None
42
+ ) -> List[GeneratedVideo]:
43
+ """
44
+ Generate video from a text prompt.
45
+
46
+ Args:
47
+ prompt: Description of desired video
48
+ duration: Target duration in seconds
49
+ style: Style preset (Cinematic, Animation, etc.)
50
+ on_progress: Callback for progress updates
51
+
52
+ Returns:
53
+ List of generated videos
54
+ """
55
+ full_prompt = prompt
56
+ if style and style != "Cinematic":
57
+ full_prompt = f"{prompt}, {style.lower()} style"
58
+
59
+ options = {
60
+ "duration": duration
61
+ }
62
+
63
+ result = await self.client.submit_media_task(
64
+ action=MediaAction.DESCRIBE_VIDEO,
65
+ prompt=full_prompt,
66
+ options=options,
67
+ on_progress=on_progress
68
+ )
69
+
70
+ if not result.success:
71
+ raise Exception(result.error or "Video generation failed")
72
+
73
+ return self._parse_video_result(result.data, prompt)
74
+
75
+ async def animate_image(
76
+ self,
77
+ image_url: str,
78
+ motion_prompt: str,
79
+ duration: int = 5,
80
+ on_progress: Optional[Callable[[float, str], None]] = None
81
+ ) -> List[GeneratedVideo]:
82
+ """
83
+ Animate a static image into video.
84
+
85
+ Args:
86
+ image_url: URL to source image
87
+ motion_prompt: Description of desired motion
88
+ duration: Target duration in seconds
89
+ on_progress: Progress callback
90
+
91
+ Returns:
92
+ List of animated videos
93
+ """
94
+ options = {
95
+ "duration": duration,
96
+ "animate_mode": True
97
+ }
98
+
99
+ result = await self.client.submit_media_task(
100
+ action=MediaAction.DESCRIBE_VIDEO,
101
+ media_url=image_url,
102
+ prompt=motion_prompt,
103
+ options=options,
104
+ on_progress=on_progress
105
+ )
106
+
107
+ if not result.success:
108
+ raise Exception(result.error or "Image animation failed")
109
+
110
+ return self._parse_video_result(result.data, motion_prompt)
111
+
112
+ def _parse_video_result(self, data: dict, prompt: str) -> List[GeneratedVideo]:
113
+ """Parse API response into GeneratedVideo objects."""
114
+ videos = []
115
+
116
+ # Handle various response formats
117
+ raw_videos = data.get("videos", [])
118
+
119
+ if not raw_videos:
120
+ # Check for single video URL
121
+ video_url = (
122
+ data.get("video_url") or
123
+ data.get("videoUrl") or
124
+ data.get("url") or
125
+ data.get("content")
126
+ )
127
+ if video_url:
128
+ raw_videos = [{"url": video_url}]
129
+
130
+ for vid_data in raw_videos:
131
+ if isinstance(vid_data, str):
132
+ video_url = vid_data
133
+ else:
134
+ video_url = (
135
+ vid_data.get("url") or
136
+ vid_data.get("video_url") or
137
+ vid_data.get("videoUrl")
138
+ )
139
+
140
+ if video_url:
141
+ videos.append(GeneratedVideo(
142
+ video_url=video_url,
143
+ thumbnail_url=vid_data.get("thumbnail") if isinstance(vid_data, dict) else None,
144
+ duration=vid_data.get("duration") if isinstance(vid_data, dict) else None,
145
+ prompt=prompt
146
+ ))
147
+
148
+ return videos
149
+
150
+ async def download_video(self, video: GeneratedVideo) -> str:
151
+ """Download a video to local file."""
152
+ if video.video_path:
153
+ return video.video_path
154
+
155
+ # Determine extension from URL
156
+ url = video.video_url
157
+ if ".webm" in url:
158
+ ext = ".webm"
159
+ elif ".mov" in url:
160
+ ext = ".mov"
161
+ else:
162
+ ext = ".mp4"
163
+
164
+ filename = f"video_{hash(url) % 10000}{ext}"
165
+ video.video_path = await self.client.download_file(url, filename)
166
+ return video.video_path
167
+
168
+ def cleanup(self):
169
+ """Clean up temporary files."""
170
+ self.client.cleanup()
src/ui/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # UI Package
2
+ from .tabs import create_all_tabs
3
+ from .handlers import Handlers
4
+
5
+ __all__ = ["create_all_tabs", "Handlers"]
src/ui/handlers.py ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Event Handlers
3
+
4
+ Handlers for all Gradio UI events.
5
+ Connects UI to services with progress tracking and error handling.
6
+ """
7
+
8
+ import asyncio
9
+ from typing import Optional, Tuple
10
+ import gradio as gr
11
+
12
+ from ..services.music import MusicService
13
+ from ..services.image import ImageService
14
+ from ..services.video import VideoService
15
+ from ..api.client import StackNetClient
16
+
17
+
18
+ def format_error(error: Exception) -> str:
19
+ """Format exception as user-friendly message."""
20
+ msg = str(error)
21
+
22
+ # Translate common errors
23
+ if "timeout" in msg.lower():
24
+ return "The operation timed out. Please try again with a simpler prompt."
25
+ if "network" in msg.lower() or "connection" in msg.lower():
26
+ return "Network error. Please check your connection and try again."
27
+ if "rate limit" in msg.lower() or "429" in msg.lower():
28
+ return "Too many requests. Please wait a moment and try again."
29
+
30
+ return f"Error: {msg}"
31
+
32
+
33
+ class Handlers:
34
+ """
35
+ Collection of event handlers for the Gradio UI.
36
+
37
+ All handlers hide API complexity and provide clean progress feedback.
38
+ """
39
+
40
+ @staticmethod
41
+ def generate_music(
42
+ prompt: str,
43
+ tags: str,
44
+ instrumental: bool,
45
+ lyrics: str,
46
+ title: str,
47
+ api_key: str = "",
48
+ progress: gr.Progress = gr.Progress()
49
+ ) -> Tuple[Optional[str], str]:
50
+ """
51
+ Handle text-to-music generation.
52
+
53
+ Returns:
54
+ Tuple of (audio_path, status_message)
55
+ """
56
+ if not prompt.strip():
57
+ return None, "Please enter a description for your music."
58
+ if not api_key.strip():
59
+ return None, "Please enter your API key in the Settings section."
60
+
61
+ client = StackNetClient(api_key=api_key.strip())
62
+ service = MusicService(client=client)
63
+
64
+ try:
65
+ progress(0, desc="Starting music generation...")
66
+
67
+ def on_progress(value: float, message: str):
68
+ progress(value, desc=message)
69
+
70
+ # Run async in event loop
71
+ loop = asyncio.new_event_loop()
72
+ asyncio.set_event_loop(loop)
73
+
74
+ try:
75
+ clips = loop.run_until_complete(
76
+ service.generate_music(
77
+ prompt=prompt,
78
+ title=title if title.strip() else None,
79
+ tags=tags if tags.strip() else None,
80
+ lyrics=lyrics if lyrics.strip() and not instrumental else None,
81
+ instrumental=instrumental,
82
+ on_progress=on_progress
83
+ )
84
+ )
85
+
86
+ if clips:
87
+ # Download first clip
88
+ audio_path = loop.run_until_complete(
89
+ service.download_clip(clips[0])
90
+ )
91
+ return audio_path, "Music generation complete!"
92
+ else:
93
+ return None, "No music was generated. Please try a different prompt."
94
+
95
+ finally:
96
+ loop.close()
97
+
98
+ except Exception as e:
99
+ return None, format_error(e)
100
+
101
+ finally:
102
+ service.cleanup()
103
+
104
+ @staticmethod
105
+ def create_cover(
106
+ audio_file: str,
107
+ style_prompt: str,
108
+ tags: str,
109
+ title: str,
110
+ api_key: str = "",
111
+ progress: gr.Progress = gr.Progress()
112
+ ) -> Tuple[Optional[str], str]:
113
+ """
114
+ Handle cover song creation.
115
+
116
+ Returns:
117
+ Tuple of (audio_path, status_message)
118
+ """
119
+ if not audio_file:
120
+ return None, "Please upload an audio file."
121
+ if not style_prompt.strip():
122
+ return None, "Please describe the style for your cover."
123
+ if not api_key.strip():
124
+ return None, "Please enter your API key in the Settings section."
125
+
126
+ client = StackNetClient(api_key=api_key.strip())
127
+ service = MusicService(client=client)
128
+
129
+ try:
130
+ progress(0, desc="Processing audio file...")
131
+
132
+ def on_progress(value: float, message: str):
133
+ progress(value, desc=message)
134
+
135
+ loop = asyncio.new_event_loop()
136
+ asyncio.set_event_loop(loop)
137
+
138
+ try:
139
+ # For file upload, we need to use the file path as URL
140
+ # In production, you'd upload to a storage service first
141
+ audio_url = f"file://{audio_file}"
142
+
143
+ clips = loop.run_until_complete(
144
+ service.create_cover(
145
+ audio_url=audio_url,
146
+ style_prompt=style_prompt,
147
+ title=title if title.strip() else None,
148
+ tags=tags if tags.strip() else None,
149
+ on_progress=on_progress
150
+ )
151
+ )
152
+
153
+ if clips:
154
+ audio_path = loop.run_until_complete(
155
+ service.download_clip(clips[0])
156
+ )
157
+ return audio_path, "Cover created successfully!"
158
+ else:
159
+ return None, "No cover was generated. Please try again."
160
+
161
+ finally:
162
+ loop.close()
163
+
164
+ except Exception as e:
165
+ return None, format_error(e)
166
+
167
+ finally:
168
+ service.cleanup()
169
+
170
+ @staticmethod
171
+ def extract_stems(
172
+ audio_file: str,
173
+ api_key: str = "",
174
+ progress: gr.Progress = gr.Progress()
175
+ ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str], str]:
176
+ """
177
+ Handle stem extraction.
178
+
179
+ Returns:
180
+ Tuple of (vocals_path, drums_path, bass_path, other_path, status_message)
181
+ """
182
+ if not audio_file:
183
+ return None, None, None, None, "Please upload an audio file."
184
+ if not api_key.strip():
185
+ return None, None, None, None, "Please enter your API key in the Settings section."
186
+
187
+ client = StackNetClient(api_key=api_key.strip())
188
+ service = MusicService(client=client)
189
+
190
+ try:
191
+ progress(0, desc="Analyzing audio...")
192
+
193
+ def on_progress(value: float, message: str):
194
+ progress(value, desc=message)
195
+
196
+ loop = asyncio.new_event_loop()
197
+ asyncio.set_event_loop(loop)
198
+
199
+ try:
200
+ audio_url = f"file://{audio_file}"
201
+
202
+ stems = loop.run_until_complete(
203
+ service.extract_stems(
204
+ audio_url=audio_url,
205
+ on_progress=on_progress
206
+ )
207
+ )
208
+
209
+ return (
210
+ stems.vocals_path,
211
+ stems.drums_path,
212
+ stems.bass_path,
213
+ stems.other_path,
214
+ "Stems extracted successfully!"
215
+ )
216
+
217
+ finally:
218
+ loop.close()
219
+
220
+ except Exception as e:
221
+ return None, None, None, None, format_error(e)
222
+
223
+ finally:
224
+ service.cleanup()
225
+
226
+ @staticmethod
227
+ def generate_image(
228
+ prompt: str,
229
+ style: str,
230
+ aspect_ratio: str,
231
+ api_key: str = "",
232
+ progress: gr.Progress = gr.Progress()
233
+ ) -> Tuple[Optional[str], str]:
234
+ """
235
+ Handle text-to-image generation.
236
+
237
+ Returns:
238
+ Tuple of (image_path, status_message)
239
+ """
240
+ if not prompt.strip():
241
+ return None, "Please enter a description for your image."
242
+ if not api_key.strip():
243
+ return None, "Please enter your API key in the Settings section."
244
+
245
+ client = StackNetClient(api_key=api_key.strip())
246
+ service = ImageService(client=client)
247
+
248
+ try:
249
+ progress(0, desc="Generating image...")
250
+
251
+ def on_progress(value: float, message: str):
252
+ progress(value, desc=message)
253
+
254
+ loop = asyncio.new_event_loop()
255
+ asyncio.set_event_loop(loop)
256
+
257
+ try:
258
+ images = loop.run_until_complete(
259
+ service.generate_image(
260
+ prompt=prompt,
261
+ style=style,
262
+ aspect_ratio=aspect_ratio,
263
+ on_progress=on_progress
264
+ )
265
+ )
266
+
267
+ if images:
268
+ image_path = loop.run_until_complete(
269
+ service.download_image(images[0])
270
+ )
271
+ return image_path, "Image generated successfully!"
272
+ else:
273
+ return None, "No image was generated. Please try a different prompt."
274
+
275
+ finally:
276
+ loop.close()
277
+
278
+ except Exception as e:
279
+ return None, format_error(e)
280
+
281
+ finally:
282
+ service.cleanup()
283
+
284
+ @staticmethod
285
+ def edit_image(
286
+ input_image: str,
287
+ edit_prompt: str,
288
+ strength: float,
289
+ api_key: str = "",
290
+ progress: gr.Progress = gr.Progress()
291
+ ) -> Tuple[Optional[str], str]:
292
+ """
293
+ Handle image-to-image editing.
294
+
295
+ Returns:
296
+ Tuple of (image_path, status_message)
297
+ """
298
+ if not input_image:
299
+ return None, "Please upload an image."
300
+ if not edit_prompt.strip():
301
+ return None, "Please describe how you want to edit the image."
302
+ if not api_key.strip():
303
+ return None, "Please enter your API key in the Settings section."
304
+
305
+ client = StackNetClient(api_key=api_key.strip())
306
+ service = ImageService(client=client)
307
+
308
+ try:
309
+ progress(0, desc="Processing image...")
310
+
311
+ def on_progress(value: float, message: str):
312
+ progress(value, desc=message)
313
+
314
+ loop = asyncio.new_event_loop()
315
+ asyncio.set_event_loop(loop)
316
+
317
+ try:
318
+ image_url = f"file://{input_image}"
319
+
320
+ images = loop.run_until_complete(
321
+ service.edit_image(
322
+ image_url=image_url,
323
+ edit_prompt=edit_prompt,
324
+ strength=strength,
325
+ on_progress=on_progress
326
+ )
327
+ )
328
+
329
+ if images:
330
+ image_path = loop.run_until_complete(
331
+ service.download_image(images[0])
332
+ )
333
+ return image_path, "Image edited successfully!"
334
+ else:
335
+ return None, "No edited image was generated. Please try again."
336
+
337
+ finally:
338
+ loop.close()
339
+
340
+ except Exception as e:
341
+ return None, format_error(e)
342
+
343
+ finally:
344
+ service.cleanup()
345
+
346
+ @staticmethod
347
+ def generate_video(
348
+ prompt: str,
349
+ duration: int,
350
+ style: str,
351
+ api_key: str = "",
352
+ progress: gr.Progress = gr.Progress()
353
+ ) -> Tuple[Optional[str], str]:
354
+ """
355
+ Handle text-to-video generation.
356
+
357
+ Returns:
358
+ Tuple of (video_path, status_message)
359
+ """
360
+ if not prompt.strip():
361
+ return None, "Please enter a description for your video."
362
+ if not api_key.strip():
363
+ return None, "Please enter your API key in the Settings section."
364
+
365
+ client = StackNetClient(api_key=api_key.strip())
366
+ service = VideoService(client=client)
367
+
368
+ try:
369
+ progress(0, desc="Generating video...")
370
+
371
+ def on_progress(value: float, message: str):
372
+ progress(value, desc=message)
373
+
374
+ loop = asyncio.new_event_loop()
375
+ asyncio.set_event_loop(loop)
376
+
377
+ try:
378
+ videos = loop.run_until_complete(
379
+ service.generate_video(
380
+ prompt=prompt,
381
+ duration=int(duration),
382
+ style=style,
383
+ on_progress=on_progress
384
+ )
385
+ )
386
+
387
+ if videos:
388
+ video_path = loop.run_until_complete(
389
+ service.download_video(videos[0])
390
+ )
391
+ return video_path, "Video generated successfully!"
392
+ else:
393
+ return None, "No video was generated. Please try a different prompt."
394
+
395
+ finally:
396
+ loop.close()
397
+
398
+ except Exception as e:
399
+ return None, format_error(e)
400
+
401
+ finally:
402
+ service.cleanup()
403
+
404
+ @staticmethod
405
+ def animate_image(
406
+ input_image: str,
407
+ motion_prompt: str,
408
+ duration: int,
409
+ api_key: str = "",
410
+ progress: gr.Progress = gr.Progress()
411
+ ) -> Tuple[Optional[str], str]:
412
+ """
413
+ Handle image-to-video animation.
414
+
415
+ Returns:
416
+ Tuple of (video_path, status_message)
417
+ """
418
+ if not input_image:
419
+ return None, "Please upload an image."
420
+ if not motion_prompt.strip():
421
+ return None, "Please describe the motion you want."
422
+ if not api_key.strip():
423
+ return None, "Please enter your API key in the Settings section."
424
+
425
+ client = StackNetClient(api_key=api_key.strip())
426
+ service = VideoService(client=client)
427
+
428
+ try:
429
+ progress(0, desc="Animating image...")
430
+
431
+ def on_progress(value: float, message: str):
432
+ progress(value, desc=message)
433
+
434
+ loop = asyncio.new_event_loop()
435
+ asyncio.set_event_loop(loop)
436
+
437
+ try:
438
+ image_url = f"file://{input_image}"
439
+
440
+ videos = loop.run_until_complete(
441
+ service.animate_image(
442
+ image_url=image_url,
443
+ motion_prompt=motion_prompt,
444
+ duration=int(duration),
445
+ on_progress=on_progress
446
+ )
447
+ )
448
+
449
+ if videos:
450
+ video_path = loop.run_until_complete(
451
+ service.download_video(videos[0])
452
+ )
453
+ return video_path, "Image animated successfully!"
454
+ else:
455
+ return None, "No video was generated. Please try again."
456
+
457
+ finally:
458
+ loop.close()
459
+
460
+ except Exception as e:
461
+ return None, format_error(e)
462
+
463
+ finally:
464
+ service.cleanup()
src/ui/tabs.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio Tab Definitions
3
+
4
+ Defines all 6 tabs for the StackNetdemo application.
5
+ """
6
+
7
+ import gradio as gr
8
+
9
+
10
+ def create_text_to_music_tab():
11
+ """Create the Text to Music tab components."""
12
+ with gr.Column():
13
+ gr.Markdown("### Generate original music from a text description")
14
+
15
+ prompt = gr.Textbox(
16
+ label="Describe your music",
17
+ placeholder="e.g., upbeat jazz with piano and saxophone, cheerful summer vibes",
18
+ lines=3
19
+ )
20
+
21
+ with gr.Row():
22
+ tags = gr.Textbox(
23
+ label="Genre/Style Tags",
24
+ placeholder="jazz, piano, instrumental",
25
+ scale=2
26
+ )
27
+ instrumental = gr.Checkbox(
28
+ label="Instrumental Only",
29
+ value=False,
30
+ scale=1
31
+ )
32
+
33
+ lyrics = gr.Textbox(
34
+ label="Lyrics (optional)",
35
+ placeholder="Write your lyrics here...",
36
+ lines=4,
37
+ visible=True
38
+ )
39
+
40
+ title = gr.Textbox(
41
+ label="Song Title (optional)",
42
+ placeholder="My Song"
43
+ )
44
+
45
+ generate_btn = gr.Button("Generate Music", variant="primary", size="lg")
46
+
47
+ status = gr.Textbox(label="Status", interactive=False, visible=False)
48
+
49
+ output_audio = gr.Audio(label="Generated Music", type="filepath")
50
+
51
+ # Toggle lyrics visibility based on instrumental checkbox
52
+ instrumental.change(
53
+ fn=lambda x: gr.update(visible=not x),
54
+ inputs=[instrumental],
55
+ outputs=[lyrics],
56
+ api_name=None
57
+ )
58
+
59
+ return {
60
+ "prompt": prompt,
61
+ "tags": tags,
62
+ "instrumental": instrumental,
63
+ "lyrics": lyrics,
64
+ "title": title,
65
+ "generate_btn": generate_btn,
66
+ "status": status,
67
+ "output_audio": output_audio
68
+ }
69
+
70
+
71
+ def create_music_to_music_tab():
72
+ """Create the Music to Music tab with sub-tabs for Cover and Stems."""
73
+ with gr.Tabs() as sub_tabs:
74
+ # Cover Song Sub-tab
75
+ with gr.Tab("Create Cover"):
76
+ with gr.Column():
77
+ gr.Markdown("### Create music from reference audio (Diffusion)")
78
+
79
+ cover_audio_input = gr.Audio(
80
+ label="Upload Audio",
81
+ type="filepath"
82
+ )
83
+
84
+ cover_style_prompt = gr.Textbox(
85
+ label="Style Direction",
86
+ placeholder="e.g., rock version with electric guitar, female vocalist",
87
+ lines=2
88
+ )
89
+
90
+ cover_tags = gr.Textbox(
91
+ label="Style Tags",
92
+ placeholder="rock, electric guitar"
93
+ )
94
+
95
+ cover_title = gr.Textbox(
96
+ label="Title (optional)",
97
+ placeholder="My Song"
98
+ )
99
+
100
+ cover_btn = gr.Button("Create", variant="primary", size="lg")
101
+
102
+ cover_status = gr.Textbox(label="Status", interactive=False, visible=False)
103
+
104
+ cover_output = gr.Audio(label="Song", type="filepath")
105
+
106
+ # Extract Stems Sub-tab
107
+ with gr.Tab("Extract Stems"):
108
+ with gr.Column():
109
+ gr.Markdown("### Separate audio into individual stems")
110
+
111
+ stems_audio_input = gr.Audio(
112
+ label="Upload Audio",
113
+ type="filepath"
114
+ )
115
+
116
+ stems_btn = gr.Button("Extract Stems", variant="primary", size="lg")
117
+
118
+ stems_status = gr.Textbox(label="Status", interactive=False, visible=False)
119
+
120
+ gr.Markdown("**Extracted Stems:**")
121
+ with gr.Row():
122
+ vocals_output = gr.Audio(label="Vocals", type="filepath")
123
+ drums_output = gr.Audio(label="Drums", type="filepath")
124
+ with gr.Row():
125
+ bass_output = gr.Audio(label="Bass", type="filepath")
126
+ other_output = gr.Audio(label="Other", type="filepath")
127
+
128
+ return {
129
+ # Cover components
130
+ "cover_audio_input": cover_audio_input,
131
+ "cover_style_prompt": cover_style_prompt,
132
+ "cover_tags": cover_tags,
133
+ "cover_title": cover_title,
134
+ "cover_btn": cover_btn,
135
+ "cover_status": cover_status,
136
+ "cover_output": cover_output,
137
+ # Stems components
138
+ "stems_audio_input": stems_audio_input,
139
+ "stems_btn": stems_btn,
140
+ "stems_status": stems_status,
141
+ "vocals_output": vocals_output,
142
+ "drums_output": drums_output,
143
+ "bass_output": bass_output,
144
+ "other_output": other_output
145
+ }
146
+
147
+
148
+ def create_text_to_image_tab():
149
+ """Create the Text to Image tab components."""
150
+ with gr.Column():
151
+ gr.Markdown("### Generate images from a text description")
152
+
153
+ prompt = gr.Textbox(
154
+ label="Describe your image",
155
+ placeholder="e.g., a serene mountain landscape at sunset with snow-capped peaks",
156
+ lines=3
157
+ )
158
+
159
+ with gr.Row():
160
+ style = gr.Dropdown(
161
+ label="Style",
162
+ choices=["Photorealistic", "Digital Art", "Oil Painting", "Watercolor", "Sketch", "Anime"],
163
+ value="Photorealistic",
164
+ scale=1
165
+ )
166
+ aspect_ratio = gr.Dropdown(
167
+ label="Aspect Ratio",
168
+ choices=["1:1", "16:9", "9:16", "4:3", "3:4"],
169
+ value="1:1",
170
+ scale=1
171
+ )
172
+
173
+ generate_btn = gr.Button("Generate Image", variant="primary", size="lg")
174
+
175
+ status = gr.Textbox(label="Status", interactive=False, visible=False)
176
+
177
+ output_image = gr.Image(label="Generated Image", type="filepath")
178
+
179
+ return {
180
+ "prompt": prompt,
181
+ "style": style,
182
+ "aspect_ratio": aspect_ratio,
183
+ "generate_btn": generate_btn,
184
+ "status": status,
185
+ "output_image": output_image
186
+ }
187
+
188
+
189
+ def create_image_to_image_tab():
190
+ """Create the Image to Image tab components."""
191
+ with gr.Column():
192
+ gr.Markdown("### Transform or edit an existing image")
193
+
194
+ with gr.Row():
195
+ input_image = gr.Image(
196
+ label="Upload Image",
197
+ type="filepath",
198
+ scale=1
199
+ )
200
+
201
+ with gr.Column(scale=1):
202
+ edit_prompt = gr.Textbox(
203
+ label="Edit Instructions",
204
+ placeholder="e.g., add dramatic sunset lighting, make it look like a painting",
205
+ lines=3
206
+ )
207
+
208
+ strength = gr.Slider(
209
+ label="Edit Strength",
210
+ minimum=0.1,
211
+ maximum=1.0,
212
+ value=0.5,
213
+ step=0.1
214
+ )
215
+
216
+ edit_btn = gr.Button("Transform Image", variant="primary", size="lg")
217
+
218
+ status = gr.Textbox(label="Status", interactive=False, visible=False)
219
+
220
+ output_image = gr.Image(label="Transformed Image", type="filepath")
221
+
222
+ return {
223
+ "input_image": input_image,
224
+ "edit_prompt": edit_prompt,
225
+ "strength": strength,
226
+ "edit_btn": edit_btn,
227
+ "status": status,
228
+ "output_image": output_image
229
+ }
230
+
231
+
232
+ def create_text_to_video_tab():
233
+ """Create the Text to Video tab components."""
234
+ with gr.Column():
235
+ gr.Markdown("### Generate videos from a text description")
236
+
237
+ prompt = gr.Textbox(
238
+ label="Describe your video",
239
+ placeholder="e.g., a drone shot flying over a tropical beach at golden hour",
240
+ lines=3
241
+ )
242
+
243
+ with gr.Row():
244
+ duration = gr.Slider(
245
+ label="Duration (seconds)",
246
+ minimum=3,
247
+ maximum=30,
248
+ value=10,
249
+ step=1,
250
+ scale=1
251
+ )
252
+ style = gr.Dropdown(
253
+ label="Style",
254
+ choices=["Cinematic", "Animation", "Documentary", "Abstract"],
255
+ value="Cinematic",
256
+ scale=1
257
+ )
258
+
259
+ generate_btn = gr.Button("Generate Video", variant="primary", size="lg")
260
+
261
+ status = gr.Textbox(label="Status", interactive=False, visible=False)
262
+
263
+ output_video = gr.Video(label="Generated Video")
264
+
265
+ return {
266
+ "prompt": prompt,
267
+ "duration": duration,
268
+ "style": style,
269
+ "generate_btn": generate_btn,
270
+ "status": status,
271
+ "output_video": output_video
272
+ }
273
+
274
+
275
+ def create_image_to_video_tab():
276
+ """Create the Image to Video tab components."""
277
+ with gr.Column():
278
+ gr.Markdown("### Animate a static image into video")
279
+
280
+ with gr.Row():
281
+ input_image = gr.Image(
282
+ label="Upload Image",
283
+ type="filepath",
284
+ scale=1
285
+ )
286
+
287
+ with gr.Column(scale=1):
288
+ motion_prompt = gr.Textbox(
289
+ label="Motion Description",
290
+ placeholder="e.g., gentle zoom in, clouds moving slowly, water rippling",
291
+ lines=3
292
+ )
293
+
294
+ duration = gr.Slider(
295
+ label="Duration (seconds)",
296
+ minimum=3,
297
+ maximum=15,
298
+ value=5,
299
+ step=1
300
+ )
301
+
302
+ animate_btn = gr.Button("Animate Image", variant="primary", size="lg")
303
+
304
+ status = gr.Textbox(label="Status", interactive=False, visible=False)
305
+
306
+ output_video = gr.Video(label="Animated Video")
307
+
308
+ return {
309
+ "input_image": input_image,
310
+ "motion_prompt": motion_prompt,
311
+ "duration": duration,
312
+ "animate_btn": animate_btn,
313
+ "status": status,
314
+ "output_video": output_video
315
+ }
316
+
317
+
318
+ def create_all_tabs():
319
+ """Create all tabs and return component references."""
320
+ tabs = {}
321
+
322
+ with gr.Tab("Text to Music", id="text-to-music"):
323
+ tabs["text_to_music"] = create_text_to_music_tab()
324
+
325
+ with gr.Tab("Music to Music", id="music-to-music"):
326
+ tabs["music_to_music"] = create_music_to_music_tab()
327
+
328
+ with gr.Tab("Text to Image", id="text-to-image"):
329
+ tabs["text_to_image"] = create_text_to_image_tab()
330
+
331
+ with gr.Tab("Image to Image", id="image-to-image"):
332
+ tabs["image_to_image"] = create_image_to_image_tab()
333
+
334
+ with gr.Tab("Text to Video", id="text-to-video"):
335
+ tabs["text_to_video"] = create_text_to_video_tab()
336
+
337
+ with gr.Tab("Image to Video", id="image-to-video"):
338
+ tabs["image_to_video"] = create_image_to_video_tab()
339
+
340
+ return tabs