coder-music-cli 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/PKG-INFO +7 -3
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/PKG-INFO +7 -3
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/requires.txt +7 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/__init__.py +1 -1
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/client.py +14 -2
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/player/base.py +7 -2
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/player/ffplay.py +13 -3
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/sources/ai_generator.py +53 -26
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/pyproject.toml +7 -2
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/tests/test_config.py +0 -3
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/tests/test_context.py +0 -2
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/tests/test_history.py +0 -2
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/LICENSE +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/README.md +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/SOURCES.txt +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/dependency_links.txt +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/entry_points.txt +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/top_level.txt +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/__main__.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/cli.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/config.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/context/__init__.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/context/mood.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/context/temporal.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/daemon.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/history.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/player/__init__.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/sources/__init__.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/sources/local.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/sources/radio.py +0 -0
- {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coder-music-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A command-line music application for coders with daemon support, radio streaming, and AI-generated music
|
|
5
5
|
Author-email: Luong Nguyen <luongnv89@gmail.com>
|
|
6
6
|
Maintainer-email: Luong Nguyen <luongnv89@gmail.com>
|
|
@@ -30,8 +30,12 @@ Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
|
30
30
|
Requires-Dist: tomli-w>=1.0
|
|
31
31
|
Provides-Extra: ai
|
|
32
32
|
Requires-Dist: torch>=2.0; extra == "ai"
|
|
33
|
-
Requires-Dist: transformers>=4.
|
|
34
|
-
Requires-Dist:
|
|
33
|
+
Requires-Dist: transformers>=4.31; extra == "ai"
|
|
34
|
+
Requires-Dist: scipy>=1.10; extra == "ai"
|
|
35
|
+
Provides-Extra: ai-full
|
|
36
|
+
Requires-Dist: torch>=2.0; extra == "ai-full"
|
|
37
|
+
Requires-Dist: transformers>=4.30; extra == "ai-full"
|
|
38
|
+
Requires-Dist: audiocraft>=1.0; python_version < "3.14" and extra == "ai-full"
|
|
35
39
|
Provides-Extra: dev
|
|
36
40
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
37
41
|
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coder-music-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A command-line music application for coders with daemon support, radio streaming, and AI-generated music
|
|
5
5
|
Author-email: Luong Nguyen <luongnv89@gmail.com>
|
|
6
6
|
Maintainer-email: Luong Nguyen <luongnv89@gmail.com>
|
|
@@ -30,8 +30,12 @@ Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
|
30
30
|
Requires-Dist: tomli-w>=1.0
|
|
31
31
|
Provides-Extra: ai
|
|
32
32
|
Requires-Dist: torch>=2.0; extra == "ai"
|
|
33
|
-
Requires-Dist: transformers>=4.
|
|
34
|
-
Requires-Dist:
|
|
33
|
+
Requires-Dist: transformers>=4.31; extra == "ai"
|
|
34
|
+
Requires-Dist: scipy>=1.10; extra == "ai"
|
|
35
|
+
Provides-Extra: ai-full
|
|
36
|
+
Requires-Dist: torch>=2.0; extra == "ai-full"
|
|
37
|
+
Requires-Dist: transformers>=4.30; extra == "ai-full"
|
|
38
|
+
Requires-Dist: audiocraft>=1.0; python_version < "3.14" and extra == "ai-full"
|
|
35
39
|
Provides-Extra: dev
|
|
36
40
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
37
41
|
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
@@ -12,6 +12,8 @@ logger = logging.getLogger(__name__)
|
|
|
12
12
|
# Constants
|
|
13
13
|
SOCKET_BUFFER_SIZE = 4096
|
|
14
14
|
MAX_RESPONSE_SIZE = 10 * 1024 * 1024 # 10MB limit
|
|
15
|
+
DEFAULT_TIMEOUT = 10.0
|
|
16
|
+
AI_TIMEOUT = 300.0 # 5 minutes for AI generation
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class DaemonClient:
|
|
@@ -21,12 +23,15 @@ class DaemonClient:
|
|
|
21
23
|
self.config = get_config()
|
|
22
24
|
self.socket_path = str(self.config.socket_path)
|
|
23
25
|
|
|
24
|
-
def send_command(
|
|
26
|
+
def send_command(
|
|
27
|
+
self, command: str, args: dict | None = None, timeout: float | None = None
|
|
28
|
+
) -> dict[str, Any]:
|
|
25
29
|
"""Send a command to the daemon and get response.
|
|
26
30
|
|
|
27
31
|
Args:
|
|
28
32
|
command: Command name (play, stop, pause, resume, status, etc.)
|
|
29
33
|
args: Command arguments
|
|
34
|
+
timeout: Socket timeout in seconds (default: 10s, AI commands: 300s)
|
|
30
35
|
|
|
31
36
|
Returns:
|
|
32
37
|
Response dictionary from daemon
|
|
@@ -37,6 +42,13 @@ class DaemonClient:
|
|
|
37
42
|
if args is None:
|
|
38
43
|
args = {}
|
|
39
44
|
|
|
45
|
+
# Use longer timeout for AI commands
|
|
46
|
+
if timeout is None:
|
|
47
|
+
if command == "play" and args.get("mode") == "ai":
|
|
48
|
+
timeout = AI_TIMEOUT
|
|
49
|
+
else:
|
|
50
|
+
timeout = DEFAULT_TIMEOUT
|
|
51
|
+
|
|
40
52
|
request = {
|
|
41
53
|
"command": command,
|
|
42
54
|
"args": args,
|
|
@@ -44,7 +56,7 @@ class DaemonClient:
|
|
|
44
56
|
|
|
45
57
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
46
58
|
try:
|
|
47
|
-
sock.settimeout(
|
|
59
|
+
sock.settimeout(timeout)
|
|
48
60
|
sock.connect(self.socket_path)
|
|
49
61
|
|
|
50
62
|
sock.sendall(json.dumps(request).encode())
|
|
@@ -70,8 +70,13 @@ class Player(ABC):
|
|
|
70
70
|
self._on_track_end = callback
|
|
71
71
|
|
|
72
72
|
@abstractmethod
|
|
73
|
-
async def play(self, track: TrackInfo) -> bool:
|
|
74
|
-
"""Start playing a track. Returns True if successful.
|
|
73
|
+
async def play(self, track: TrackInfo, loop: bool = False) -> bool:
|
|
74
|
+
"""Start playing a track. Returns True if successful.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
track: Track information to play
|
|
78
|
+
loop: If True, loop the track indefinitely
|
|
79
|
+
"""
|
|
75
80
|
pass
|
|
76
81
|
|
|
77
82
|
@abstractmethod
|
|
@@ -23,8 +23,13 @@ class FFplayPlayer(Player):
|
|
|
23
23
|
if not shutil.which("ffplay"):
|
|
24
24
|
logger.warning("ffplay not found in PATH. Please install FFmpeg.")
|
|
25
25
|
|
|
26
|
-
async def play(self, track: TrackInfo) -> bool:
|
|
27
|
-
"""Start playing a track using ffplay.
|
|
26
|
+
async def play(self, track: TrackInfo, loop: bool = False) -> bool:
|
|
27
|
+
"""Start playing a track using ffplay.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
track: Track information to play
|
|
31
|
+
loop: If True, loop the track indefinitely (useful for AI-generated short clips)
|
|
32
|
+
"""
|
|
28
33
|
# Stop any current playback
|
|
29
34
|
await self.stop()
|
|
30
35
|
|
|
@@ -36,13 +41,18 @@ class FFplayPlayer(Player):
|
|
|
36
41
|
cmd = [
|
|
37
42
|
"ffplay",
|
|
38
43
|
"-nodisp", # No display window
|
|
39
|
-
"-autoexit", # Exit when done (for files)
|
|
40
44
|
"-loglevel",
|
|
41
45
|
"quiet", # Suppress output
|
|
42
46
|
"-volume",
|
|
43
47
|
str(self._volume),
|
|
44
48
|
]
|
|
45
49
|
|
|
50
|
+
# Loop mode for AI tracks or explicit loop request
|
|
51
|
+
if loop or track.source_type == "ai":
|
|
52
|
+
cmd.extend(["-loop", "0"]) # 0 = infinite loop
|
|
53
|
+
else:
|
|
54
|
+
cmd.append("-autoexit") # Exit when done (for files)
|
|
55
|
+
|
|
46
56
|
# For streams, add reconnect options
|
|
47
57
|
if track.source_type == "radio":
|
|
48
58
|
cmd.extend(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""AI music generation using MusicGen (optional feature)."""
|
|
1
|
+
"""AI music generation using MusicGen via HuggingFace Transformers (optional feature)."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import tempfile
|
|
@@ -11,6 +11,7 @@ logger = logging.getLogger(__name__)
|
|
|
11
11
|
# Flag to track if AI dependencies are available
|
|
12
12
|
_AI_AVAILABLE: bool | None = None
|
|
13
13
|
_musicgen_model = None
|
|
14
|
+
_musicgen_processor = None
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def is_ai_available() -> bool:
|
|
@@ -22,10 +23,10 @@ def is_ai_available() -> bool:
|
|
|
22
23
|
|
|
23
24
|
try:
|
|
24
25
|
import torch # noqa: F401
|
|
25
|
-
from
|
|
26
|
+
from transformers import AutoProcessor, MusicgenForConditionalGeneration # noqa: F401
|
|
26
27
|
|
|
27
28
|
_AI_AVAILABLE = True
|
|
28
|
-
logger.info("AI music generation is available")
|
|
29
|
+
logger.info("AI music generation is available (using HuggingFace Transformers)")
|
|
29
30
|
except ImportError as e:
|
|
30
31
|
_AI_AVAILABLE = False
|
|
31
32
|
logger.info(f"AI music generation not available: {e}")
|
|
@@ -34,26 +35,28 @@ def is_ai_available() -> bool:
|
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
def _get_model():
|
|
37
|
-
"""Lazy-load the MusicGen model."""
|
|
38
|
-
global _musicgen_model
|
|
38
|
+
"""Lazy-load the MusicGen model via HuggingFace Transformers."""
|
|
39
|
+
global _musicgen_model, _musicgen_processor
|
|
39
40
|
|
|
40
41
|
if not is_ai_available():
|
|
41
|
-
return None
|
|
42
|
+
return None, None
|
|
42
43
|
|
|
43
44
|
if _musicgen_model is None:
|
|
44
45
|
try:
|
|
45
|
-
from
|
|
46
|
+
from transformers import AutoProcessor, MusicgenForConditionalGeneration
|
|
47
|
+
|
|
48
|
+
model_name = "facebook/musicgen-small"
|
|
49
|
+
logger.info(f"Loading MusicGen model ({model_name}) - this may take a moment...")
|
|
50
|
+
|
|
51
|
+
_musicgen_processor = AutoProcessor.from_pretrained(model_name) # nosec B615
|
|
52
|
+
_musicgen_model = MusicgenForConditionalGeneration.from_pretrained(model_name) # nosec B615
|
|
46
53
|
|
|
47
|
-
logger.info("Loading MusicGen model (this may take a moment)...")
|
|
48
|
-
# Use the small model for faster loading and lower memory usage
|
|
49
|
-
_musicgen_model = MusicGen.get_pretrained("facebook/musicgen-small")
|
|
50
|
-
_musicgen_model.set_generation_params(duration=30) # 30 seconds default
|
|
51
54
|
logger.info("MusicGen model loaded successfully")
|
|
52
55
|
except Exception as e:
|
|
53
56
|
logger.error(f"Failed to load MusicGen model: {e}")
|
|
54
|
-
return None
|
|
57
|
+
return None, None
|
|
55
58
|
|
|
56
|
-
return _musicgen_model
|
|
59
|
+
return _musicgen_model, _musicgen_processor
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
class AIGenerator:
|
|
@@ -86,28 +89,45 @@ class AIGenerator:
|
|
|
86
89
|
|
|
87
90
|
Args:
|
|
88
91
|
prompt: Text description of the music to generate.
|
|
89
|
-
duration: Duration in seconds (5-
|
|
92
|
+
duration: Duration in seconds (5-60 for reasonable generation time).
|
|
90
93
|
filename: Optional output filename.
|
|
91
94
|
|
|
92
95
|
Returns:
|
|
93
96
|
TrackInfo for the generated audio, or None if generation failed.
|
|
94
97
|
"""
|
|
95
|
-
model = _get_model()
|
|
96
|
-
if model is None:
|
|
98
|
+
model, processor = _get_model()
|
|
99
|
+
if model is None or processor is None:
|
|
97
100
|
logger.warning("AI model not available")
|
|
98
101
|
return None
|
|
99
102
|
|
|
100
103
|
try:
|
|
101
104
|
import scipy.io.wavfile
|
|
105
|
+
import torch
|
|
106
|
+
|
|
107
|
+
# Clamp duration (keep reasonable for generation time)
|
|
108
|
+
duration = max(5, min(60, duration))
|
|
102
109
|
|
|
103
|
-
#
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
# Calculate max_new_tokens based on duration
|
|
111
|
+
# MusicGen generates at ~50 tokens per second of audio
|
|
112
|
+
tokens_per_second = 50
|
|
113
|
+
max_new_tokens = duration * tokens_per_second
|
|
106
114
|
|
|
107
115
|
logger.info(f"Generating {duration}s of music: {prompt[:50]}...")
|
|
108
116
|
|
|
117
|
+
# Process the prompt
|
|
118
|
+
inputs = processor(
|
|
119
|
+
text=[prompt],
|
|
120
|
+
padding=True,
|
|
121
|
+
return_tensors="pt",
|
|
122
|
+
)
|
|
123
|
+
|
|
109
124
|
# Generate audio
|
|
110
|
-
|
|
125
|
+
with torch.no_grad():
|
|
126
|
+
audio_values = model.generate(
|
|
127
|
+
**inputs,
|
|
128
|
+
max_new_tokens=max_new_tokens,
|
|
129
|
+
do_sample=True,
|
|
130
|
+
)
|
|
111
131
|
|
|
112
132
|
# Generate filename if not provided
|
|
113
133
|
if filename is None:
|
|
@@ -124,11 +144,15 @@ class AIGenerator:
|
|
|
124
144
|
output_path = self.output_dir / filename
|
|
125
145
|
|
|
126
146
|
# Get the audio tensor and sample rate
|
|
127
|
-
|
|
128
|
-
sample_rate = model.
|
|
147
|
+
# MusicGen uses 32kHz sample rate
|
|
148
|
+
sample_rate = model.config.audio_encoder.sampling_rate
|
|
149
|
+
audio = audio_values[0, 0].cpu().numpy()
|
|
150
|
+
|
|
151
|
+
# Normalize audio to int16 range for WAV
|
|
152
|
+
audio = (audio * 32767).astype("int16")
|
|
129
153
|
|
|
130
154
|
# Save as WAV
|
|
131
|
-
scipy.io.wavfile.write(str(output_path), sample_rate, audio
|
|
155
|
+
scipy.io.wavfile.write(str(output_path), sample_rate, audio)
|
|
132
156
|
|
|
133
157
|
logger.info(f"Generated audio saved to: {output_path}")
|
|
134
158
|
|
|
@@ -145,6 +169,9 @@ class AIGenerator:
|
|
|
145
169
|
|
|
146
170
|
except Exception as e:
|
|
147
171
|
logger.error(f"Failed to generate music: {e}")
|
|
172
|
+
import traceback
|
|
173
|
+
|
|
174
|
+
traceback.print_exc()
|
|
148
175
|
return None
|
|
149
176
|
|
|
150
177
|
def generate_for_context(
|
|
@@ -208,12 +235,12 @@ def get_ai_install_instructions() -> str:
|
|
|
208
235
|
AI music generation requires additional dependencies.
|
|
209
236
|
Install them with:
|
|
210
237
|
|
|
211
|
-
pip install 'music-cli[ai]'
|
|
238
|
+
pip install 'coder-music-cli[ai]'
|
|
212
239
|
|
|
213
240
|
Or install manually:
|
|
214
241
|
|
|
215
|
-
pip install torch transformers
|
|
242
|
+
pip install torch transformers scipy
|
|
216
243
|
|
|
217
244
|
Note: This requires significant disk space (~5GB) and RAM (~8GB minimum).
|
|
218
|
-
The first generation will download the MusicGen model (~
|
|
245
|
+
The first generation will download the MusicGen model (~1.5GB).
|
|
219
246
|
"""
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "coder-music-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "A command-line music application for coders with daemon support, radio streaming, and AI-generated music"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -38,9 +38,14 @@ dependencies = [
|
|
|
38
38
|
|
|
39
39
|
[project.optional-dependencies]
|
|
40
40
|
ai = [
|
|
41
|
+
"torch>=2.0",
|
|
42
|
+
"transformers>=4.31",
|
|
43
|
+
"scipy>=1.10",
|
|
44
|
+
]
|
|
45
|
+
ai-full = [
|
|
41
46
|
"torch>=2.0",
|
|
42
47
|
"transformers>=4.30",
|
|
43
|
-
"audiocraft>=1.0",
|
|
48
|
+
"audiocraft>=1.0;python_version<'3.14'",
|
|
44
49
|
]
|
|
45
50
|
dev = [
|
|
46
51
|
"pytest>=7.0",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|