mycelium-ai 0.5.0__py3-none-any.whl

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 (164) hide show
  1. mycelium/__init__.py +0 -0
  2. mycelium/api/__init__.py +0 -0
  3. mycelium/api/app.py +1147 -0
  4. mycelium/api/client_app.py +170 -0
  5. mycelium/api/generated_sources/__init__.py +0 -0
  6. mycelium/api/generated_sources/server_schemas/__init__.py +97 -0
  7. mycelium/api/generated_sources/server_schemas/api/__init__.py +5 -0
  8. mycelium/api/generated_sources/server_schemas/api/default_api.py +2473 -0
  9. mycelium/api/generated_sources/server_schemas/api_client.py +766 -0
  10. mycelium/api/generated_sources/server_schemas/api_response.py +25 -0
  11. mycelium/api/generated_sources/server_schemas/configuration.py +434 -0
  12. mycelium/api/generated_sources/server_schemas/exceptions.py +166 -0
  13. mycelium/api/generated_sources/server_schemas/models/__init__.py +41 -0
  14. mycelium/api/generated_sources/server_schemas/models/api_section.py +71 -0
  15. mycelium/api/generated_sources/server_schemas/models/chroma_section.py +69 -0
  16. mycelium/api/generated_sources/server_schemas/models/clap_section.py +75 -0
  17. mycelium/api/generated_sources/server_schemas/models/compute_on_server200_response.py +79 -0
  18. mycelium/api/generated_sources/server_schemas/models/compute_on_server_request.py +67 -0
  19. mycelium/api/generated_sources/server_schemas/models/compute_text_search_request.py +69 -0
  20. mycelium/api/generated_sources/server_schemas/models/config_request.py +81 -0
  21. mycelium/api/generated_sources/server_schemas/models/config_response.py +107 -0
  22. mycelium/api/generated_sources/server_schemas/models/create_playlist_request.py +71 -0
  23. mycelium/api/generated_sources/server_schemas/models/get_similar_by_track200_response.py +143 -0
  24. mycelium/api/generated_sources/server_schemas/models/library_stats_response.py +77 -0
  25. mycelium/api/generated_sources/server_schemas/models/logging_section.py +67 -0
  26. mycelium/api/generated_sources/server_schemas/models/media_server_section.py +67 -0
  27. mycelium/api/generated_sources/server_schemas/models/playlist_response.py +73 -0
  28. mycelium/api/generated_sources/server_schemas/models/plex_section.py +71 -0
  29. mycelium/api/generated_sources/server_schemas/models/processing_response.py +90 -0
  30. mycelium/api/generated_sources/server_schemas/models/save_config_response.py +73 -0
  31. mycelium/api/generated_sources/server_schemas/models/scan_library_response.py +75 -0
  32. mycelium/api/generated_sources/server_schemas/models/search_result_response.py +75 -0
  33. mycelium/api/generated_sources/server_schemas/models/server_section.py +67 -0
  34. mycelium/api/generated_sources/server_schemas/models/stop_processing_response.py +71 -0
  35. mycelium/api/generated_sources/server_schemas/models/task_status_response.py +87 -0
  36. mycelium/api/generated_sources/server_schemas/models/track_database_stats.py +75 -0
  37. mycelium/api/generated_sources/server_schemas/models/track_response.py +77 -0
  38. mycelium/api/generated_sources/server_schemas/models/tracks_list_response.py +81 -0
  39. mycelium/api/generated_sources/server_schemas/rest.py +329 -0
  40. mycelium/api/generated_sources/server_schemas/test/__init__.py +0 -0
  41. mycelium/api/generated_sources/server_schemas/test/test_api_section.py +57 -0
  42. mycelium/api/generated_sources/server_schemas/test/test_chroma_section.py +55 -0
  43. mycelium/api/generated_sources/server_schemas/test/test_clap_section.py +60 -0
  44. mycelium/api/generated_sources/server_schemas/test/test_compute_on_server200_response.py +52 -0
  45. mycelium/api/generated_sources/server_schemas/test/test_compute_on_server_request.py +53 -0
  46. mycelium/api/generated_sources/server_schemas/test/test_compute_text_search_request.py +54 -0
  47. mycelium/api/generated_sources/server_schemas/test/test_config_request.py +66 -0
  48. mycelium/api/generated_sources/server_schemas/test/test_config_response.py +97 -0
  49. mycelium/api/generated_sources/server_schemas/test/test_create_playlist_request.py +60 -0
  50. mycelium/api/generated_sources/server_schemas/test/test_default_api.py +150 -0
  51. mycelium/api/generated_sources/server_schemas/test/test_get_similar_by_track200_response.py +61 -0
  52. mycelium/api/generated_sources/server_schemas/test/test_library_stats_response.py +63 -0
  53. mycelium/api/generated_sources/server_schemas/test/test_logging_section.py +53 -0
  54. mycelium/api/generated_sources/server_schemas/test/test_media_server_section.py +53 -0
  55. mycelium/api/generated_sources/server_schemas/test/test_playlist_response.py +58 -0
  56. mycelium/api/generated_sources/server_schemas/test/test_plex_section.py +56 -0
  57. mycelium/api/generated_sources/server_schemas/test/test_processing_response.py +61 -0
  58. mycelium/api/generated_sources/server_schemas/test/test_save_config_response.py +58 -0
  59. mycelium/api/generated_sources/server_schemas/test/test_scan_library_response.py +61 -0
  60. mycelium/api/generated_sources/server_schemas/test/test_search_result_response.py +69 -0
  61. mycelium/api/generated_sources/server_schemas/test/test_server_section.py +53 -0
  62. mycelium/api/generated_sources/server_schemas/test/test_stop_processing_response.py +55 -0
  63. mycelium/api/generated_sources/server_schemas/test/test_task_status_response.py +71 -0
  64. mycelium/api/generated_sources/server_schemas/test/test_track_database_stats.py +60 -0
  65. mycelium/api/generated_sources/server_schemas/test/test_track_response.py +63 -0
  66. mycelium/api/generated_sources/server_schemas/test/test_tracks_list_response.py +75 -0
  67. mycelium/api/generated_sources/worker_schemas/__init__.py +61 -0
  68. mycelium/api/generated_sources/worker_schemas/api/__init__.py +5 -0
  69. mycelium/api/generated_sources/worker_schemas/api/default_api.py +318 -0
  70. mycelium/api/generated_sources/worker_schemas/api_client.py +766 -0
  71. mycelium/api/generated_sources/worker_schemas/api_response.py +25 -0
  72. mycelium/api/generated_sources/worker_schemas/configuration.py +434 -0
  73. mycelium/api/generated_sources/worker_schemas/exceptions.py +166 -0
  74. mycelium/api/generated_sources/worker_schemas/models/__init__.py +23 -0
  75. mycelium/api/generated_sources/worker_schemas/models/save_config_response.py +73 -0
  76. mycelium/api/generated_sources/worker_schemas/models/worker_clap_section.py +75 -0
  77. mycelium/api/generated_sources/worker_schemas/models/worker_client_api_section.py +69 -0
  78. mycelium/api/generated_sources/worker_schemas/models/worker_client_section.py +79 -0
  79. mycelium/api/generated_sources/worker_schemas/models/worker_config_request.py +73 -0
  80. mycelium/api/generated_sources/worker_schemas/models/worker_config_response.py +89 -0
  81. mycelium/api/generated_sources/worker_schemas/models/worker_logging_section.py +67 -0
  82. mycelium/api/generated_sources/worker_schemas/rest.py +329 -0
  83. mycelium/api/generated_sources/worker_schemas/test/__init__.py +0 -0
  84. mycelium/api/generated_sources/worker_schemas/test/test_default_api.py +45 -0
  85. mycelium/api/generated_sources/worker_schemas/test/test_save_config_response.py +58 -0
  86. mycelium/api/generated_sources/worker_schemas/test/test_worker_clap_section.py +60 -0
  87. mycelium/api/generated_sources/worker_schemas/test/test_worker_client_api_section.py +55 -0
  88. mycelium/api/generated_sources/worker_schemas/test/test_worker_client_section.py +65 -0
  89. mycelium/api/generated_sources/worker_schemas/test/test_worker_config_request.py +59 -0
  90. mycelium/api/generated_sources/worker_schemas/test/test_worker_config_response.py +89 -0
  91. mycelium/api/generated_sources/worker_schemas/test/test_worker_logging_section.py +53 -0
  92. mycelium/api/worker_models.py +99 -0
  93. mycelium/application/__init__.py +11 -0
  94. mycelium/application/job_queue.py +323 -0
  95. mycelium/application/library_management_use_cases.py +292 -0
  96. mycelium/application/search_use_cases.py +96 -0
  97. mycelium/application/services.py +340 -0
  98. mycelium/client.py +554 -0
  99. mycelium/client_config.py +251 -0
  100. mycelium/client_frontend_dist/404.html +1 -0
  101. mycelium/client_frontend_dist/_next/static/a4iyRdfsvkjdyMAK9cE9Y/_buildManifest.js +1 -0
  102. mycelium/client_frontend_dist/_next/static/a4iyRdfsvkjdyMAK9cE9Y/_ssgManifest.js +1 -0
  103. mycelium/client_frontend_dist/_next/static/chunks/4bd1b696-cf72ae8a39fa05aa.js +1 -0
  104. mycelium/client_frontend_dist/_next/static/chunks/964-830f77d7ce1c2463.js +1 -0
  105. mycelium/client_frontend_dist/_next/static/chunks/app/_not-found/page-d25eede5a9099bd3.js +1 -0
  106. mycelium/client_frontend_dist/_next/static/chunks/app/layout-9b3d32f96dfe13b6.js +1 -0
  107. mycelium/client_frontend_dist/_next/static/chunks/app/page-cc6bad295789134e.js +1 -0
  108. mycelium/client_frontend_dist/_next/static/chunks/framework-7c95b8e5103c9e90.js +1 -0
  109. mycelium/client_frontend_dist/_next/static/chunks/main-6b37be50736577a2.js +1 -0
  110. mycelium/client_frontend_dist/_next/static/chunks/main-app-4153d115599d3126.js +1 -0
  111. mycelium/client_frontend_dist/_next/static/chunks/pages/_app-0a0020ddd67f79cf.js +1 -0
  112. mycelium/client_frontend_dist/_next/static/chunks/pages/_error-03529f2c21436739.js +1 -0
  113. mycelium/client_frontend_dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  114. mycelium/client_frontend_dist/_next/static/chunks/webpack-c81e624915b2ea70.js +1 -0
  115. mycelium/client_frontend_dist/_next/static/css/1eb7f0e2c78e0734.css +1 -0
  116. mycelium/client_frontend_dist/favicon.ico +0 -0
  117. mycelium/client_frontend_dist/file.svg +1 -0
  118. mycelium/client_frontend_dist/globe.svg +1 -0
  119. mycelium/client_frontend_dist/index.html +1 -0
  120. mycelium/client_frontend_dist/index.txt +20 -0
  121. mycelium/client_frontend_dist/next.svg +1 -0
  122. mycelium/client_frontend_dist/vercel.svg +1 -0
  123. mycelium/client_frontend_dist/window.svg +1 -0
  124. mycelium/config.py +346 -0
  125. mycelium/domain/__init__.py +13 -0
  126. mycelium/domain/models.py +71 -0
  127. mycelium/domain/repositories.py +98 -0
  128. mycelium/domain/worker.py +77 -0
  129. mycelium/frontend_dist/404.html +1 -0
  130. mycelium/frontend_dist/_next/static/chunks/4bd1b696-cf72ae8a39fa05aa.js +1 -0
  131. mycelium/frontend_dist/_next/static/chunks/964-830f77d7ce1c2463.js +1 -0
  132. mycelium/frontend_dist/_next/static/chunks/app/_not-found/page-d25eede5a9099bd3.js +1 -0
  133. mycelium/frontend_dist/_next/static/chunks/app/layout-9b3d32f96dfe13b6.js +1 -0
  134. mycelium/frontend_dist/_next/static/chunks/app/page-a761463485e0540b.js +1 -0
  135. mycelium/frontend_dist/_next/static/chunks/framework-7c95b8e5103c9e90.js +1 -0
  136. mycelium/frontend_dist/_next/static/chunks/main-6b37be50736577a2.js +1 -0
  137. mycelium/frontend_dist/_next/static/chunks/main-app-4153d115599d3126.js +1 -0
  138. mycelium/frontend_dist/_next/static/chunks/pages/_app-0a0020ddd67f79cf.js +1 -0
  139. mycelium/frontend_dist/_next/static/chunks/pages/_error-03529f2c21436739.js +1 -0
  140. mycelium/frontend_dist/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  141. mycelium/frontend_dist/_next/static/chunks/webpack-c81e624915b2ea70.js +1 -0
  142. mycelium/frontend_dist/_next/static/css/1eb7f0e2c78e0734.css +1 -0
  143. mycelium/frontend_dist/_next/static/glVJ0yJSL0zWN7anTTG3_/_buildManifest.js +1 -0
  144. mycelium/frontend_dist/_next/static/glVJ0yJSL0zWN7anTTG3_/_ssgManifest.js +1 -0
  145. mycelium/frontend_dist/favicon.ico +0 -0
  146. mycelium/frontend_dist/file.svg +1 -0
  147. mycelium/frontend_dist/globe.svg +1 -0
  148. mycelium/frontend_dist/index.html +10 -0
  149. mycelium/frontend_dist/index.txt +20 -0
  150. mycelium/frontend_dist/next.svg +1 -0
  151. mycelium/frontend_dist/vercel.svg +1 -0
  152. mycelium/frontend_dist/window.svg +1 -0
  153. mycelium/infrastructure/__init__.py +17 -0
  154. mycelium/infrastructure/chroma_adapter.py +232 -0
  155. mycelium/infrastructure/clap_adapter.py +280 -0
  156. mycelium/infrastructure/plex_adapter.py +145 -0
  157. mycelium/infrastructure/track_database.py +467 -0
  158. mycelium/main.py +183 -0
  159. mycelium_ai-0.5.0.dist-info/METADATA +312 -0
  160. mycelium_ai-0.5.0.dist-info/RECORD +164 -0
  161. mycelium_ai-0.5.0.dist-info/WHEEL +5 -0
  162. mycelium_ai-0.5.0.dist-info/entry_points.txt +2 -0
  163. mycelium_ai-0.5.0.dist-info/licenses/LICENSE +21 -0
  164. mycelium_ai-0.5.0.dist-info/top_level.txt +1 -0
mycelium/config.py ADDED
@@ -0,0 +1,346 @@
1
+ """Configuration management for Mycelium"""
2
+
3
+ import logging
4
+ import os
5
+ from dataclasses import dataclass, asdict
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import yaml
10
+
11
+ from mycelium.domain.models import MediaServerType
12
+
13
+
14
+ def get_user_data_dir() -> Path:
15
+ """Get the user data directory for Mycelium (platform-specific)."""
16
+ if os.name == 'nt': # Windows
17
+ base_dir = os.getenv('LOCALAPPDATA', os.path.expanduser('~/AppData/Local'))
18
+ elif os.uname().sysname == 'Darwin': # macOS
19
+ base_dir = os.path.expanduser('~/Library/Application Support')
20
+ else: # Linux/Unix
21
+ base_dir = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
22
+
23
+ data_dir = Path(base_dir) / 'mycelium'
24
+ data_dir.mkdir(parents=True, exist_ok=True)
25
+ return data_dir
26
+
27
+
28
+ def get_user_log_dir() -> Path:
29
+ """Get the user log directory for Mycelium (platform-specific)."""
30
+ if os.name == 'nt': # Windows
31
+ base_dir = os.getenv('LOCALAPPDATA', os.path.expanduser('~/AppData/Local'))
32
+ elif os.uname().sysname == 'Darwin': # macOS
33
+ base_dir = os.path.expanduser('~/Library/Logs')
34
+ else: # Linux/Unix
35
+ base_dir = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
36
+
37
+ log_dir = Path(base_dir) / 'mycelium'
38
+ log_dir.mkdir(parents=True, exist_ok=True)
39
+ return log_dir
40
+
41
+
42
+ def get_config_dir() -> Path:
43
+ """Get the configuration directory for Mycelium."""
44
+ if os.name == 'nt': # Windows
45
+ base_dir = os.getenv('APPDATA', os.path.expanduser('~/AppData/Roaming'))
46
+ else: # macOS and Linux/Unix
47
+ base_dir = os.path.expanduser('~/.config')
48
+
49
+ config_dir = Path(base_dir) / 'mycelium'
50
+ config_dir.mkdir(parents=True, exist_ok=True)
51
+ return config_dir
52
+
53
+
54
+ def get_config_file_path() -> Path:
55
+ """Get the configuration file path."""
56
+ return get_config_dir() / 'config.yml'
57
+
58
+
59
+ @dataclass
60
+ class PlexConfig:
61
+ """Configuration for Plex connection."""
62
+ url: str = "http://localhost:32400"
63
+ token: Optional[str] = None
64
+ music_library_name: str = "Music"
65
+
66
+ @dataclass
67
+ class ServerConfig:
68
+ """Configuration for server settings."""
69
+ gpu_batch_size: int = 16
70
+
71
+ @dataclass
72
+ class CLAPConfig:
73
+ """Configuration for CLAP model."""
74
+ model_id: str = "laion/larger_clap_music_and_speech"
75
+ target_sr: int = 48000
76
+ chunk_duration_s: int = 10
77
+ num_chunks: int = 3
78
+ max_load_duration_s: Optional[int] = 120
79
+
80
+ @dataclass
81
+ class MediaServerConfig:
82
+ """Configuration for media server."""
83
+ type: MediaServerType
84
+
85
+ def __post_init__(self):
86
+ """Convert string type to MediaServerType enum if needed."""
87
+ if isinstance(self.type, str):
88
+ self.type = MediaServerType(self.type.lower())
89
+
90
+ @dataclass
91
+ class ChromaConfig:
92
+ """Configuration for ChromaDB."""
93
+ collection_name: str = "my_music_library"
94
+ batch_size: int = 1000
95
+
96
+ @staticmethod
97
+ def get_db_path() -> str:
98
+ """Get the actual database path for ChromaDB."""
99
+ return str(get_user_data_dir() / "music_vector_db")
100
+
101
+
102
+ @dataclass
103
+ class DatabaseConfig:
104
+ """Configuration for track metadata database."""
105
+
106
+ @staticmethod
107
+ def get_db_path() -> str:
108
+ """Get the actual database path for track metadata."""
109
+ return str(get_user_data_dir() / "mycelium_tracks.db")
110
+
111
+
112
+ @dataclass
113
+ class APIConfig:
114
+ """Configuration for API server."""
115
+ host: str = "0.0.0.0"
116
+ port: int = 8000
117
+ reload: bool = False
118
+
119
+
120
+ @dataclass
121
+ class LoggingConfig:
122
+ """Configuration for logging system."""
123
+ level: str = "INFO"
124
+ format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
125
+ file: Optional[str] = None
126
+
127
+
128
+ @dataclass
129
+ class MyceliumConfig:
130
+ """Main configuration class."""
131
+ server: ServerConfig
132
+ media_server: MediaServerConfig
133
+ plex: PlexConfig
134
+ clap: CLAPConfig
135
+ chroma: ChromaConfig
136
+ database: DatabaseConfig
137
+ api: APIConfig
138
+ logging: LoggingConfig
139
+
140
+ @classmethod
141
+ def load_from_yaml(cls, config_path: Optional[Path] = None) -> "MyceliumConfig":
142
+ """Load configuration"""
143
+ if config_path is None:
144
+ config_path = get_config_file_path()
145
+
146
+ # Load YAML config if it exists
147
+ config_data = {}
148
+ config_exists = config_path.exists()
149
+ if config_exists:
150
+ with open(config_path, 'r', encoding='utf-8') as f:
151
+ config_data = yaml.safe_load(f) or {}
152
+
153
+ plex_config = PlexConfig(
154
+ url=config_data.get("plex", {}).get("url", "http://localhost:32400"),
155
+ token=config_data.get("plex", {}).get("token", "replace_with_your_token"),
156
+ music_library_name=config_data.get("plex", {}).get("music_library_name", "Music")
157
+ )
158
+
159
+ server_config = ServerConfig(
160
+ gpu_batch_size=config_data.get("server", {}).get("gpu_batch_size", 16)
161
+ )
162
+
163
+ clap_config = CLAPConfig(
164
+ model_id=config_data.get("clap", {}).get("model_id", "laion/larger_clap_music_and_speech"),
165
+ target_sr=config_data.get("clap", {}).get("target_sr", 48000),
166
+ chunk_duration_s=config_data.get("clap", {}).get("chunk_duration_s", 10),
167
+ num_chunks=config_data.get("clap", {}).get("num_chunks", 3),
168
+ max_load_duration_s=config_data.get("clap", {}).get("max_load_duration_s", 120)
169
+ )
170
+
171
+ chroma_config = ChromaConfig(
172
+ collection_name=config_data.get("chroma", {}).get("collection_name", "my_music_library"),
173
+ batch_size=config_data.get("chroma", {}).get("batch_size", 1000)
174
+ )
175
+
176
+ media_server = MediaServerConfig(
177
+ type=MediaServerType[config_data.get("media_server", {}).get("type", "plex").upper()]
178
+ )
179
+
180
+ database_config = DatabaseConfig()
181
+
182
+ api_config = APIConfig(
183
+ host=config_data.get("api", {}).get("host", "0.0.0.0"),
184
+ port=config_data.get("api", {}).get("port", 8000),
185
+ reload=config_data.get("api", {}).get("reload", False)
186
+ )
187
+
188
+ # Handle logging configuration with default log file path
189
+ logging_data = config_data.get("logging", {})
190
+ log_file = logging_data.get("file")
191
+ if log_file is None:
192
+ log_file = str(get_user_log_dir() / "mycelium.log")
193
+
194
+ logging_config = LoggingConfig(
195
+ level=logging_data.get("level", "INFO"),
196
+ format=logging_data.get("format", "%(asctime)s - %(name)s - %(levelname)s - %(message)s"),
197
+ file=log_file
198
+ )
199
+
200
+ cfg = cls(
201
+ media_server=media_server,
202
+ plex=plex_config,
203
+ clap=clap_config,
204
+ chroma=chroma_config,
205
+ database=database_config,
206
+ api=api_config,
207
+ logging=logging_config,
208
+ server=server_config
209
+ )
210
+
211
+ # If no config file existed, create one with current values for convenience
212
+ if not config_exists:
213
+ # Ensure config directory exists
214
+ config_path.parent.mkdir(parents=True, exist_ok=True)
215
+ try:
216
+ cfg.save_to_yaml(config_path)
217
+ except Exception:
218
+ # Best-effort; ignore failures to avoid blocking startup
219
+ pass
220
+
221
+ return cfg
222
+
223
+ def save_to_yaml(self, config_path: Optional[Path] = None) -> None:
224
+ """Save configuration to YAML file."""
225
+ if config_path is None:
226
+ config_path = get_config_file_path()
227
+
228
+ def custom_dict_factory(data):
229
+ result = []
230
+ for k, v in data:
231
+ if isinstance(v, MediaServerType):
232
+ result.append((k, v.value))
233
+ else:
234
+ result.append((k, v))
235
+ return dict(result)
236
+
237
+ config_dict = {
238
+ "media_server": asdict(self.media_server, dict_factory=custom_dict_factory),
239
+ "plex": asdict(self.plex),
240
+ "clap": asdict(self.clap),
241
+ "chroma": asdict(self.chroma),
242
+ "database": asdict(self.database),
243
+ "api": asdict(self.api),
244
+ "logging": asdict(self.logging),
245
+ "server": asdict(self.server)
246
+ }
247
+
248
+ with open(config_path, 'w', encoding='utf-8') as f:
249
+ yaml.dump(config_dict, f, default_flow_style=False, indent=2)
250
+
251
+ def to_dict(self) -> dict:
252
+ """Convert configuration to dictionary format"""
253
+ return {
254
+ "media_server": {
255
+ "type": self.media_server.type.value
256
+ },
257
+ "plex": {
258
+ "url": self.plex.url,
259
+ "token": self.plex.token,
260
+ "music_library_name": self.plex.music_library_name
261
+ },
262
+ "server": {
263
+ "gpu_batch_size": self.server.gpu_batch_size
264
+ },
265
+ "api": {
266
+ "host": self.api.host,
267
+ "port": self.api.port,
268
+ "reload": self.api.reload
269
+ },
270
+ "chroma": {
271
+ "collection_name": self.chroma.collection_name,
272
+ "batch_size": self.chroma.batch_size
273
+ },
274
+ "clap": {
275
+ "model_id": self.clap.model_id,
276
+ "target_sr": self.clap.target_sr,
277
+ "chunk_duration_s": self.clap.chunk_duration_s,
278
+ "num_chunks": self.clap.num_chunks,
279
+ "max_load_duration_s": self.clap.max_load_duration_s
280
+ },
281
+ "logging": {
282
+ "level": self.logging.level
283
+ }
284
+ }
285
+
286
+ def setup_logging(self) -> None:
287
+ """Setup logging configuration."""
288
+ # Configure the root logger
289
+ level = getattr(logging, self.logging.level.upper(), logging.INFO)
290
+
291
+ # Create log directory if needed
292
+ log_file_path = Path(self.logging.file)
293
+ log_file_path.parent.mkdir(parents=True, exist_ok=True)
294
+
295
+ # Get the root logger
296
+ root_logger = logging.getLogger()
297
+
298
+ # Remove all existing handlers to ensure clean reconfiguration
299
+ for handler in root_logger.handlers[:]:
300
+ root_logger.removeHandler(handler)
301
+ handler.close()
302
+
303
+ # Set the new level on the root logger
304
+ root_logger.setLevel(level)
305
+
306
+ # Create and add new handlers
307
+ file_handler = logging.FileHandler(self.logging.file)
308
+ file_handler.setLevel(level)
309
+ file_handler.setFormatter(logging.Formatter(self.logging.format))
310
+
311
+ console_handler = logging.StreamHandler()
312
+ console_handler.setLevel(level)
313
+ console_handler.setFormatter(logging.Formatter(self.logging.format))
314
+
315
+ root_logger.addHandler(file_handler)
316
+ root_logger.addHandler(console_handler)
317
+
318
+ # Set log level for third-party libraries to reduce noise
319
+ logging.getLogger('chromadb').setLevel(logging.WARNING)
320
+ logging.getLogger('urllib3').setLevel(logging.WARNING)
321
+ logging.getLogger('requests').setLevel(logging.WARNING)
322
+
323
+ # Configure Uvicorn loggers to prevent unwanted access logs
324
+ logging.getLogger('uvicorn.access').setLevel(logging.WARNING)
325
+ logging.getLogger('uvicorn').setLevel(logging.WARNING)
326
+
327
+ # Disable verbose numba logging
328
+ logging.getLogger('numba.core').setLevel(logging.WARNING)
329
+
330
+
331
+ # Export all necessary components
332
+ __all__ = [
333
+ "MediaServerConfig",
334
+ "MyceliumConfig",
335
+ "PlexConfig",
336
+ "CLAPConfig",
337
+ "ChromaConfig",
338
+ "DatabaseConfig",
339
+ "APIConfig",
340
+ "LoggingConfig",
341
+ "ServerConfig",
342
+ "get_config_file_path",
343
+ "get_user_data_dir",
344
+ "get_user_log_dir",
345
+ "get_config_dir"
346
+ ]
@@ -0,0 +1,13 @@
1
+ """Domain package initialization."""
2
+
3
+ from .models import Track, TrackEmbedding, SearchResult
4
+ from .repositories import MediaServerRepository, EmbeddingRepository, EmbeddingGenerator
5
+
6
+ __all__ = [
7
+ "Track",
8
+ "TrackEmbedding",
9
+ "SearchResult",
10
+ "MediaServerRepository",
11
+ "EmbeddingRepository",
12
+ "EmbeddingGenerator",
13
+ ]
@@ -0,0 +1,71 @@
1
+ """Domain models for the Mycelium application."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import List, Optional
7
+ from datetime import datetime
8
+
9
+
10
+ class MediaServerType(Enum):
11
+ """Supported media server types."""
12
+ PLEX = "plex"
13
+ JELLYFIN = "jellyfin"
14
+
15
+
16
+ @dataclass
17
+ class Track:
18
+ """Represents a music track from a media server."""
19
+
20
+ media_server_rating_key: str
21
+ media_server_type: MediaServerType
22
+ artist: str = ""
23
+ album: str = ""
24
+ title: str = ""
25
+ filepath: Optional[Path] = None
26
+
27
+ @property
28
+ def display_name(self) -> str:
29
+ """Get a display-friendly name for the track."""
30
+ return f"{self.artist} - {self.title}"
31
+
32
+ @property
33
+ def unique_id(self) -> str:
34
+ """Get a unique identifier for the track across media servers."""
35
+ return f"{self.media_server_type.value}:{self.media_server_rating_key}"
36
+
37
+
38
+
39
+
40
+ @dataclass
41
+ class TrackEmbedding:
42
+ """Represents a track with its embedding from a specific model."""
43
+
44
+ track: Track
45
+ embedding: List[float]
46
+ model_id: str
47
+ processed_at: Optional[datetime] = None
48
+
49
+
50
+ @dataclass
51
+ class SearchResult:
52
+ """Represents a search result with similarity score."""
53
+
54
+ track: Track
55
+ similarity_score: float
56
+ distance: float
57
+
58
+
59
+ @dataclass
60
+ class Playlist:
61
+ """Represents a playlist created from recommendations."""
62
+
63
+ name: str
64
+ tracks: List[Track]
65
+ created_at: Optional[datetime] = None
66
+ server_id: Optional[str] = None
67
+
68
+ @property
69
+ def track_count(self) -> int:
70
+ """Get the number of tracks in the playlist."""
71
+ return len(self.tracks)
@@ -0,0 +1,98 @@
1
+ """Repository interfaces for domain layer."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ from .models import Track, TrackEmbedding, SearchResult, Playlist
8
+
9
+
10
+ class MediaServerRepository(ABC):
11
+ """Interface for media server operations (Plex, Jellyfin, etc.)."""
12
+
13
+ @abstractmethod
14
+ def get_all_tracks(self) -> List[Track]:
15
+ """Get all tracks from the music library."""
16
+ pass
17
+
18
+ @abstractmethod
19
+ def get_track_by_id(self, track_id: str) -> Optional[Track]:
20
+ """Get a specific track by its ID."""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def create_playlist(self, playlist: Playlist, batch_size: int = 100) -> Playlist:
25
+ """Create a playlist on the media server.
26
+
27
+ Args:
28
+ playlist: The playlist to create
29
+ batch_size: Number of tracks to add per batch for large playlists (default: 100)
30
+ """
31
+ pass
32
+
33
+
34
+ class EmbeddingRepository(ABC):
35
+ """Interface for managing track embeddings."""
36
+
37
+ @abstractmethod
38
+ def save_embeddings(self, embeddings: List[TrackEmbedding]) -> None:
39
+ """Save track embeddings to storage."""
40
+ pass
41
+
42
+ @abstractmethod
43
+ def save_embedding(self, track_embedding: TrackEmbedding) -> None:
44
+ """Save track embeddings to storage."""
45
+ pass
46
+
47
+ @abstractmethod
48
+ def search_by_embedding(self, embedding: List[float], n_results: int = 10) -> List[SearchResult]:
49
+ """Search for similar tracks by embedding."""
50
+ pass
51
+
52
+ @abstractmethod
53
+ def get_embedding_count(self) -> int:
54
+ """Get the total number of embeddings stored."""
55
+ pass
56
+
57
+ @abstractmethod
58
+ def get_embedding_by_track_id(self, track_id: str) -> Optional[List[float]]:
59
+ """Get embedding for a specific track by its ID."""
60
+ pass
61
+
62
+ @abstractmethod
63
+ def has_embedding(self, track_id: str) -> bool:
64
+ """Check if an embedding exists for a specific track."""
65
+ pass
66
+
67
+
68
+ class EmbeddingGenerator(ABC):
69
+ """Interface for generating embeddings from audio files."""
70
+
71
+ @abstractmethod
72
+ def generate_embedding(self, filepath: Path) -> Optional[List[float]]:
73
+ """Generate embedding for an audio file."""
74
+ pass
75
+
76
+ @abstractmethod
77
+ def generate_embedding_batch(self, filepaths: List[Path]) -> List[Optional[List[float]]]:
78
+ """Generate embeddings for multiple audio files in a batch."""
79
+ pass
80
+
81
+ @abstractmethod
82
+ def generate_text_embedding(self, text: str) -> Optional[List[float]]:
83
+ """Generate embedding for text description."""
84
+ pass
85
+
86
+ @abstractmethod
87
+ def generate_text_embedding_batch(self, texts: List[str]) -> List[Optional[List[float]]]:
88
+ """Generate embeddings for multiple text queries in a batch."""
89
+ pass
90
+
91
+ @staticmethod
92
+ def get_best_device() -> str:
93
+ """Get the best device for running the embedding model (e.g., 'cpu', 'cuda')."""
94
+ pass
95
+
96
+ @staticmethod
97
+ def can_use_half_precision() -> bool:
98
+ """Checks once if the device supports half precision."""
@@ -0,0 +1,77 @@
1
+ """Worker domain models for client-server coordination."""
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from typing import List, Optional
7
+
8
+ from mycelium.domain import SearchResult
9
+
10
+
11
+ class TaskType(str, Enum):
12
+ """Type of tasks that can be assigned to workers."""
13
+ COMPUTE_TEXT_EMBEDDING = "compute_text_embedding"
14
+ COMPUTE_AUDIO_EMBEDDING = "compute_audio_embedding"
15
+
16
+ class ContextType(str, Enum):
17
+ """Type of context for the task."""
18
+ AUDIO_SEARCH = "audio_search"
19
+ AUDIO_PROCESSING = "audio_processing"
20
+ TEXT_SEARCH = "text_search"
21
+ SIMILAR_TRACKS = "similar_tracks"
22
+
23
+
24
+ class TaskStatus(str, Enum):
25
+ """Status of a task."""
26
+ PENDING = "pending"
27
+ IN_PROGRESS = "in_progress"
28
+ SUCCESS = "success"
29
+ FAILED = "failed"
30
+
31
+
32
+ @dataclass
33
+ class Worker:
34
+ """Represents a worker in the system."""
35
+ id: str
36
+ ip_address: str
37
+ registration_time: datetime
38
+ last_heartbeat: datetime
39
+ is_active: bool = True
40
+
41
+
42
+ @dataclass
43
+ class Task:
44
+ """Represents a task to be processed by a worker."""
45
+ task_id: str
46
+ task_type: TaskType
47
+ context_type: ContextType
48
+ track_id: str
49
+ download_url: str
50
+ status: TaskStatus = TaskStatus.PENDING
51
+ assigned_worker_id: Optional[str] = None
52
+ created_at: Optional[datetime] = None
53
+ started_at: Optional[datetime] = None
54
+ completed_at: Optional[datetime] = None
55
+ error_message: Optional[str] = None
56
+ # Additional fields for search tasks
57
+ text_query: Optional[str] = None # For text search tasks
58
+ audio_filename: Optional[str] = None # For audio search tasks
59
+ n_results: Optional[int] = None # For search tasks - number of results to return
60
+ # Results storage for search tasks
61
+ search_results: Optional[List[SearchResult]] = None
62
+
63
+ def __post_init__(self):
64
+ if self.created_at is None:
65
+ self.created_at = datetime.now()
66
+
67
+
68
+ @dataclass
69
+ class TaskResult:
70
+ """Result of a completed task."""
71
+ task_id: str
72
+ track_id: str
73
+ status: TaskStatus
74
+ embedding: Optional[List[float]] = None
75
+ error_message: Optional[str] = None
76
+ # For search tasks, we store the search results directly
77
+ search_results: Optional[List[dict]] = None
@@ -0,0 +1 @@
1
+ <!DOCTYPE html><!--glVJ0yJSL0zWN7anTTG3_--><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="stylesheet" href="/_next/static/css/1eb7f0e2c78e0734.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-c81e624915b2ea70.js"/><script src="/_next/static/chunks/4bd1b696-cf72ae8a39fa05aa.js" async=""></script><script src="/_next/static/chunks/964-830f77d7ce1c2463.js" async=""></script><script src="/_next/static/chunks/main-app-4153d115599d3126.js" async=""></script><meta name="robots" content="noindex"/><title>404: This page could not be found.</title><title>Mycelium - AI Music Discovery</title><meta name="description" content="Discover your music collection like never before with AI-powered embeddings"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="antialiased"><div hidden=""><!--$--><!--/$--></div><div style="font-family:system-ui,&quot;Segoe UI&quot;,Roboto,Helvetica,Arial,sans-serif,&quot;Apple Color Emoji&quot;,&quot;Segoe UI Emoji&quot;;height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding:0 23px 0 0;font-size:24px;font-weight:500;vertical-align:top;line-height:49px">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:49px;margin:0">This page could not be found.</h2></div></div></div><!--$--><!--/$--><script src="/_next/static/chunks/webpack-c81e624915b2ea70.js" id="_R_" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0])</script><script>self.__next_f.push([1,"1:\"$Sreact.fragment\"\n2:I[7555,[],\"\"]\n3:I[1295,[],\"\"]\n4:I[9665,[],\"OutletBoundary\"]\n6:I[4911,[],\"AsyncMetadataOutlet\"]\n8:I[9665,[],\"ViewportBoundary\"]\na:I[9665,[],\"MetadataBoundary\"]\nb:\"$Sreact.suspense\"\nd:I[8393,[],\"\"]\n:HL[\"/_next/static/css/1eb7f0e2c78e0734.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"0:{\"P\":null,\"b\":\"glVJ0yJSL0zWN7anTTG3_\",\"p\":\"\",\"c\":[\"\",\"_not-found\"],\"i\":false,\"f\":[[[\"\",{\"children\":[\"/_not-found\",{\"children\":[\"__PAGE__\",{}]}]},\"$undefined\",\"$undefined\",true],[\"\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/1eb7f0e2c78e0734.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\",\"nonce\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"antialiased\",\"children\":[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]}]}]]}],{\"children\":[\"/_not-found\",[\"$\",\"$1\",\"c\",{\"children\":[null,[\"$\",\"$L2\",null,{\"parallelRouterKey\":\"children\",\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L3\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"forbidden\":\"$undefined\",\"unauthorized\":\"$undefined\"}]]}],{\"children\":[\"__PAGE__\",[\"$\",\"$1\",\"c\",{\"children\":[[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":404}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],null,[\"$\",\"$L4\",null,{\"children\":[\"$L5\",[\"$\",\"$L6\",null,{\"promise\":\"$@7\"}]]}]]}],{},null,false]},null,false]},null,false],[\"$\",\"$1\",\"h\",{\"children\":[[\"$\",\"meta\",null,{\"name\":\"robots\",\"content\":\"noindex\"}],[[\"$\",\"$L8\",null,{\"children\":\"$L9\"}],null],[\"$\",\"$La\",null,{\"children\":[\"$\",\"div\",null,{\"hidden\":true,\"children\":[\"$\",\"$b\",null,{\"fallback\":null,\"children\":\"$Lc\"}]}]}]]}],false]],\"m\":\"$undefined\",\"G\":[\"$d\",[]],\"s\":false,\"S\":true}\n"])</script><script>self.__next_f.push([1,"9:[[\"$\",\"meta\",\"0\",{\"charSet\":\"utf-8\"}],[\"$\",\"meta\",\"1\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}]]\n5:null\n"])</script><script>self.__next_f.push([1,"e:I[8175,[],\"IconMark\"]\n7:{\"metadata\":[[\"$\",\"title\",\"0\",{\"children\":\"Mycelium - AI Music Discovery\"}],[\"$\",\"meta\",\"1\",{\"name\":\"description\",\"content\":\"Discover your music collection like never before with AI-powered embeddings\"}],[\"$\",\"link\",\"2\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$\",\"$Le\",\"3\",{}]],\"error\":null,\"digest\":\"$undefined\"}\n"])</script><script>self.__next_f.push([1,"c:\"$7:metadata\"\n"])</script></body></html>