diff --git a/app_chat.py b/app_chat.py index f98d3bb1..8c87d256 100644 --- a/app_chat.py +++ b/app_chat.py @@ -497,8 +497,6 @@ def _run_chat_pipeline(data, log_messages): all_songs = plan_result['songs'] song_sources = plan_result['song_sources'] tools_used_history = plan_result['tools_used_history'] - tool_execution_summary = plan_result['tool_execution_summary'] - detected_min_rating = plan_result['detected_min_rating'] plan_notes = plan_result.get('plan_notes', []) executed_query_str = plan_result['executed_query_str'] filter_applied = plan_result.get('filter_applied', False) diff --git a/app_provider_migration.py b/app_provider_migration.py index 7f400034..1eb24897 100644 --- a/app_provider_migration.py +++ b/app_provider_migration.py @@ -1481,7 +1481,6 @@ def matched_albums(session_id): dry = state.get('dry_run') or {} auto_matches = dry.get('matches') or {} - match_tiers = dry.get('match_tiers') or {} manual_matches = state.get('manual_matches') or {} manual_unmatches = set(state.get('manual_unmatches') or []) new_meta = state.get('new_meta') or {} diff --git a/config.py b/config.py index c3ee924b..8f4ff9a6 100644 --- a/config.py +++ b/config.py @@ -127,6 +127,11 @@ def _compute_headers(): # --- GPU Acceleration for Clustering (Optional, requires NVIDIA GPU and RAPIDS cuML) --- USE_GPU_CLUSTERING = os.environ.get("USE_GPU_CLUSTERING", "False").lower() == "true" +# --- Clustering Cleanup Behavior --- +# When True (default), existing '_automatic' playlists are deleted before new clusters are created. +# Set to False to preserve old automatic playlists when running clustering. +CLUSTERING_CLEANING = os.environ.get("CLUSTERING_CLEANING", "True").lower() == "true" + # --- DBSCAN Only Constants (Ranges for Evolutionary Approach) --- # Default ranges for DBSCAN parameters DBSCAN_EPS_MIN = float(os.getenv("DBSCAN_EPS_MIN", "0.1")) diff --git a/query/brainstorm_real_gmm_080.py b/query/brainstorm_real_gmm_080.py index eac26981..08363f5e 100644 --- a/query/brainstorm_real_gmm_080.py +++ b/query/brainstorm_real_gmm_080.py @@ -170,7 +170,6 @@ def main(): item_ids, all_X = load_all_embeddings() clap_scores = load_clap_scores() if USE_CLAP else {} - id_to_idx = {iid: i for i, iid in enumerate(item_ids)} pred_w = load_prediction_weights() print("Loaded MSD prediction weights") diff --git a/static/menu.css b/static/menu.css index b12f9baa..cee3f133 100644 --- a/static/menu.css +++ b/static/menu.css @@ -85,7 +85,6 @@ html.sidebar-open .sidebar { color: #d1d5db; text-decoration: none; padding: 0.75rem 0; - font-size: 1rem; white-space: nowrap; transition: background-color 0.2s, color 0.2s; border-radius: 0.375rem; diff --git a/static/setup.js b/static/setup.js index 606d26ba..03c259a3 100644 --- a/static/setup.js +++ b/static/setup.js @@ -315,7 +315,7 @@ function renderAdvancedFields(fields) { label: field.name, placeholder: field.default ? field.default : '', type: field.type === 'bool' ? 'boolean' : field.type, - inputType: field.type === 'boolean' ? 'text' : 'text', + inputType: 'text', secret: secret, has_value: field.has_value, options: Array.isArray(field.options) ? field.options : null, diff --git a/static/sunburst.js b/static/sunburst.js index 8708eb5b..01b84cd2 100644 --- a/static/sunburst.js +++ b/static/sunburst.js @@ -23,7 +23,7 @@ const SunburstChart = (() => { function buildTree(data) { const TAG_DEPTH = 3; const root = {name: 'root', children: [], depth: 0}; - for (const mood of Object.keys(data).sort()) { + for (const mood of Object.keys(data).sort((a, b) => a.localeCompare(b))) { const moodNode = {name: mood, children: [], depth: 1, mood: mood}; data[mood].forEach(c => { let parent = moodNode; diff --git a/tasks/artist_gmm_manager.py b/tasks/artist_gmm_manager.py index e879af56..be4f661a 100644 --- a/tasks/artist_gmm_manager.py +++ b/tasks/artist_gmm_manager.py @@ -149,9 +149,6 @@ def fit_artist_gmm(artist_name: str, track_embeddings: List[np.ndarray]) -> Opti if n_samples < 5: logger.info(f"Artist '{artist_name}' has {n_samples} tracks - using each song as a GMM component with equal weights") - # Use a small fixed covariance for numerical stability - # This acts like narrow Gaussians centered on each actual song - fixed_variance = 0.01 # Each song becomes one component with equal weight n_components = n_samples diff --git a/tasks/clap_analyzer.py b/tasks/clap_analyzer.py index 0b8b3b0a..fd1b19fe 100644 --- a/tasks/clap_analyzer.py +++ b/tasks/clap_analyzer.py @@ -265,7 +265,6 @@ def _load_text_model(): provider_options=[p[1] for p in provider_options] ) - active_provider = session.get_providers()[0] logger.info(f"✓ CLAP text model loaded successfully (~478MB)") except Exception as e: @@ -694,9 +693,7 @@ def get_text_embeddings_batch(query_texts: list) -> Optional[np.ndarray]: # Get text-only model for text search session = get_clap_text_model() tokenizer = get_tokenizer() - - batch_size = len(query_texts) - + # Tokenize all texts at once (max_length=77 for CLAP) encoded = tokenizer( query_texts, diff --git a/tasks/clustering.py b/tasks/clustering.py index 7ac90c12..69736405 100644 --- a/tasks/clustering.py +++ b/tasks/clustering.py @@ -19,21 +19,23 @@ from psycopg2.extras import DictCursor # Import configuration -from config import MAX_SONGS_PER_CLUSTER, MOOD_LABELS, STRATIFIED_GENRES, MUTATION_KMEANS_COORD_FRACTION, MUTATION_INT_ABS_DELTA, MUTATION_FLOAT_ABS_DELTA, TOP_N_ELITES, EXPLOITATION_START_FRACTION, EXPLOITATION_PROBABILITY_CONFIG, SAMPLING_PERCENTAGE_CHANGE_PER_RUN, ITERATIONS_PER_BATCH_JOB, MAX_CONCURRENT_BATCH_JOBS, MIN_PLAYLIST_SIZE_FOR_TOP_N, CLUSTERING_BATCH_TIMEOUT_MINUTES, CLUSTERING_MAX_FAILED_BATCHES +from config import MAX_SONGS_PER_CLUSTER, MOOD_LABELS, STRATIFIED_GENRES, MUTATION_KMEANS_COORD_FRACTION, MUTATION_INT_ABS_DELTA, MUTATION_FLOAT_ABS_DELTA, TOP_N_ELITES, EXPLOITATION_START_FRACTION, EXPLOITATION_PROBABILITY_CONFIG, SAMPLING_PERCENTAGE_CHANGE_PER_RUN, ITERATIONS_PER_BATCH_JOB, MAX_CONCURRENT_BATCH_JOBS, MIN_PLAYLIST_SIZE_FOR_TOP_N, CLUSTERING_BATCH_TIMEOUT_MINUTES, CLUSTERING_MAX_FAILED_BATCHES, CLUSTERING_CLEANING from error import error_manager from error.error_dictionary import ERR_CLUSTERING_FAILED # Import AI naming function and prompt template -from tasks.ai.api import get_ai_playlist_name -from tasks.ai.prompts import creative_prompt_template +# (used by clustering_helper._try_ai_name_playlist, imported there) # Import media server functions from .mediaserver import create_playlist, delete_automatic_playlists # Import refactored clustering helpers from .clustering_helper import ( _get_stratified_song_subset, get_job_result_safely, - _perform_single_clustering_iteration + _perform_single_clustering_iteration, + _shuffle_playlist_songs, + _assign_playlist_chunks, + _try_ai_name_playlist, ) # Import post-processing functions from dedicated module from .clustering_postprocessing import ( @@ -399,15 +401,12 @@ def _log_and_update(message, progress, details_to_add_or_update=None, task_state # --- 2. Batch Job Orchestration --- num_total_batches = (num_clustering_runs + ITERATIONS_PER_BATCH_JOB - 1) // ITERATIONS_PER_BATCH_JOB if ITERATIONS_PER_BATCH_JOB > 0 else 0 next_batch_to_launch = 0 - batches_completed_count = 0 # STATE RECOVERY child_tasks_from_db = get_child_tasks_from_db(current_task_id) if child_tasks_from_db: logger.info(f"Found {len(child_tasks_from_db)} existing child tasks. Attempting state recovery.") _monitor_and_process_batches(_main_task_accumulated_details, current_task_id, initial_check=True) - # Count batches processed during recovery (these are now in processed_job_ids) - batches_completed_count = len(_main_task_accumulated_details.get('processed_job_ids', set())) # Determine next batch to launch based on total runs accounted for runs_accounted_for = _main_task_accumulated_details["runs_completed"] @@ -540,12 +539,14 @@ def _log_and_update(message, progress, details_to_add_or_update=None, task_state ollama_model_name_param, openai_server_url_param, openai_model_name_param, openai_api_key_param, gemini_api_key_param, gemini_model_name_param, - mistral_api_key_param, mistral_model_name_param, - enable_clustering_embeddings_param + mistral_api_key_param, mistral_model_name_param ) - _log_and_update("Deleting existing automatic playlists...", 97) - delete_automatic_playlists() + if CLUSTERING_CLEANING: + _log_and_update("Deleting existing automatic playlists...", 97) + delete_automatic_playlists() + else: + _log_and_update("CLUSTERING_CLEANING is disabled — skipping deletion of existing automatic playlists.", 97) # *** ABSOLUTE FINAL SHUFFLE: Guarantee random order right before database storage *** logger.info("=== ABSOLUTE FINAL SHUFFLE: Randomizing all playlists before database storage ===") @@ -905,13 +906,12 @@ def _launch_batch_job(state_dict, parent_task_id, batch_idx, total_runs, genre_m logger.info(f"Enqueued batch job {new_job.id} for runs {start_run}-{start_run + num_iterations - 1}.") -def _name_and_prepare_playlists(best_result, ai_provider, ollama_url, ollama_model, openai_url, openai_model, openai_key, gemini_key, gemini_model, mistral_key, mistral_model, embeddings_used): +def _name_and_prepare_playlists(best_result, ai_provider, ollama_url, ollama_model, openai_url, openai_model, openai_key, gemini_key, gemini_model, mistral_key, mistral_model): """ Uses AI to name playlists and formats them for creation. Returns a dictionary mapping final playlist names to lists of song tuples (id, title, author). """ final_playlists = {} - centroids = best_result.get("playlist_centroids", {}) named_playlists = best_result.get("named_playlists", {}) max_songs = best_result.get("parameters", {}).get("max_songs_per_cluster", MAX_SONGS_PER_CLUSTER) @@ -919,36 +919,22 @@ def _name_and_prepare_playlists(best_result, ai_provider, ollama_url, ollama_mod if not songs: continue - final_name = original_name - if ai_provider in ["OLLAMA", "OPENAI", "GEMINI", "MISTRAL"]: + if ai_provider in ("OLLAMA", "OPENAI", "GEMINI", "MISTRAL"): try: - # Simplified feature extraction for AI prompt - name_parts = original_name.split('_') - feature1 = name_parts[0] if len(name_parts) > 0 else "Music" - feature2 = name_parts[1] if len(name_parts) > 1 else "Vibes" - feature3 = name_parts[2] if len(name_parts) > 2 else "Collection" - if embeddings_used: - feature1, feature2, feature3 = "Vibe", "Focused", "Collection" - - ai_config = { - 'provider': ai_provider, - 'ollama_url': ollama_url, 'ollama_model': ollama_model, - 'openai_url': openai_url, 'openai_model': openai_model, 'openai_key': openai_key, - 'gemini_key': gemini_key, 'gemini_model': gemini_model, - 'mistral_key': mistral_key, 'mistral_model': mistral_model, - } - ai_name = get_ai_playlist_name( - creative_prompt_template, - [{'title': s_title, 'author': s_author} for _, s_title, s_author in songs], - centroids.get(original_name, {}), - ai_config, + final_name = _try_ai_name_playlist( + original_name, songs, + best_result.get("playlist_centroids", {}), + ai_provider, + ollama_url, ollama_model, + openai_url, openai_model, openai_key, + gemini_key, gemini_model, + mistral_key, mistral_model, ) - if ai_name and "Error" not in ai_name: - final_name = ai_name.strip().replace("\n", " ") - else: - logger.warning(f"AI naming failed for '{original_name}': {ai_name}. Using original name.") except Exception as e: logger.warning(f"AI naming failed for '{original_name}': {e}. Using original name.") + final_name = original_name + else: + final_name = original_name # Ensure unique names temp_name = final_name @@ -958,34 +944,9 @@ def _name_and_prepare_playlists(best_result, ai_provider, ollama_url, ollama_mod temp_name = f"{final_name} ({suffix})" final_name = temp_name - # Add suffix and handle chunking - base_name_with_suffix = f"{final_name}_automatic" - - # The 'songs' variable is already the list of tuples: [(item_id, title, author), ...] - # *** FINAL SAFETY SHUFFLE: Ensure songs are randomized in final playlists *** - final_songs = songs.copy() - n = len(final_songs) - - if n > 1: - # FISHER-YATES MANUAL SHUFFLE - GUARANTEED TO RANDOMIZE - current_time_seed = int(time.time() * 1000000) % 1000000 - - for i in range(n - 1, 0, -1): - j = (random.randint(0, i) + current_time_seed + i * 7) % (i + 1) - final_songs[i], final_songs[j] = final_songs[j], final_songs[i] - current_time_seed = (current_time_seed * 1103515245 + 12345) % (2**31) - - logger.info(f"FINAL FISHER-YATES SHUFFLE applied to '{base_name_with_suffix}': {len(final_songs)} songs") - logger.info(f"FINAL ORDER: First song = '{final_songs[0][1]}', Last song = '{final_songs[-1][1]}'") - else: - logger.info(f"FINAL: '{base_name_with_suffix}' has only {n} songs - no shuffling needed") - - if max_songs > 0 and len(final_songs) > max_songs: - chunks = [final_songs[i:i+max_songs] for i in range(0, len(final_songs), max_songs)] - for idx, chunk in enumerate(chunks, 1): - final_playlists[f"{base_name_with_suffix} ({idx})"] = chunk # Store the chunk of tuples - else: - final_playlists[base_name_with_suffix] = final_songs # Store the list of tuples + base_name = f"{final_name}_automatic" + shuffled = _shuffle_playlist_songs(songs, base_name) + _assign_playlist_chunks(shuffled, max_songs, base_name, final_playlists) return final_playlists diff --git a/tasks/clustering_helper.py b/tasks/clustering_helper.py index bfdfd4fb..3ce5e236 100644 --- a/tasks/clustering_helper.py +++ b/tasks/clustering_helper.py @@ -3,6 +3,7 @@ import json import random import logging +import time import numpy as np from collections import defaultdict # time, re, and cdist imports moved to clustering_postprocessing.py @@ -38,6 +39,65 @@ USE_GPU_CLUSTERING) from .commons import score_vector +# Import AI naming for playlist helpers +from tasks.ai.api import get_ai_playlist_name +from tasks.ai.prompts import creative_prompt_template + + +# --- Playlist Naming & Shuffling Helpers --- + +def _shuffle_playlist_songs(songs, playlist_name): + """Fisher-Yates shuffle a list of song tuples; log the result.""" + final_songs = songs.copy() + n = len(final_songs) + if n <= 1: + logger.info("FINAL: '%s' has only %d songs - no shuffling needed", playlist_name, n) + return final_songs + + current_time_seed = int(time.time() * 1000000) % 1000000 + for i in range(n - 1, 0, -1): + j = (random.randint(0, i) + current_time_seed + i * 7) % (i + 1) + final_songs[i], final_songs[j] = final_songs[j], final_songs[i] + current_time_seed = (current_time_seed * 1103515245 + 12345) % (2 ** 31) + + logger.info("FINAL FISHER-YATES SHUFFLE applied to '%s': %d songs", playlist_name, len(final_songs)) + logger.info("FINAL ORDER: First song = '%s', Last song = '%s'", final_songs[0][1], final_songs[-1][1]) + return final_songs + + +def _assign_playlist_chunks(final_songs, max_songs, base_name, final_playlists): + """Chunk oversized playlists or store the list as-is.""" + if max_songs > 0 and len(final_songs) > max_songs: + chunks = [final_songs[i:i + max_songs] for i in range(0, len(final_songs), max_songs)] + for idx, chunk in enumerate(chunks, 1): + final_playlists[f"{base_name} ({idx})"] = chunk + else: + final_playlists[base_name] = final_songs + + +def _try_ai_name_playlist(original_name, songs, centroids, ai_provider, + ollama_url, ollama_model, openai_url, openai_model, openai_key, + gemini_key, gemini_model, mistral_key, mistral_model): + """Attempt AI naming; return the original name on failure.""" + ai_config = { + 'provider': ai_provider, + 'ollama_url': ollama_url, 'ollama_model': ollama_model, + 'openai_url': openai_url, 'openai_model': openai_model, 'openai_key': openai_key, + 'gemini_key': gemini_key, 'gemini_model': gemini_model, + 'mistral_key': mistral_key, 'mistral_model': mistral_model, + } + ai_name = get_ai_playlist_name( + creative_prompt_template, + [{'title': s_title, 'author': s_author} for _, s_title, s_author in songs], + centroids.get(original_name, {}), + ai_config, + ) + if ai_name and "Error" not in ai_name: + return ai_name.strip().replace("\n", " ") + logger.warning("AI naming failed for '%s': %s. Using original name.", original_name, ai_name) + return original_name + + # --- Main Orchestrator for a Single Iteration --- def _perform_single_clustering_iteration( diff --git a/tasks/index_build_helpers.py b/tasks/index_build_helpers.py index 871a6055..a4ab18ab 100644 --- a/tasks/index_build_helpers.py +++ b/tasks/index_build_helpers.py @@ -880,7 +880,7 @@ def load_segmented_blob( row = cur.fetchone() if row and row[0]: data = row[0] - return bytes(data) if not isinstance(data, (bytes, bytearray)) else bytes(data) + return bytes(data) cur.execute(select_segments_sql, (like_pattern,)) rows = cur.fetchall() diff --git a/tasks/mediaserver_emby.py b/tasks/mediaserver_emby.py index d962950d..39bd57f1 100644 --- a/tasks/mediaserver_emby.py +++ b/tasks/mediaserver_emby.py @@ -541,19 +541,15 @@ def _select_best_artist(item, title="Unknown"): if item.get('ArtistItems') and len(item['ArtistItems']) > 0: track_artist = item['ArtistItems'][0].get('Name', 'Unknown Artist') artist_id = item['ArtistItems'][0].get('Id') - used_field = 'ArtistItems[0]' elif item.get('Artists') and len(item['Artists']) > 0: track_artist = item['Artists'][0] # Take first artist if multiple artist_id = None - used_field = 'Artists[0]' elif item.get('AlbumArtist'): track_artist = item['AlbumArtist'] artist_id = None - used_field = 'AlbumArtist' else: track_artist = 'Unknown Artist' artist_id = None - used_field = 'fallback' return track_artist, artist_id diff --git a/tasks/mediaserver_jellyfin.py b/tasks/mediaserver_jellyfin.py index 205fec52..42663b23 100644 --- a/tasks/mediaserver_jellyfin.py +++ b/tasks/mediaserver_jellyfin.py @@ -298,19 +298,15 @@ def _select_best_artist(item, title="Unknown"): if item.get('ArtistItems') and len(item['ArtistItems']) > 0: track_artist = item['ArtistItems'][0].get('Name', 'Unknown Artist') artist_id = item['ArtistItems'][0].get('Id') - used_field = 'ArtistItems[0]' elif item.get('Artists') and len(item['Artists']) > 0: track_artist = item['Artists'][0] # Take first artist if multiple artist_id = None - used_field = 'Artists[0]' elif item.get('AlbumArtist'): track_artist = item['AlbumArtist'] artist_id = None - used_field = 'AlbumArtist' else: track_artist = 'Unknown Artist' artist_id = None - used_field = 'fallback' return track_artist, artist_id diff --git a/tasks/mediaserver_lyrion.py b/tasks/mediaserver_lyrion.py index 5e0288a3..e3f8e6f1 100644 --- a/tasks/mediaserver_lyrion.py +++ b/tasks/mediaserver_lyrion.py @@ -711,22 +711,16 @@ def get_all_songs(user_creds=None): # Prioritize track artist over album artist to avoid "Various Artists" if song.get('trackartist'): track_artist = song.get('trackartist') - used_field = 'trackartist' elif song.get('contributor'): track_artist = song.get('contributor') - used_field = 'contributor' elif song.get('artist'): track_artist = song.get('artist') - used_field = 'artist' elif song.get('albumartist'): track_artist = song.get('albumartist') - used_field = 'albumartist' elif song.get('band'): track_artist = song.get('band') - used_field = 'band' else: track_artist = 'Unknown Artist' - used_field = 'fallback' mapped_song = { 'Id': song.get('id'), @@ -1058,22 +1052,16 @@ def get_tracks_from_album(album_id, user_creds=None): # Prioritize track artist over album artist to avoid "Various Artists" if s.get('trackartist'): artist = s.get('trackartist') - used_field = 'trackartist' elif s.get('contributor'): artist = s.get('contributor') - used_field = 'contributor' elif s.get('artist'): artist = s.get('artist') - used_field = 'artist' elif s.get('albumartist'): artist = s.get('albumartist') - used_field = 'albumartist' elif s.get('band'): artist = s.get('band') - used_field = 'band' else: artist = 'Unknown Artist' - used_field = 'fallback' path = s.get('url') or s.get('Path') or s.get('path') or '' mapped.append({ @@ -1109,22 +1097,16 @@ def get_top_played_songs(limit): # Prioritize track artist over album artist to avoid "Various Artists" if s.get('trackartist'): track_artist = s.get('trackartist') - used_field = 'trackartist' elif s.get('contributor'): track_artist = s.get('contributor') - used_field = 'contributor' elif s.get('artist'): track_artist = s.get('artist') - used_field = 'artist' elif s.get('albumartist'): track_artist = s.get('albumartist') - used_field = 'albumartist' elif s.get('band'): track_artist = s.get('band') - used_field = 'band' else: track_artist = 'Unknown Artist' - used_field = 'fallback' mapped_songs.append({ 'Id': s.get('id'), diff --git a/tasks/mediaserver_navidrome.py b/tasks/mediaserver_navidrome.py index 2a865041..86eeadde 100644 --- a/tasks/mediaserver_navidrome.py +++ b/tasks/mediaserver_navidrome.py @@ -278,15 +278,12 @@ def _select_best_artist(song_item, title="Unknown"): if song_item.get('artist'): track_artist = song_item['artist'] artist_id = song_item.get('artistId') - used_field = 'artist' elif song_item.get('albumArtist'): track_artist = song_item['albumArtist'] artist_id = song_item.get('albumArtistId') - used_field = 'albumArtist' else: track_artist = 'Unknown Artist' artist_id = None - used_field = 'fallback' return track_artist, artist_id diff --git a/tasks/memory_utils.py b/tasks/memory_utils.py index e159209b..06cb3aad 100644 --- a/tasks/memory_utils.py +++ b/tasks/memory_utils.py @@ -304,8 +304,6 @@ def comprehensive_memory_cleanup(force_cuda: bool = True, reset_onnx_pool: bool # Final garbage collection + return the freed heap to the OS so RSS drops results['malloc_trim'] = release_memory_to_os() - successful_cleanups = sum(results.values()) - total_methods = len([k for k, v in {'cuda': force_cuda, 'onnx_pool': reset_onnx_pool, 'gc': True, 'malloc_trim': True}.items() if v]) return results diff --git a/tasks/voyager_manager.py b/tasks/voyager_manager.py index de05cfb8..82f87bd0 100644 --- a/tasks/voyager_manager.py +++ b/tasks/voyager_manager.py @@ -1077,7 +1077,6 @@ def _accept_at_index(i): best_i = None best_score = float('inf') - best_d = None for i in avail_idxs: meta = items[i] @@ -1123,7 +1122,6 @@ def _accept_at_index(i): if score < best_score: best_score = score best_i = i - best_d = dist_prev if best_i is None: break @@ -1168,7 +1166,7 @@ def _accept_at_index(i): start_id = None if bi == 0: start_id = playlist_ids[0] if playlist_ids else None - sub = _walk_single_bucket(bi, start_item_id=start_id) + _walk_single_bucket(bi, start_item_id=start_id) processed_buckets += 1 if len(playlist_ids) >= n: break diff --git a/templates/alchemy.html b/templates/alchemy.html index 20fe7d00..c64ba49d 100644 --- a/templates/alchemy.html +++ b/templates/alchemy.html @@ -729,8 +729,7 @@

Saved Anchors

if (isNaN(temperature) && cfg && cfg.alchemy_temperature) temperature = cfg.alchemy_temperature; const payload = { items: valid, n, temperature }; - if (metric === 'angular') payload.subtract_distance = parseFloat(subtractSlider.value); - else payload.subtract_distance = parseFloat(subtractSlider.value); + payload.subtract_distance = parseFloat(subtractSlider.value); const resp = await fetch("{{ url_for('alchemy_bp.alchemy_api') }}", { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await resp.json(); diff --git a/templates/waveform.html b/templates/waveform.html index ae17f511..f38a6c0f 100644 --- a/templates/waveform.html +++ b/templates/waveform.html @@ -116,7 +116,7 @@

Track Title

const waveformCanvas = document.getElementById('waveform-canvas'); const trackTitle = document.getElementById('track-title'); const trackArtist = document.getElementById('track-artist'); - const trackAlbum = document.getElementById('track-album'); + let trackAlbum = document.getElementById('track-album'); const statusDiv = document.getElementById('status'); // --- Autocomplete Logic --- diff --git a/test/test_gpu_status.py b/test/test_gpu_status.py index 824eb102..584d5650 100644 --- a/test/test_gpu_status.py +++ b/test/test_gpu_status.py @@ -85,7 +85,7 @@ def test_onnx_runtime(): if has_cuda: try: # Create a minimal test to verify CUDA actually works - sess_options = ort.SessionOptions() + ort.SessionOptions() # Just check that we can request CUDA without error print_result("CUDA EP available", True, "CUDAExecutionProvider ready") except Exception as e: diff --git a/tests/unit/test_app_analysis.py b/tests/unit/test_app_analysis.py index ff283ce6..0caa966f 100644 --- a/tests/unit/test_app_analysis.py +++ b/tests/unit/test_app_analysis.py @@ -339,7 +339,7 @@ def test_analysis_handles_enqueue_failure( # Should raise exception (Flask will handle with 500) with pytest.raises(Exception): - response = client.post('/api/analysis/start', json={}) + client.post('/api/analysis/start', json={}) @patch('app_helper.get_active_main_task', return_value={'task_id': 'existing-cleaning-123', 'status': 'STARTED', 'task_type': 'cleaning'}) @patch('app_helper.rq_queue_high') @@ -369,7 +369,7 @@ def test_cleaning_handles_enqueue_failure( # Should raise exception (Flask will handle with 500) with pytest.raises(Exception): - response = client.post('/api/cleaning/start') + client.post('/api/cleaning/start') class TestBlueprintIntegration: diff --git a/tests/unit/test_app_chat.py b/tests/unit/test_app_chat.py index 33e82f37..fa6577e5 100644 --- a/tests/unit/test_app_chat.py +++ b/tests/unit/test_app_chat.py @@ -16,7 +16,6 @@ def test_song_similarity_empty_title_rejected(self): # This test validates the logic without calling the full endpoint # It tests the rejection criteria: title must be non-empty title = "" - artist = "Artist" # Check if title passes validation is_valid = bool(title.strip()) @@ -24,7 +23,6 @@ def test_song_similarity_empty_title_rejected(self): def test_song_similarity_empty_artist_rejected(self): """song_similarity with empty artist should be skipped.""" - title = "Song" artist = "" # Check if artist passes validation diff --git a/tests/unit/test_mcp_server.py b/tests/unit/test_mcp_server.py index 22ba9c83..17fa8d1a 100644 --- a/tests/unit/test_mcp_server.py +++ b/tests/unit/test_mcp_server.py @@ -614,11 +614,6 @@ def test_search_database_energy_conversion(self): "energy_max": 0.8 }, {}) - # Check the raw energy values passed to the query function - if mock_query.called: - kwargs = mock_query.call_args[1] if mock_query.call_args[1] else {} - args = mock_query.call_args[0] if mock_query.call_args[0] else () - # energy should have been converted from 0-1 to raw finally: cfg.ENERGY_MIN = orig_min cfg.ENERGY_MAX = orig_max @@ -657,7 +652,7 @@ def test_exact_match_case_insensitive(self): mock_voyager.find_nearest_neighbors_by_id = mock_nn with patch.object(mod, 'get_db_connection', return_value=conn), \ patch.dict(sys.modules, {'tasks.voyager_manager': mock_voyager}): - result = mod._song_similarity_api_sync("bohemian rhapsody", "queen", 10) + mod._song_similarity_api_sync("bohemian rhapsody", "queen", 10) # Should have tried a DB lookup assert cur.execute.called @@ -893,7 +888,7 @@ def test_get_songs_limits_results(self): with patch.object(mod, 'get_db_connection', return_value=conn), \ patch.dict(sys.modules, {'tasks.artist_gmm_manager': gmm_mod}): - result = mod._artist_similarity_api_sync("Coldplay", count=5, get_songs=5) + mod._artist_similarity_api_sync("Coldplay", count=5, get_songs=5) execute_calls = cur.execute.call_args_list for c in execute_calls: diff --git a/tests/unit/test_mediaserver.py b/tests/unit/test_mediaserver.py index 9305d436..2d9da000 100644 --- a/tests/unit/test_mediaserver.py +++ b/tests/unit/test_mediaserver.py @@ -1597,8 +1597,8 @@ def test_calls_playlists_delete_command(self, mock_request): mock_request.return_value = {'count': 1} - result = delete_playlist('playlist-123') - + delete_playlist('playlist-123') + call_args = mock_request.call_args # First arg is command assert call_args[0][0] == 'playlists', \ @@ -1998,7 +1998,7 @@ def test_handles_standalone_track_pseudo_albums(self, mock_config, mock_get): mock_get.return_value = mock_response # Call with pseudo-album ID - tracks = get_tracks_from_album('standalone_real-track-id') + get_tracks_from_album('standalone_real-track-id') # Should fetch the track directly, not list children call_url = mock_get.call_args[0][0] diff --git a/tests/unit/test_memory_cleanup.py b/tests/unit/test_memory_cleanup.py index e3e1d912..e0613824 100644 --- a/tests/unit/test_memory_cleanup.py +++ b/tests/unit/test_memory_cleanup.py @@ -115,7 +115,7 @@ def test_no_cleanup_with_album_sessions( # Call with pre-loaded sessions with patch('tasks.analysis.cleanup_onnx_session') as mock_cleanup: - result = analyze_track( + analyze_track( "/tmp/test.mp3", ["happy", "sad"], { @@ -209,7 +209,7 @@ def test_cleanup_all_models_in_finally( mock_clap_loaded.return_value = True # Call function (should complete successfully) - result = analyze_album_task("album_123", "Empty Album", 5, None) + analyze_album_task("album_123", "Empty Album", 5, None) # Verify all cleanup functions were called assert mock_memory_cleanup.called @@ -277,7 +277,7 @@ def test_cleanup_onnx_sessions_on_success( # Call function with patch('tasks.clap_analyzer.is_clap_available', return_value=False): - result = analyze_album_task("album_123", "Test Album", 5, None) + analyze_album_task("album_123", "Test Album", 5, None) # Verify session cleanup was called for all loaded sessions # Should be called 2 times (embedding + prediction; secondary models removed in v4.0.0) diff --git a/tests/unit/test_playlist_ordering.py b/tests/unit/test_playlist_ordering.py index 0d97e85d..c041321f 100644 --- a/tests/unit/test_playlist_ordering.py +++ b/tests/unit/test_playlist_ordering.py @@ -112,7 +112,7 @@ def test_empty_input(self): def test_minimum_songs_no_ordering(self): """3+ songs with len <= 2 orderable → return input unchanged.""" - mod = _load_playlist_ordering() + _load_playlist_ordering() # This simulates the case where we have 3 songs but fewer than 3 with DB data # Since the function checks if len(orderable_ids) <= 2 and returns early, # we verify this behavior by checking the algorithm logic itself. diff --git a/tests/unit/test_provider_migration_execute.py b/tests/unit/test_provider_migration_execute.py index 678ba93c..a08286c9 100644 --- a/tests/unit/test_provider_migration_execute.py +++ b/tests/unit/test_provider_migration_execute.py @@ -314,10 +314,6 @@ def test_voyager_id_map_rewrite_happens(self, mig): # The migration should have issued an UPDATE voyager_index_data after rewriting # (we can't easily verify the exact JSON written without capturing params, but # we can verify the statement was executed) - executed_upper = '\n'.join( - s.upper() for s in mig._get_dedicated_conn.return_value.cursor.return_value.execute.call_args_list - if isinstance(s, str) - ) # Fallback: walk the call_args_list and check sql strings calls = mig._get_dedicated_conn.return_value.cursor.return_value.execute.call_args_list sqls = [c[0][0] for c in calls] diff --git a/tests/unit/test_sem_grove_manager.py b/tests/unit/test_sem_grove_manager.py index db14f959..8c6dbf92 100644 --- a/tests/unit/test_sem_grove_manager.py +++ b/tests/unit/test_sem_grove_manager.py @@ -510,7 +510,6 @@ def fake_stream(table, column, dim, where_clause=None, **kwargs): whitening_json = whitening_row[1] # id_map_json column holds whitening stats index_binary = index_row[0] index_idmap = index_row[1] - index_dim = index_row[2] # Patch _load_sem_grove_index_from_db to use stored bytes directly def fake_load(): @@ -525,7 +524,6 @@ def fake_load(): w_a = float(whitening["w_audio"]) ld_ = int(whitening["lyrics_dim"]) ad_ = int(whitening["audio_dim"]) - merged = ld_ + ad_ stream = io.BytesIO(index_binary) loaded = _voyager.Index.load(stream) diff --git a/windows/embedded_pg.py b/windows/embedded_pg.py index 68ea22a0..b2bcd17e 100644 --- a/windows/embedded_pg.py +++ b/windows/embedded_pg.py @@ -146,7 +146,6 @@ def stop(): if _running_proc is None: return data_dir = paths.pgdata_dir() - port = str(paths.pg_port()) logger.info("Stopping PostgreSQL") try: subprocess.run(