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.
Files changed (31) hide show
  1. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/PKG-INFO +7 -3
  2. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/PKG-INFO +7 -3
  3. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/requires.txt +7 -0
  4. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/__init__.py +1 -1
  5. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/client.py +14 -2
  6. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/player/base.py +7 -2
  7. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/player/ffplay.py +13 -3
  8. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/sources/ai_generator.py +53 -26
  9. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/pyproject.toml +7 -2
  10. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/tests/test_config.py +0 -3
  11. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/tests/test_context.py +0 -2
  12. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/tests/test_history.py +0 -2
  13. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/LICENSE +0 -0
  14. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/README.md +0 -0
  15. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/SOURCES.txt +0 -0
  16. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/dependency_links.txt +0 -0
  17. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/entry_points.txt +0 -0
  18. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/coder_music_cli.egg-info/top_level.txt +0 -0
  19. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/__main__.py +0 -0
  20. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/cli.py +0 -0
  21. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/config.py +0 -0
  22. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/context/__init__.py +0 -0
  23. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/context/mood.py +0 -0
  24. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/context/temporal.py +0 -0
  25. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/daemon.py +0 -0
  26. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/history.py +0 -0
  27. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/player/__init__.py +0 -0
  28. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/sources/__init__.py +0 -0
  29. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/sources/local.py +0 -0
  30. {coder_music_cli-0.1.0 → coder_music_cli-0.2.0}/music_cli/sources/radio.py +0 -0
  31. {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.1.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.30; extra == "ai"
34
- Requires-Dist: audiocraft>=1.0; extra == "ai"
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.1.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.30; extra == "ai"
34
- Requires-Dist: audiocraft>=1.0; extra == "ai"
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"
@@ -6,7 +6,14 @@ tomli>=2.0
6
6
 
7
7
  [ai]
8
8
  torch>=2.0
9
+ transformers>=4.31
10
+ scipy>=1.10
11
+
12
+ [ai-full]
13
+ torch>=2.0
9
14
  transformers>=4.30
15
+
16
+ [ai-full:python_version < "3.14"]
10
17
  audiocraft>=1.0
11
18
 
12
19
  [dev]
@@ -1,3 +1,3 @@
1
1
  """music-cli: A command-line music application for coders."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.2.0"
@@ -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(self, command: str, args: dict | None = None) -> dict[str, Any]:
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(10.0)
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 audiocraft.models import MusicGen # noqa: F401
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 audiocraft.models import MusicGen
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-300).
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
- # Clamp duration
104
- duration = max(5, min(300, duration))
105
- model.set_generation_params(duration=duration)
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
- wav = model.generate([prompt])
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
- audio = wav[0].cpu().numpy()
128
- sample_rate = model.sample_rate
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.T)
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 audiocraft
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 (~3GB).
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.1.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",
@@ -1,10 +1,7 @@
1
1
  """Tests for configuration module."""
2
2
 
3
- import tempfile
4
3
  from pathlib import Path
5
4
 
6
- import pytest
7
-
8
5
  from music_cli.config import Config
9
6
 
10
7
 
@@ -2,8 +2,6 @@
2
2
 
3
3
  from datetime import datetime
4
4
 
5
- import pytest
6
-
7
5
  from music_cli.context.mood import Mood, MoodContext
8
6
  from music_cli.context.temporal import DayType, Season, TemporalContext, TimePeriod
9
7
 
@@ -2,8 +2,6 @@
2
2
 
3
3
  from pathlib import Path
4
4
 
5
- import pytest
6
-
7
5
  from music_cli.history import History, HistoryEntry
8
6
 
9
7
 
File without changes