From b20949c3e43a47a242add618fab672c894b466c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:05:37 +0000 Subject: [PATCH 1/4] Initial plan From 3d87c70623e23244e600bac39ca22a4d2efafcef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:10:38 +0000 Subject: [PATCH 2/4] Production-ready cleanup: fix debug markers, entropy bug, unused imports, label UnboundLocalError, indentation, and stray comments Co-authored-by: Blueion76 <128919662+Blueion76@users.noreply.github.com> --- octogen/ai/engine.py | 13 ++--- octogen/main.py | 136 +++++++++++++++++++++---------------------- 2 files changed, 71 insertions(+), 78 deletions(-) diff --git a/octogen/ai/engine.py b/octogen/ai/engine.py index 8384ee5..14e6098 100644 --- a/octogen/ai/engine.py +++ b/octogen/ai/engine.py @@ -4,6 +4,7 @@ import json from json_repair import repair_json import logging +import math import os import random import re @@ -146,10 +147,10 @@ def analyze_listening_profile(self, favorited_songs: List[Dict], top_artists: Li # Diversity score: higher when more evenly distributed if total > 0: - # Calculate normalized entropy - entropy = sum(-(count/total) * (count/total).bit_length() for count in artist_counts.values() if count > 0) - max_entropy = total.bit_length() if total > 1 else 1 - profile["diversity_score"] = entropy / max_entropy if max_entropy > 0 else 0 + # Calculate normalized Shannon entropy + entropy = -sum((count/total) * math.log2(count/total) for count in artist_counts.values() if count > 0) + max_entropy = math.log2(len(artist_counts)) if len(artist_counts) > 1 else 1 + profile["diversity_score"] = min(entropy / max_entropy, 1.0) if max_entropy > 0 else 0 profile["artist_distribution"] = dict(artist_counts.most_common(10)) @@ -594,7 +595,6 @@ def _generate_with_gemini( logger.warning("Thinking budget nearly exhausted (%d/%d tokens)", thoughts, thinking_budget) - # === FIX START === # Check for empty response if not response.text or response.text.strip() == "": logger.error("Gemini returned empty response") @@ -609,7 +609,6 @@ def _generate_with_gemini( raise ValueError("Invalid JSON response from Gemini") from e return response.text - # === FIX END === def _generate_with_openai( self, @@ -860,4 +859,4 @@ def _generate_with_retry(self, generate_func, *args, **kwargs) -> str: logger.error("Non-rate-limit error: %s", str(e)[:200]) raise - raise Exception("Max retries exceeded") \ No newline at end of file + raise Exception("Max retries exceeded") diff --git a/octogen/main.py b/octogen/main.py index f6c0d92..4457a48 100644 --- a/octogen/main.py +++ b/octogen/main.py @@ -11,7 +11,6 @@ import random import time import argparse -import asyncio import re from datetime import datetime, timedelta, timezone from pathlib import Path @@ -35,7 +34,6 @@ from octogen.api.listenbrainz import ListenBrainzAPI from octogen.api.audiomuse import AudioMuseClient from octogen.ai.engine import AIRecommendationEngine -from octogen.config import load_config_from_env from octogen.models.tracker import ServiceTracker, RunTracker from octogen.web.health import write_health_status from octogen.scheduler.cron import calculate_next_run, wait_until, calculate_cron_interval @@ -467,7 +465,7 @@ def _record_successful_run(self) -> None: 'last_run_timestamp': now.isoformat(), 'last_run_date': now.strftime("%Y-%m-%d"), 'last_run_formatted': now.strftime("%Y-%m-%d %H:%M:%S"), - 'next_scheduled_run': next_scheduled_run, # ✅ Added this! + 'next_scheduled_run': next_scheduled_run, 'services': services_data }, f, indent=2) logger.info("✓ Recorded successful run timestamp with service tracking") @@ -599,7 +597,7 @@ def seen_key(a: str, t: str) -> Tuple[str, str]: artist = (rec.get("artist") or "").strip() title = (rec.get("title") or "").strip() - mbid = rec.get("mbid") # <-- NEW + mbid = rec.get("mbid") if not artist or not title: continue @@ -697,8 +695,6 @@ def seen_key(a: str, t: str) -> Tuple[str, str]: return song_ids[:max_songs] - - def create_playlist(self, name: str, recommendations: List[Dict], max_songs: int = 100) -> None: """Create a playlist from recommendations.""" @@ -740,6 +736,7 @@ def _generate_hybrid_daily_mix( List of song dicts: [{"artist": "...", "title": "..."}] """ songs = [] + label = f"Daily Mix {mix_number}" if mix_number in [1,2,3,4,5,6] else playlist_name # Get configuration audiomuse_songs_count = self.config["audiomuse"]["songs_per_mix"] @@ -783,7 +780,6 @@ def _generate_hybrid_daily_mix( break songs.extend(audiomuse_collected) audiomuse_actual_count = len(audiomuse_collected) - label = f"Daily Mix {mix_number}" if mix_number in [1,2,3,4,5,6] else playlist_name logger.info(f"📻 {label}: Got {audiomuse_actual_count} songs from AudioMuse-AI") if audiomuse_actual_count < audiomuse_songs_count: logger.debug(f"AudioMuse returned fewer songs than requested ({audiomuse_actual_count}/{audiomuse_songs_count})") @@ -1076,69 +1072,69 @@ def run(self) -> None: sys.exit(1) if should_generate_regular and all_playlists: - # Handle hybrid playlists if AudioMuse is enabled - if self.audiomuse_client: - logger.info("=" * 70) - logger.info("GENERATING HYBRID PLAYLISTS (AudioMuse + LLM)") - logger.info("=" * 70) - - playlists_before_audiomuse = self.stats["playlists_created"] - - # Define all hybrid playlist configurations (everything except Discovery) - hybrid_playlist_configs = [ - # Daily Mixes (num 1-6) - {"name": "Daily Mix 1", "genre": top_genres[0] if len(top_genres) > 0 else DEFAULT_DAILY_MIX_GENRES[0], "characteristics": "energetic", "num": 1}, - {"name": "Daily Mix 2", "genre": top_genres[1] if len(top_genres) > 1 else DEFAULT_DAILY_MIX_GENRES[1], "characteristics": "catchy upbeat", "num": 2}, - {"name": "Daily Mix 3", "genre": top_genres[2] if len(top_genres) > 2 else DEFAULT_DAILY_MIX_GENRES[2], "characteristics": "danceable rhythmic", "num": 3}, - {"name": "Daily Mix 4", "genre": top_genres[3] if len(top_genres) > 3 else DEFAULT_DAILY_MIX_GENRES[3], "characteristics": "rhythmic bass-heavy", "num": 4}, - {"name": "Daily Mix 5", "genre": top_genres[4] if len(top_genres) > 4 else DEFAULT_DAILY_MIX_GENRES[4], "characteristics": "alternative atmospheric", "num": 5}, - {"name": "Daily Mix 6", "genre": top_genres[5] if len(top_genres) > 5 else DEFAULT_DAILY_MIX_GENRES[5], "characteristics": "smooth melodic", "num": 6}, - # Mood/Activity playlists (no num) - {"name": "Chill Vibes", "genre": "ambient", "characteristics": "relaxing calm peaceful", "num": None}, - {"name": "Workout Energy", "genre": "high-energy", "characteristics": "upbeat motivating intense", "num": None}, - {"name": "Focus Flow", "genre": "instrumental", "characteristics": "ambient atmospheric concentration", "num": None}, - {"name": "Drive Time", "genre": "upbeat", "characteristics": "driving energetic feel-good", "num": None} - ] - - # Generate and create hybrid playlists - for mix_config in hybrid_playlist_configs: - playlist_name = mix_config["name"] - mix_number = mix_config.get("num") - hybrid_songs = self._generate_hybrid_daily_mix( - mix_number=mix_number, - genre_focus=mix_config["genre"], - characteristics=mix_config["characteristics"], - top_artists=top_artists, - top_genres=top_genres, - favorited_songs=favorited_songs, - low_rated_songs=low_rated_songs, - playlist_name=playlist_name - ) - if hybrid_songs: - self.create_playlist(playlist_name, hybrid_songs, max_songs=30) - - # Track AudioMuse service - audiomuse_playlists = self.stats["playlists_created"] - playlists_before_audiomuse - self.service_tracker.record( - "audiomuse", - success=True, - playlists=audiomuse_playlists + # Handle hybrid playlists if AudioMuse is enabled + if self.audiomuse_client: + logger.info("=" * 70) + logger.info("GENERATING HYBRID PLAYLISTS (AudioMuse + LLM)") + logger.info("=" * 70) + + playlists_before_audiomuse = self.stats["playlists_created"] + + # Define all hybrid playlist configurations (everything except Discovery) + hybrid_playlist_configs = [ + # Daily Mixes (num 1-6) + {"name": "Daily Mix 1", "genre": top_genres[0] if len(top_genres) > 0 else DEFAULT_DAILY_MIX_GENRES[0], "characteristics": "energetic", "num": 1}, + {"name": "Daily Mix 2", "genre": top_genres[1] if len(top_genres) > 1 else DEFAULT_DAILY_MIX_GENRES[1], "characteristics": "catchy upbeat", "num": 2}, + {"name": "Daily Mix 3", "genre": top_genres[2] if len(top_genres) > 2 else DEFAULT_DAILY_MIX_GENRES[2], "characteristics": "danceable rhythmic", "num": 3}, + {"name": "Daily Mix 4", "genre": top_genres[3] if len(top_genres) > 3 else DEFAULT_DAILY_MIX_GENRES[3], "characteristics": "rhythmic bass-heavy", "num": 4}, + {"name": "Daily Mix 5", "genre": top_genres[4] if len(top_genres) > 4 else DEFAULT_DAILY_MIX_GENRES[4], "characteristics": "alternative atmospheric", "num": 5}, + {"name": "Daily Mix 6", "genre": top_genres[5] if len(top_genres) > 5 else DEFAULT_DAILY_MIX_GENRES[5], "characteristics": "smooth melodic", "num": 6}, + # Mood/Activity playlists (no num) + {"name": "Chill Vibes", "genre": "ambient", "characteristics": "relaxing calm peaceful", "num": None}, + {"name": "Workout Energy", "genre": "high-energy", "characteristics": "upbeat motivating intense", "num": None}, + {"name": "Focus Flow", "genre": "instrumental", "characteristics": "ambient atmospheric concentration", "num": None}, + {"name": "Drive Time", "genre": "upbeat", "characteristics": "driving energetic feel-good", "num": None} + ] + + # Generate and create hybrid playlists + for mix_config in hybrid_playlist_configs: + playlist_name = mix_config["name"] + mix_number = mix_config.get("num") + hybrid_songs = self._generate_hybrid_daily_mix( + mix_number=mix_number, + genre_focus=mix_config["genre"], + characteristics=mix_config["characteristics"], + top_artists=top_artists, + top_genres=top_genres, + favorited_songs=favorited_songs, + low_rated_songs=low_rated_songs, + playlist_name=playlist_name ) - logger.info("AudioMuse-AI service succeeded: %d playlists", audiomuse_playlists) - - # Create Discovery from AI response (LLM-only for new discoveries) - if "Discovery" in all_playlists: - discovery_songs = all_playlists["Discovery"] - if isinstance(discovery_songs, list) and discovery_songs: - logger.info("=" * 70) - logger.info("DISCOVERY (LLM-only for new discoveries)") - logger.info("=" * 70) - self.create_playlist("Discovery", discovery_songs, max_songs=50) - else: - # Original behavior: use all AI-generated playlists - for playlist_name, songs in all_playlists.items(): - if isinstance(songs, list) and songs: - self.create_playlist(playlist_name, songs, max_songs=100) + if hybrid_songs: + self.create_playlist(playlist_name, hybrid_songs, max_songs=30) + + # Track AudioMuse service + audiomuse_playlists = self.stats["playlists_created"] - playlists_before_audiomuse + self.service_tracker.record( + "audiomuse", + success=True, + playlists=audiomuse_playlists + ) + logger.info("AudioMuse-AI service succeeded: %d playlists", audiomuse_playlists) + + # Create Discovery from AI response (LLM-only for new discoveries) + if "Discovery" in all_playlists: + discovery_songs = all_playlists["Discovery"] + if isinstance(discovery_songs, list) and discovery_songs: + logger.info("=" * 70) + logger.info("DISCOVERY (LLM-only for new discoveries)") + logger.info("=" * 70) + self.create_playlist("Discovery", discovery_songs, max_songs=50) + else: + # Original behavior: use all AI-generated playlists + for playlist_name, songs in all_playlists.items(): + if isinstance(songs, list) and songs: + self.create_playlist(playlist_name, songs, max_songs=100) # External services (run regardless of starred songs, but only if should_generate_regular) @@ -1396,7 +1392,6 @@ def run(self) -> None: llm_response = response.choices[0].message.content # Parse response - import json llm_data = json.loads(llm_response) llm_songs = llm_data.get("songs", []) @@ -1510,7 +1505,6 @@ def run(self) -> None: sys.exit(1) - # ============================================================================ # SCHEDULING SUPPORT # ============================================================================ From 3ec8c6dbd3efd3210d6e596915baf0c79a10dab2 Mon Sep 17 00:00:00 2001 From: Blueion <128919662+Blueion76@users.noreply.github.com> Date: Thu, 5 Mar 2026 21:15:45 -0600 Subject: [PATCH 3/4] Update octogen/main.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- octogen/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octogen/main.py b/octogen/main.py index 4457a48..86da4fa 100644 --- a/octogen/main.py +++ b/octogen/main.py @@ -1108,7 +1108,7 @@ def run(self) -> None: top_genres=top_genres, favorited_songs=favorited_songs, low_rated_songs=low_rated_songs, - playlist_name=playlist_name + playlist_name=playlist_name ) if hybrid_songs: self.create_playlist(playlist_name, hybrid_songs, max_songs=30) From ee936826ed6b839fd9157099a0f94f454690dbcb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:16:47 +0000 Subject: [PATCH 4/4] Use label in all debug messages, fix except block indentation Co-authored-by: Blueion76 <128919662+Blueion76@users.noreply.github.com> --- octogen/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/octogen/main.py b/octogen/main.py index 86da4fa..7b4a5a0 100644 --- a/octogen/main.py +++ b/octogen/main.py @@ -745,7 +745,7 @@ def _generate_hybrid_daily_mix( # Get songs from AudioMuse-AI if enabled audiomuse_actual_count = 0 if self.audiomuse_client: - logger.debug(f"Requesting {audiomuse_songs_count} songs from AudioMuse-AI for Daily Mix {mix_number}") + logger.debug(f"Requesting {audiomuse_songs_count} songs from AudioMuse-AI for {label}") # --- Begin multi-version prompt logic --- modifiers = characteristics.split() if characteristics else [] prompt_variants = [] @@ -796,7 +796,7 @@ def _generate_hybrid_daily_mix( logger.info(f"🔄 AudioMuse returned {audiomuse_actual_count}/{audiomuse_songs_count} songs, " f"requesting {num_llm_songs} from LLM (includes {buffer} song buffer)") - logger.debug(f"Requesting {num_llm_songs} songs from LLM for Daily Mix {mix_number}") + logger.debug(f"Requesting {num_llm_songs} songs from LLM for {label}") # We'll use the AI engine to generate just the LLM portion llm_songs = self._generate_llm_songs_for_daily_mix( mix_number=mix_number, @@ -1500,9 +1500,9 @@ def run(self) -> None: self._record_successful_run() except Exception as e: - write_health_status(BASE_DIR, "unhealthy", f"Error: {str(e)[:200]}") - logger.error("Fatal error: %s", e, exc_info=True) - sys.exit(1) + write_health_status(BASE_DIR, "unhealthy", f"Error: {str(e)[:200]}") + logger.error("Fatal error: %s", e, exc_info=True) + sys.exit(1) # ============================================================================