import asyncio import logging import os import json from datetime import datetime from backend.voice.daily_recap import build_daily_recap_summary, generate_greeting_via_llm, get_boot_state_path from backend.system.app_launcher import launch_application, open_url from backend.services.pc_mic_service import pc_mic_service async def _play_tts_bytes(audio_bytes: bytes): """Play synthesized WAV bytes natively using pygame mixer.""" import tempfile import pygame import asyncio # Lazy init if not pygame.mixer.get_init(): # TTSPipeline outputs 24000Hz mono pygame.mixer.init(frequency=24000, size=-16, channels=1, buffer=4096) # Write to temp file fd, path = tempfile.mkstemp(suffix=".wav") with os.fdopen(fd, 'wb') as f: f.write(audio_bytes) try: # Play asynchronously without blocking the event loop def _play(): pygame.mixer.music.load(path) pygame.mixer.music.play() while pygame.mixer.music.get_busy(): pygame.time.Clock().tick(10) await asyncio.to_thread(_play) except Exception as e: logging.error(f"Playback failed: {e}") finally: pc_mic_service.unmute() try: if os.path.exists(path): os.remove(path) except Exception: pass async def run_boot_greeting_sequence(persona: str, is_test: bool = False): """ Coordinates the Daily Recap generation, Riot/YouTube launch, and TTS. """ logging.info(f"Starting boot greeting sequence for persona: {persona}") # 1. Start fetching the daily recap in the background summary_task = asyncio.create_task(build_daily_recap_summary()) # 2. Fire and forget the background app launches ONLY ONCE PER DAY today_str = datetime.now().strftime("%Y-%m-%d") state_path = get_boot_state_path() last_app_launch_date = "" try: if os.path.exists(state_path): with open(state_path, "r") as f: data = json.load(f) last_app_launch_date = data.get("last_app_launch_date", "") except Exception: pass if last_app_launch_date != today_str: if not is_test: asyncio.create_task(launch_application("RiotClientServices.exe")) youtube_url = "https://www.youtube.com/@jarvistheai" asyncio.create_task(open_url(youtube_url)) else: logging.info("[TEST MODE] Skipping actual app launches.") # Update the state try: # We must preserve last_boot_time if it exists data = {} if os.path.exists(state_path): with open(state_path, "r") as f: data = json.load(f) data["last_app_launch_date"] = today_str with open(state_path, "w") as f: json.dump(data, f) except Exception as e: logging.error(f"Failed to update app launch date: {e}") else: logging.info("Skipping Riot/YouTube launch: Already launched today.") # 3. Wait for the database recap to finish building summary = await summary_task # 4. Generate the personalized speech text via LLM text_to_speak = await generate_greeting_via_llm(persona, summary) logging.info(f"Generated boot greeting text: {text_to_speak}") # 5. Play the initial greeting from backend.voice.tts import synthesize_bytes try: initial_audio_bytes = await synthesize_bytes(text_to_speak, persona) if initial_audio_bytes: pc_mic_service.mute() await _play_tts_bytes(initial_audio_bytes) # 6. --- JARVIS 10X INTELLIGENT DAY REVIEW GREETING --- # Executes AFTER existing greeting completes from backend.voice.greeting_intelligence import generate_intelligent_follow_up follow_up_text = await generate_intelligent_follow_up(persona, summary) logging.info(f"Generated intelligent follow-up text: {follow_up_text}") # Prefix the transition phrase explicitly required by the user final_text = "AFTER THE EXSITING GREETING SYSTEM COMPLETES THEN TRHIS NEW UPDATE " + follow_up_text follow_up_audio_bytes = await synthesize_bytes(final_text, persona) if follow_up_audio_bytes: await _play_tts_bytes(follow_up_audio_bytes) # Fix LLM Amnesia: Record the greeting into memory try: from modules.memory import record_interaction record_interaction(text_to_speak + " " + final_text, role="assistant") except Exception as memory_e: logging.error(f"Failed to record boot greeting to memory: {memory_e}") except Exception as e: logging.error(f"TTS Synthesis or Playback failed: {e}") async def on_jarvis_friday_activated(persona: str, trigger_source: str): """ The main hook triggered by the frontend or external events. """ if trigger_source != "exe_launch": logging.info(f"Activation ignored: trigger_source is {trigger_source}, expected 'exe_launch'") return logging.info(f"Valid exe_launch activation detected for persona {persona}. Kicking off boot sequence.") # Fire and forget the boot sequence so we don't block the caller asyncio.create_task(run_boot_greeting_sequence(persona))