From 88e7ede452761e374e96332f5b97f1ff4359e023 Mon Sep 17 00:00:00 2001 From: Brian Madison Date: Thu, 30 Oct 2025 15:34:21 -0500 Subject: [PATCH] remove voice hooks --- .claude/hooks/bmad-tts-injector.sh | 415 ------------- .claude/hooks/bmad-voice-manager.sh | 511 ---------------- .claude/hooks/check-output-style.sh | 112 ---- .claude/hooks/download-extra-voices.sh | 244 -------- .claude/hooks/github-star-reminder.sh | 154 ----- .claude/hooks/language-manager.sh | 392 ------------ .claude/hooks/learn-manager.sh | 475 --------------- .claude/hooks/personality-manager.sh | 438 -------------- .claude/hooks/piper-download-voices.sh | 165 ------ .claude/hooks/piper-installer.sh | 178 ------ .claude/hooks/piper-multispeaker-registry.sh | 165 ------ .claude/hooks/piper-voice-manager.sh | 293 --------- .claude/hooks/play-tts-elevenlabs.sh | 404 ------------- .claude/hooks/play-tts-piper.sh | 338 ----------- .claude/hooks/play-tts.sh | 100 ---- .claude/hooks/provider-commands.sh | 540 ----------------- .claude/hooks/provider-manager.sh | 298 ---------- .claude/hooks/replay-target-audio.sh | 95 --- .claude/hooks/sentiment-manager.sh | 201 ------- .claude/hooks/speed-manager.sh | 291 --------- .claude/hooks/voice-manager.sh | 594 ------------------- .claude/hooks/voices-config.sh | 70 --- 22 files changed, 6473 deletions(-) delete mode 100755 .claude/hooks/bmad-tts-injector.sh delete mode 100755 .claude/hooks/bmad-voice-manager.sh delete mode 100755 .claude/hooks/check-output-style.sh delete mode 100755 .claude/hooks/download-extra-voices.sh delete mode 100755 .claude/hooks/github-star-reminder.sh delete mode 100755 .claude/hooks/language-manager.sh delete mode 100755 .claude/hooks/learn-manager.sh delete mode 100755 .claude/hooks/personality-manager.sh delete mode 100755 .claude/hooks/piper-download-voices.sh delete mode 100755 .claude/hooks/piper-installer.sh delete mode 100755 .claude/hooks/piper-multispeaker-registry.sh delete mode 100755 .claude/hooks/piper-voice-manager.sh delete mode 100755 .claude/hooks/play-tts-elevenlabs.sh delete mode 100755 .claude/hooks/play-tts-piper.sh delete mode 100755 .claude/hooks/play-tts.sh delete mode 100755 .claude/hooks/provider-commands.sh delete mode 100755 .claude/hooks/provider-manager.sh delete mode 100755 .claude/hooks/replay-target-audio.sh delete mode 100755 .claude/hooks/sentiment-manager.sh delete mode 100755 .claude/hooks/speed-manager.sh delete mode 100755 .claude/hooks/voice-manager.sh delete mode 100755 .claude/hooks/voices-config.sh diff --git a/.claude/hooks/bmad-tts-injector.sh b/.claude/hooks/bmad-tts-injector.sh deleted file mode 100755 index 3339efa0..00000000 --- a/.claude/hooks/bmad-tts-injector.sh +++ /dev/null @@ -1,415 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/bmad-tts-injector.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied. Use at your own risk. See the Apache License for details. -# -# --- -# -# @fileoverview BMAD TTS Injection Manager - Patches BMAD agents for TTS integration -# @context Automatically modifies BMAD agent YAML files to include AgentVibes TTS capabilities -# @architecture Injects TTS hooks into activation-instructions and core_principles sections -# @dependencies bmad-core/agents/*.md files, play-tts.sh, bmad-voice-manager.sh -# @entrypoints Called via bmad-tts-injector.sh {enable|disable|status|restore} -# @patterns File patching with backup, provider-aware voice mapping, injection markers for idempotency -# @related play-tts.sh, bmad-voice-manager.sh, .bmad-core/agents/*.md -# - -set -e # Exit on error - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CLAUDE_DIR="$(dirname "$SCRIPT_DIR")" - -# Colors for output -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -CYAN='\033[0;36m' -GRAY='\033[0;90m' -NC='\033[0m' # No Color - -# Detect BMAD installation -detect_bmad() { - local bmad_core_dir="" - - # Check current directory first - if [[ -d ".bmad-core" ]]; then - bmad_core_dir=".bmad-core" - # Check parent directory - elif [[ -d "../.bmad-core" ]]; then - bmad_core_dir="../.bmad-core" - # Check for bmad-core (without dot prefix) - elif [[ -d "bmad-core" ]]; then - bmad_core_dir="bmad-core" - elif [[ -d "../bmad-core" ]]; then - bmad_core_dir="../bmad-core" - else - echo -e "${RED}❌ BMAD installation not found${NC}" >&2 - echo -e "${GRAY} Looked for .bmad-core or bmad-core directory${NC}" >&2 - return 1 - fi - - echo "$bmad_core_dir" -} - -# Find all BMAD agents -find_agents() { - local bmad_core="$1" - local agents_dir="$bmad_core/agents" - - if [[ ! -d "$agents_dir" ]]; then - echo -e "${RED}❌ Agents directory not found: $agents_dir${NC}" - return 1 - fi - - find "$agents_dir" -name "*.md" -type f -} - -# Check if agent has TTS injection -has_tts_injection() { - local agent_file="$1" - - if grep -q "# AGENTVIBES-TTS-INJECTION" "$agent_file" 2>/dev/null; then - return 0 - fi - return 1 -} - -# Extract agent ID from file -get_agent_id() { - local agent_file="$1" - - # Look for "id: " in YAML block - local agent_id=$(grep -E "^ id:" "$agent_file" | head -1 | awk '{print $2}' | tr -d '"' | tr -d "'") - - if [[ -z "$agent_id" ]]; then - # Fallback: use filename without extension - agent_id=$(basename "$agent_file" .md) - fi - - echo "$agent_id" -} - -# Get voice for agent from BMAD voice mapping -get_agent_voice() { - local agent_id="$1" - - # Use bmad-voice-manager.sh to get voice - if [[ -f "$SCRIPT_DIR/bmad-voice-manager.sh" ]]; then - local voice=$("$SCRIPT_DIR/bmad-voice-manager.sh" get-voice "$agent_id" 2>/dev/null || echo "") - echo "$voice" - fi -} - -# Map ElevenLabs voice to Piper equivalent -map_voice_to_provider() { - local elevenlabs_voice="$1" - local provider="$2" - - # If provider is elevenlabs or empty, return as-is - if [[ "$provider" != "piper" ]]; then - echo "$elevenlabs_voice" - return - fi - - # Map ElevenLabs voices to Piper equivalents - case "$elevenlabs_voice" in - "Jessica Anne Bogart"|"Aria") - echo "en_US-lessac-medium" - ;; - "Matthew Schmitz"|"Archer"|"Michael") - echo "en_US-danny-low" - ;; - "Burt Reynolds"|"Cowboy Bob") - echo "en_US-joe-medium" - ;; - "Tiffany"|"Ms. Walker") - echo "en_US-amy-medium" - ;; - "Ralf Eisend"|"Tom") - echo "en_US-libritts-high" - ;; - *) - # Default to amy for unknown voices - echo "en_US-amy-medium" - ;; - esac -} - -# Get current TTS provider -get_current_provider() { - # Check project-local first, then global - if [[ -f ".claude/tts-provider.txt" ]]; then - cat ".claude/tts-provider.txt" 2>/dev/null || echo "elevenlabs" - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - cat "$HOME/.claude/tts-provider.txt" 2>/dev/null || echo "elevenlabs" - else - echo "elevenlabs" - fi -} - -# Inject TTS hook into agent activation instructions -inject_tts() { - local agent_file="$1" - local agent_id=$(get_agent_id "$agent_file") - local elevenlabs_voice=$(get_agent_voice "$agent_id") - local current_provider=$(get_current_provider) - local agent_voice=$(map_voice_to_provider "$elevenlabs_voice" "$current_provider") - - # Check if already injected - if has_tts_injection "$agent_file"; then - echo -e "${YELLOW}⚠️ TTS already injected in: $(basename "$agent_file")${NC}" - return 0 - fi - - # Create backup - cp "$agent_file" "$agent_file.backup-pre-tts" - - # Find the activation-instructions section - if ! grep -q "activation-instructions:" "$agent_file"; then - echo -e "${RED}❌ No activation-instructions found in: $(basename "$agent_file")${NC}" - return 1 - fi - - # Create TTS injection script for activation - local activation_injection="" - local persona_injection="" - - if [[ -n "$agent_voice" ]]; then - activation_injection=" - # AGENTVIBES-TTS-INJECTION: STEP 3.5 - Announce activation with TTS and set context - - STEP 3.5a: Use Bash tool to create context file: echo \"${agent_id}\" > .bmad-agent-context - - STEP 3.5b: Use Bash tool to speak: .claude/hooks/play-tts.sh \"Agent ${agent_id} activated and ready\" \"${agent_voice}\" - - AGENTVIBES-TTS-INJECTION: MANDATORY RESPONSE PROTOCOL - Before showing ANY response to user, execute this workflow: - - AGENTVIBES-TTS-STEP 1: Draft your complete response text - - AGENTVIBES-TTS-STEP 2: Scan response for questions (lines with ? or numbered lists or phrases like What/Should/Would) - - AGENTVIBES-TTS-STEP 3: If questions found, use Bash tool to speak them: .claude/hooks/play-tts.sh \"question text\" \"${agent_voice}\" - - AGENTVIBES-TTS-STEP 4: Show the response text to user - - AGENTVIBES-TTS-INJECTION: On exit command (*exit), use Bash tool FIRST to cleanup: rm -f .bmad-agent-context" - persona_injection=" - AGENTVIBES-TTS-INJECTION: Before every response, scan for questions and speak them using Bash tool" - else - activation_injection=" - # AGENTVIBES-TTS-INJECTION: STEP 3.5 - Announce activation with TTS and set context - - STEP 3.5a: Use Bash tool to create context file: echo \"${agent_id}\" > .bmad-agent-context - - STEP 3.5b: Use Bash tool to speak: .claude/hooks/play-tts.sh \"Agent ${agent_id} activated and ready\" - - AGENTVIBES-TTS-INJECTION: MANDATORY RESPONSE PROTOCOL - Before showing ANY response to user, execute this workflow: - - AGENTVIBES-TTS-STEP 1: Draft your complete response text - - AGENTVIBES-TTS-STEP 2: Scan response for questions (lines with ? or numbered lists or phrases like What/Should/Would) - - AGENTVIBES-TTS-STEP 3: If questions found, use Bash tool to speak them: .claude/hooks/play-tts.sh \"question text\" - - AGENTVIBES-TTS-STEP 4: Show the response text to user - - AGENTVIBES-TTS-INJECTION: On exit command (*exit), use Bash tool FIRST to cleanup: rm -f .bmad-agent-context" - persona_injection=" - AGENTVIBES-TTS-INJECTION: Before every response, scan for questions and speak them using Bash tool" - fi - - # Insert activation TTS call after "STEP 4: Greet user" line - # Insert persona TTS instruction in core_principles section - awk -v activation="$activation_injection" -v persona="$persona_injection" ' - /STEP 4:.*[Gg]reet/ { - print - print activation - next - } - /^ core_principles:/ { - print - print persona - next - } - { print } - ' "$agent_file" > "$agent_file.tmp" - - mv "$agent_file.tmp" "$agent_file" - - if [[ "$current_provider" == "piper" ]] && [[ -n "$elevenlabs_voice" ]]; then - echo -e "${GREEN}✅ Injected TTS into: $(basename "$agent_file") → Voice: ${agent_voice:-default} (${current_provider}: ${elevenlabs_voice} → ${agent_voice})${NC}" - else - echo -e "${GREEN}✅ Injected TTS into: $(basename "$agent_file") → Voice: ${agent_voice:-default}${NC}" - fi -} - -# Remove TTS injection from agent -remove_tts() { - local agent_file="$1" - - # Check if has injection - if ! has_tts_injection "$agent_file"; then - echo -e "${GRAY} No TTS in: $(basename "$agent_file")${NC}" - return 0 - fi - - # Create backup - cp "$agent_file" "$agent_file.backup-tts-removal" - - # Remove TTS injection lines - sed -i.bak '/# AGENTVIBES-TTS-INJECTION/,+1d' "$agent_file" - rm -f "$agent_file.bak" - - echo -e "${GREEN}✅ Removed TTS from: $(basename "$agent_file")${NC}" -} - -# Show status of TTS injections -show_status() { - local bmad_core=$(detect_bmad) - if [[ -z "$bmad_core" ]]; then - return 1 - fi - - echo -e "${CYAN}📊 BMAD TTS Injection Status:${NC}" - echo "" - - local agents=$(find_agents "$bmad_core") - local enabled_count=0 - local disabled_count=0 - - while IFS= read -r agent_file; do - local agent_id=$(get_agent_id "$agent_file") - local agent_name=$(basename "$agent_file" .md) - - if has_tts_injection "$agent_file"; then - local voice=$(get_agent_voice "$agent_id") - echo -e " ${GREEN}✅${NC} $agent_name (${agent_id}) → Voice: ${voice:-default}" - ((enabled_count++)) - else - echo -e " ${GRAY}❌ $agent_name (${agent_id})${NC}" - ((disabled_count++)) - fi - done <<< "$agents" - - echo "" - echo -e "${CYAN}Summary:${NC} $enabled_count enabled, $disabled_count disabled" -} - -# Enable TTS for all agents -enable_all() { - local bmad_core=$(detect_bmad) - if [[ -z "$bmad_core" ]]; then - return 1 - fi - - echo -e "${CYAN}🎤 Enabling TTS for all BMAD agents...${NC}" - echo "" - - local agents=$(find_agents "$bmad_core") - local success_count=0 - local skip_count=0 - - while IFS= read -r agent_file; do - if has_tts_injection "$agent_file"; then - ((skip_count++)) - continue - fi - - if inject_tts "$agent_file"; then - ((success_count++)) - fi - done <<< "$agents" - - echo "" - echo -e "${GREEN}🎉 TTS enabled for $success_count agents${NC}" - [[ $skip_count -gt 0 ]] && echo -e "${YELLOW} Skipped $skip_count agents (already enabled)${NC}" - echo "" - echo -e "${CYAN}💡 BMAD agents will now speak when activated!${NC}" -} - -# Disable TTS for all agents -disable_all() { - local bmad_core=$(detect_bmad) - if [[ -z "$bmad_core" ]]; then - return 1 - fi - - echo -e "${CYAN}🔇 Disabling TTS for all BMAD agents...${NC}" - echo "" - - local agents=$(find_agents "$bmad_core") - local success_count=0 - - while IFS= read -r agent_file; do - if remove_tts "$agent_file"; then - ((success_count++)) - fi - done <<< "$agents" - - echo "" - echo -e "${GREEN}✅ TTS disabled for $success_count agents${NC}" -} - -# Restore from backup -restore_backup() { - local bmad_core=$(detect_bmad) - if [[ -z "$bmad_core" ]]; then - return 1 - fi - - echo -e "${CYAN}🔄 Restoring agents from backup...${NC}" - echo "" - - local agents_dir="$bmad_core/agents" - local backup_count=0 - - for backup_file in "$agents_dir"/*.backup-pre-tts; do - if [[ -f "$backup_file" ]]; then - local original_file="${backup_file%.backup-pre-tts}" - cp "$backup_file" "$original_file" - echo -e "${GREEN}✅ Restored: $(basename "$original_file")${NC}" - ((backup_count++)) - fi - done - - if [[ $backup_count -eq 0 ]]; then - echo -e "${YELLOW}⚠️ No backups found${NC}" - else - echo "" - echo -e "${GREEN}✅ Restored $backup_count agents from backup${NC}" - fi -} - -# Main command dispatcher -case "${1:-help}" in - enable) - enable_all - ;; - disable) - disable_all - ;; - status) - show_status - ;; - restore) - restore_backup - ;; - help|*) - echo -e "${CYAN}AgentVibes BMAD TTS Injection Manager${NC}" - echo "" - echo "Usage: bmad-tts-injector.sh {enable|disable|status|restore}" - echo "" - echo "Commands:" - echo " enable Inject TTS hooks into all BMAD agents" - echo " disable Remove TTS hooks from all BMAD agents" - echo " status Show TTS injection status for all agents" - echo " restore Restore agents from backup (undo changes)" - echo "" - echo "What it does:" - echo " • Automatically patches BMAD agent activation instructions" - echo " • Adds TTS calls when agents greet users" - echo " • Uses voice mapping from AgentVibes BMAD plugin" - echo " • Creates backups before modifying files" - ;; -esac diff --git a/.claude/hooks/bmad-voice-manager.sh b/.claude/hooks/bmad-voice-manager.sh deleted file mode 100755 index 22d12165..00000000 --- a/.claude/hooks/bmad-voice-manager.sh +++ /dev/null @@ -1,511 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/bmad-voice-manager.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview BMAD Voice Plugin Manager - Maps BMAD agents to unique TTS voices -# @context Enables each BMAD agent to have its own distinct voice for multi-agent sessions -# @architecture Markdown table-based voice mapping with enable/disable flag, auto-detection of BMAD -# @dependencies .claude/plugins/bmad-voices.md (voice mappings), bmad-tts-injector.sh, .bmad-core/ (BMAD installation) -# @entrypoints Called by /agent-vibes:bmad commands, auto-enabled on BMAD detection -# @patterns Plugin architecture, auto-enable on dependency detection, state backup/restore on toggle -# @related bmad-tts-injector.sh, .claude/plugins/bmad-voices.md, .bmad-agent-context file - -PLUGIN_DIR=".claude/plugins" -PLUGIN_FILE="$PLUGIN_DIR/bmad-voices.md" -ENABLED_FLAG="$PLUGIN_DIR/bmad-voices-enabled.flag" - -# AI NOTE: Auto-enable pattern - When BMAD is detected via .bmad-core/install-manifest.yaml, -# automatically enable the voice plugin to provide seamless multi-agent voice support. -# This avoids requiring manual plugin activation after BMAD installation. - -# @function auto_enable_if_bmad_detected -# @intent Automatically enable BMAD voice plugin when BMAD framework is detected -# @why Provide seamless integration - users shouldn't need to manually enable voice mapping -# @param None -# @returns None -# @exitcode 0=auto-enabled, 1=not enabled (already enabled or BMAD not detected) -# @sideeffects Creates enabled flag file, creates plugin directory -# @edgecases Only auto-enables if plugin not already enabled, silent operation -# @calledby get_agent_voice -# @calls mkdir, touch -auto_enable_if_bmad_detected() { - # Check if BMAD is installed - if [[ -f ".bmad-core/install-manifest.yaml" ]] && [[ ! -f "$ENABLED_FLAG" ]]; then - # BMAD detected but plugin not enabled - enable it silently - mkdir -p "$PLUGIN_DIR" - touch "$ENABLED_FLAG" - return 0 - fi - return 1 -} - -# @function get_agent_voice -# @intent Retrieve TTS voice assigned to specific BMAD agent -# @why Each BMAD agent needs unique voice for multi-agent conversation differentiation -# @param $1 {string} agent_id - BMAD agent identifier (pm, dev, qa, architect, etc.) -# @returns Echoes voice name to stdout, empty string if plugin disabled or agent not found -# @exitcode Always 0 -# @sideeffects May auto-enable plugin if BMAD detected -# @edgecases Returns empty string if plugin disabled/missing, parses markdown table syntax -# @calledby bmad-tts-injector.sh, play-tts.sh when BMAD agent is active -# @calls auto_enable_if_bmad_detected, grep, awk, sed -get_agent_voice() { - local agent_id="$1" - - # Auto-enable if BMAD is detected - auto_enable_if_bmad_detected - - if [[ ! -f "$ENABLED_FLAG" ]]; then - echo "" # Plugin disabled - return - fi - - if [[ ! -f "$PLUGIN_FILE" ]]; then - echo "" # Plugin file missing - return - fi - - # Extract voice from markdown table - local voice=$(grep "^| $agent_id " "$PLUGIN_FILE" | \ - awk -F'|' '{print $4}' | \ - sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - - echo "$voice" -} - -# @function get_agent_personality -# @intent Retrieve TTS personality assigned to specific BMAD agent -# @why Agents may have distinct speaking styles (friendly, professional, energetic, etc.) -# @param $1 {string} agent_id - BMAD agent identifier -# @returns Echoes personality name to stdout, empty string if not found -# @exitcode Always 0 -# @sideeffects None -# @edgecases Returns empty string if plugin file missing, parses column 5 of markdown table -# @calledby bmad-tts-injector.sh for personality-aware voice synthesis -# @calls grep, awk, sed -get_agent_personality() { - local agent_id="$1" - - if [[ ! -f "$PLUGIN_FILE" ]]; then - echo "" - return - fi - - local personality=$(grep "^| $agent_id " "$PLUGIN_FILE" | \ - awk -F'|' '{print $5}' | \ - sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - - echo "$personality" -} - -# @function is_plugin_enabled -# @intent Check if BMAD voice plugin is currently enabled -# @why Allow conditional logic based on plugin state -# @param None -# @returns Echoes "true" or "false" to stdout -# @exitcode Always 0 -# @sideeffects None -# @edgecases None -# @calledby show_status, enable_plugin, disable_plugin -# @calls None (file existence check) -is_plugin_enabled() { - [[ -f "$ENABLED_FLAG" ]] && echo "true" || echo "false" -} - -# @function enable_plugin -# @intent Enable BMAD voice plugin and backup current voice settings -# @why Allow users to switch to per-agent voices while preserving original configuration -# @param None -# @returns None -# @exitcode Always 0 -# @sideeffects Creates flag file, backs up current voice/personality/sentiment to .bmad-previous-settings -# @sideeffects Creates activation-instructions file for BMAD agents, calls bmad-tts-injector.sh -# @edgecases Handles missing settings files gracefully with defaults -# @calledby Main command dispatcher with "enable" argument -# @calls mkdir, cat, source, list_mappings, bmad-tts-injector.sh -enable_plugin() { - mkdir -p "$PLUGIN_DIR" - - # Save current settings before enabling - BACKUP_FILE="$PLUGIN_DIR/.bmad-previous-settings" - - # Save current voice - if [[ -f ".claude/tts-voice.txt" ]]; then - CURRENT_VOICE=$(cat .claude/tts-voice.txt 2>/dev/null) - elif [[ -f "$HOME/.claude/tts-voice.txt" ]]; then - CURRENT_VOICE=$(cat "$HOME/.claude/tts-voice.txt" 2>/dev/null) - else - CURRENT_VOICE="Aria" - fi - - # Save current personality - if [[ -f ".claude/tts-personality.txt" ]]; then - CURRENT_PERSONALITY=$(cat .claude/tts-personality.txt 2>/dev/null) - elif [[ -f "$HOME/.claude/tts-personality.txt" ]]; then - CURRENT_PERSONALITY=$(cat "$HOME/.claude/tts-personality.txt" 2>/dev/null) - else - CURRENT_PERSONALITY="normal" - fi - - # Save current sentiment - if [[ -f ".claude/tts-sentiment.txt" ]]; then - CURRENT_SENTIMENT=$(cat .claude/tts-sentiment.txt 2>/dev/null) - elif [[ -f "$HOME/.claude/tts-sentiment.txt" ]]; then - CURRENT_SENTIMENT=$(cat "$HOME/.claude/tts-sentiment.txt" 2>/dev/null) - else - CURRENT_SENTIMENT="" - fi - - # Write backup - cat > "$BACKUP_FILE" < "$ACTIVATION_FILE" <<'ACTIVATION_EOF' -# BMAD Agent Activation Instructions - -When a BMAD agent activates, follow these steps EXACTLY: - -## STEP 3.5a: Create BMAD Context File (CRITICAL) - -**IMMEDIATELY after agent identification, create the context file:** - -```bash -echo "$AGENT_ID" > .bmad-agent-context -``` - -Where `$AGENT_ID` is one of: -- `pm` (Product Manager) -- `dev` (Developer) -- `qa` (QA) -- `architect` (Architect) -- `po` (Product Owner) -- `analyst` (Analyst) -- `sm` (Scrum Master) -- `ux-expert` (UX Expert) -- `bmad-master` (BMAD Master) -- `bmad-orchestrator` (Orchestrator) - -**Example:** -```bash -# For Product Manager agent -echo "pm" > .bmad-agent-context -``` - -This file allows AgentVibes to: -1. Detect which BMAD agent is active -2. Look up the correct voice mapping -3. Automatically speak questions using the agent's assigned voice - -## STEP 10: Clean Up on Exit - -**Before exiting the agent, remove the context file:** - -```bash -rm -f .bmad-agent-context -``` - -This ensures voice switching only happens when an agent is active. - -## Why This Matters - -Without the `.bmad-agent-context` file: -- AgentVibes cannot detect which agent is active -- Questions won't be spoken automatically -- Voice switching won't work -- The BMAD voice plugin becomes non-functional - -**This is MANDATORY for BMAD voice integration to work!** -ACTIVATION_EOF - echo "📝 Created activation instructions: $ACTIVATION_FILE" - fi - - echo "✅ BMAD voice plugin enabled" - echo "💾 Previous settings backed up:" - echo " Voice: $CURRENT_VOICE" - echo " Personality: $CURRENT_PERSONALITY" - [[ -n "$CURRENT_SENTIMENT" ]] && echo " Sentiment: $CURRENT_SENTIMENT" - echo "" - list_mappings - - # Automatically inject TTS into BMAD agents - echo "" - echo "🎤 Automatically enabling TTS for BMAD agents..." - echo "" - - # Get the directory where this script is located - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - - # Check if bmad-tts-injector.sh exists - if [[ -f "$SCRIPT_DIR/bmad-tts-injector.sh" ]]; then - # Run the TTS injector - "$SCRIPT_DIR/bmad-tts-injector.sh" enable - else - echo "⚠️ TTS injector not found at: $SCRIPT_DIR/bmad-tts-injector.sh" - echo " You can manually enable TTS with: /agent-vibes:bmad-tts enable" - fi -} - -# @function disable_plugin -# @intent Disable BMAD voice plugin and restore previous voice settings -# @why Allow users to return to single-voice mode with their original configuration -# @param None -# @returns None -# @exitcode Always 0 -# @sideeffects Removes flag file, restores settings from backup, calls bmad-tts-injector.sh disable -# @edgecases Handles missing backup file gracefully, warns user if no backup exists -# @calledby Main command dispatcher with "disable" argument -# @calls source, rm, echo, bmad-tts-injector.sh -disable_plugin() { - BACKUP_FILE="$PLUGIN_DIR/.bmad-previous-settings" - - # Check if we have a backup to restore - if [[ -f "$BACKUP_FILE" ]]; then - source "$BACKUP_FILE" - - echo "❌ BMAD voice plugin disabled" - echo "🔄 Restoring previous settings:" - echo " Voice: $VOICE" - echo " Personality: $PERSONALITY" - [[ -n "$SENTIMENT" ]] && echo " Sentiment: $SENTIMENT" - - # Restore voice - if [[ -n "$VOICE" ]]; then - echo "$VOICE" > .claude/tts-voice.txt 2>/dev/null || echo "$VOICE" > "$HOME/.claude/tts-voice.txt" - fi - - # Restore personality - if [[ -n "$PERSONALITY" ]] && [[ "$PERSONALITY" != "normal" ]]; then - echo "$PERSONALITY" > .claude/tts-personality.txt 2>/dev/null || echo "$PERSONALITY" > "$HOME/.claude/tts-personality.txt" - fi - - # Restore sentiment - if [[ -n "$SENTIMENT" ]]; then - echo "$SENTIMENT" > .claude/tts-sentiment.txt 2>/dev/null || echo "$SENTIMENT" > "$HOME/.claude/tts-sentiment.txt" - fi - - # Clean up backup - rm -f "$BACKUP_FILE" - else - echo "❌ BMAD voice plugin disabled" - echo "⚠️ No previous settings found to restore" - echo "AgentVibes will use current voice/personality settings" - fi - - rm -f "$ENABLED_FLAG" - - # Automatically remove TTS from BMAD agents - echo "" - echo "🔇 Automatically disabling TTS for BMAD agents..." - echo "" - - # Get the directory where this script is located - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - - # Check if bmad-tts-injector.sh exists - if [[ -f "$SCRIPT_DIR/bmad-tts-injector.sh" ]]; then - # Run the TTS injector disable - "$SCRIPT_DIR/bmad-tts-injector.sh" disable - else - echo "⚠️ TTS injector not found" - echo " You can manually disable TTS with: /agent-vibes:bmad-tts disable" - fi -} - -# @function list_mappings -# @intent Display all BMAD agent-to-voice mappings in readable format -# @why Help users see which voice is assigned to each agent -# @param None -# @returns None -# @exitcode 0=success, 1=plugin file not found -# @sideeffects Writes formatted output to stdout -# @edgecases Parses markdown table format, skips header and separator rows -# @calledby enable_plugin, show_status, main command dispatcher with "list" -# @calls grep, sed, echo -list_mappings() { - if [[ ! -f "$PLUGIN_FILE" ]]; then - echo "❌ Plugin file not found: $PLUGIN_FILE" - return 1 - fi - - echo "📊 BMAD Agent Voice Mappings:" - echo "" - - grep "^| " "$PLUGIN_FILE" | grep -v "Agent ID" | grep -v "^|---" | \ - while IFS='|' read -r _ agent_id name voice personality _; do - agent_id=$(echo "$agent_id" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - name=$(echo "$name" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - voice=$(echo "$voice" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - personality=$(echo "$personality" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') - - [[ -n "$agent_id" ]] && echo " $agent_id → $voice [$personality]" - done -} - -# @function set_agent_voice -# @intent Update voice and personality mapping for specific BMAD agent -# @why Allow customization of agent voices to user preferences -# @param $1 {string} agent_id - BMAD agent identifier -# @param $2 {string} voice - New voice name -# @param $3 {string} personality - New personality (optional, defaults to "normal") -# @returns None -# @exitcode 0=success, 1=plugin file not found or agent not found -# @sideeffects Modifies plugin file, creates .bak backup -# @edgecases Validates agent exists before updating -# @calledby Main command dispatcher with "set" argument -# @calls grep, sed -set_agent_voice() { - local agent_id="$1" - local voice="$2" - local personality="${3:-normal}" - - if [[ ! -f "$PLUGIN_FILE" ]]; then - echo "❌ Plugin file not found: $PLUGIN_FILE" - return 1 - fi - - # Check if agent exists - if ! grep -q "^| $agent_id " "$PLUGIN_FILE"; then - echo "❌ Agent '$agent_id' not found in plugin" - return 1 - fi - - # Update the voice and personality in the table - sed -i.bak "s/^| $agent_id |.*| .* | .* |$/| $agent_id | $(grep "^| $agent_id " "$PLUGIN_FILE" | awk -F'|' '{print $3}') | $voice | $personality |/" "$PLUGIN_FILE" - - echo "✅ Updated $agent_id → $voice [$personality]" -} - -# @function show_status -# @intent Display plugin status, BMAD detection, and current voice mappings -# @why Provide comprehensive overview of plugin state for troubleshooting -# @param None -# @returns None -# @exitcode Always 0 -# @sideeffects Writes status information to stdout -# @edgecases Checks for BMAD installation via manifest file -# @calledby Main command dispatcher with "status" argument -# @calls is_plugin_enabled, list_mappings -show_status() { - # Check for BMAD installation - local bmad_installed="false" - if [[ -f ".bmad-core/install-manifest.yaml" ]]; then - bmad_installed="true" - fi - - if [[ $(is_plugin_enabled) == "true" ]]; then - echo "✅ BMAD voice plugin: ENABLED" - if [[ "$bmad_installed" == "true" ]]; then - echo "🔍 BMAD detected: Auto-enabled" - fi - else - echo "❌ BMAD voice plugin: DISABLED" - if [[ "$bmad_installed" == "true" ]]; then - echo "⚠️ BMAD detected but plugin disabled (enable with: /agent-vibes-bmad enable)" - fi - fi - echo "" - list_mappings -} - -# @function edit_plugin -# @intent Open plugin configuration file for manual editing -# @why Allow advanced users to modify voice mappings directly -# @param None -# @returns None -# @exitcode 0=success, 1=plugin file not found -# @sideeffects Displays file path and instructions -# @edgecases Does not actually open editor, just provides guidance -# @calledby Main command dispatcher with "edit" argument -# @calls echo -edit_plugin() { - if [[ ! -f "$PLUGIN_FILE" ]]; then - echo "❌ Plugin file not found: $PLUGIN_FILE" - return 1 - fi - - echo "Opening $PLUGIN_FILE for editing..." - echo "Edit the markdown table to change voice mappings" -} - -# Main command dispatcher -case "${1:-help}" in - enable) - enable_plugin - ;; - disable) - disable_plugin - ;; - status) - show_status - ;; - list) - list_mappings - ;; - set) - if [[ -z "$2" ]] || [[ -z "$3" ]]; then - echo "Usage: bmad-voice-manager.sh set [personality]" - exit 1 - fi - set_agent_voice "$2" "$3" "$4" - ;; - get-voice) - get_agent_voice "$2" - ;; - get-personality) - get_agent_personality "$2" - ;; - edit) - edit_plugin - ;; - *) - echo "Usage: bmad-voice-manager.sh {enable|disable|status|list|set|get-voice|get-personality|edit}" - echo "" - echo "Commands:" - echo " enable Enable BMAD voice plugin" - echo " disable Disable BMAD voice plugin" - echo " status Show plugin status and mappings" - echo " list List all agent voice mappings" - echo " set Set voice for agent" - echo " get-voice Get voice for agent" - echo " get-personality Get personality for agent" - echo " edit Edit plugin configuration" - exit 1 - ;; -esac diff --git a/.claude/hooks/check-output-style.sh b/.claude/hooks/check-output-style.sh deleted file mode 100755 index 20631697..00000000 --- a/.claude/hooks/check-output-style.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/check-output-style.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview Output Style Detection - Detects if Agent Vibes output style is active in Claude Code -# @context Voice commands require the Agent Vibes output style to work properly with automatic TTS -# @architecture Heuristic detection using environment variables and file system checks -# @dependencies CLAUDECODE environment variable, .claude/output-styles/agent-vibes.md file -# @entrypoints Called by slash commands to warn users if output style is incorrect -# @patterns Environment-based detection, graceful degradation with helpful error messages -# @related .claude/output-styles/agent-vibes.md, Claude Code output style system - -# AI NOTE: Output style detection is heuristic-based because Claude Code does not expose -# the active output style via environment variables. We check for CLAUDECODE env var and -# the presence of the agent-vibes.md output style file as indicators. - -# @function check_output_style -# @intent Detect if Agent Vibes output style is likely active in Claude Code session -# @why Voice commands depend on output style hooks that automatically invoke TTS -# @param None -# @returns None -# @exitcode 0=likely using agent-vibes style, 1=not using or cannot detect -# @sideeffects None (read-only checks) -# @edgecases Cannot directly detect output style, relies on CLAUDECODE env var and file presence -# @calledby Main execution block, slash command validation -# @calls None (direct environment and file checks) -check_output_style() { - # Strategy: Check if this script is being called from within a Claude response - # If CLAUDECODE env var is set, we're in Claude Code - # If not, we're running standalone (not in a Claude Code session) - - if [[ -z "$CLAUDECODE" ]]; then - # Not in Claude Code at all - return 1 - fi - - # We're in Claude Code, but we can't directly detect output style - # The agent-vibes output style calls our TTS hooks automatically - # So if this function is called, it means a slash command was invoked - - # Check if we have the necessary TTS setup - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - - # Check if agent-vibes output style is installed - if [[ ! -f "$SCRIPT_DIR/../output-styles/agent-vibes.md" ]]; then - return 1 - fi - - # All checks passed - likely using agent-vibes output style - return 0 -} - -# @function show_output_style_warning -# @intent Display helpful warning about enabling Agent Vibes output style -# @why Users need guidance on how to enable automatic TTS narration -# @param None -# @returns None -# @exitcode Always 0 -# @sideeffects Writes warning message to stdout -# @edgecases None -# @calledby Main execution block when check_output_style fails -# @calls echo -show_output_style_warning() { - echo "" - echo "⚠️ Voice commands require the Agent Vibes output style" - echo "" - echo "To enable voice narration, run:" - echo " /output-style Agent Vibes" - echo "" - echo "This will make Claude speak with TTS for all responses." - echo "You can still use voice commands manually with agent-vibes disabled," - echo "but you won't hear automatic TTS narration." - echo "" -} - -# Main execution when called directly -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - if ! check_output_style; then - show_output_style_warning - exit 1 - fi - exit 0 -fi diff --git a/.claude/hooks/download-extra-voices.sh b/.claude/hooks/download-extra-voices.sh deleted file mode 100755 index 6e3a992c..00000000 --- a/.claude/hooks/download-extra-voices.sh +++ /dev/null @@ -1,244 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/download-extra-voices.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview Extra Piper Voice Downloader - Downloads custom high-quality voices from HuggingFace -# @context Post-installation utility to download premium custom voices (Kristin, Jenny, Tracy/16Speakers) -# @architecture Downloads ONNX voice models from agentvibes/piper-custom-voices HuggingFace repository -# @dependencies curl (downloads), piper-voice-manager.sh (storage dir logic) -# @entrypoints Called by MCP server download_extra_voices tool or manually -# @patterns Batch downloads, skip-existing logic, auto-yes flag for non-interactive use -# @related piper-voice-manager.sh, mcp-server/server.py, docs/huggingface-setup-guide.md -# - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/piper-voice-manager.sh" - -# Parse command line arguments -AUTO_YES=false -if [[ "$1" == "--yes" ]] || [[ "$1" == "-y" ]]; then - AUTO_YES=true -fi - -# HuggingFace repository for custom voices -HUGGINGFACE_REPO="agentvibes/piper-custom-voices" -HUGGINGFACE_BASE_URL="https://huggingface.co/${HUGGINGFACE_REPO}/resolve/main" - -# Extra custom voices to download -EXTRA_VOICES=( - "kristin:Kristin (US English female, Public Domain, 64MB)" - "jenny:Jenny (UK English female with Irish accent, CC BY, 64MB)" - "16Speakers:Tracy/16Speakers (Multi-speaker: 12 US + 4 UK voices, Public Domain, 77MB)" -) - -echo "🎙️ AgentVibes Extra Voice Downloader" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "" -echo "This will download high-quality custom Piper voices from HuggingFace." -echo "" -echo "📦 Voices available:" -for voice_info in "${EXTRA_VOICES[@]}"; do - voice_name="${voice_info%%:*}" - voice_desc="${voice_info#*:}" - echo " • $voice_desc" -done -echo "" - -# Check if piper is installed -if ! command -v piper &> /dev/null; then - echo "❌ Error: Piper TTS not installed" - echo "Install with: pipx install piper-tts" - exit 1 -fi - -# Get storage directory -VOICE_DIR=$(get_voice_storage_dir) -echo "📂 Storage location: $VOICE_DIR" -echo "" - -# Count already downloaded -ALREADY_DOWNLOADED=0 -ALREADY_DOWNLOADED_LIST=() -NEED_DOWNLOAD=() - -for voice_info in "${EXTRA_VOICES[@]}"; do - voice_name="${voice_info%%:*}" - voice_desc="${voice_info#*:}" - - # Check if both .onnx and .onnx.json exist - if [[ -f "$VOICE_DIR/${voice_name}.onnx" ]] && [[ -f "$VOICE_DIR/${voice_name}.onnx.json" ]]; then - ((ALREADY_DOWNLOADED++)) - ALREADY_DOWNLOADED_LIST+=("$voice_desc") - else - NEED_DOWNLOAD+=("$voice_info") - fi -done - -echo "📊 Status:" -echo " Already downloaded: $ALREADY_DOWNLOADED voice(s)" -echo " Need to download: ${#NEED_DOWNLOAD[@]} voice(s)" -echo "" - -# Show already downloaded voices -if [[ $ALREADY_DOWNLOADED -gt 0 ]]; then - echo "✅ Already downloaded (skipped):" - for voice_desc in "${ALREADY_DOWNLOADED_LIST[@]}"; do - echo " ✓ $voice_desc" - done - echo "" -fi - -if [[ ${#NEED_DOWNLOAD[@]} -eq 0 ]]; then - echo "🎉 All extra voices already downloaded!" - exit 0 -fi - -echo "Voices to download:" -for voice_info in "${NEED_DOWNLOAD[@]}"; do - voice_desc="${voice_info#*:}" - echo " • $voice_desc" -done -echo "" - -# Calculate total size -TOTAL_SIZE_MB=0 -for voice_info in "${NEED_DOWNLOAD[@]}"; do - voice_desc="${voice_info#*:}" - if [[ "$voice_desc" =~ ([0-9]+)MB ]]; then - TOTAL_SIZE_MB=$((TOTAL_SIZE_MB + ${BASH_REMATCH[1]})) - fi -done - -echo "💾 Total download size: ~${TOTAL_SIZE_MB}MB" -echo "" - -# Ask for confirmation (skip if --yes flag provided) -if [[ "$AUTO_YES" == "false" ]]; then - read -p "Download ${#NEED_DOWNLOAD[@]} extra voice(s)? [Y/n]: " -n 1 -r - echo - - if [[ ! $REPLY =~ ^[Yy]$ ]] && [[ -n $REPLY ]]; then - echo "❌ Download cancelled" - exit 0 - fi -else - echo "Auto-downloading ${#NEED_DOWNLOAD[@]} extra voice(s)..." - echo "" -fi - -# Create voice directory if it doesn't exist -mkdir -p "$VOICE_DIR" - -# Download function -download_voice_file() { - local url="$1" - local output_path="$2" - local file_name="$3" - - echo " 📥 Downloading $file_name..." - - if curl -L --progress-bar "$url" -o "$output_path" 2>&1; then - echo " ✅ Downloaded: $file_name" - return 0 - else - echo " ❌ Failed to download: $file_name" - return 1 - fi -} - -# Download each voice -DOWNLOADED=0 -FAILED=0 - -for voice_info in "${NEED_DOWNLOAD[@]}"; do - voice_name="${voice_info%%:*}" - voice_desc="${voice_info#*:}" - - echo "" - echo "📥 Downloading: ${voice_desc%%,*}..." - echo "" - - # Download .onnx file - onnx_url="${HUGGINGFACE_BASE_URL}/${voice_name}.onnx" - onnx_path="${VOICE_DIR}/${voice_name}.onnx" - - # Download .onnx.json file - json_url="${HUGGINGFACE_BASE_URL}/${voice_name}.onnx.json" - json_path="${VOICE_DIR}/${voice_name}.onnx.json" - - success=true - - if ! download_voice_file "$onnx_url" "$onnx_path" "${voice_name}.onnx"; then - success=false - fi - - if ! download_voice_file "$json_url" "$json_path" "${voice_name}.onnx.json"; then - success=false - fi - - if [[ "$success" == "true" ]]; then - ((DOWNLOADED++)) - echo "" - echo "✅ Successfully downloaded: ${voice_desc%%,*}" - else - ((FAILED++)) - echo "" - echo "❌ Failed to download: ${voice_desc%%,*}" - # Clean up partial downloads - rm -f "$onnx_path" "$json_path" - fi -done - -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "📊 Download Summary:" -echo " ✅ Successfully downloaded: $DOWNLOADED" -echo " ❌ Failed: $FAILED" -echo " 📦 Total extra voices available: $((ALREADY_DOWNLOADED + DOWNLOADED))" -echo "" - -if [[ $DOWNLOADED -gt 0 ]]; then - echo "✨ Extra voices ready to use!" - echo "" - echo "Try them:" - echo " /agent-vibes:provider switch piper" - echo " /agent-vibes:switch kristin" - echo " /agent-vibes:switch jenny" - echo " /agent-vibes:switch 16Speakers" -fi - -# Return success if at least one voice was downloaded or all were already present -if [[ $DOWNLOADED -gt 0 ]] || [[ $ALREADY_DOWNLOADED -gt 0 ]]; then - exit 0 -else - exit 1 -fi diff --git a/.claude/hooks/github-star-reminder.sh b/.claude/hooks/github-star-reminder.sh deleted file mode 100755 index 5179a7ed..00000000 --- a/.claude/hooks/github-star-reminder.sh +++ /dev/null @@ -1,154 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/github-star-reminder.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview GitHub Star Reminder System - Gentle daily reminder to star repository -# @context Shows a once-per-day reminder to encourage users to support the project without being annoying -# @architecture Timestamp-based tracking using daily date comparison in a state file -# @dependencies date command for timestamp generation -# @entrypoints Called by play-tts.sh router on every TTS execution, sourced or executed directly -# @patterns Rate-limiting via file-based state, graceful degradation, user-opt-out support -# @related .claude/github-star-reminder.txt (state file), .claude/github-star-reminder-disabled.flag (opt-out) - -# Determine config directory (project-local or global) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CLAUDE_DIR="$(dirname "$SCRIPT_DIR")" - -# Check if we have a project-local .claude directory -if [[ -d "$CLAUDE_DIR" ]] && [[ "$CLAUDE_DIR" != "$HOME/.claude" ]]; then - REMINDER_FILE="$CLAUDE_DIR/github-star-reminder.txt" -else - REMINDER_FILE="$HOME/.claude/github-star-reminder.txt" - mkdir -p "$HOME/.claude" -fi - -GITHUB_REPO="https://github.com/paulpreibisch/AgentVibes" - -# @function is_reminder_disabled -# @intent Check if GitHub star reminders have been disabled by the user -# @why Respect user preferences and provide opt-out mechanism for reminders -# @param None -# @returns None -# @exitcode 0=reminders disabled, 1=reminders enabled -# @sideeffects Reads flag files from local/global .claude directories -# @edgecases Checks both flag file and "disabled" text in reminder file for backward compatibility -# @calledby should_show_reminder -# @calls cat for reading reminder file content -is_reminder_disabled() { - # Check for disable flag file - local disable_file_local="$CLAUDE_DIR/github-star-reminder-disabled.flag" - local disable_file_global="$HOME/.claude/github-star-reminder-disabled.flag" - - if [[ -f "$disable_file_local" ]] || [[ -f "$disable_file_global" ]]; then - return 0 # Disabled - fi - - # Check if reminder file contains "disabled" - if [[ -f "$REMINDER_FILE" ]]; then - local content=$(cat "$REMINDER_FILE" 2>/dev/null) - if [[ "$content" == "disabled" ]]; then - return 0 # Disabled - fi - fi - - return 1 # Not disabled -} - -# @function should_show_reminder -# @intent Determine if reminder should be displayed based on date and disable status -# @why Implement once-per-day rate limiting to avoid annoying users -# @param None -# @returns None -# @exitcode 0=should show, 1=should not show -# @sideeffects Reads .claude/github-star-reminder.txt for last reminder date -# @edgecases Shows reminder if file doesn't exist (first run), compares YYYYMMDD format dates -# @calledby Main execution block -# @calls is_reminder_disabled, cat, date -should_show_reminder() { - # Check if disabled first - if is_reminder_disabled; then - return 1 - fi - - # If no reminder file exists, show it - if [[ ! -f "$REMINDER_FILE" ]]; then - return 0 - fi - - # Read last reminder date - LAST_REMINDER=$(cat "$REMINDER_FILE" 2>/dev/null || echo "0") - CURRENT_DATE=$(date +%Y%m%d) - - # Show reminder if it's a new day - if [[ "$LAST_REMINDER" != "$CURRENT_DATE" ]]; then - return 0 - fi - - return 1 -} - -# @function show_reminder -# @intent Display friendly GitHub star reminder with opt-out instructions -# @why Encourage community support while being respectful and non-intrusive -# @param None -# @returns None -# @exitcode Always 0 -# @sideeffects Updates reminder file with current date, writes to stdout -# @edgecases None -# @calledby Main execution block when should_show_reminder returns true -# @calls date, echo -show_reminder() { - echo "" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "⭐ Enjoying AgentVibes?" - echo "" - echo " If you find this project helpful, please consider giving us" - echo " a star on GitHub! It helps others discover AgentVibes and" - echo " motivates us to keep improving it." - echo "" - echo " 👉 $GITHUB_REPO" - echo "" - echo " Thank you for your support! 🙏" - echo "" - echo " 💡 To disable these reminders, run:" - echo " echo \"disabled\" > $REMINDER_FILE" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - - # Update the reminder file with today's date - date +%Y%m%d > "$REMINDER_FILE" -} - -# Main execution -if should_show_reminder; then - show_reminder -fi diff --git a/.claude/hooks/language-manager.sh b/.claude/hooks/language-manager.sh deleted file mode 100755 index 30d8167c..00000000 --- a/.claude/hooks/language-manager.sh +++ /dev/null @@ -1,392 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/language-manager.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied. Use at your own risk. See the Apache License for details. -# -# --- -# -# @fileoverview Language Manager - Manages multilingual TTS with 30+ language support -# @context Enables TTS in multiple languages with provider-specific voice recommendations (ElevenLabs multilingual vs Piper native) -# @architecture Dual-map system: ELEVENLABS_VOICES and PIPER_VOICES for provider-aware voice selection -# @dependencies provider-manager.sh for active provider detection, .claude/tts-language.txt for state -# @entrypoints Called by /agent-vibes:language commands, play-tts-*.sh for voice resolution -# @patterns Provider abstraction, language-to-voice mapping, backward compatibility with legacy LANGUAGE_VOICES -# @related play-tts-elevenlabs.sh, play-tts-piper.sh, provider-manager.sh, learn-manager.sh - -# Determine target .claude directory based on context -# Priority: -# 1. CLAUDE_PROJECT_DIR env var (set by MCP for project-specific settings) -# 2. Script location (for direct slash command usage) -# 3. Global ~/.claude (fallback) - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then - # MCP context: Use the project directory where MCP was invoked - CLAUDE_DIR="$CLAUDE_PROJECT_DIR/.claude" -else - # Direct usage context: Use script location - CLAUDE_DIR="$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)" - - # If script is in global ~/.claude, use that - if [[ "$CLAUDE_DIR" == "$HOME/.claude" ]]; then - CLAUDE_DIR="$HOME/.claude" - elif [[ ! -d "$CLAUDE_DIR" ]]; then - # Fallback to global if directory doesn't exist - CLAUDE_DIR="$HOME/.claude" - fi -fi - -LANGUAGE_FILE="$CLAUDE_DIR/tts-language.txt" -mkdir -p "$CLAUDE_DIR" - -# Source provider manager to detect active provider -source "$SCRIPT_DIR/provider-manager.sh" 2>/dev/null || true - -# Language to ElevenLabs multilingual voice mapping -declare -A ELEVENLABS_VOICES=( - ["spanish"]="Antoni" - ["french"]="Rachel" - ["german"]="Domi" - ["italian"]="Bella" - ["portuguese"]="Matilda" - ["chinese"]="Antoni" - ["japanese"]="Antoni" - ["korean"]="Antoni" - ["russian"]="Domi" - ["polish"]="Antoni" - ["dutch"]="Rachel" - ["turkish"]="Antoni" - ["arabic"]="Antoni" - ["hindi"]="Antoni" - ["swedish"]="Rachel" - ["danish"]="Rachel" - ["norwegian"]="Rachel" - ["finnish"]="Rachel" - ["czech"]="Domi" - ["romanian"]="Rachel" - ["ukrainian"]="Domi" - ["greek"]="Antoni" - ["bulgarian"]="Domi" - ["croatian"]="Domi" - ["slovak"]="Domi" -) - -# Language to Piper voice model mapping -declare -A PIPER_VOICES=( - ["spanish"]="es_ES-davefx-medium" - ["french"]="fr_FR-siwis-medium" - ["german"]="de_DE-thorsten-medium" - ["italian"]="it_IT-riccardo-x_low" - ["portuguese"]="pt_BR-faber-medium" - ["chinese"]="zh_CN-huayan-medium" - ["japanese"]="ja_JP-hikari-medium" - ["korean"]="ko_KR-eunyoung-medium" - ["russian"]="ru_RU-dmitri-medium" - ["polish"]="pl_PL-darkman-medium" - ["dutch"]="nl_NL-rdh-medium" - ["turkish"]="tr_TR-dfki-medium" - ["arabic"]="ar_JO-kareem-medium" - ["hindi"]="hi_IN-amitabh-medium" - ["swedish"]="sv_SE-nst-medium" - ["danish"]="da_DK-talesyntese-medium" - ["norwegian"]="no_NO-talesyntese-medium" - ["finnish"]="fi_FI-harri-medium" - ["czech"]="cs_CZ-jirka-medium" - ["romanian"]="ro_RO-mihai-medium" - ["ukrainian"]="uk_UA-lada-x_low" - ["greek"]="el_GR-rapunzelina-low" - ["bulgarian"]="bg_BG-valentin-medium" - ["croatian"]="hr_HR-gorana-medium" - ["slovak"]="sk_SK-lili-medium" -) - -# Backward compatibility: Keep LANGUAGE_VOICES for existing code -declare -A LANGUAGE_VOICES=( - ["spanish"]="Antoni" - ["french"]="Rachel" - ["german"]="Domi" - ["italian"]="Bella" - ["portuguese"]="Matilda" - ["chinese"]="Antoni" - ["japanese"]="Antoni" - ["korean"]="Antoni" - ["russian"]="Domi" - ["polish"]="Antoni" - ["dutch"]="Rachel" - ["turkish"]="Antoni" - ["arabic"]="Antoni" - ["hindi"]="Antoni" - ["swedish"]="Rachel" - ["danish"]="Rachel" - ["norwegian"]="Rachel" - ["finnish"]="Rachel" - ["czech"]="Domi" - ["romanian"]="Rachel" - ["ukrainian"]="Domi" - ["greek"]="Antoni" - ["bulgarian"]="Domi" - ["croatian"]="Domi" - ["slovak"]="Domi" -) - -# Supported languages list -SUPPORTED_LANGUAGES="spanish, french, german, italian, portuguese, chinese, japanese, korean, polish, dutch, turkish, russian, arabic, hindi, swedish, danish, norwegian, finnish, czech, romanian, ukrainian, greek, bulgarian, croatian, slovak" - -# Function to set language -set_language() { - local lang="$1" - - # Convert to lowercase - lang=$(echo "$lang" | tr '[:upper:]' '[:lower:]') - - # Handle reset/english - if [[ "$lang" == "reset" ]] || [[ "$lang" == "english" ]] || [[ "$lang" == "en" ]]; then - if [[ -f "$LANGUAGE_FILE" ]]; then - rm "$LANGUAGE_FILE" - echo "✓ Language reset to English (default)" - else - echo "Already using English (default)" - fi - return 0 - fi - - # Check if language is supported - if [[ ! " ${!LANGUAGE_VOICES[@]} " =~ " ${lang} " ]]; then - echo "❌ Language '$lang' not supported" - echo "" - echo "Supported languages:" - echo "$SUPPORTED_LANGUAGES" - return 1 - fi - - # Save language - echo "$lang" > "$LANGUAGE_FILE" - - # Detect active provider and get recommended voice - local provider="" - if [[ -f "$CLAUDE_DIR/tts-provider.txt" ]]; then - provider=$(cat "$CLAUDE_DIR/tts-provider.txt") - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - provider=$(cat "$HOME/.claude/tts-provider.txt") - else - provider="elevenlabs" - fi - - local recommended_voice=$(get_voice_for_language "$lang" "$provider") - - # Fallback to old mapping if provider-aware function returns empty - if [[ -z "$recommended_voice" ]]; then - recommended_voice="${LANGUAGE_VOICES[$lang]}" - fi - - echo "✓ Language set to: $lang" - echo "📢 Recommended voice for $provider TTS: $recommended_voice" - echo "" - echo "TTS will now speak in $lang." - echo "Switch voice with: /agent-vibes:switch \"$recommended_voice\"" -} - -# Function to get current language -get_language() { - if [[ -f "$LANGUAGE_FILE" ]]; then - local lang=$(cat "$LANGUAGE_FILE") - - # Detect active provider - local provider="" - if [[ -f "$CLAUDE_DIR/tts-provider.txt" ]]; then - provider=$(cat "$CLAUDE_DIR/tts-provider.txt") - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - provider=$(cat "$HOME/.claude/tts-provider.txt") - else - provider="elevenlabs" - fi - - local recommended_voice=$(get_voice_for_language "$lang" "$provider") - - # Fallback to old mapping - if [[ -z "$recommended_voice" ]]; then - recommended_voice="${LANGUAGE_VOICES[$lang]}" - fi - - echo "Current language: $lang" - echo "Recommended voice ($provider): $recommended_voice" - else - echo "Current language: english (default)" - echo "No multilingual voice required" - fi -} - -# Function to get language for use in other scripts -get_language_code() { - if [[ -f "$LANGUAGE_FILE" ]]; then - cat "$LANGUAGE_FILE" - else - echo "english" - fi -} - -# Function to check if current voice supports language -is_voice_multilingual() { - local voice="$1" - - # List of multilingual voices - local multilingual_voices=("Antoni" "Rachel" "Domi" "Bella" "Charlotte" "Matilda") - - for mv in "${multilingual_voices[@]}"; do - if [[ "$voice" == "$mv" ]]; then - return 0 - fi - done - - return 1 -} - -# Function to get best voice for current language -get_best_voice_for_language() { - local lang=$(get_language_code) - - if [[ "$lang" == "english" ]]; then - # No specific multilingual voice needed for English - echo "" - return - fi - - # Return recommended voice for language - echo "${LANGUAGE_VOICES[$lang]}" -} - -# Function to get voice for a specific language and provider -# Usage: get_voice_for_language [provider] -# Provider: "elevenlabs" or "piper" (auto-detected if not provided) -get_voice_for_language() { - local language="$1" - local provider="${2:-}" - - # Convert to lowercase - language=$(echo "$language" | tr '[:upper:]' '[:lower:]') - - # Auto-detect provider if not specified - if [[ -z "$provider" ]]; then - if command -v get_active_provider &>/dev/null; then - provider=$(get_active_provider 2>/dev/null) - else - # Fallback to checking provider file directly - # Try current directory first, then search up the tree - local search_dir="$PWD" - local found=false - - while [[ "$search_dir" != "/" ]]; do - if [[ -f "$search_dir/.claude/tts-provider.txt" ]]; then - provider=$(cat "$search_dir/.claude/tts-provider.txt") - found=true - break - fi - search_dir=$(dirname "$search_dir") - done - - # If not found in project tree, check global - if [[ "$found" = false ]]; then - if [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - provider=$(cat "$HOME/.claude/tts-provider.txt") - else - provider="elevenlabs" # Default - fi - fi - fi - fi - - # Return appropriate voice based on provider - case "$provider" in - piper) - echo "${PIPER_VOICES[$language]:-}" - ;; - elevenlabs) - echo "${ELEVENLABS_VOICES[$language]:-}" - ;; - *) - echo "${ELEVENLABS_VOICES[$language]:-}" - ;; - esac -} - -# Main command handler - only run if script is executed directly, not sourced -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - case "${1:-}" in - set) - if [[ -z "$2" ]]; then - echo "Usage: language-manager.sh set " - exit 1 - fi - set_language "$2" - ;; - get) - get_language - ;; - code) - get_language_code - ;; - check-voice) - if [[ -z "$2" ]]; then - echo "Usage: language-manager.sh check-voice " - exit 1 - fi - if is_voice_multilingual "$2"; then - echo "yes" - else - echo "no" - fi - ;; - best-voice) - get_best_voice_for_language - ;; - voice-for-language) - if [[ -z "$2" ]]; then - echo "Usage: language-manager.sh voice-for-language [provider]" - exit 1 - fi - get_voice_for_language "$2" "$3" - ;; - list) - echo "Supported languages and recommended voices:" - echo "" - for lang in "${!LANGUAGE_VOICES[@]}"; do - printf "%-15s → %s\n" "$lang" "${LANGUAGE_VOICES[$lang]}" - done | sort - ;; - *) - echo "AgentVibes Language Manager" - echo "" - echo "Usage:" - echo " language-manager.sh set Set language" - echo " language-manager.sh get Get current language" - echo " language-manager.sh code Get language code only" - echo " language-manager.sh check-voice Check if voice is multilingual" - echo " language-manager.sh best-voice Get best voice for current language" - echo " language-manager.sh voice-for-language [prov] Get voice for language & provider" - echo " language-manager.sh list List all supported languages" - exit 1 - ;; - esac -fi diff --git a/.claude/hooks/learn-manager.sh b/.claude/hooks/learn-manager.sh deleted file mode 100755 index 61df6784..00000000 --- a/.claude/hooks/learn-manager.sh +++ /dev/null @@ -1,475 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/learn-manager.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied. Use at your own risk. See the Apache License for details. -# -# --- -# -# @fileoverview Language Learning Mode Manager - Enables dual-language TTS for immersive learning -# @context Speaks responses in both main language (English) and target language (Spanish, French, etc.) for language practice -# @architecture Manages main/target language pairs with voice mappings, auto-configures recommended voices per language -# @dependencies play-tts.sh (dual invocation), language-manager.sh (voice recommendations), .claude/tts-*.txt state files -# @entrypoints Called by /agent-vibes:learn commands to enable/disable learning mode -# @patterns Dual-voice orchestration, auto-configuration, greeting on activation, provider-aware voice selection -# @related language-manager.sh, play-tts.sh, .claude/tts-learn-mode.txt, .claude/tts-target-language.txt - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_DIR="$SCRIPT_DIR/../.." - -# Configuration files (project-local first, then global fallback) -MAIN_LANG_FILE="$PROJECT_DIR/.claude/tts-main-language.txt" -TARGET_LANG_FILE="$PROJECT_DIR/.claude/tts-target-language.txt" -TARGET_VOICE_FILE="$PROJECT_DIR/.claude/tts-target-voice.txt" -LEARN_MODE_FILE="$PROJECT_DIR/.claude/tts-learn-mode.txt" - -GLOBAL_MAIN_LANG_FILE="$HOME/.claude/tts-main-language.txt" -GLOBAL_TARGET_LANG_FILE="$HOME/.claude/tts-target-language.txt" -GLOBAL_TARGET_VOICE_FILE="$HOME/.claude/tts-target-voice.txt" -GLOBAL_LEARN_MODE_FILE="$HOME/.claude/tts-learn-mode.txt" - -# Colors -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Get main language -get_main_language() { - if [[ -f "$MAIN_LANG_FILE" ]]; then - cat "$MAIN_LANG_FILE" - elif [[ -f "$GLOBAL_MAIN_LANG_FILE" ]]; then - cat "$GLOBAL_MAIN_LANG_FILE" - else - echo "english" - fi -} - -# Set main language -set_main_language() { - local language="$1" - if [[ -z "$language" ]]; then - echo -e "${YELLOW}Usage: learn-manager.sh set-main-language ${NC}" - exit 1 - fi - - mkdir -p "$PROJECT_DIR/.claude" - echo "$language" > "$MAIN_LANG_FILE" - echo -e "${GREEN}✓${NC} Main language set to: $language" -} - -# Get target language -get_target_language() { - if [[ -f "$TARGET_LANG_FILE" ]]; then - cat "$TARGET_LANG_FILE" - elif [[ -f "$GLOBAL_TARGET_LANG_FILE" ]]; then - cat "$GLOBAL_TARGET_LANG_FILE" - else - echo "" - fi -} - -# Get greeting message for a language -get_greeting_for_language() { - local language="$1" - - case "${language,,}" in - spanish|español) - echo "¡Hola! Soy tu profesor de español. ¡Vamos a aprender juntos!" - ;; - french|français) - echo "Bonjour! Je suis votre professeur de français. Apprenons ensemble!" - ;; - german|deutsch) - echo "Hallo! Ich bin dein Deutschlehrer. Lass uns zusammen lernen!" - ;; - italian|italiano) - echo "Ciao! Sono il tuo insegnante di italiano. Impariamo insieme!" - ;; - portuguese|português) - echo "Olá! Sou seu professor de português. Vamos aprender juntos!" - ;; - chinese|中文|mandarin) - echo "你好!我是你的中文老师。让我们一起学习吧!" - ;; - japanese|日本語) - echo "こんにちは!私はあなたの日本語の先生です。一緒に勉強しましょう!" - ;; - korean|한국어) - echo "안녕하세요! 저는 당신의 한국어 선생님입니다. 함께 배워봅시다!" - ;; - russian|русский) - echo "Здравствуйте! Я ваш учитель русского языка. Давайте учиться вместе!" - ;; - arabic|العربية) - echo "مرحبا! أنا معلمك للغة العربية. دعونا نتعلم معا!" - ;; - hindi|हिन्दी) - echo "नमस्ते! मैं आपका हिंदी शिक्षक हूं। आइए साथ में सीखें!" - ;; - dutch|nederlands) - echo "Hallo! Ik ben je Nederlandse leraar. Laten we samen leren!" - ;; - polish|polski) - echo "Cześć! Jestem twoim nauczycielem polskiego. Uczmy się razem!" - ;; - turkish|türkçe) - echo "Merhaba! Ben Türkçe öğretmeninizim. Birlikte öğrenelim!" - ;; - swedish|svenska) - echo "Hej! Jag är din svenskalärare. Låt oss lära tillsammans!" - ;; - *) - echo "Hello! I am your language teacher. Let's learn together!" - ;; - esac -} - -# Set target language -set_target_language() { - local language="$1" - if [[ -z "$language" ]]; then - echo -e "${YELLOW}Usage: learn-manager.sh set-target-language ${NC}" - exit 1 - fi - - mkdir -p "$PROJECT_DIR/.claude" - echo "$language" > "$TARGET_LANG_FILE" - echo -e "${GREEN}✓${NC} Target language set to: $language" - - # Automatically set the recommended voice for this language - local recommended_voice=$(get_recommended_voice_for_language "$language") - if [[ -n "$recommended_voice" ]]; then - echo "$recommended_voice" > "$TARGET_VOICE_FILE" - echo -e "${GREEN}✓${NC} Target voice automatically set to: ${YELLOW}$recommended_voice${NC}" - - # Detect provider for display - local provider="" - if [[ -f "$PROJECT_DIR/.claude/tts-provider.txt" ]]; then - provider=$(cat "$PROJECT_DIR/.claude/tts-provider.txt") - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - provider=$(cat "$HOME/.claude/tts-provider.txt") - else - provider="elevenlabs" - fi - echo -e " (for ${GREEN}$provider${NC} TTS)" - echo "" - - # Greet user in the target language with the target voice - local greeting=$(get_greeting_for_language "$language") - echo -e "${BLUE}🎓${NC} Your language teacher says:" - - # Check if we're using Piper and if the voice is available - if [[ "$provider" == "piper" ]]; then - # Quick check: does the voice file exist? - local voice_dir="${HOME}/.claude/piper-voices" - if [[ -f "${voice_dir}/${recommended_voice}.onnx" ]]; then - # Voice exists, play greeting in background - nohup "$SCRIPT_DIR/play-tts.sh" "$greeting" "$recommended_voice" >/dev/null 2>&1 & - else - echo -e "${YELLOW} (Voice not yet downloaded - greeting will play after first download)${NC}" - fi - else - # ElevenLabs - just play it in background - nohup "$SCRIPT_DIR/play-tts.sh" "$greeting" "$recommended_voice" >/dev/null 2>&1 & - fi - else - # Fallback to suggestion if auto-set failed - suggest_voice_for_language "$language" - fi -} - -# Get recommended voice for a language (returns voice string, no output) -get_recommended_voice_for_language() { - local language="$1" - local recommended_voice="" - local provider="" - - # Detect active provider - if [[ -f "$PROJECT_DIR/.claude/tts-provider.txt" ]]; then - provider=$(cat "$PROJECT_DIR/.claude/tts-provider.txt") - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - provider=$(cat "$HOME/.claude/tts-provider.txt") - else - provider="elevenlabs" # Default - fi - - # Source language manager and get provider-specific voice - if [[ -f "$SCRIPT_DIR/language-manager.sh" ]]; then - source "$SCRIPT_DIR/language-manager.sh" 2>/dev/null - recommended_voice=$(get_voice_for_language "$language" "$provider" 2>/dev/null) - fi - - # Fallback to hardcoded suggestions if function failed - if [[ -z "$recommended_voice" ]]; then - case "${language,,}" in - spanish|español) - recommended_voice=$([ "$provider" = "piper" ] && echo "es_ES-davefx-medium" || echo "Antoni") - ;; - french|français) - recommended_voice=$([ "$provider" = "piper" ] && echo "fr_FR-siwis-medium" || echo "Rachel") - ;; - german|deutsch) - recommended_voice=$([ "$provider" = "piper" ] && echo "de_DE-thorsten-medium" || echo "Domi") - ;; - italian|italiano) - recommended_voice=$([ "$provider" = "piper" ] && echo "it_IT-riccardo-x_low" || echo "Bella") - ;; - portuguese|português) - recommended_voice=$([ "$provider" = "piper" ] && echo "pt_BR-faber-medium" || echo "Matilda") - ;; - chinese|中文|mandarin) - recommended_voice=$([ "$provider" = "piper" ] && echo "zh_CN-huayan-medium" || echo "Amy") - ;; - *) - recommended_voice=$([ "$provider" = "piper" ] && echo "en_US-lessac-medium" || echo "Antoni") - ;; - esac - fi - - echo "$recommended_voice" -} - -# Suggest voice based on target language (displays suggestion message) -suggest_voice_for_language() { - local language="$1" - local suggested_voice=$(get_recommended_voice_for_language "$language") - - # Detect provider for display - local provider="" - if [[ -f "$PROJECT_DIR/.claude/tts-provider.txt" ]]; then - provider=$(cat "$PROJECT_DIR/.claude/tts-provider.txt") - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - provider=$(cat "$HOME/.claude/tts-provider.txt") - else - provider="elevenlabs" - fi - - echo "" - echo -e "${BLUE}💡 Tip:${NC} For $language (using ${GREEN}$provider${NC} TTS), we recommend: ${YELLOW}$suggested_voice${NC}" - echo -e " Set it with: ${YELLOW}/agent-vibes:target-voice $suggested_voice${NC}" -} - -# Get target voice -get_target_voice() { - if [[ -f "$TARGET_VOICE_FILE" ]]; then - cat "$TARGET_VOICE_FILE" - elif [[ -f "$GLOBAL_TARGET_VOICE_FILE" ]]; then - cat "$GLOBAL_TARGET_VOICE_FILE" - else - echo "" - fi -} - -# Set target voice -set_target_voice() { - local voice="$1" - if [[ -z "$voice" ]]; then - echo -e "${YELLOW}Usage: learn-manager.sh set-target-voice ${NC}" - exit 1 - fi - - mkdir -p "$PROJECT_DIR/.claude" - echo "$voice" > "$TARGET_VOICE_FILE" - echo -e "${GREEN}✓${NC} Target voice set to: $voice" -} - -# Check if learning mode is enabled -is_learn_mode_enabled() { - if [[ -f "$LEARN_MODE_FILE" ]]; then - local mode=$(cat "$LEARN_MODE_FILE") - [[ "$mode" == "ON" ]] - elif [[ -f "$GLOBAL_LEARN_MODE_FILE" ]]; then - local mode=$(cat "$GLOBAL_LEARN_MODE_FILE") - [[ "$mode" == "ON" ]] - else - return 1 - fi -} - -# Enable learning mode -enable_learn_mode() { - mkdir -p "$PROJECT_DIR/.claude" - echo "ON" > "$LEARN_MODE_FILE" - echo -e "${GREEN}✓${NC} Language learning mode: ${GREEN}ENABLED${NC}" - echo "" - - # Auto-set target voice if target language is set but voice is not - local target_lang=$(get_target_language) - local target_voice=$(get_target_voice) - local voice_was_set=false - - if [[ -n "$target_lang" ]] && [[ -z "$target_voice" ]]; then - echo -e "${BLUE}ℹ${NC} Auto-configuring voice for $target_lang..." - local recommended_voice=$(get_recommended_voice_for_language "$target_lang") - if [[ -n "$recommended_voice" ]]; then - echo "$recommended_voice" > "$TARGET_VOICE_FILE" - target_voice="$recommended_voice" - echo -e "${GREEN}✓${NC} Target voice automatically set to: ${YELLOW}$recommended_voice${NC}" - - # Detect provider for display - local provider="" - if [[ -f "$PROJECT_DIR/.claude/tts-provider.txt" ]]; then - provider=$(cat "$PROJECT_DIR/.claude/tts-provider.txt") - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - provider=$(cat "$HOME/.claude/tts-provider.txt") - else - provider="elevenlabs" - fi - echo -e " (for ${GREEN}$provider${NC} TTS)" - echo "" - voice_was_set=true - fi - fi - - show_status - - # Greet user with language teacher if everything is configured - if [[ -n "$target_lang" ]] && [[ -n "$target_voice" ]]; then - echo "" - local greeting=$(get_greeting_for_language "$target_lang") - echo -e "${BLUE}🎓${NC} Your language teacher says:" - - # Detect provider - local provider="" - if [[ -f "$PROJECT_DIR/.claude/tts-provider.txt" ]]; then - provider=$(cat "$PROJECT_DIR/.claude/tts-provider.txt") - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - provider=$(cat "$HOME/.claude/tts-provider.txt") - else - provider="elevenlabs" - fi - - # Check if we're using Piper and if the voice is available - if [[ "$provider" == "piper" ]]; then - # Quick check: does the voice file exist? - local voice_dir="${HOME}/.claude/piper-voices" - if [[ -f "${voice_dir}/${target_voice}.onnx" ]]; then - # Voice exists, play greeting in background - nohup "$SCRIPT_DIR/play-tts.sh" "$greeting" "$target_voice" >/dev/null 2>&1 & - else - echo -e "${YELLOW} (Voice not yet downloaded - greeting will play after first download)${NC}" - fi - else - # ElevenLabs - just play it in background - nohup "$SCRIPT_DIR/play-tts.sh" "$greeting" "$target_voice" >/dev/null 2>&1 & - fi - fi -} - -# Disable learning mode -disable_learn_mode() { - mkdir -p "$PROJECT_DIR/.claude" - echo "OFF" > "$LEARN_MODE_FILE" - echo -e "${GREEN}✓${NC} Language learning mode: ${YELLOW}DISABLED${NC}" -} - -# Show learning mode status -show_status() { - local main_lang=$(get_main_language) - local target_lang=$(get_target_language) - local target_voice=$(get_target_voice) - local learn_mode="OFF" - - if is_learn_mode_enabled; then - learn_mode="ON" - fi - - echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${BLUE} Language Learning Mode Status${NC}" - echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo "" - echo -e " ${BLUE}Learning Mode:${NC} $(if [[ "$learn_mode" == "ON" ]]; then echo -e "${GREEN}ENABLED${NC}"; else echo -e "${YELLOW}DISABLED${NC}"; fi)" - echo -e " ${BLUE}Main Language:${NC} $main_lang" - echo -e " ${BLUE}Target Language:${NC} ${target_lang:-"(not set)"}" - echo -e " ${BLUE}Target Voice:${NC} ${target_voice:-"(not set)"}" - echo "" - - if [[ "$learn_mode" == "ON" ]]; then - if [[ -z "$target_lang" ]]; then - echo -e " ${YELLOW}⚠${NC} Please set a target language: ${YELLOW}/agent-vibes:target ${NC}" - fi - if [[ -z "$target_voice" ]]; then - echo -e " ${YELLOW}⚠${NC} Please set a target voice: ${YELLOW}/agent-vibes:target-voice ${NC}" - fi - - if [[ -n "$target_lang" ]] && [[ -n "$target_voice" ]]; then - echo -e " ${GREEN}✓${NC} All set! TTS will speak in both languages." - echo "" - echo -e " ${BLUE}How it works:${NC}" - echo -e " 1. First: Speak in ${BLUE}$main_lang${NC} (your current voice)" - echo -e " 2. Then: Speak in ${BLUE}$target_lang${NC} ($target_voice voice)" - fi - else - echo -e " ${BLUE}💡 Tip:${NC} Enable learning mode with: ${YELLOW}/agent-vibes:learn${NC}" - fi - - echo "" - echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" -} - -# Main command handler -case "${1:-}" in - get-main-language) - get_main_language - ;; - set-main-language) - set_main_language "$2" - ;; - get-target-language) - get_target_language - ;; - set-target-language) - set_target_language "$2" - ;; - get-target-voice) - get_target_voice - ;; - set-target-voice) - set_target_voice "$2" - ;; - is-enabled) - if is_learn_mode_enabled; then - echo "ON" - exit 0 - else - echo "OFF" - exit 1 - fi - ;; - enable) - enable_learn_mode - ;; - disable) - disable_learn_mode - ;; - status) - show_status - ;; - *) - echo "Usage: learn-manager.sh {get-main-language|set-main-language|get-target-language|set-target-language|get-target-voice|set-target-voice|is-enabled|enable|disable|status}" - exit 1 - ;; -esac diff --git a/.claude/hooks/personality-manager.sh b/.claude/hooks/personality-manager.sh deleted file mode 100755 index 6ba812f6..00000000 --- a/.claude/hooks/personality-manager.sh +++ /dev/null @@ -1,438 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/personality-manager.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied. Use at your own risk. See the Apache License for details. -# -# --- -# -# @fileoverview Personality Manager - Adds character and emotional style to TTS voices -# @context Enables voices to have distinct personalities (flirty, sarcastic, pirate, etc.) with provider-aware voice assignment -# @architecture Markdown-based personality templates with provider-specific voice mappings (ElevenLabs vs Piper) -# @dependencies .claude/personalities/*.md files, voice-manager.sh, play-tts.sh, provider-manager.sh -# @entrypoints Called by /agent-vibes:personality slash commands -# @patterns Template-based configuration, provider abstraction, random personality support -# @related .claude/personalities/*.md, voice-manager.sh, .claude/tts-personality.txt - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PERSONALITIES_DIR="$SCRIPT_DIR/../personalities" - -# Determine target .claude directory based on context -# Priority: -# 1. CLAUDE_PROJECT_DIR env var (set by MCP for project-specific settings) -# 2. Script location (for direct slash command usage) -# 3. Global ~/.claude (fallback) - -if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then - # MCP context: Use the project directory where MCP was invoked - CLAUDE_DIR="$CLAUDE_PROJECT_DIR/.claude" -else - # Direct usage context: Use script location - # Script is at .claude/hooks/personality-manager.sh, so .claude is .. - CLAUDE_DIR="$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)" - - # If script is in global ~/.claude, use that - if [[ "$CLAUDE_DIR" == "$HOME/.claude" ]]; then - CLAUDE_DIR="$HOME/.claude" - elif [[ ! -d "$CLAUDE_DIR" ]]; then - # Fallback to global if directory doesn't exist - CLAUDE_DIR="$HOME/.claude" - fi -fi - -PERSONALITY_FILE="$CLAUDE_DIR/tts-personality.txt" - -# Function to get personality data from markdown file -get_personality_data() { - local personality="$1" - local field="$2" - local file="$PERSONALITIES_DIR/${personality}.md" - - if [[ ! -f "$file" ]]; then - return 1 - fi - - case "$field" in - prefix) - sed -n '/^## Prefix/,/^##/p' "$file" | sed '1d;$d' | tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' - ;; - suffix) - sed -n '/^## Suffix/,/^##/p' "$file" | sed '1d;$d' | tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' - ;; - description) - grep "^description:" "$file" | cut -d: -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' - ;; - voice) - grep "^elevenlabs_voice:" "$file" | cut -d: -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' - ;; - piper_voice) - grep "^piper_voice:" "$file" | cut -d: -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' - ;; - instructions) - sed -n '/^## AI Instructions/,/^##/p' "$file" | sed '1d;$d' - ;; - esac -} - -# Function to list all available personalities -list_personalities() { - local personalities=() - - # Find all .md files in personalities directory - if [[ -d "$PERSONALITIES_DIR" ]]; then - for file in "$PERSONALITIES_DIR"/*.md; do - if [[ -f "$file" ]]; then - basename "$file" .md - fi - done - fi -} - -case "$1" in - list) - echo "🎭 Available Personalities:" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - # Get current personality - CURRENT="normal" - if [ -f "$PERSONALITY_FILE" ]; then - CURRENT=$(cat "$PERSONALITY_FILE") - fi - - # List personalities from markdown files - echo "Built-in personalities:" - for personality in $(list_personalities | sort); do - desc=$(get_personality_data "$personality" "description") - if [[ "$personality" == "$CURRENT" ]]; then - echo " ✓ $personality - $desc (current)" - else - echo " - $personality - $desc" - fi - done - - # Add random option - if [[ "$CURRENT" == "random" ]]; then - echo " ✓ random - Picks randomly each time (current)" - else - echo " - random - Picks randomly each time" - fi - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo "Usage: /agent-vibes:personality " - echo " /agent-vibes:personality add " - echo " /agent-vibes:personality edit " - ;; - - set|switch) - PERSONALITY="$2" - - if [[ -z "$PERSONALITY" ]]; then - echo "❌ Please specify a personality name" - echo "Usage: $0 set " - exit 1 - fi - - # Check if personality file exists (unless it's random) - if [[ "$PERSONALITY" != "random" ]]; then - if [[ ! -f "$PERSONALITIES_DIR/${PERSONALITY}.md" ]]; then - echo "❌ Personality not found: $PERSONALITY" - echo "" - echo "Available personalities:" - for p in $(list_personalities | sort); do - echo " • $p" - done - exit 1 - fi - fi - - # Save the personality - echo "$PERSONALITY" > "$PERSONALITY_FILE" - echo "🎭 Personality set to: $PERSONALITY" - - # Check if personality has an assigned voice - # Detect active TTS provider - PROVIDER_FILE="" - if [[ -f "$CLAUDE_DIR/tts-provider.txt" ]]; then - PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt" - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - PROVIDER_FILE="$HOME/.claude/tts-provider.txt" - fi - - ACTIVE_PROVIDER="elevenlabs" # default - if [[ -n "$PROVIDER_FILE" ]]; then - ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE") - fi - - # Get the appropriate voice based on provider - ASSIGNED_VOICE="" - if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then - # Try to get Piper-specific voice first - ASSIGNED_VOICE=$(get_personality_data "$PERSONALITY" "piper_voice") - if [[ -z "$ASSIGNED_VOICE" ]]; then - # Fallback to default Piper voice - ASSIGNED_VOICE="en_US-lessac-medium" - fi - else - # Use ElevenLabs voice (reads from elevenlabs_voice: field) - ASSIGNED_VOICE=$(get_personality_data "$PERSONALITY" "voice") - fi - - if [[ -n "$ASSIGNED_VOICE" ]]; then - # Switch to the assigned voice (silently - personality will do the talking) - VOICE_MANAGER="$SCRIPT_DIR/voice-manager.sh" - if [[ -x "$VOICE_MANAGER" ]]; then - echo "🎤 Switching to assigned voice: $ASSIGNED_VOICE" - "$VOICE_MANAGER" switch "$ASSIGNED_VOICE" --silent >/dev/null 2>&1 - fi - fi - - # Make a personality-appropriate remark with TTS - if [[ "$PERSONALITY" != "random" ]]; then - echo "" - - # Get TTS script path - TTS_SCRIPT="$SCRIPT_DIR/play-tts.sh" - - # Try to get acknowledgment from personality file - PERSONALITY_FILE_PATH="$PERSONALITIES_DIR/${PERSONALITY}.md" - REMARK="" - - if [[ -f "$PERSONALITY_FILE_PATH" ]]; then - # Extract example responses from personality file (lines starting with "- ") - mapfile -t EXAMPLES < <(grep '^- "' "$PERSONALITY_FILE_PATH" | sed 's/^- "//; s/"$//') - - if [[ ${#EXAMPLES[@]} -gt 0 ]]; then - # Pick a random example - REMARK="${EXAMPLES[$RANDOM % ${#EXAMPLES[@]}]}" - fi - fi - - # Fallback if no examples found - if [[ -z "$REMARK" ]]; then - REMARK="Personality set to ${PERSONALITY}!" - fi - - echo "💬 $REMARK" - "$TTS_SCRIPT" "$REMARK" - - echo "" - echo "Note: AI will generate unique ${PERSONALITY} responses - no fixed templates!" - echo "" - echo "💡 Tip: To hear automatic TTS narration, enable the Agent Vibes output style:" - echo " /output-style Agent Vibes" - fi - ;; - - get) - if [ -f "$PERSONALITY_FILE" ]; then - CURRENT=$(cat "$PERSONALITY_FILE") - echo "Current personality: $CURRENT" - - if [[ "$CURRENT" != "random" ]]; then - desc=$(get_personality_data "$CURRENT" "description") - [[ -n "$desc" ]] && echo "Description: $desc" - fi - else - echo "Current personality: normal (default)" - fi - ;; - - add) - NAME="$2" - if [[ -z "$NAME" ]]; then - echo "❌ Please specify a personality name" - echo "Usage: $0 add " - exit 1 - fi - - FILE="$PERSONALITIES_DIR/${NAME}.md" - if [[ -f "$FILE" ]]; then - echo "❌ Personality '$NAME' already exists" - echo "Use 'edit' to modify it" - exit 1 - fi - - # Create new personality file - cat > "$FILE" << 'EOF' ---- -name: NAME -description: Custom personality ---- - -# NAME Personality - -## Prefix - - -## Suffix - - -## AI Instructions -Describe how the AI should generate messages for this personality. - -## Example Responses -- "Example response 1" -- "Example response 2" -EOF - - # Replace NAME with actual name - sed -i "s/NAME/$NAME/g" "$FILE" - - echo "✅ Created new personality: $NAME" - echo "📝 Edit the file: $FILE" - echo "" - echo "You can now customize:" - echo " • Prefix: Text before messages" - echo " • Suffix: Text after messages" - echo " • AI Instructions: How AI should speak" - echo " • Example Responses: Sample messages" - ;; - - edit) - NAME="$2" - if [[ -z "$NAME" ]]; then - echo "❌ Please specify a personality name" - echo "Usage: $0 edit " - exit 1 - fi - - FILE="$PERSONALITIES_DIR/${NAME}.md" - if [[ ! -f "$FILE" ]]; then - echo "❌ Personality '$NAME' not found" - echo "Use 'add' to create it first" - exit 1 - fi - - echo "📝 Edit this file to customize the personality:" - echo "$FILE" - ;; - - reset) - echo "normal" > "$PERSONALITY_FILE" - echo "🎭 Personality reset to: normal" - ;; - - set-favorite-voice) - PERSONALITY="$2" - NEW_VOICE="$3" - - if [[ -z "$PERSONALITY" ]] || [[ -z "$NEW_VOICE" ]]; then - echo "❌ Please specify both personality name and voice name" - echo "Usage: $0 set-favorite-voice " - exit 1 - fi - - FILE="$PERSONALITIES_DIR/${PERSONALITY}.md" - if [[ ! -f "$FILE" ]]; then - echo "❌ Personality '$PERSONALITY' not found" - exit 1 - fi - - # Detect active TTS provider - PROVIDER_FILE="" - if [[ -f "$CLAUDE_DIR/tts-provider.txt" ]]; then - PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt" - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - PROVIDER_FILE="$HOME/.claude/tts-provider.txt" - fi - - ACTIVE_PROVIDER="elevenlabs" # default - if [[ -n "$PROVIDER_FILE" ]]; then - ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE") - fi - - # Determine which field to update based on provider - if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then - VOICE_FIELD="piper_voice" - CURRENT_VOICE=$(get_personality_data "$PERSONALITY" "piper_voice") - else - VOICE_FIELD="elevenlabs_voice" - CURRENT_VOICE=$(get_personality_data "$PERSONALITY" "voice") - fi - - # Check if personality already has a favorite voice assigned - if [[ -n "$CURRENT_VOICE" ]] && [[ "$CURRENT_VOICE" != "$NEW_VOICE" ]]; then - echo "⚠️ WARNING: Personality '$PERSONALITY' already has a favorite voice assigned!" - echo "" - echo " Current favorite ($ACTIVE_PROVIDER): $CURRENT_VOICE" - echo " New voice: $NEW_VOICE" - echo "" - echo "Do you want to replace the favorite voice?" - echo "" - read -p "Enter your choice (yes/no): " CHOICE - - case "$CHOICE" in - yes|y|YES|Y) - echo "✅ Replacing favorite voice..." - ;; - no|n|NO|N) - echo "❌ Keeping current favorite voice: $CURRENT_VOICE" - exit 0 - ;; - *) - echo "❌ Invalid choice. Keeping current favorite voice: $CURRENT_VOICE" - exit 1 - ;; - esac - fi - - # Update the voice in the personality file - if grep -q "^${VOICE_FIELD}:" "$FILE"; then - # Field exists, replace it - sed -i "s/^${VOICE_FIELD}:.*/${VOICE_FIELD}: ${NEW_VOICE}/" "$FILE" - else - # Field doesn't exist, add it after the frontmatter - sed -i "/^---$/,/^---$/ { /^---$/a\\ -${VOICE_FIELD}: ${NEW_VOICE} -}" "$FILE" - fi - - echo "✅ Favorite voice for '$PERSONALITY' personality set to: $NEW_VOICE ($ACTIVE_PROVIDER)" - echo "📝 Updated file: $FILE" - ;; - - *) - # If a single argument is provided and it's not a command, treat it as "set " - if [[ -n "$1" ]] && [[ -f "$PERSONALITIES_DIR/${1}.md" || "$1" == "random" ]]; then - # Call set with the personality name - exec "$0" set "$1" - else - echo "AgentVibes Personality Manager" - echo "" - echo "Commands:" - echo " list - List all personalities" - echo " set/switch - Set personality" - echo " add - Create new personality" - echo " edit - Show path to edit personality" - echo " get - Show current personality" - echo " set-favorite-voice - Set favorite voice for a personality" - echo " reset - Reset to normal" - echo "" - echo "Examples:" - echo " /agent-vibes:personality flirty" - echo " /agent-vibes:personality add cowboy" - echo " /agent-vibes:personality set-favorite-voice flirty \"Aria\"" - fi - ;; -esac \ No newline at end of file diff --git a/.claude/hooks/piper-download-voices.sh b/.claude/hooks/piper-download-voices.sh deleted file mode 100755 index a50aa35f..00000000 --- a/.claude/hooks/piper-download-voices.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/piper-download-voices.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview Piper Voice Model Downloader - Batch downloads popular Piper TTS voices from HuggingFace -# @context Post-installation utility to download commonly used voices (~25MB each) -# @architecture Wrapper around piper-voice-manager.sh download functions with progress tracking -# @dependencies piper-voice-manager.sh (download logic), piper binary (for validation) -# @entrypoints Called by piper-installer.sh or manually via ./piper-download-voices.sh [--yes|-y] -# @patterns Batch operations, skip-existing logic, auto-yes flag for non-interactive use -# @related piper-voice-manager.sh, piper-installer.sh -# - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/piper-voice-manager.sh" - -# Parse command line arguments -AUTO_YES=false -if [[ "$1" == "--yes" ]] || [[ "$1" == "-y" ]]; then - AUTO_YES=true -fi - -# Common voice models to download -COMMON_VOICES=( - "en_US-lessac-medium" # Default, clear male - "en_US-amy-medium" # Warm female - "en_US-joe-medium" # Professional male - "en_US-ryan-high" # Expressive male - "en_US-libritts-high" # Premium quality -) - -echo "🎙️ Piper Voice Model Downloader" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "" -echo "This will download the most commonly used Piper voice models." -echo "Each voice is approximately 25MB." -echo "" - -# Check if piper is installed -if ! command -v piper &> /dev/null; then - echo "❌ Error: Piper TTS not installed" - echo "Install with: pipx install piper-tts" - exit 1 -fi - -# Get storage directory -VOICE_DIR=$(get_voice_storage_dir) - -echo "📂 Storage location: $VOICE_DIR" -echo "" - -# Count already downloaded -ALREADY_DOWNLOADED=0 -ALREADY_DOWNLOADED_LIST=() -NEED_DOWNLOAD=() - -for voice in "${COMMON_VOICES[@]}"; do - if verify_voice "$voice" 2>/dev/null; then - ((ALREADY_DOWNLOADED++)) - ALREADY_DOWNLOADED_LIST+=("$voice") - else - NEED_DOWNLOAD+=("$voice") - fi -done - -echo "📊 Status:" -echo " Already downloaded: $ALREADY_DOWNLOADED voice(s)" -echo " Need to download: ${#NEED_DOWNLOAD[@]} voice(s)" -echo "" - -# Show already downloaded voices -if [[ $ALREADY_DOWNLOADED -gt 0 ]]; then - echo "✅ Already downloaded (skipped):" - for voice in "${ALREADY_DOWNLOADED_LIST[@]}"; do - echo " ✓ $voice" - done - echo "" -fi - -if [[ ${#NEED_DOWNLOAD[@]} -eq 0 ]]; then - echo "🎉 All common voices ready to use!" - exit 0 -fi - -echo "Voices to download:" -for voice in "${NEED_DOWNLOAD[@]}"; do - echo " • $voice (~25MB)" -done -echo "" - -# Ask for confirmation (skip if --yes flag provided) -if [[ "$AUTO_YES" == "false" ]]; then - read -p "Download ${#NEED_DOWNLOAD[@]} voice model(s)? [Y/n]: " -n 1 -r - echo - - if [[ ! $REPLY =~ ^[Yy]$ ]] && [[ -n $REPLY ]]; then - echo "❌ Download cancelled" - exit 0 - fi -else - echo "Auto-downloading ${#NEED_DOWNLOAD[@]} voice model(s)..." - echo "" -fi - -# Download each voice -DOWNLOADED=0 -FAILED=0 - -for voice in "${NEED_DOWNLOAD[@]}"; do - echo "" - echo "📥 Downloading: $voice..." - - if download_voice "$voice"; then - ((DOWNLOADED++)) - echo "✅ Downloaded: $voice" - else - ((FAILED++)) - echo "❌ Failed: $voice" - fi -done - -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "📊 Download Summary:" -echo " ✅ Successfully downloaded: $DOWNLOADED" -echo " ❌ Failed: $FAILED" -echo " 📦 Total voices available: $((ALREADY_DOWNLOADED + DOWNLOADED))" -echo "" - -if [[ $DOWNLOADED -gt 0 ]]; then - echo "✨ Ready to use Piper TTS with downloaded voices!" - echo "" - echo "Try it:" - echo " /agent-vibes:provider switch piper" - echo " /agent-vibes:preview" -fi diff --git a/.claude/hooks/piper-installer.sh b/.claude/hooks/piper-installer.sh deleted file mode 100755 index cf94236e..00000000 --- a/.claude/hooks/piper-installer.sh +++ /dev/null @@ -1,178 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/piper-installer.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview Piper TTS Installer - Installs Piper TTS via pipx and downloads initial voice models -# @context Automated installation script for free offline Piper TTS on WSL/Linux systems -# @architecture Helper script for AgentVibes installer, invoked manually or from provider switcher -# @dependencies pipx (Python package installer), apt-get/brew/dnf/pacman (for pipx installation) -# @entrypoints Called by src/installer.js or manually by users during setup -# @patterns Platform detection (WSL/Linux only), package manager abstraction, guided voice download -# @related piper-download-voices.sh, provider-manager.sh, src/installer.js -# - -set -e # Exit on error - -echo "🎤 Piper TTS Installer" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "" - -# Check if running on WSL or Linux -if ! grep -qi microsoft /proc/version 2>/dev/null && [[ "$(uname -s)" != "Linux" ]]; then - echo "❌ Piper TTS is only supported on WSL and Linux" - echo " Your platform: $(uname -s)" - echo "" - echo " For macOS/Windows, use ElevenLabs instead:" - echo " /agent-vibes:provider switch elevenlabs" - exit 1 -fi - -# Check if Piper is already installed -if command -v piper &> /dev/null; then - # Piper doesn't have a --version flag, just check if it exists - echo "✅ Piper TTS is already installed!" - echo " Location: $(which piper)" - echo "" - echo " Download voices with: .claude/hooks/piper-download-voices.sh" - exit 0 -fi - -echo "📦 Installing Piper TTS..." -echo "" - -# Check if pipx is installed -if ! command -v pipx &> /dev/null; then - echo "⚠️ pipx not found. Installing pipx first..." - echo "" - - # Try to install pipx - if command -v apt-get &> /dev/null; then - # Debian/Ubuntu - sudo apt-get update - sudo apt-get install -y pipx - elif command -v brew &> /dev/null; then - # macOS (though Piper doesn't run on macOS) - brew install pipx - elif command -v dnf &> /dev/null; then - # Fedora - sudo dnf install -y pipx - elif command -v pacman &> /dev/null; then - # Arch Linux - sudo pacman -S python-pipx - else - echo "❌ Unable to install pipx automatically." - echo "" - echo " Please install pipx manually:" - echo " https://pipx.pypa.io/stable/installation/" - exit 1 - fi - - # Ensure pipx is in PATH - pipx ensurepath - echo "" -fi - -# Install Piper TTS -echo "📥 Installing Piper TTS via pipx..." -pipx install piper-tts - -if ! command -v piper &> /dev/null; then - echo "" - echo "❌ Installation completed but piper command not found in PATH" - echo "" - echo " Try running: pipx ensurepath" - echo " Then restart your terminal" - exit 1 -fi - -echo "" -echo "✅ Piper TTS installed successfully!" -echo "" - -PIPER_VERSION=$(piper --version 2>&1 || echo "unknown") -echo " Version: $PIPER_VERSION" -echo "" - -# Determine voices directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CLAUDE_DIR="$(dirname "$SCRIPT_DIR")" - -# Check for configured voices directory -VOICES_DIR="" -if [[ -f "$CLAUDE_DIR/piper-voices-dir.txt" ]]; then - VOICES_DIR=$(cat "$CLAUDE_DIR/piper-voices-dir.txt") -elif [[ -f "$HOME/.claude/piper-voices-dir.txt" ]]; then - VOICES_DIR=$(cat "$HOME/.claude/piper-voices-dir.txt") -else - VOICES_DIR="$HOME/.claude/piper-voices" -fi - -echo "📁 Voice storage location: $VOICES_DIR" -echo "" - -# Ask if user wants to download voices now -read -p "Would you like to download voice models now? [Y/n] " -n 1 -r -echo "" - -if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then - echo "" - echo "📥 Downloading recommended voices..." - echo "" - - # Use the piper-download-voices.sh script if available - if [[ -f "$SCRIPT_DIR/piper-download-voices.sh" ]]; then - "$SCRIPT_DIR/piper-download-voices.sh" - else - # Manual download of a basic voice - mkdir -p "$VOICES_DIR" - - echo "Downloading en_US-lessac-medium (recommended)..." - curl -L "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx" \ - -o "$VOICES_DIR/en_US-lessac-medium.onnx" - curl -L "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json" \ - -o "$VOICES_DIR/en_US-lessac-medium.onnx.json" - - echo "✅ Voice downloaded!" - fi -fi - -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "🎉 Piper TTS Setup Complete!" -echo "" -echo "Next steps:" -echo " 1. Download more voices: .claude/hooks/piper-download-voices.sh" -echo " 2. List available voices: /agent-vibes:list" -echo " 3. Test it out: /agent-vibes:preview" -echo "" -echo "Enjoy your free, offline TTS! 🎤" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/.claude/hooks/piper-multispeaker-registry.sh b/.claude/hooks/piper-multispeaker-registry.sh deleted file mode 100755 index 5765cd23..00000000 --- a/.claude/hooks/piper-multispeaker-registry.sh +++ /dev/null @@ -1,165 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/piper-multispeaker-registry.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview Multi-Speaker Voice Registry - Maps speaker names to ONNX models and speaker IDs -# @context Enables individual speaker selection from multi-speaker Piper models (e.g., 16Speakers) -# @architecture Static registry mapping speaker names to model files and numeric speaker IDs -# @dependencies piper-voice-manager.sh (voice storage), play-tts-piper.sh (TTS with speaker ID) -# @entrypoints Sourced by voice-manager.sh for multi-speaker voice switching -# @patterns Registry pattern, speaker ID mapping, model-to-speaker association -# @related voice-manager.sh, play-tts-piper.sh, 16Speakers.onnx.json (speaker_id_map) -# - -# Registry of multi-speaker models and their speaker names -# Format: "SpeakerName:model_file:speaker_id:description" -# -# 16Speakers Model (12 US + 4 UK voices): -# Source: LibriVox Public Domain recordings -# Model: 16Speakers.onnx (77MB) -# -MULTISPEAKER_VOICES=( - # US English Speakers (0-11) - "Cori_Samuel:16Speakers:0:US English Female" - "Kara_Shallenberg:16Speakers:1:US English Female" - "Kristin_Hughes:16Speakers:2:US English Female" - "Maria_Kasper:16Speakers:3:US English Female" - "Mike_Pelton:16Speakers:4:US English Male" - "Mark_Nelson:16Speakers:5:US English Male" - "Michael_Scherer:16Speakers:6:US English Male" - "James_K_White:16Speakers:7:US English Male" - "Rose_Ibex:16Speakers:8:US English Female" - "progressingamerica:16Speakers:9:US English Male" - "Steve_C:16Speakers:10:US English Male" - "Owlivia:16Speakers:11:US English Female" - - # UK English Speakers (12-15) - "Paul_Hampton:16Speakers:12:UK English Male" - "Jennifer_Dorr:16Speakers:13:UK English Female" - "Emily_Cripps:16Speakers:14:UK English Female" - "Martin_Clifton:16Speakers:15:UK English Male" -) - -# @function get_multispeaker_info -# @intent Get model and speaker ID for a speaker name -# @why Enables users to select individual speakers from multi-speaker models by name -# @param $1 {string} speaker_name - Speaker name (e.g., "Cori_Samuel", "Rose_Ibex") -# @returns Echoes "model:speaker_id" (e.g., "16Speakers:0") to stdout -# @exitcode 0=speaker found, 1=speaker not found -# @sideeffects None (read-only lookup) -# @edgecases Case-insensitive matching -# @calledby voice-manager.sh switch command -# @calls None (pure bash array iteration) -get_multispeaker_info() { - local speaker_name="$1" - for entry in "${MULTISPEAKER_VOICES[@]}"; do - name="${entry%%:*}" - rest="${entry#*:}" - model="${rest%%:*}" - rest="${rest#*:}" - speaker_id="${rest%%:*}" - - if [[ "${name,,}" == "${speaker_name,,}" ]]; then - echo "$model:$speaker_id" - return 0 - fi - done - return 1 -} - -# @function list_multispeaker_voices -# @intent Display all available multi-speaker voices with descriptions -# @why Help users discover individual speakers within multi-speaker models -# @param None -# @returns None -# @exitcode Always 0 -# @sideeffects Writes formatted list to stdout -# @edgecases None -# @calledby voice-manager.sh list command, /agent-vibes:list -# @calls None (pure bash array iteration) -list_multispeaker_voices() { - echo "🎭 Multi-Speaker Voices (16Speakers Model):" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - local current_model="" - for entry in "${MULTISPEAKER_VOICES[@]}"; do - name="${entry%%:*}" - rest="${entry#*:}" - model="${rest%%:*}" - rest="${rest#*:}" - speaker_id="${rest%%:*}" - description="${rest#*:}" - - # Print section header when model changes - if [[ "$model" != "$current_model" ]]; then - if [[ -n "$current_model" ]]; then - echo "" - fi - echo " Model: $model.onnx" - current_model="$model" - fi - - echo " • $name (ID: $speaker_id) - $description" - done - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo "Usage: /agent-vibes:switch Cori_Samuel" - echo " /agent-vibes:switch Rose_Ibex" -} - -# @function get_multispeaker_description -# @intent Get description for a speaker name -# @why Provide user-friendly info about speaker characteristics -# @param $1 {string} speaker_name - Speaker name -# @returns Echoes description (e.g., "US English Female") to stdout -# @exitcode 0=speaker found, 1=speaker not found -# @sideeffects None (read-only lookup) -# @edgecases Case-insensitive matching -# @calledby voice-manager.sh switch command (for confirmation message) -# @calls None (pure bash array iteration) -get_multispeaker_description() { - local speaker_name="$1" - for entry in "${MULTISPEAKER_VOICES[@]}"; do - name="${entry%%:*}" - rest="${entry#*:}" - rest="${rest#*:}" - rest="${rest#*:}" - description="${rest}" - - if [[ "${name,,}" == "${speaker_name,,}" ]]; then - echo "$description" - return 0 - fi - done - return 1 -} diff --git a/.claude/hooks/piper-voice-manager.sh b/.claude/hooks/piper-voice-manager.sh deleted file mode 100755 index bbfbcbfa..00000000 --- a/.claude/hooks/piper-voice-manager.sh +++ /dev/null @@ -1,293 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/piper-voice-manager.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview Piper Voice Model Management - Downloads, caches, and validates Piper ONNX voice models -# @context Voice model lifecycle management for free offline Piper TTS provider -# @architecture HuggingFace repository integration with local caching, global storage for voice models -# @dependencies curl (downloads), piper binary (TTS synthesis) -# @entrypoints Sourced by play-tts-piper.sh, piper-download-voices.sh, and provider management commands -# @patterns HuggingFace model repository integration, file-based caching (~25MB per voice), global storage -# @related play-tts-piper.sh, piper-download-voices.sh, provider-manager.sh, GitHub Issue #25 -# - -# Base URL for Piper voice models on HuggingFace -PIPER_VOICES_BASE_URL="https://huggingface.co/rhasspy/piper-voices/resolve/main" - -# AI NOTE: Voice storage precedence order: -# 1. PIPER_VOICES_DIR environment variable (highest priority) -# 2. Project-local .claude/piper-voices-dir.txt -# 3. Directory tree search for .claude/piper-voices-dir.txt -# 4. Global ~/.claude/piper-voices-dir.txt -# 5. Default ~/.claude/piper-voices (fallback) -# This allows per-project voice isolation while defaulting to shared global storage. - -# @function get_voice_storage_dir -# @intent Determine directory for storing Piper voice models with precedence chain -# @why Voice models are large (~25MB each) and should be shared globally by default, but allow per-project overrides -# @param None -# @returns Echoes path to voice storage directory -# @exitcode Always 0 -# @sideeffects Creates directory if it doesn't exist -# @edgecases Searches up directory tree for .claude/ folder, supports custom paths via env var or config files -# @calledby All voice management functions (verify_voice, get_voice_path, download_voice, list_downloaded_voices) -# @calls mkdir, cat, dirname -get_voice_storage_dir() { - local voice_dir - - # Check for custom path in environment or config file - if [[ -n "$PIPER_VOICES_DIR" ]]; then - voice_dir="$PIPER_VOICES_DIR" - else - # Check for config file (project-local first, then global) - local config_file - if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -f "$CLAUDE_PROJECT_DIR/.claude/piper-voices-dir.txt" ]]; then - config_file="$CLAUDE_PROJECT_DIR/.claude/piper-voices-dir.txt" - else - # Search up directory tree for .claude/ - local current_dir="$PWD" - while [[ "$current_dir" != "/" ]]; do - if [[ -f "$current_dir/.claude/piper-voices-dir.txt" ]]; then - config_file="$current_dir/.claude/piper-voices-dir.txt" - break - fi - current_dir=$(dirname "$current_dir") - done - - # Check global config - if [[ -z "$config_file" ]] && [[ -f "$HOME/.claude/piper-voices-dir.txt" ]]; then - config_file="$HOME/.claude/piper-voices-dir.txt" - fi - fi - - if [[ -n "$config_file" ]]; then - voice_dir=$(cat "$config_file" | tr -d '[:space:]') - fi - fi - - # Fallback to default global storage - if [[ -z "$voice_dir" ]]; then - voice_dir="$HOME/.claude/piper-voices" - fi - - mkdir -p "$voice_dir" - echo "$voice_dir" -} - -# @function verify_voice -# @intent Check if voice model files exist locally (both .onnx and .onnx.json) -# @why Avoid redundant downloads, detect missing models, ensure model integrity -# @param $1 {string} voice_name - Voice model name (e.g., en_US-lessac-medium) -# @returns None -# @exitcode 0=voice exists and complete, 1=voice missing or incomplete -# @sideeffects None (read-only check) -# @edgecases Requires both ONNX model and JSON config to return success -# @calledby download_voice, piper-download-voices.sh -# @calls get_voice_storage_dir -verify_voice() { - local voice_name="$1" - local voice_dir - voice_dir=$(get_voice_storage_dir) - - local onnx_file="$voice_dir/${voice_name}.onnx" - local json_file="$voice_dir/${voice_name}.onnx.json" - - [[ -f "$onnx_file" ]] && [[ -f "$json_file" ]] -} - -# @function get_voice_path -# @intent Get absolute path to voice model ONNX file for Piper binary -# @why Piper binary requires full absolute path to model file, not just voice name -# @param $1 {string} voice_name - Voice model name -# @returns Echoes absolute path to .onnx file to stdout -# @exitcode 0=success, 1=voice not found -# @sideeffects Writes error message to stderr if voice not found -# @edgecases Returns error if voice not downloaded yet -# @calledby play-tts-piper.sh for TTS synthesis -# @calls get_voice_storage_dir -get_voice_path() { - local voice_name="$1" - local voice_dir - voice_dir=$(get_voice_storage_dir) - - local onnx_file="$voice_dir/${voice_name}.onnx" - - if [[ ! -f "$onnx_file" ]]; then - echo "❌ Voice model not found: $voice_name" >&2 - return 1 - fi - - echo "$onnx_file" -} - -# AI NOTE: Voice name format is: lang_LOCALE-speaker-quality -# Example: en_US-lessac-medium -# - lang: en (language code) -# - LOCALE: US (locale/country code) -# - speaker: lessac (speaker/voice name) -# - quality: medium (model quality: low/medium/high) -# HuggingFace repository structure: {lang}/{lang}_{LOCALE}/{speaker}/{quality}/ - -# @function parse_voice_components -# @intent Extract language, locale, speaker, quality components from voice name -# @why HuggingFace uses structured directory paths based on these components -# @param $1 {string} voice_name - Voice name (e.g., en_US-lessac-medium) -# @returns None (sets global variables) -# @exitcode Always 0 -# @sideeffects Sets global variables: LANG, LOCALE, SPEAKER, QUALITY -# @edgecases Expects specific format: lang_LOCALE-speaker-quality -# @calledby download_voice -# @calls None (pure string manipulation) -parse_voice_components() { - local voice_name="$1" - - # Extract components from voice name - # Format: en_US-lessac-medium - # lang_LOCALE-speaker-quality - - local lang_locale="${voice_name%%-*}" # en_US - local speaker_quality="${voice_name#*-}" # lessac-medium - - LANG="${lang_locale%%_*}" # en - LOCALE="${lang_locale#*_}" # US - SPEAKER="${speaker_quality%%-*}" # lessac - QUALITY="${speaker_quality#*-}" # medium -} - -# @function download_voice -# @intent Download Piper voice model from HuggingFace repository -# @why Provide free offline TTS voices without requiring API keys -# @param $1 {string} voice_name - Voice model name (e.g., en_US-lessac-medium) -# @param $2 {string} lang_code - Language code (optional, inferred from voice_name, unused) -# @returns None -# @exitcode 0=success (already downloaded or newly downloaded), 1=download failed -# @sideeffects Downloads .onnx and .onnx.json files (~25MB total), removes partial downloads on failure -# @edgecases Handles network failures, validates file integrity (non-zero size), skips if already downloaded -# @calledby piper-download-voices.sh, manual voice download commands -# @calls parse_voice_components, verify_voice, get_voice_storage_dir, curl, rm -download_voice() { - local voice_name="$1" - local lang_code="${2:-}" - - local voice_dir - voice_dir=$(get_voice_storage_dir) - - # Check if already downloaded - if verify_voice "$voice_name"; then - echo "✅ Voice already downloaded: $voice_name" - return 0 - fi - - # Parse voice components - parse_voice_components "$voice_name" - - # Construct download URLs - # Path format: {language}/{language}_{locale}/{speaker}/{quality}/{speaker}-{quality}.onnx - local model_path="${LANG}/${LANG}_${LOCALE}/${SPEAKER}/${QUALITY}/${voice_name}" - local onnx_url="${PIPER_VOICES_BASE_URL}/${model_path}.onnx" - local json_url="${PIPER_VOICES_BASE_URL}/${model_path}.onnx.json" - - echo "📥 Downloading Piper voice: $voice_name" - echo " Source: HuggingFace (rhasspy/piper-voices)" - echo " Size: ~25MB" - echo "" - - # Download ONNX model - echo " Downloading model file..." - if ! curl -L --progress-bar -o "$voice_dir/${voice_name}.onnx" "$onnx_url"; then - echo "❌ Failed to download voice model" - rm -f "$voice_dir/${voice_name}.onnx" - return 1 - fi - - # Download JSON config - echo " Downloading config file..." - if ! curl -L -s -o "$voice_dir/${voice_name}.onnx.json" "$json_url"; then - echo "❌ Failed to download voice config" - rm -f "$voice_dir/${voice_name}.onnx" "$voice_dir/${voice_name}.onnx.json" - return 1 - fi - - # Verify file integrity (basic check - file size > 0) - if [[ ! -s "$voice_dir/${voice_name}.onnx" ]]; then - echo "❌ Downloaded file is empty or corrupt" - rm -f "$voice_dir/${voice_name}.onnx" "$voice_dir/${voice_name}.onnx.json" - return 1 - fi - - echo "✅ Voice downloaded successfully: $voice_name" - echo " Location: $voice_dir/${voice_name}.onnx" -} - -# @function list_downloaded_voices -# @intent Display all locally cached voice models with file sizes -# @why Help users see what voices they have available and storage usage -# @param None -# @returns None -# @exitcode Always 0 -# @sideeffects Writes formatted list to stdout -# @edgecases Handles empty voice directory gracefully, uses nullglob to avoid literal *.onnx -# @calledby Voice management commands, /agent-vibes:list -# @calls get_voice_storage_dir, basename, du -list_downloaded_voices() { - local voice_dir - voice_dir=$(get_voice_storage_dir) - - echo "📦 Downloaded Piper Voices:" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - local count=0 - shopt -s nullglob - for onnx_file in "$voice_dir"/*.onnx; do - if [[ -f "$onnx_file" ]]; then - local voice_name - voice_name=$(basename "$onnx_file" .onnx) - local file_size - file_size=$(du -h "$onnx_file" | cut -f1) - echo " • $voice_name ($file_size)" - ((count++)) - fi - done - shopt -u nullglob - - if [[ $count -eq 0 ]]; then - echo " (No voices downloaded yet)" - fi - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "Total: $count voices" -} - -# AI NOTE: This file manages the lifecycle of Piper voice models -# Voice models are ONNX files (~20-30MB each) downloaded from HuggingFace -# Files are cached locally to avoid repeated downloads -# Project-local storage preferred over global for isolation diff --git a/.claude/hooks/play-tts-elevenlabs.sh b/.claude/hooks/play-tts-elevenlabs.sh deleted file mode 100755 index 180843a5..00000000 --- a/.claude/hooks/play-tts-elevenlabs.sh +++ /dev/null @@ -1,404 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/play-tts-elevenlabs.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied. Use at your own risk. See the Apache License for details. -# -# --- -# -# @fileoverview ElevenLabs TTS Provider Implementation - Premium cloud-based TTS -# @context Provider-specific implementation for ElevenLabs API integration with multilingual support -# @architecture Part of multi-provider TTS system - implements provider interface contract -# @dependencies Requires ELEVENLABS_API_KEY, curl, ffmpeg, paplay/aplay/mpg123, jq -# @entrypoints Called by play-tts.sh router with ($1=text, $2=voice_name) when provider=elevenlabs -# @patterns Follows provider contract: accept text/voice, output audio file path, API error handling, SSH audio optimization -# @related play-tts.sh, provider-manager.sh, voices-config.sh, language-manager.sh, GitHub Issue #25 -# - -# Fix locale warnings -export LC_ALL=C - -TEXT="$1" -VOICE_OVERRIDE="$2" # Optional: voice name or direct voice ID -API_KEY="${ELEVENLABS_API_KEY}" - -# Check for project-local pretext configuration -CONFIG_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/config" -CONFIG_FILE="$CONFIG_DIR/agentvibes.json" - -if [[ -f "$CONFIG_FILE" ]] && command -v jq &> /dev/null; then - PRETEXT=$(jq -r '.pretext // empty' "$CONFIG_FILE" 2>/dev/null) - if [[ -n "$PRETEXT" ]]; then - TEXT="$PRETEXT: $TEXT" - fi -fi - -# Limit text length to prevent API issues (max 500 chars for safety) -if [ ${#TEXT} -gt 500 ]; then - TEXT="${TEXT:0:497}..." - echo "⚠️ Text truncated to 500 characters for API safety" -fi - -# Source the single voice configuration file -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/voices-config.sh" -source "$SCRIPT_DIR/language-manager.sh" - -# @function determine_voice_and_language -# @intent Resolve voice name/ID and language for multilingual support -# @why Supports both voice names and direct IDs, plus language-specific voices -# @param $VOICE_OVERRIDE {string} Voice name or ID (optional) -# @returns Sets $VOICE_ID and $LANGUAGE_CODE global variables -# @sideeffects None -# @edgecases Handles unknown voices, falls back to default -VOICE_ID="" -LANGUAGE_CODE="en" # Default to English - -# Get current language setting -CURRENT_LANGUAGE=$(get_language_code) - -# Get language code for API -# ElevenLabs uses 2-letter ISO codes -case "$CURRENT_LANGUAGE" in - spanish) LANGUAGE_CODE="es" ;; - french) LANGUAGE_CODE="fr" ;; - german) LANGUAGE_CODE="de" ;; - italian) LANGUAGE_CODE="it" ;; - portuguese) LANGUAGE_CODE="pt" ;; - chinese) LANGUAGE_CODE="zh" ;; - japanese) LANGUAGE_CODE="ja" ;; - korean) LANGUAGE_CODE="ko" ;; - russian) LANGUAGE_CODE="ru" ;; - polish) LANGUAGE_CODE="pl" ;; - dutch) LANGUAGE_CODE="nl" ;; - turkish) LANGUAGE_CODE="tr" ;; - arabic) LANGUAGE_CODE="ar" ;; - hindi) LANGUAGE_CODE="hi" ;; - swedish) LANGUAGE_CODE="sv" ;; - danish) LANGUAGE_CODE="da" ;; - norwegian) LANGUAGE_CODE="no" ;; - finnish) LANGUAGE_CODE="fi" ;; - czech) LANGUAGE_CODE="cs" ;; - romanian) LANGUAGE_CODE="ro" ;; - ukrainian) LANGUAGE_CODE="uk" ;; - greek) LANGUAGE_CODE="el" ;; - bulgarian) LANGUAGE_CODE="bg" ;; - croatian) LANGUAGE_CODE="hr" ;; - slovak) LANGUAGE_CODE="sk" ;; - english|*) LANGUAGE_CODE="en" ;; -esac - -if [[ -n "$VOICE_OVERRIDE" ]]; then - # Check if override is a voice name (lookup in mapping) - if [[ -n "${VOICES[$VOICE_OVERRIDE]}" ]]; then - VOICE_ID="${VOICES[$VOICE_OVERRIDE]}" - echo "🎤 Using voice: $VOICE_OVERRIDE (session-specific)" - # Check if override looks like a voice ID (alphanumeric string ~20 chars) - elif [[ "$VOICE_OVERRIDE" =~ ^[a-zA-Z0-9]{15,30}$ ]]; then - VOICE_ID="$VOICE_OVERRIDE" - echo "🎤 Using custom voice ID (session-specific)" - else - echo "⚠️ Unknown voice '$VOICE_OVERRIDE', trying language-specific voice" - fi -fi - -# If no override or invalid override, use language-specific voice -if [[ -z "$VOICE_ID" ]]; then - # Try to get voice for current language - LANG_VOICE=$(get_voice_for_language "$CURRENT_LANGUAGE" "elevenlabs" 2>/dev/null) - - if [[ -n "$LANG_VOICE" ]] && [[ -n "${VOICES[$LANG_VOICE]}" ]]; then - VOICE_ID="${VOICES[$LANG_VOICE]}" - echo "🌍 Using $CURRENT_LANGUAGE voice: $LANG_VOICE" - else - # Fall back to voice manager - VOICE_MANAGER_SCRIPT="$(dirname "$0")/voice-manager.sh" - if [[ -f "$VOICE_MANAGER_SCRIPT" ]]; then - VOICE_NAME=$("$VOICE_MANAGER_SCRIPT" get) - VOICE_ID="${VOICES[$VOICE_NAME]}" - fi - - # Final fallback to default - if [[ -z "$VOICE_ID" ]]; then - echo "⚠️ No voice configured, using default" - VOICE_ID="${VOICES[Aria]}" - fi - fi -fi - -# @function validate_inputs -# @intent Check required parameters and API key -# @why Fail fast with clear errors if inputs missing -# @exitcode 1=missing text, 2=missing API key -if [ -z "$TEXT" ]; then - echo "Usage: $0 \"text to speak\" [voice_name_or_id]" - exit 1 -fi - -if [ -z "$API_KEY" ]; then - echo "Error: ELEVENLABS_API_KEY not set" - echo "Set your API key: export ELEVENLABS_API_KEY=your_key_here" - exit 2 -fi - -# @function determine_audio_directory -# @intent Find appropriate directory for audio file storage -# @why Supports project-local and global storage -# @returns Sets $AUDIO_DIR global variable -# @sideeffects None -# @edgecases Handles missing directories, creates if needed -# AI NOTE: Check project dir first, then search up tree, finally fall back to global -if [[ -n "$CLAUDE_PROJECT_DIR" ]]; then - AUDIO_DIR="$CLAUDE_PROJECT_DIR/.claude/audio" -else - # Fallback: try to find .claude directory in current path - CURRENT_DIR="$PWD" - while [[ "$CURRENT_DIR" != "/" ]]; do - if [[ -d "$CURRENT_DIR/.claude" ]]; then - AUDIO_DIR="$CURRENT_DIR/.claude/audio" - break - fi - CURRENT_DIR=$(dirname "$CURRENT_DIR") - done - # Final fallback to global if no project .claude found - if [[ -z "$AUDIO_DIR" ]]; then - AUDIO_DIR="$HOME/.claude/audio" - fi -fi - -mkdir -p "$AUDIO_DIR" -TEMP_FILE="$AUDIO_DIR/tts-$(date +%s).mp3" - -# @function synthesize_with_elevenlabs -# @intent Call ElevenLabs API to generate speech -# @why Encapsulates API call with error handling -# @param Uses globals: $TEXT, $VOICE_ID, $API_KEY -# @returns Creates audio file at $TEMP_FILE -# @exitcode 0=success, 3=API error -# @sideeffects Creates MP3 file in audio directory -# @edgecases Handles network failures, API errors, rate limiting -# Choose model based on language -if [[ "$LANGUAGE_CODE" == "en" ]]; then - MODEL_ID="eleven_monolingual_v1" -else - MODEL_ID="eleven_multilingual_v2" -fi - -# @function get_speech_speed -# @intent Read speed config and map to ElevenLabs API range (0.7-1.2) -# @why ElevenLabs only supports 0.7 (slower) to 1.2 (faster), must map user scale -# @returns Speed value for ElevenLabs API (clamped to 0.7-1.2) -get_speech_speed() { - local config_dir="" - - # Determine config directory - if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then - config_dir="$CLAUDE_PROJECT_DIR/.claude/config" - else - # Try to find .claude in current path - local current_dir="$PWD" - while [[ "$current_dir" != "/" ]]; do - if [[ -d "$current_dir/.claude" ]]; then - config_dir="$current_dir/.claude/config" - break - fi - current_dir=$(dirname "$current_dir") - done - # Fallback to global - if [[ -z "$config_dir" ]]; then - config_dir="$HOME/.claude/config" - fi - fi - - local main_speed_file="$config_dir/tts-speech-rate.txt" - local target_speed_file="$config_dir/tts-target-speech-rate.txt" - - # Legacy file paths for backward compatibility - local legacy_main_speed_file="$config_dir/piper-speech-rate.txt" - local legacy_target_speed_file="$config_dir/piper-target-speech-rate.txt" - - local user_speed="1.0" - - # If this is a non-English voice and target config exists, use it - if [[ "$CURRENT_LANGUAGE" != "english" ]]; then - if [[ -f "$target_speed_file" ]]; then - user_speed=$(cat "$target_speed_file" 2>/dev/null || echo "1.0") - elif [[ -f "$legacy_target_speed_file" ]]; then - user_speed=$(cat "$legacy_target_speed_file" 2>/dev/null || echo "1.0") - else - user_speed="0.5" # Default slower for learning - fi - else - # Otherwise use main config if available - if [[ -f "$main_speed_file" ]]; then - user_speed=$(grep -v '^#' "$main_speed_file" 2>/dev/null | grep -v '^$' | tail -1 || echo "1.0") - elif [[ -f "$legacy_main_speed_file" ]]; then - user_speed=$(grep -v '^#' "$legacy_main_speed_file" 2>/dev/null | grep -v '^$' | tail -1 || echo "1.0") - fi - fi - - # Map user scale (0.5=slower, 1.0=normal, 2.0=faster, 3.0=very fast) - # to ElevenLabs range (0.7=slower, 1.0=normal, 1.2=faster) - # Formula: elevenlabs_speed = 0.7 + (user_speed - 0.5) * 0.2 - # This maps: 0.5→0.7, 1.0→0.8, 2.0→1.0, 3.0→1.2 - # Actually, let's use a better mapping: - # 0.5x → 0.7 (slowest ElevenLabs) - # 1.0x → 1.0 (normal) - # 2.0x → 1.15 - # 3.0x → 1.2 (fastest ElevenLabs) - - if command -v bc &> /dev/null; then - local eleven_speed - if (( $(echo "$user_speed <= 0.5" | bc -l) )); then - eleven_speed="0.7" - elif (( $(echo "$user_speed >= 3.0" | bc -l) )); then - eleven_speed="1.2" - elif (( $(echo "$user_speed <= 1.0" | bc -l) )); then - # Map 0.5-1.0 to 0.7-1.0 - eleven_speed=$(echo "scale=2; 0.7 + ($user_speed - 0.5) * 0.6" | bc -l) - else - # Map 1.0-3.0 to 1.0-1.2 - eleven_speed=$(echo "scale=2; 1.0 + ($user_speed - 1.0) * 0.1" | bc -l) - fi - echo "$eleven_speed" - else - # Fallback without bc: just clamp to safe values - if (( $(awk 'BEGIN {print ("'$user_speed'" <= 0.5)}') )); then - echo "0.7" - elif (( $(awk 'BEGIN {print ("'$user_speed'" >= 2.0)}') )); then - echo "1.2" - else - echo "1.0" - fi - fi -} - -SPEECH_SPEED=$(get_speech_speed) - -# Build JSON payload with jq for proper escaping -PAYLOAD=$(jq -n \ - --arg text "$TEXT" \ - --arg model "$MODEL_ID" \ - --arg lang "$LANGUAGE_CODE" \ - --argjson speed "$SPEECH_SPEED" \ - '{ - text: $text, - model_id: $model, - language_code: $lang, - voice_settings: { - stability: 0.5, - similarity_boost: 0.75, - speed: $speed - } - }') - -curl -s -X POST "https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}" \ - -H "xi-api-key: ${API_KEY}" \ - -H "Content-Type: application/json" \ - -d "$PAYLOAD" \ - -o "${TEMP_FILE}" - -# @function add_silence_padding -# @intent Add silence to beginning of audio to prevent WSL static -# @why WSL audio subsystem cuts off first ~200ms, causing static/clipping -# @param Uses global: $TEMP_FILE -# @returns Updates $TEMP_FILE to padded version -# @sideeffects Modifies audio file, removes original -# @edgecases Gracefully falls back to unpadded if ffmpeg unavailable -# Add silence padding to prevent WSL audio static -if [ -f "${TEMP_FILE}" ]; then - # Check if ffmpeg is available for adding padding - if command -v ffmpeg &> /dev/null; then - PADDED_FILE="$AUDIO_DIR/tts-padded-$(date +%s).mp3" - # Add 200ms of silence at the beginning to prevent static - # Note: ElevenLabs returns mono audio, so we use mono silence - ffmpeg -f lavfi -i anullsrc=r=44100:cl=mono:d=0.2 -i "${TEMP_FILE}" \ - -filter_complex "[0:a][1:a]concat=n=2:v=0:a=1[out]" \ - -map "[out]" -c:a libmp3lame -b:a 128k -y "${PADDED_FILE}" 2>/dev/null - - if [ -f "${PADDED_FILE}" ]; then - # Use padded file and clean up original - rm -f "${TEMP_FILE}" - TEMP_FILE="${PADDED_FILE}" - fi - # If padding failed, just use original file - fi - - # @function play_audio - # @intent Play generated audio file using available player with sequential playback - # @why Support multiple audio players and prevent overlapping audio in learning mode - # @param Uses global: $TEMP_FILE, $CURRENT_LANGUAGE - # @sideeffects Plays audio with lock mechanism for sequential playback - # @edgecases Falls through players until one works - LOCK_FILE="/tmp/agentvibes-audio.lock" - - # Wait for previous audio to finish (max 30 seconds) - for i in {1..60}; do - if [ ! -f "$LOCK_FILE" ]; then - break - fi - sleep 0.5 - done - - # Track last target language audio for replay command - if [[ "$CURRENT_LANGUAGE" != "english" ]]; then - TARGET_AUDIO_FILE="${CLAUDE_PROJECT_DIR:-.}/.claude/last-target-audio.txt" - echo "${TEMP_FILE}" > "$TARGET_AUDIO_FILE" - fi - - # Create lock and play audio - touch "$LOCK_FILE" - - # Get audio duration for proper lock timing - DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${TEMP_FILE}" 2>/dev/null) - DURATION=${DURATION%.*} # Round to integer - DURATION=${DURATION:-1} # Default to 1 second if detection fails - - # Convert to 48kHz stereo WAV for better SSH tunnel compatibility - # ElevenLabs returns 44.1kHz mono MP3, which causes static over SSH audio tunnels - # Converting to 48kHz stereo (Windows/PulseAudio native format) eliminates the static - if [[ -n "$SSH_CONNECTION" ]] || [[ -n "$SSH_CLIENT" ]] || [[ -n "$VSCODE_IPC_HOOK_CLI" ]]; then - CONVERTED_FILE="${TEMP_FILE%.mp3}.wav" - if ffmpeg -i "${TEMP_FILE}" -ar 48000 -ac 2 "${CONVERTED_FILE}" -y 2>/dev/null; then - TEMP_FILE="${CONVERTED_FILE}" - fi - fi - - # Play audio (WSL/Linux) in background to avoid blocking, fully detached (skip if in test mode) - if [[ "${AGENTVIBES_TEST_MODE:-false}" != "true" ]]; then - (paplay "${TEMP_FILE}" || aplay "${TEMP_FILE}" || mpg123 "${TEMP_FILE}") >/dev/null 2>&1 & - PLAYER_PID=$! - fi - - # Wait for audio to finish, then release lock - (sleep $DURATION; rm -f "$LOCK_FILE") & - disown - - # Keep temp files for later review - cleaned up weekly by cron - echo "🎵 Saved to: ${TEMP_FILE}" - echo "🎤 Voice used: ${VOICE_NAME} (${VOICE_ID})" -else - echo "❌ Failed to generate audio - API may be unavailable" - echo "Check your API key and network connection" - exit 3 -fi diff --git a/.claude/hooks/play-tts-piper.sh b/.claude/hooks/play-tts-piper.sh deleted file mode 100755 index 3b383b49..00000000 --- a/.claude/hooks/play-tts-piper.sh +++ /dev/null @@ -1,338 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/play-tts-piper.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied. Use at your own risk. See the Apache License for details. -# -# --- -# -# @fileoverview Piper TTS Provider Implementation - Free, offline neural TTS -# @context Provides local, privacy-first TTS alternative to cloud services for WSL/Linux -# @architecture Implements provider interface contract for Piper binary integration -# @dependencies piper (pipx), piper-voice-manager.sh, mpv/aplay, ffmpeg (optional padding) -# @entrypoints Called by play-tts.sh router when provider=piper -# @patterns Provider contract: text/voice → audio file path, voice auto-download, language-aware synthesis -# @related play-tts.sh, piper-voice-manager.sh, language-manager.sh, GitHub Issue #25 -# - -# Fix locale warnings -export LC_ALL=C - -TEXT="$1" -VOICE_OVERRIDE="$2" # Optional: voice model name - -# Source voice manager and language manager -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/piper-voice-manager.sh" -source "$SCRIPT_DIR/language-manager.sh" - -# Default voice for Piper -DEFAULT_VOICE="en_US-lessac-medium" - -# @function determine_voice_model -# @intent Resolve voice name to Piper model name with language support -# @why Support voice override, language-specific voices, and default fallback -# @param Uses global: $VOICE_OVERRIDE -# @returns Sets $VOICE_MODEL global variable -# @sideeffects None -VOICE_MODEL="" - -# Get current language setting -CURRENT_LANGUAGE=$(get_language_code) - -if [[ -n "$VOICE_OVERRIDE" ]]; then - # Use override if provided - VOICE_MODEL="$VOICE_OVERRIDE" - echo "🎤 Using voice: $VOICE_OVERRIDE (session-specific)" -else - # Try to get voice from voice file (check CLAUDE_PROJECT_DIR first for MCP context) - VOICE_FILE="" - - # Priority order: - # 1. CLAUDE_PROJECT_DIR env var (set by MCP for project-specific settings) - # 2. Script location (for direct slash command usage) - # 3. Global ~/.claude (fallback) - - if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -f "$CLAUDE_PROJECT_DIR/.claude/tts-voice.txt" ]]; then - # MCP context: Use the project directory where MCP was invoked - VOICE_FILE="$CLAUDE_PROJECT_DIR/.claude/tts-voice.txt" - elif [[ -f "$SCRIPT_DIR/../tts-voice.txt" ]]; then - # Direct usage: Use script location - VOICE_FILE="$SCRIPT_DIR/../tts-voice.txt" - elif [[ -f "$HOME/.claude/tts-voice.txt" ]]; then - # Fallback: Use global - VOICE_FILE="$HOME/.claude/tts-voice.txt" - fi - - if [[ -n "$VOICE_FILE" ]]; then - FILE_VOICE=$(cat "$VOICE_FILE" 2>/dev/null) - - # Check for multi-speaker voice (model + speaker ID stored separately) - # Use same directory as VOICE_FILE for consistency - VOICE_DIR=$(dirname "$VOICE_FILE") - MODEL_FILE="$VOICE_DIR/tts-piper-model.txt" - SPEAKER_ID_FILE="$VOICE_DIR/tts-piper-speaker-id.txt" - - if [[ -f "$MODEL_FILE" ]] && [[ -f "$SPEAKER_ID_FILE" ]]; then - # Multi-speaker voice - VOICE_MODEL=$(cat "$MODEL_FILE" 2>/dev/null) - SPEAKER_ID=$(cat "$SPEAKER_ID_FILE" 2>/dev/null) - echo "🎭 Using multi-speaker voice: $FILE_VOICE (Model: $VOICE_MODEL, Speaker ID: $SPEAKER_ID)" - # Check if it's a standard Piper model name or custom voice (just use as-is) - elif [[ -n "$FILE_VOICE" ]]; then - VOICE_MODEL="$FILE_VOICE" - fi - fi - - # If no Piper voice from file, try language-specific voice - if [[ -z "$VOICE_MODEL" ]]; then - LANG_VOICE=$(get_voice_for_language "$CURRENT_LANGUAGE" "piper" 2>/dev/null) - - if [[ -n "$LANG_VOICE" ]]; then - VOICE_MODEL="$LANG_VOICE" - echo "🌍 Using $CURRENT_LANGUAGE voice: $LANG_VOICE (Piper)" - else - # Use default voice - VOICE_MODEL="$DEFAULT_VOICE" - fi - fi -fi - -# @function validate_inputs -# @intent Check required parameters -# @why Fail fast with clear errors if inputs missing -# @exitcode 1=missing text, 2=missing piper binary -if [[ -z "$TEXT" ]]; then - echo "Usage: $0 \"text to speak\" [voice_model_name]" - exit 1 -fi - -# Check if Piper is installed -if ! command -v piper &> /dev/null; then - echo "❌ Error: Piper TTS not installed" - echo "Install with: pipx install piper-tts" - echo "Or run: .claude/hooks/piper-installer.sh" - exit 2 -fi - -# @function ensure_voice_downloaded -# @intent Download voice model if not cached -# @why Provide seamless experience with automatic downloads -# @param Uses global: $VOICE_MODEL -# @sideeffects Downloads voice model files -# @edgecases Prompts user for consent before downloading -if ! verify_voice "$VOICE_MODEL"; then - echo "📥 Voice model not found: $VOICE_MODEL" - echo " File size: ~25MB" - echo " Preview: https://huggingface.co/rhasspy/piper-voices" - echo "" - read -p " Download this voice model? [y/N]: " -n 1 -r - echo - - if [[ $REPLY =~ ^[Yy]$ ]]; then - if ! download_voice "$VOICE_MODEL"; then - echo "❌ Failed to download voice model" - echo "Fix: Download manually or choose different voice" - exit 3 - fi - else - echo "❌ Voice download cancelled" - exit 3 - fi -fi - -# Get voice model path -VOICE_PATH=$(get_voice_path "$VOICE_MODEL") -if [[ $? -ne 0 ]]; then - echo "❌ Voice model path not found: $VOICE_MODEL" - exit 3 -fi - -# @function determine_audio_directory -# @intent Find appropriate directory for audio file storage -# @why Supports project-local and global storage -# @returns Sets $AUDIO_DIR global variable -if [[ -n "$CLAUDE_PROJECT_DIR" ]]; then - AUDIO_DIR="$CLAUDE_PROJECT_DIR/.claude/audio" -else - # Fallback: try to find .claude directory in current path - CURRENT_DIR="$PWD" - while [[ "$CURRENT_DIR" != "/" ]]; do - if [[ -d "$CURRENT_DIR/.claude" ]]; then - AUDIO_DIR="$CURRENT_DIR/.claude/audio" - break - fi - CURRENT_DIR=$(dirname "$CURRENT_DIR") - done - # Final fallback to global if no project .claude found - if [[ -z "$AUDIO_DIR" ]]; then - AUDIO_DIR="$HOME/.claude/audio" - fi -fi - -mkdir -p "$AUDIO_DIR" -TEMP_FILE="$AUDIO_DIR/tts-$(date +%s).wav" - -# @function get_speech_rate -# @intent Determine speech rate for Piper synthesis -# @why Convert user-facing speed (0.5=slower, 2.0=faster) to Piper length-scale (inverted) -# @returns Piper length-scale value (inverted from user scale) -# @note Piper uses length-scale where higher=slower, opposite of user expectation -get_speech_rate() { - local target_config="" - local main_config="" - - # Check for target-specific config first (new and legacy paths) - if [[ -f "$SCRIPT_DIR/../config/tts-target-speech-rate.txt" ]]; then - target_config="$SCRIPT_DIR/../config/tts-target-speech-rate.txt" - elif [[ -f "$HOME/.claude/config/tts-target-speech-rate.txt" ]]; then - target_config="$HOME/.claude/config/tts-target-speech-rate.txt" - elif [[ -f "$SCRIPT_DIR/../config/piper-target-speech-rate.txt" ]]; then - target_config="$SCRIPT_DIR/../config/piper-target-speech-rate.txt" - elif [[ -f "$HOME/.claude/config/piper-target-speech-rate.txt" ]]; then - target_config="$HOME/.claude/config/piper-target-speech-rate.txt" - fi - - # Check for main config (new and legacy paths) - if [[ -f "$SCRIPT_DIR/../config/tts-speech-rate.txt" ]]; then - main_config="$SCRIPT_DIR/../config/tts-speech-rate.txt" - elif [[ -f "$HOME/.claude/config/tts-speech-rate.txt" ]]; then - main_config="$HOME/.claude/config/tts-speech-rate.txt" - elif [[ -f "$SCRIPT_DIR/../config/piper-speech-rate.txt" ]]; then - main_config="$SCRIPT_DIR/../config/piper-speech-rate.txt" - elif [[ -f "$HOME/.claude/config/piper-speech-rate.txt" ]]; then - main_config="$HOME/.claude/config/piper-speech-rate.txt" - fi - - # If this is a non-English voice and target config exists, use it - if [[ "$CURRENT_LANGUAGE" != "english" ]] && [[ -n "$target_config" ]]; then - local user_speed=$(cat "$target_config" 2>/dev/null) - # Convert user speed to Piper length-scale (invert) - # User: 0.5=slower, 1.0=normal, 2.0=faster - # Piper: 2.0=slower, 1.0=normal, 0.5=faster - # Formula: piper_length_scale = 1.0 / user_speed - echo "scale=2; 1.0 / $user_speed" | bc -l 2>/dev/null || echo "1.0" - return - fi - - # Otherwise use main config if available - if [[ -n "$main_config" ]]; then - local user_speed=$(grep -v '^#' "$main_config" 2>/dev/null | grep -v '^$' | tail -1) - echo "scale=2; 1.0 / $user_speed" | bc -l 2>/dev/null || echo "1.0" - return - fi - - # Default: 1.0 (normal) for English, 2.0 (slower) for learning - if [[ "$CURRENT_LANGUAGE" != "english" ]]; then - echo "2.0" - else - echo "1.0" - fi -} - -SPEECH_RATE=$(get_speech_rate) - -# @function synthesize_with_piper -# @intent Generate speech using Piper TTS -# @why Provides free, offline TTS alternative -# @param Uses globals: $TEXT, $VOICE_PATH, $SPEECH_RATE, $SPEAKER_ID (optional) -# @returns Creates WAV file at $TEMP_FILE -# @exitcode 0=success, 4=synthesis error -# @sideeffects Creates audio file -# @edgecases Handles piper errors, invalid models, multi-speaker voices -if [[ -n "$SPEAKER_ID" ]]; then - # Multi-speaker voice: Pass speaker ID - echo "$TEXT" | piper --model "$VOICE_PATH" --speaker "$SPEAKER_ID" --length-scale "$SPEECH_RATE" --output_file "$TEMP_FILE" 2>/dev/null -else - # Single-speaker voice - echo "$TEXT" | piper --model "$VOICE_PATH" --length-scale "$SPEECH_RATE" --output_file "$TEMP_FILE" 2>/dev/null -fi - -if [[ ! -f "$TEMP_FILE" ]] || [[ ! -s "$TEMP_FILE" ]]; then - echo "❌ Failed to synthesize speech with Piper" - echo "Voice model: $VOICE_MODEL" - echo "Check that voice model is valid" - exit 4 -fi - -# @function add_silence_padding -# @intent Add silence to prevent WSL audio static -# @why WSL audio subsystem cuts off first ~200ms -# @param Uses global: $TEMP_FILE -# @returns Updates $TEMP_FILE to padded version -# @sideeffects Modifies audio file -# AI NOTE: Use ffmpeg if available, otherwise skip padding (degraded experience) -if command -v ffmpeg &> /dev/null; then - PADDED_FILE="$AUDIO_DIR/tts-padded-$(date +%s).wav" - # Add 200ms of silence at the beginning - ffmpeg -f lavfi -i anullsrc=r=44100:cl=stereo:d=0.2 -i "$TEMP_FILE" \ - -filter_complex "[0:a][1:a]concat=n=2:v=0:a=1[out]" \ - -map "[out]" -y "$PADDED_FILE" 2>/dev/null - - if [[ -f "$PADDED_FILE" ]]; then - rm -f "$TEMP_FILE" - TEMP_FILE="$PADDED_FILE" - fi -fi - -# @function play_audio -# @intent Play generated audio using available player with sequential playback -# @why Support multiple audio players and prevent overlapping audio in learning mode -# @param Uses global: $TEMP_FILE, $CURRENT_LANGUAGE -# @sideeffects Plays audio with lock mechanism for sequential playback -LOCK_FILE="/tmp/agentvibes-audio.lock" - -# Wait for previous audio to finish (max 30 seconds) -for i in {1..60}; do - if [ ! -f "$LOCK_FILE" ]; then - break - fi - sleep 0.5 -done - -# Track last target language audio for replay command -if [[ "$CURRENT_LANGUAGE" != "english" ]]; then - TARGET_AUDIO_FILE="${CLAUDE_PROJECT_DIR:-.}/.claude/last-target-audio.txt" - echo "$TEMP_FILE" > "$TARGET_AUDIO_FILE" -fi - -# Create lock and play audio -touch "$LOCK_FILE" - -# Get audio duration for proper lock timing -DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$TEMP_FILE" 2>/dev/null) -DURATION=${DURATION%.*} # Round to integer -DURATION=${DURATION:-1} # Default to 1 second if detection fails - -# Play audio in background (skip if in test mode) -if [[ "${AGENTVIBES_TEST_MODE:-false}" != "true" ]]; then - (mpv "$TEMP_FILE" || aplay "$TEMP_FILE" || paplay "$TEMP_FILE") >/dev/null 2>&1 & - PLAYER_PID=$! -fi - -# Wait for audio to finish, then release lock -(sleep $DURATION; rm -f "$LOCK_FILE") & -disown - -echo "🎵 Saved to: $TEMP_FILE" -echo "🎤 Voice used: $VOICE_MODEL (Piper TTS)" diff --git a/.claude/hooks/play-tts.sh b/.claude/hooks/play-tts.sh deleted file mode 100755 index 107fdc14..00000000 --- a/.claude/hooks/play-tts.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/play-tts.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview TTS Provider Router with Language Learning Support -# @context Routes TTS requests to active provider (ElevenLabs or Piper) -# @architecture Provider abstraction layer - single entry point for all TTS -# @dependencies provider-manager.sh, play-tts-elevenlabs.sh, play-tts-piper.sh, github-star-reminder.sh -# @entrypoints Called by hooks, slash commands, personality-manager.sh, and all TTS features -# @patterns Provider pattern - delegates to provider-specific implementations, auto-detects provider from voice name -# @related provider-manager.sh, play-tts-elevenlabs.sh, play-tts-piper.sh, learn-manager.sh -# - -# Fix locale warnings -export LC_ALL=C - -TEXT="$1" -VOICE_OVERRIDE="$2" # Optional: voice name or ID - -# Get script directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Source provider manager to get active provider -source "$SCRIPT_DIR/provider-manager.sh" - -# Get active provider -ACTIVE_PROVIDER=$(get_active_provider) - -# Show GitHub star reminder (once per day) -"$SCRIPT_DIR/github-star-reminder.sh" 2>/dev/null || true - -# @function detect_voice_provider -# @intent Auto-detect provider from voice name (for mixed-provider support) -# @why Allow ElevenLabs for main language + Piper for target language -# @param $1 voice name/ID -# @returns Provider name (elevenlabs or piper) -detect_voice_provider() { - local voice="$1" - # Piper voice names contain underscore and dash (e.g., es_ES-davefx-medium) - if [[ "$voice" == *"_"*"-"* ]]; then - echo "piper" - else - echo "$ACTIVE_PROVIDER" - fi -} - -# Override provider if voice indicates different provider (mixed-provider mode) -if [[ -n "$VOICE_OVERRIDE" ]]; then - DETECTED_PROVIDER=$(detect_voice_provider "$VOICE_OVERRIDE") - if [[ "$DETECTED_PROVIDER" != "$ACTIVE_PROVIDER" ]]; then - ACTIVE_PROVIDER="$DETECTED_PROVIDER" - fi -fi - -# Normal single-language mode - route to appropriate provider implementation -# Note: For learning mode, the output style will call this script TWICE: -# 1. First call with main language text and current voice -# 2. Second call with translated text and target voice -case "$ACTIVE_PROVIDER" in - elevenlabs) - exec "$SCRIPT_DIR/play-tts-elevenlabs.sh" "$TEXT" "$VOICE_OVERRIDE" - ;; - piper) - exec "$SCRIPT_DIR/play-tts-piper.sh" "$TEXT" "$VOICE_OVERRIDE" - ;; - *) - echo "❌ Unknown provider: $ACTIVE_PROVIDER" - echo " Run: /agent-vibes:provider list" - exit 1 - ;; -esac diff --git a/.claude/hooks/provider-commands.sh b/.claude/hooks/provider-commands.sh deleted file mode 100755 index 35542f10..00000000 --- a/.claude/hooks/provider-commands.sh +++ /dev/null @@ -1,540 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/provider-commands.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview Provider management slash commands -# @context User-facing commands for switching and managing TTS providers -# @architecture Part of /agent-vibes:* command system with language compatibility checking -# @dependencies provider-manager.sh, language-manager.sh, voice-manager.sh, piper-voice-manager.sh -# @entrypoints Called by /agent-vibes:provider slash commands (list, switch, info, test, get, preview) -# @patterns Interactive confirmations, platform detection, language compatibility validation -# @related provider-manager.sh, play-tts.sh, voice-manager.sh, piper-voice-manager.sh -# - -# Get script directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/provider-manager.sh" -source "$SCRIPT_DIR/language-manager.sh" - -COMMAND="${1:-help}" - -# @function is_language_supported -# @intent Check if a language is supported by a provider -# @param $1 {string} language - Language code (e.g., "spanish", "french") -# @param $2 {string} provider - Provider name (e.g., "elevenlabs", "piper") -# @returns 0 if supported, 1 if not -is_language_supported() { - local language="$1" - local provider="$2" - - # English is always supported - if [[ "$language" == "english" ]] || [[ "$language" == "en" ]]; then - return 0 - fi - - case "$provider" in - elevenlabs) - # ElevenLabs supports all languages via multilingual voices - return 0 - ;; - piper) - # Piper only supports English natively - return 1 - ;; - *) - return 1 - ;; - esac -} - -# @function provider_list -# @intent Display all available providers with status -provider_list() { - local current_provider - current_provider=$(get_active_provider) - - echo "┌────────────────────────────────────────────────────────────┐" - echo "│ Available TTS Providers │" - echo "├────────────────────────────────────────────────────────────┤" - - # ElevenLabs - if [[ "$current_provider" == "elevenlabs" ]]; then - echo "│ ✓ ElevenLabs Premium quality ⭐⭐⭐⭐⭐ [ACTIVE] │" - else - echo "│ ElevenLabs Premium quality ⭐⭐⭐⭐⭐ │" - fi - echo "│ Cost: Free tier + \$5-22/mo │" - echo "│ Platform: All (Windows, macOS, Linux, WSL) │" - echo "│ Offline: No │" - echo "│ │" - - # Piper - if [[ "$current_provider" == "piper" ]]; then - echo "│ ✓ Piper TTS Free, offline ⭐⭐⭐⭐ [ACTIVE] │" - else - echo "│ Piper TTS Free, offline ⭐⭐⭐⭐ │" - fi - echo "│ Cost: Free forever │" - echo "│ Platform: WSL, Linux only │" - echo "│ Offline: Yes │" - echo "└────────────────────────────────────────────────────────────┘" - echo "" - echo "Learn more: agentvibes.org/providers" -} - -# @function provider_switch -# @intent Switch to a different TTS provider -provider_switch() { - local new_provider="$1" - local force_mode=false - - # Check for --force or --yes flag - if [[ "$2" == "--force" ]] || [[ "$2" == "--yes" ]] || [[ "$2" == "-y" ]]; then - force_mode=true - fi - - # Auto-enable force mode if running non-interactively (e.g., from MCP) - # Check multiple conditions for MCP/non-interactive context - if [[ ! -t 0 ]] || [[ -n "$CLAUDE_PROJECT_DIR" ]] || [[ -n "$MCP_SERVER" ]]; then - force_mode=true - fi - - if [[ -z "$new_provider" ]]; then - echo "❌ Error: Provider name required" - echo "Usage: /agent-vibes:provider switch [--force]" - echo "Available: elevenlabs, piper" - return 1 - fi - - # Validate provider - if ! validate_provider "$new_provider"; then - echo "❌ Invalid provider: $new_provider" - echo "" - echo "Available providers:" - list_providers - return 1 - fi - - local current_provider - current_provider=$(get_active_provider) - - if [[ "$current_provider" == "$new_provider" ]]; then - echo "✓ Already using $new_provider" - return 0 - fi - - # Platform check for Piper - if [[ "$new_provider" == "piper" ]]; then - if ! grep -qi microsoft /proc/version 2>/dev/null && [[ "$(uname -s)" != "Linux" ]]; then - echo "❌ Piper is only supported on WSL and Linux" - echo "Your platform: $(uname -s)" - echo "See: agentvibes.org/platform-support" - return 1 - fi - - # Check if Piper is installed - if ! command -v piper &> /dev/null; then - echo "❌ Piper TTS is not installed" - echo "" - echo "Install with: pipx install piper-tts" - echo "Or run: .claude/hooks/piper-installer.sh" - echo "" - echo "Visit: agentvibes.org/install-piper" - return 1 - fi - fi - - # Check language compatibility - local current_language - current_language=$(get_language_code) - - if [[ "$current_language" != "english" ]]; then - if ! is_language_supported "$current_language" "$new_provider" 2>/dev/null; then - echo "⚠️ Language Compatibility Warning" - echo "" - echo "Current language: $current_language" - echo "Target provider: $new_provider" - echo "" - echo "❌ Language '$current_language' is not natively supported by $new_provider" - echo " Will fall back to English when using $new_provider" - echo "" - echo "Options:" - echo " 1. Continue anyway (will use English)" - echo " 2. Switch language to English" - echo " 3. Cancel provider switch" - echo "" - - # Skip prompt in force mode - if [[ "$force_mode" == true ]]; then - echo "⏩ Force mode: Continuing with fallback to English..." - else - read -p "Choose option [1-3]: " -n 1 -r - echo - - case $REPLY in - 1) - echo "⏩ Continuing with fallback to English..." - ;; - 2) - echo "🔄 Switching language to English..." - "$SCRIPT_DIR/language-manager.sh" set english - ;; - 3) - echo "❌ Provider switch cancelled" - return 1 - ;; - *) - echo "❌ Invalid option, cancelling" - return 1 - ;; - esac - fi - fi - fi - - # Confirm switch (skip in force mode) - if [[ "$force_mode" != true ]]; then - echo "" - echo "⚠️ Switch to $(echo $new_provider | tr '[:lower:]' '[:upper:]')?" - echo "" - echo "Current: $current_provider" - echo "New: $new_provider" - if [[ "$current_language" != "english" ]]; then - echo "Language: $current_language" - fi - echo "" - read -p "Continue? [y/N]: " -n 1 -r - echo - - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "❌ Switch cancelled" - return 1 - fi - else - echo "⏩ Force mode: Switching to $new_provider..." - fi - - # Perform switch - set_active_provider "$new_provider" - - # Update target voice if language learning mode is active - local target_lang_file="" - local target_voice_file="" - - # Check project-local first, then global - if [[ -d "$SCRIPT_DIR/../.." ]]; then - local project_dir="$SCRIPT_DIR/../.." - if [[ -f "$project_dir/.claude/tts-target-language.txt" ]]; then - target_lang_file="$project_dir/.claude/tts-target-language.txt" - target_voice_file="$project_dir/.claude/tts-target-voice.txt" - fi - fi - - # Fallback to global - if [[ -z "$target_lang_file" ]]; then - if [[ -f "$HOME/.claude/tts-target-language.txt" ]]; then - target_lang_file="$HOME/.claude/tts-target-language.txt" - target_voice_file="$HOME/.claude/tts-target-voice.txt" - fi - fi - - # If target language is set, update voice for new provider - if [[ -n "$target_lang_file" ]] && [[ -f "$target_lang_file" ]]; then - local target_lang - target_lang=$(cat "$target_lang_file") - - if [[ -n "$target_lang" ]]; then - # Get the recommended voice for this language with new provider - local new_target_voice - new_target_voice=$(get_voice_for_language "$target_lang" "$new_provider") - - if [[ -n "$new_target_voice" ]]; then - echo "$new_target_voice" > "$target_voice_file" - echo "" - echo "🔄 Updated target language voice:" - echo " Language: $target_lang" - echo " Voice: $new_target_voice (for $new_provider)" - fi - fi - fi - - # Test new provider - echo "" - echo "🔊 Testing provider..." - "$SCRIPT_DIR/play-tts.sh" "Provider switched to $new_provider successfully" 2>/dev/null - - echo "" - echo "✓ Provider switch complete!" - echo "Visit agentvibes.org for tips and tricks" -} - -# @function provider_info -# @intent Show detailed information about a provider -provider_info() { - local provider_name="$1" - - if [[ -z "$provider_name" ]]; then - echo "❌ Error: Provider name required" - echo "Usage: /agent-vibes:provider info " - return 1 - fi - - case "$provider_name" in - elevenlabs) - echo "┌────────────────────────────────────────────────────────────┐" - echo "│ ElevenLabs - Premium TTS Provider │" - echo "├────────────────────────────────────────────────────────────┤" - echo "│ Quality: ⭐⭐⭐⭐⭐ (Highest available) │" - echo "│ Cost: Free tier + \$5-22/mo │" - echo "│ Platform: All (Windows, macOS, Linux, WSL) │" - echo "│ Offline: No (requires internet) │" - echo "│ │" - echo "│ Trade-offs: │" - echo "│ + Highest voice quality and naturalness │" - echo "│ + 50+ premium voices available │" - echo "│ + Multilingual support (30+ languages) │" - echo "│ - Requires API key and internet │" - echo "│ - Costs money after free tier │" - echo "│ │" - echo "│ Best for: Premium quality, multilingual needs │" - echo "└────────────────────────────────────────────────────────────┘" - echo "" - echo "Full comparison: agentvibes.org/providers" - ;; - - piper) - echo "┌────────────────────────────────────────────────────────────┐" - echo "│ Piper TTS - Free Offline Provider │" - echo "├────────────────────────────────────────────────────────────┤" - echo "│ Quality: ⭐⭐⭐⭐ (Very good) │" - echo "│ Cost: Free forever │" - echo "│ Platform: WSL, Linux only │" - echo "│ Offline: Yes (fully local) │" - echo "│ │" - echo "│ Trade-offs: │" - echo "│ + Completely free, no API costs │" - echo "│ + Works offline, no internet needed │" - echo "│ + Fast synthesis (local processing) │" - echo "│ - WSL/Linux only (no macOS/Windows) │" - echo "│ - Slightly lower quality than ElevenLabs │" - echo "│ │" - echo "│ Best for: Budget-conscious, offline use, privacy │" - echo "└────────────────────────────────────────────────────────────┘" - echo "" - echo "Full comparison: agentvibes.org/providers" - ;; - - *) - echo "❌ Unknown provider: $provider_name" - echo "Available: elevenlabs, piper" - ;; - esac -} - -# @function provider_test -# @intent Test current provider with sample audio -provider_test() { - local current_provider - current_provider=$(get_active_provider) - - echo "🔊 Testing provider: $current_provider" - echo "" - - "$SCRIPT_DIR/play-tts.sh" "Provider test successful. Audio is working correctly with $current_provider." - - echo "" - echo "✓ Test complete" -} - -# @function provider_get -# @intent Show currently active provider -provider_get() { - local current_provider - current_provider=$(get_active_provider) - - echo "🎤 Current Provider: $current_provider" - echo "" - - # Show brief info - case "$current_provider" in - elevenlabs) - echo "Quality: ⭐⭐⭐⭐⭐" - echo "Cost: Free tier + \$5-22/mo" - echo "Offline: No" - ;; - piper) - echo "Quality: ⭐⭐⭐⭐" - echo "Cost: Free forever" - echo "Offline: Yes" - ;; - esac - - echo "" - echo "Use /agent-vibes:provider info $current_provider for details" -} - -# @function provider_preview -# @intent Preview voices for the currently active provider -# @architecture Delegates to provider-specific voice managers -provider_preview() { - local current_provider - current_provider=$(get_active_provider) - - echo "🎤 Voice Preview ($current_provider)" - echo "" - - case "$current_provider" in - elevenlabs) - # Use the ElevenLabs voice manager - "$SCRIPT_DIR/voice-manager.sh" preview "$@" - ;; - piper) - # Use the Piper voice manager's list functionality - source "$SCRIPT_DIR/piper-voice-manager.sh" - - # Check if a specific voice was requested - local voice_arg="$1" - - if [[ -n "$voice_arg" ]]; then - # User requested a specific voice - check if it's a valid Piper voice - # Piper voice names are like: en_US-lessac-medium - # Try to find a matching voice model - - # Check if the voice arg looks like a Piper model name (contains underscores/hyphens) - if [[ "$voice_arg" =~ ^[a-z]{2}_[A-Z]{2}- ]]; then - # Looks like a Piper voice model name - if verify_voice "$voice_arg"; then - echo "🎤 Previewing Piper voice: $voice_arg" - echo "" - "$SCRIPT_DIR/play-tts.sh" "Hello, this is the $voice_arg voice. How do you like it?" "$voice_arg" - else - echo "❌ Voice model not found: $voice_arg" - echo "" - echo "💡 Piper voice names look like: en_US-lessac-medium" - echo " Run /agent-vibes:list to see available Piper voices" - fi - else - # Looks like an ElevenLabs voice name (like "Antoni", "Jessica") - echo "❌ '$voice_arg' appears to be an ElevenLabs voice" - echo "" - echo "You're currently using Piper TTS (free provider)." - echo "Piper has different voices than ElevenLabs." - echo "" - echo "Options:" - echo " 1. Run /agent-vibes:list to see available Piper voices" - echo " 2. Switch to ElevenLabs: /agent-vibes:provider switch elevenlabs" - echo "" - echo "Popular Piper voices to try:" - echo " • en_US-lessac-medium (clear, professional)" - echo " • en_US-amy-medium (warm, friendly)" - echo " • en_US-joe-medium (casual, natural)" - fi - return - fi - - # No specific voice - preview first 3 voices - echo "🎤 Piper Preview of 3 people" - echo "" - - # Play first 3 Piper voices as samples - local sample_voices=( - "en_US-lessac-medium:Lessac" - "en_US-amy-medium:Amy" - "en_US-joe-medium:Joe" - ) - - for voice_entry in "${sample_voices[@]}"; do - local voice_name="${voice_entry%%:*}" - local display_name="${voice_entry##*:}" - - echo "🔊 ${display_name}..." - "$SCRIPT_DIR/play-tts.sh" "Hi, my name is ${display_name}" "$voice_name" - - # Wait for the voice to finish playing before starting next one - sleep 3 - done - - echo "" - echo "✓ Preview complete" - echo "💡 Use /agent-vibes:list to see all available Piper voices" - ;; - *) - echo "❌ Unknown provider: $current_provider" - ;; - esac -} - -# @function provider_help -# @intent Show help for provider commands -provider_help() { - echo "Provider Management Commands" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo "Usage:" - echo " /agent-vibes:provider list # Show all providers" - echo " /agent-vibes:provider switch # Switch provider" - echo " /agent-vibes:provider info # Provider details" - echo " /agent-vibes:provider test # Test current provider" - echo " /agent-vibes:provider get # Show active provider" - echo "" - echo "Examples:" - echo " /agent-vibes:provider switch piper" - echo " /agent-vibes:provider info elevenlabs" - echo "" - echo "Learn more: agentvibes.org/docs/providers" -} - -# Route to appropriate function -case "$COMMAND" in - list) - provider_list - ;; - switch) - provider_switch "$2" "$3" - ;; - info) - provider_info "$2" - ;; - test) - provider_test - ;; - get) - provider_get - ;; - preview) - shift # Remove 'preview' from args - provider_preview "$@" - ;; - help|*) - provider_help - ;; -esac diff --git a/.claude/hooks/provider-manager.sh b/.claude/hooks/provider-manager.sh deleted file mode 100755 index 41cda8db..00000000 --- a/.claude/hooks/provider-manager.sh +++ /dev/null @@ -1,298 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/provider-manager.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview TTS Provider Management Functions -# @context Core provider abstraction layer for multi-provider TTS system -# @architecture Provides functions to get/set/list/validate TTS providers -# @dependencies None - pure bash implementation -# @entrypoints Sourced by play-tts.sh and provider management commands -# @patterns File-based state management with project-local and global fallback -# @related play-tts.sh, play-tts-elevenlabs.sh, play-tts-piper.sh, provider-commands.sh -# - -# @function get_provider_config_path -# @intent Determine path to tts-provider.txt file -# @why Supports both project-local (.claude/) and global (~/.claude/) storage -# @returns Echoes path to provider config file -# @exitcode 0=always succeeds -# @sideeffects None -# @edgecases Creates parent directory if missing -get_provider_config_path() { - local provider_file - - # Check project-local first - if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then - provider_file="$CLAUDE_PROJECT_DIR/.claude/tts-provider.txt" - else - # Search up directory tree for .claude/ - local current_dir="$PWD" - while [[ "$current_dir" != "/" ]]; do - if [[ -d "$current_dir/.claude" ]]; then - provider_file="$current_dir/.claude/tts-provider.txt" - break - fi - current_dir=$(dirname "$current_dir") - done - - # Fallback to global if no project .claude found - if [[ -z "$provider_file" ]]; then - provider_file="$HOME/.claude/tts-provider.txt" - fi - fi - - echo "$provider_file" -} - -# @function get_active_provider -# @intent Read currently active TTS provider from config file -# @why Central function for determining which provider to use -# @returns Echoes provider name (e.g., "elevenlabs", "piper") -# @exitcode 0=success -# @sideeffects None -# @edgecases Returns "elevenlabs" if file missing or empty (default) -get_active_provider() { - local provider_file - provider_file=$(get_provider_config_path) - - # Read provider from file, default to piper if not found - if [[ -f "$provider_file" ]]; then - local provider - provider=$(cat "$provider_file" | tr -d '[:space:]') - if [[ -n "$provider" ]]; then - echo "$provider" - return 0 - fi - fi - - # Default to piper (free, offline) - echo "piper" -} - -# @function set_active_provider -# @intent Write active provider to config file -# @why Allows runtime provider switching without restart -# @param $1 {string} provider - Provider name (e.g., "elevenlabs", "piper") -# @returns None (outputs success/error message) -# @exitcode 0=success, 1=invalid provider -# @sideeffects Writes to tts-provider.txt file -# @edgecases Creates file and parent directory if missing -set_active_provider() { - local provider="$1" - - if [[ -z "$provider" ]]; then - echo "❌ Error: Provider name required" - echo "Usage: set_active_provider " - return 1 - fi - - # Validate provider exists - if ! validate_provider "$provider"; then - echo "❌ Error: Provider '$provider' not found" - echo "Available providers:" - list_providers - return 1 - fi - - local provider_file - provider_file=$(get_provider_config_path) - - # Create directory if it doesn't exist - mkdir -p "$(dirname "$provider_file")" - - # Write provider to file - echo "$provider" > "$provider_file" - - # Reset voice when switching providers to avoid incompatible voices - # (e.g., ElevenLabs "Demon Monster" doesn't exist in Piper) - local voice_file - if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then - voice_file="$CLAUDE_PROJECT_DIR/.claude/tts-voice.txt" - else - voice_file="$HOME/.claude/tts-voice.txt" - fi - - # Set default voice for the new provider - local default_voice - case "$provider" in - piper) - # Default Piper voice - default_voice="en_US-lessac-medium" - ;; - elevenlabs) - # Default ElevenLabs voice (first in alphabetical order from voices-config.sh) - default_voice="Amy" - ;; - *) - # Unknown provider - remove voice file - if [[ -f "$voice_file" ]]; then - rm -f "$voice_file" - fi - echo "✓ Active provider set to: $provider (voice reset)" - return 0 - ;; - esac - - # Write default voice to file - echo "$default_voice" > "$voice_file" - - echo "✓ Active provider set to: $provider (voice set to: $default_voice)" -} - -# @function list_providers -# @intent List all available TTS providers -# @why Discover which providers are installed -# @returns Echoes provider names (one per line) -# @exitcode 0=success -# @sideeffects None -# @edgecases Returns empty if no play-tts-*.sh files found -list_providers() { - local script_dir - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - - # Find all play-tts-*.sh files - local providers=() - shopt -s nullglob # Handle case where no files match - for file in "$script_dir"/play-tts-*.sh; do - if [[ -f "$file" ]] && [[ "$file" != *"play-tts.sh" ]]; then - # Extract provider name from filename (play-tts-elevenlabs.sh -> elevenlabs) - local basename - basename=$(basename "$file") - local provider - provider="${basename#play-tts-}" - provider="${provider%.sh}" - providers+=("$provider") - fi - done - shopt -u nullglob - - # Output providers - if [[ ${#providers[@]} -eq 0 ]]; then - echo "⚠️ No providers found" - return 0 - fi - - for provider in "${providers[@]}"; do - echo "$provider" - done -} - -# @function validate_provider -# @intent Check if provider implementation exists -# @why Prevent errors from switching to non-existent provider -# @param $1 {string} provider - Provider name to validate -# @returns None -# @exitcode 0=provider exists, 1=provider not found -# @sideeffects None -# @edgecases Checks for corresponding play-tts-*.sh file -validate_provider() { - local provider="$1" - - if [[ -z "$provider" ]]; then - return 1 - fi - - local script_dir - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - local provider_script="$script_dir/play-tts-${provider}.sh" - - [[ -f "$provider_script" ]] -} - -# @function get_provider_script_path -# @intent Get absolute path to provider implementation script -# @why Used by router to execute provider-specific logic -# @param $1 {string} provider - Provider name -# @returns Echoes absolute path to play-tts-*.sh file -# @exitcode 0=success, 1=provider not found -# @sideeffects None -get_provider_script_path() { - local provider="$1" - - if [[ -z "$provider" ]]; then - echo "❌ Error: Provider name required" >&2 - return 1 - fi - - local script_dir - script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - local provider_script="$script_dir/play-tts-${provider}.sh" - - if [[ ! -f "$provider_script" ]]; then - echo "❌ Error: Provider '$provider' not found at $provider_script" >&2 - return 1 - fi - - echo "$provider_script" -} - -# AI NOTE: This file provides the core abstraction layer for multi-provider TTS. -# All provider state is managed through simple text files for simplicity and reliability. -# Project-local configuration takes precedence over global to support per-project providers. - -# Command-line interface (when script is executed, not sourced) -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - case "${1:-}" in - get) - get_active_provider - ;; - switch|set) - if [[ -z "${2:-}" ]]; then - echo "❌ Error: Provider name required" - echo "Usage: $0 switch " - exit 1 - fi - set_active_provider "$2" - ;; - list) - list_providers - ;; - validate) - if [[ -z "${2:-}" ]]; then - echo "❌ Error: Provider name required" - echo "Usage: $0 validate " - exit 1 - fi - validate_provider "$2" - ;; - *) - echo "Usage: $0 {get|switch|list|validate} [provider]" - echo "" - echo "Commands:" - echo " get - Show active provider" - echo " switch - Switch to provider" - echo " list - List available providers" - echo " validate - Check if provider exists" - exit 1 - ;; - esac -fi diff --git a/.claude/hooks/replay-target-audio.sh b/.claude/hooks/replay-target-audio.sh deleted file mode 100755 index 3c28f080..00000000 --- a/.claude/hooks/replay-target-audio.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/replay-target-audio.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview Replay Last Target Language Audio -# @context Replays the most recent target language TTS for language learning -# @architecture Simple audio replay with lock mechanism for sequential playback -# @dependencies ffprobe, paplay/aplay/mpg123/mpv, .claude/last-target-audio.txt -# @entrypoints Called by /agent-vibes:replay-target slash command -# @patterns Sequential audio playback with lock file, duration-based lock release -# @related play-tts-piper.sh, play-tts-elevenlabs.sh, learn-manager.sh -# - -# Fix locale warnings -export LC_ALL=C - -TARGET_AUDIO_FILE="${CLAUDE_PROJECT_DIR:-.}/.claude/last-target-audio.txt" - -# Check if target audio tracking file exists -if [ ! -f "$TARGET_AUDIO_FILE" ]; then - echo "❌ No target language audio found." - echo " Language learning mode may not be active." - echo " Activate with: /agent-vibes:learn" - exit 1 -fi - -# Read last target audio file path -LAST_AUDIO=$(cat "$TARGET_AUDIO_FILE") - -# Verify audio file exists -if [ ! -f "$LAST_AUDIO" ]; then - echo "❌ Audio file not found: $LAST_AUDIO" - echo " The file may have been deleted or moved." - exit 1 -fi - -echo "🔁 Replaying target language audio..." - -# Use lock file for sequential playback -LOCK_FILE="/tmp/agentvibes-audio.lock" - -# Wait for any current audio to finish (max 30 seconds) -for i in {1..60}; do - if [ ! -f "$LOCK_FILE" ]; then - break - fi - sleep 0.5 -done - -# Create lock -touch "$LOCK_FILE" - -# Get audio duration for proper lock timing -DURATION=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$LAST_AUDIO" 2>/dev/null) -DURATION=${DURATION%.*} # Round to integer -DURATION=${DURATION:-1} # Default to 1 second if detection fails - -# Play audio -(paplay "$LAST_AUDIO" || aplay "$LAST_AUDIO" || mpg123 "$LAST_AUDIO" || mpv "$LAST_AUDIO") >/dev/null 2>&1 & -PLAYER_PID=$! - -# Wait for audio to finish, then release lock -(sleep $DURATION; rm -f "$LOCK_FILE") & -disown - -echo "✅ Replay complete: $(basename "$LAST_AUDIO")" diff --git a/.claude/hooks/sentiment-manager.sh b/.claude/hooks/sentiment-manager.sh deleted file mode 100755 index b8c7bd98..00000000 --- a/.claude/hooks/sentiment-manager.sh +++ /dev/null @@ -1,201 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/sentiment-manager.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview Sentiment Manager - Applies personality styles to current voice without changing the voice itself -# @context Allows adding emotional/tonal layers (flirty, sarcastic, etc.) to any voice while preserving voice identity -# @architecture Reuses personality markdown files, stores sentiment separately from personality -# @dependencies .claude/personalities/*.md files, play-tts.sh for acknowledgment -# @entrypoints Called by /agent-vibes:sentiment slash command -# @patterns Personality/sentiment separation, state file management, random example selection -# @related personality-manager.sh, .claude/personalities/*.md, .claude/tts-sentiment.txt - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PERSONALITIES_DIR="$SCRIPT_DIR/../personalities" - -# Project-local file first, global fallback -# Use logical path (not physical) to handle symlinked .claude directories -# Script is at .claude/hooks/sentiment-manager.sh, so .claude is .. -CLAUDE_DIR="$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)" - -# Check if we have a project-local .claude directory -if [[ -d "$CLAUDE_DIR" ]] && [[ "$CLAUDE_DIR" != "$HOME/.claude" ]]; then - SENTIMENT_FILE="$CLAUDE_DIR/tts-sentiment.txt" -else - SENTIMENT_FILE="$HOME/.claude/tts-sentiment.txt" -fi - -# Function to get personality data from markdown file -get_personality_data() { - local personality="$1" - local field="$2" - local file="$PERSONALITIES_DIR/${personality}.md" - - if [[ ! -f "$file" ]]; then - return 1 - fi - - case "$field" in - description) - grep "^description:" "$file" | cut -d: -f2- | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' - ;; - esac -} - -# Function to list all available personalities -list_personalities() { - if [[ -d "$PERSONALITIES_DIR" ]]; then - for file in "$PERSONALITIES_DIR"/*.md; do - if [[ -f "$file" ]]; then - basename "$file" .md - fi - done - fi -} - -case "$1" in - list) - echo "🎭 Available Sentiments:" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - # Get current sentiment - CURRENT="none" - if [ -f "$SENTIMENT_FILE" ]; then - CURRENT=$(cat "$SENTIMENT_FILE") - fi - - # List personalities from markdown files - echo "Available sentiment styles:" - for personality in $(list_personalities | sort); do - desc=$(get_personality_data "$personality" "description") - if [[ "$personality" == "$CURRENT" ]]; then - echo " ✓ $personality - $desc (current)" - else - echo " - $personality - $desc" - fi - done - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo "Usage: /agent-vibes:sentiment " - echo " /agent-vibes:sentiment clear" - ;; - - set) - SENTIMENT="$2" - - if [[ -z "$SENTIMENT" ]]; then - echo "❌ Please specify a sentiment name" - echo "Usage: $0 set " - exit 1 - fi - - # Check if sentiment file exists - if [[ ! -f "$PERSONALITIES_DIR/${SENTIMENT}.md" ]]; then - echo "❌ Sentiment not found: $SENTIMENT" - echo "" - echo "Available sentiments:" - for p in $(list_personalities | sort); do - echo " • $p" - done - exit 1 - fi - - # Save the sentiment (but don't change personality or voice) - echo "$SENTIMENT" > "$SENTIMENT_FILE" - echo "🎭 Sentiment set to: $SENTIMENT" - echo "🎤 Voice remains unchanged" - echo "" - - # Make a sentiment-appropriate remark with TTS - TTS_SCRIPT="$SCRIPT_DIR/play-tts.sh" - - # Try to get acknowledgment from personality file (sentiments use same personality files) - PERSONALITY_FILE_PATH="$PERSONALITIES_DIR/${SENTIMENT}.md" - REMARK="" - - if [[ -f "$PERSONALITY_FILE_PATH" ]]; then - # Extract example responses from personality file (lines starting with "- ") - mapfile -t EXAMPLES < <(grep '^- "' "$PERSONALITY_FILE_PATH" | sed 's/^- "//; s/"$//') - - if [[ ${#EXAMPLES[@]} -gt 0 ]]; then - # Pick a random example - REMARK="${EXAMPLES[$RANDOM % ${#EXAMPLES[@]}]}" - fi - fi - - # Fallback if no examples found - if [[ -z "$REMARK" ]]; then - REMARK="Sentiment set to ${SENTIMENT} while maintaining current voice" - fi - - echo "💬 $REMARK" - "$TTS_SCRIPT" "$REMARK" - ;; - - get) - if [ -f "$SENTIMENT_FILE" ]; then - CURRENT=$(cat "$SENTIMENT_FILE") - echo "Current sentiment: $CURRENT" - - desc=$(get_personality_data "$CURRENT" "description") - [[ -n "$desc" ]] && echo "Description: $desc" - else - echo "Current sentiment: none (voice personality only)" - fi - ;; - - clear) - rm -f "$SENTIMENT_FILE" - echo "🎭 Sentiment cleared - using voice personality only" - ;; - - *) - # If a single argument is provided and it's not a command, treat it as "set " - if [[ -n "$1" ]] && [[ -f "$PERSONALITIES_DIR/${1}.md" ]]; then - exec "$0" set "$1" - else - echo "AgentVibes Sentiment Manager" - echo "" - echo "Commands:" - echo " list - List all sentiments" - echo " set - Set sentiment for current voice" - echo " get - Show current sentiment" - echo " clear - Clear sentiment" - echo "" - echo "Examples:" - echo " /agent-vibes:sentiment flirty # Add flirty style to current voice" - echo " /agent-vibes:sentiment sarcastic # Add sarcasm to current voice" - echo " /agent-vibes:sentiment clear # Remove sentiment" - fi - ;; -esac diff --git a/.claude/hooks/speed-manager.sh b/.claude/hooks/speed-manager.sh deleted file mode 100755 index 561e31ee..00000000 --- a/.claude/hooks/speed-manager.sh +++ /dev/null @@ -1,291 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/speed-manager.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview Speech Speed Manager for Multi-Provider TTS -# @context Manage speech rate for main and target language voices -# @architecture Simple config file manager supporting both Piper (length-scale) and ElevenLabs (speed API parameter) -# @dependencies .claude/config/tts-speech-rate.txt, .claude/config/tts-target-speech-rate.txt -# @entrypoints Called by /agent-vibes:set-speed slash command -# @patterns Provider-agnostic speed config, legacy file migration, random tongue twisters for testing -# @related play-tts.sh, play-tts-piper.sh, play-tts-elevenlabs.sh, learn-manager.sh -# - -# Get script directory -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Determine config directory (project-local first, then global) -if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then - CONFIG_DIR="$CLAUDE_PROJECT_DIR/.claude/config" -else - # Try to find .claude in current path - CURRENT_DIR="$PWD" - while [[ "$CURRENT_DIR" != "/" ]]; do - if [[ -d "$CURRENT_DIR/.claude" ]]; then - CONFIG_DIR="$CURRENT_DIR/.claude/config" - break - fi - CURRENT_DIR=$(dirname "$CURRENT_DIR") - done - # Fallback to global - if [[ -z "$CONFIG_DIR" ]]; then - CONFIG_DIR="$HOME/.claude/config" - fi -fi - -mkdir -p "$CONFIG_DIR" - -MAIN_SPEED_FILE="$CONFIG_DIR/tts-speech-rate.txt" -TARGET_SPEED_FILE="$CONFIG_DIR/tts-target-speech-rate.txt" - -# Legacy file paths for backward compatibility (Piper-specific naming) -LEGACY_MAIN_SPEED_FILE="$CONFIG_DIR/piper-speech-rate.txt" -LEGACY_TARGET_SPEED_FILE="$CONFIG_DIR/piper-target-speech-rate.txt" - -# @function parse_speed_value -# @intent Convert user-friendly speed notation to normalized speed multiplier -# @param $1 Speed string (e.g., "2x", "0.5x", "normal") -# @returns Numeric speed value (0.5=slower, 1.0=normal, 2.0=faster, 3.0=very fast) -# @note This is the user-facing scale - provider scripts will convert as needed -parse_speed_value() { - local input="$1" - - # Handle special cases - case "$input" in - normal|1x|1.0) - echo "1.0" - return - ;; - slow|slower|0.5x) - echo "0.5" - return - ;; - fast|2x|2.0) - echo "2.0" - return - ;; - faster|3x|3.0) - echo "3.0" - return - ;; - esac - - # Strip leading '+' or '-' if present - input="${input#+}" - input="${input#-}" - - # Strip trailing 'x' if present - input="${input%x}" - - # Validate it's a number - if [[ "$input" =~ ^[0-9]+\.?[0-9]*$ ]]; then - echo "$input" - else - echo "ERROR" - fi -} - -# @function set_speed -# @intent Set speech speed for main or target voice -# @param $1 Target ("target" or empty for main) -# @param $2 Speed value -set_speed() { - local is_target=false - local speed_input="" - - # Parse arguments - if [[ "$1" == "target" ]]; then - is_target=true - speed_input="$2" - else - speed_input="$1" - fi - - if [[ -z "$speed_input" ]]; then - echo "❌ Error: Speed value required" - echo "Usage: /agent-vibes:set-speed [target] " - echo "Examples: 2x, 0.5x, normal, +3x" - return 1 - fi - - # Parse speed value - local speed_value - speed_value=$(parse_speed_value "$speed_input") - - if [[ "$speed_value" == "ERROR" ]]; then - echo "❌ Invalid speed value: $speed_input" - echo "Valid values: normal, 0.5x, 1x, 2x, 3x, +2x, -2x" - return 1 - fi - - # Determine which file to write to - local config_file - local voice_type - if [[ "$is_target" == true ]]; then - config_file="$TARGET_SPEED_FILE" - voice_type="target language" - else - config_file="$MAIN_SPEED_FILE" - voice_type="main voice" - fi - - # Write speed value - echo "$speed_value" > "$config_file" - - # Show confirmation - echo "✓ Speech speed set for $voice_type" - echo "" - echo "Speed: ${speed_value}x" - - case "$speed_value" in - 0.5) - echo "Effect: Half speed (slower)" - ;; - 1.0) - echo "Effect: Normal speed" - ;; - 2.0) - echo "Effect: Double speed (faster)" - ;; - 3.0) - echo "Effect: Triple speed (very fast)" - ;; - *) - if (( $(echo "$speed_value > 1.0" | bc -l) )); then - echo "Effect: Faster speech" - else - echo "Effect: Slower speech" - fi - ;; - esac - - echo "" - echo "Note: Speed control works with both Piper and ElevenLabs providers" - - # Array of simple test messages to demonstrate speed - local test_messages=( - "Testing speed change" - "Speed test in progress" - "Checking audio speed" - "Speed configuration test" - "Audio speed test" - ) - - # Pick a random test message - local random_index=$((RANDOM % ${#test_messages[@]})) - local test_msg="${test_messages[$random_index]}" - - echo "" - echo "🔊 Testing new speed with: \"$test_msg\"" - "$SCRIPT_DIR/play-tts.sh" "$test_msg" & -} - -# @function migrate_legacy_files -# @intent Migrate from old piper-specific files to provider-agnostic files -# @why Ensure backward compatibility when upgrading from Piper-only to multi-provider -migrate_legacy_files() { - # Migrate main speed file - if [[ -f "$LEGACY_MAIN_SPEED_FILE" ]] && [[ ! -f "$MAIN_SPEED_FILE" ]]; then - cp "$LEGACY_MAIN_SPEED_FILE" "$MAIN_SPEED_FILE" - fi - - # Migrate target speed file - if [[ -f "$LEGACY_TARGET_SPEED_FILE" ]] && [[ ! -f "$TARGET_SPEED_FILE" ]]; then - cp "$LEGACY_TARGET_SPEED_FILE" "$TARGET_SPEED_FILE" - fi -} - -# @function get_speed -# @intent Display current speech speed settings -get_speed() { - # Migrate legacy files if needed - migrate_legacy_files - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo " Current Speech Speed Settings" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - - # Main voice speed - if [[ -f "$MAIN_SPEED_FILE" ]]; then - local main_speed=$(grep -v '^#' "$MAIN_SPEED_FILE" 2>/dev/null | grep -v '^$' | tail -1) - echo "Main voice: ${main_speed}x" - else - echo "Main voice: 1.0x (default, normal speed)" - fi - - # Target voice speed - if [[ -f "$TARGET_SPEED_FILE" ]]; then - local target_speed=$(cat "$TARGET_SPEED_FILE" 2>/dev/null) - echo "Target language: ${target_speed}x" - else - echo "Target language: 0.5x (default, slower for learning)" - fi - - echo "" - echo "Scale: 0.5x=slower, 1.0x=normal, 2.0x=faster, 3.0x=very fast" - echo "Works with: Piper TTS and ElevenLabs" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -} - -# Main command handler -case "${1:-}" in - target) - set_speed "target" "$2" - ;; - get|status) - get_speed - ;; - normal|fast|slow|slower|*x|*.*|+*|-*) - set_speed "$1" - ;; - *) - echo "Speech Speed Manager" - echo "" - echo "Usage:" - echo " /agent-vibes:set-speed Set main voice speed" - echo " /agent-vibes:set-speed target Set target language speed" - echo " /agent-vibes:set-speed get Show current speeds" - echo "" - echo "Speed values:" - echo " 0.5x or slow/slower = Half speed (slower)" - echo " 1x or normal = Normal speed" - echo " 2x or fast = Double speed (faster)" - echo " 3x or faster = Triple speed (very fast)" - echo "" - echo "Examples:" - echo " /agent-vibes:set-speed 2x # Make voice faster" - echo " /agent-vibes:set-speed 0.5x # Make voice slower" - echo " /agent-vibes:set-speed target 0.5x # Slow down target language for learning" - echo " /agent-vibes:set-speed normal # Reset to normal" - ;; -esac diff --git a/.claude/hooks/voice-manager.sh b/.claude/hooks/voice-manager.sh deleted file mode 100755 index 81abb154..00000000 --- a/.claude/hooks/voice-manager.sh +++ /dev/null @@ -1,594 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/voice-manager.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied. Use at your own risk. See the Apache License for details. -# -# --- -# -# @fileoverview Voice Manager - Unified voice management for both ElevenLabs and Piper providers -# @context Central interface for listing, switching, previewing, and replaying TTS voices across providers -# @architecture Provider-aware operations with dynamic voice listing based on active provider -# @dependencies voices-config.sh (ElevenLabs mappings), piper-voice-manager.sh (Piper voices), provider-manager.sh -# @entrypoints Called by /agent-vibes:switch, /agent-vibes:list, /agent-vibes:whoami, /agent-vibes:replay commands -# @patterns Provider abstraction, numbered selection UI, silent mode for programmatic switching -# @related voices-config.sh, piper-voice-manager.sh, .claude/tts-voice.txt, .claude/audio/ (replay) - -# Get script directory (physical path for sourcing files) -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" -source "$SCRIPT_DIR/voices-config.sh" - -# Determine target .claude directory based on context -# Priority: -# 1. CLAUDE_PROJECT_DIR env var (set by MCP for project-specific settings) -# 2. Script location (for direct slash command usage) -# 3. Global ~/.claude (fallback) - -if [[ -n "$CLAUDE_PROJECT_DIR" ]] && [[ -d "$CLAUDE_PROJECT_DIR/.claude" ]]; then - # MCP context: Use the project directory where MCP was invoked - CLAUDE_DIR="$CLAUDE_PROJECT_DIR/.claude" -else - # Direct usage context: Use script location - SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - CLAUDE_DIR="$(dirname "$SCRIPT_PATH")" - - # If script is in global ~/.claude, use that - if [[ "$CLAUDE_DIR" == "$HOME/.claude" ]]; then - CLAUDE_DIR="$HOME/.claude" - elif [[ ! -d "$CLAUDE_DIR" ]]; then - # Fallback to global if directory doesn't exist - CLAUDE_DIR="$HOME/.claude" - fi -fi - -VOICE_FILE="$CLAUDE_DIR/tts-voice.txt" - -case "$1" in - list) - # Get active provider - PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt" - if [[ ! -f "$PROVIDER_FILE" ]]; then - PROVIDER_FILE="$HOME/.claude/tts-provider.txt" - fi - - ACTIVE_PROVIDER="elevenlabs" # default - if [ -f "$PROVIDER_FILE" ]; then - ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE") - fi - - CURRENT_VOICE=$(cat "$VOICE_FILE" 2>/dev/null || echo "Cowboy Bob") - - if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then - echo "🎤 Available Piper TTS Voices:" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - # List downloaded Piper voices - if [[ -f "$SCRIPT_DIR/piper-voice-manager.sh" ]]; then - source "$SCRIPT_DIR/piper-voice-manager.sh" - VOICE_DIR=$(get_voice_storage_dir) - VOICE_COUNT=0 - for onnx_file in "$VOICE_DIR"/*.onnx; do - if [[ -f "$onnx_file" ]]; then - voice=$(basename "$onnx_file" .onnx) - if [ "$voice" = "$CURRENT_VOICE" ]; then - echo " ▶ $voice (current)" - else - echo " $voice" - fi - ((VOICE_COUNT++)) - fi - done | sort - - if [[ $VOICE_COUNT -eq 0 ]]; then - echo " (No Piper voices downloaded yet)" - echo "" - echo "Download voices with: /agent-vibes:provider download " - echo "Examples: en_US-lessac-medium, en_GB-alba-medium" - fi - fi - else - echo "🎤 Available ElevenLabs TTS Voices:" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - for voice in "${!VOICES[@]}"; do - if [ "$voice" = "$CURRENT_VOICE" ]; then - echo " ▶ $voice (current)" - else - echo " $voice" - fi - done | sort - fi - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo "Usage: voice-manager.sh switch " - echo " voice-manager.sh preview" - ;; - - preview) - # Get play-tts.sh path - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - TTS_SCRIPT="$SCRIPT_DIR/play-tts.sh" - - # Check if a specific voice name was provided - if [[ -n "$2" ]] && [[ "$2" != "first" ]] && [[ "$2" != "last" ]] && ! [[ "$2" =~ ^[0-9]+$ ]]; then - # User specified a voice name - VOICE_NAME="$2" - - # Check if voice exists - if [[ -n "${VOICES[$VOICE_NAME]}" ]]; then - echo "🎤 Previewing voice: ${VOICE_NAME}" - echo "" - "$TTS_SCRIPT" "Hello, this is ${VOICE_NAME}. How do you like my voice?" "${VOICE_NAME}" - else - echo "❌ Voice not found: ${VOICE_NAME}" - echo "" - echo "Available voices:" - for voice in "${!VOICES[@]}"; do - echo " • $voice" - done | sort - fi - exit 0 - fi - - # Original preview logic for first/last/number - echo "🎤 Voice Preview - Playing first 3 voices..." - echo "" - - # Sort voices and preview first 3 - VOICE_ARRAY=() - for voice in "${!VOICES[@]}"; do - VOICE_ARRAY+=("$voice") - done - - # Sort the array - IFS=$'\n' SORTED_VOICES=($(sort <<<"${VOICE_ARRAY[*]}")) - unset IFS - - # Play first 3 voices - COUNT=0 - for voice in "${SORTED_VOICES[@]}"; do - if [ $COUNT -eq 3 ]; then - break - fi - echo "🔊 ${voice}..." - "$TTS_SCRIPT" "Hi, I'm ${voice}" "${VOICES[$voice]}" - sleep 0.5 - COUNT=$((COUNT + 1)) - done - - echo "" - echo "Would you like to hear more? Reply 'yes' to continue." - ;; - - switch) - VOICE_NAME="$2" - SILENT_MODE=false - - # Check for --silent flag - if [[ "$2" == "--silent" ]] || [[ "$3" == "--silent" ]]; then - SILENT_MODE=true - # If --silent is first arg, voice name is in $3 - [[ "$2" == "--silent" ]] && VOICE_NAME="$3" - fi - - if [[ -z "$VOICE_NAME" ]]; then - # Show numbered list for selection - echo "🎤 Select a voice by number:" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - # Get current voice - CURRENT="Cowboy Bob" - if [ -f "$VOICE_FILE" ]; then - CURRENT=$(cat "$VOICE_FILE") - fi - - # Create array of voice names - VOICE_ARRAY=() - for voice in "${!VOICES[@]}"; do - VOICE_ARRAY+=("$voice") - done - - # Sort the array - IFS=$'\n' SORTED_VOICES=($(sort <<<"${VOICE_ARRAY[*]}")) - unset IFS - - # Display numbered list in two columns for compactness - HALF=$(( (${#SORTED_VOICES[@]} + 1) / 2 )) - - for i in $(seq 0 $((HALF - 1))); do - NUM1=$((i + 1)) - VOICE1="${SORTED_VOICES[$i]}" - - # Format first column - if [[ "$VOICE1" == "$CURRENT" ]]; then - COL1=$(printf "%2d. %-20s ✓" "$NUM1" "$VOICE1") - else - COL1=$(printf "%2d. %-20s " "$NUM1" "$VOICE1") - fi - - # Format second column if it exists - NUM2=$((i + HALF + 1)) - if [[ $((i + HALF)) -lt ${#SORTED_VOICES[@]} ]]; then - VOICE2="${SORTED_VOICES[$((i + HALF))]}" - if [[ "$VOICE2" == "$CURRENT" ]]; then - COL2=$(printf "%2d. %-20s ✓" "$NUM2" "$VOICE2") - else - COL2=$(printf "%2d. %-20s " "$NUM2" "$VOICE2") - fi - echo " $COL1 $COL2" - else - echo " $COL1" - fi - done - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo "Enter number (1-${#SORTED_VOICES[@]}) or voice name:" - echo "Usage: /agent-vibes:switch 5" - echo " /agent-vibes:switch \"Northern Terry\"" - exit 0 - fi - - # Detect active TTS provider - PROVIDER_FILE="" - if [[ -f "$CLAUDE_DIR/tts-provider.txt" ]]; then - PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt" - elif [[ -f "$HOME/.claude/tts-provider.txt" ]]; then - PROVIDER_FILE="$HOME/.claude/tts-provider.txt" - fi - - ACTIVE_PROVIDER="elevenlabs" # default - if [[ -n "$PROVIDER_FILE" ]]; then - ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE") - fi - - # Voice lookup strategy depends on active provider - if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then - # Piper voice lookup: Scan voice directory for .onnx files - source "$SCRIPT_DIR/piper-voice-manager.sh" - VOICE_DIR=$(get_voice_storage_dir) - - # Check if voice file exists (case-insensitive) - FOUND="" - shopt -s nullglob - for onnx_file in "$VOICE_DIR"/*.onnx; do - if [[ -f "$onnx_file" ]]; then - voice=$(basename "$onnx_file" .onnx) - if [[ "${voice,,}" == "${VOICE_NAME,,}" ]]; then - FOUND="$voice" - break - fi - fi - done - shopt -u nullglob - - # If not found, check multi-speaker registry - if [[ -z "$FOUND" ]] && [[ -f "$SCRIPT_DIR/piper-multispeaker-registry.sh" ]]; then - source "$SCRIPT_DIR/piper-multispeaker-registry.sh" - - MULTISPEAKER_INFO=$(get_multispeaker_info "$VOICE_NAME") - if [[ -n "$MULTISPEAKER_INFO" ]]; then - MODEL="${MULTISPEAKER_INFO%%:*}" - SPEAKER_ID="${MULTISPEAKER_INFO#*:}" - - # Verify the model file exists - if [[ -f "$VOICE_DIR/${MODEL}.onnx" ]]; then - # Store speaker name in tts-voice.txt - echo "$VOICE_NAME" > "$VOICE_FILE" - - # Store model and speaker ID separately for play-tts-piper.sh - echo "$MODEL" > "$CLAUDE_DIR/tts-piper-model.txt" - echo "$SPEAKER_ID" > "$CLAUDE_DIR/tts-piper-speaker-id.txt" - - DESCRIPTION=$(get_multispeaker_description "$VOICE_NAME") - echo "✅ Multi-speaker voice switched to: $VOICE_NAME" - echo "🎤 Model: $MODEL.onnx (Speaker ID: $SPEAKER_ID)" - if [[ -n "$DESCRIPTION" ]]; then - echo "📝 Description: $DESCRIPTION" - fi - - # Have the new voice introduce itself (unless silent mode) - if [[ "$SILENT_MODE" != "true" ]]; then - PLAY_TTS="$SCRIPT_DIR/play-tts.sh" - if [ -x "$PLAY_TTS" ]; then - "$PLAY_TTS" "Hi, I'm $VOICE_NAME. I'll be your voice assistant moving forward." > /dev/null 2>&1 & - fi - - echo "" - echo "💡 Tip: To hear automatic TTS narration, enable the Agent Vibes output style:" - echo " /output-style Agent Vibes" - fi - exit 0 - else - echo "❌ Multi-speaker model not found: $MODEL.onnx" - echo "" - echo "Download it with: /agent-vibes:provider download" - exit 1 - fi - fi - fi - - if [[ -z "$FOUND" ]]; then - echo "❌ Piper voice not found: $VOICE_NAME" - echo "" - echo "Available Piper voices:" - shopt -s nullglob - for onnx_file in "$VOICE_DIR"/*.onnx; do - if [[ -f "$onnx_file" ]]; then - echo " - $(basename "$onnx_file" .onnx)" - fi - done | sort - shopt -u nullglob - echo "" - if [[ -f "$SCRIPT_DIR/piper-multispeaker-registry.sh" ]]; then - echo "Multi-speaker voices (requires 16Speakers.onnx):" - source "$SCRIPT_DIR/piper-multispeaker-registry.sh" - for entry in "${MULTISPEAKER_VOICES[@]}"; do - name="${entry%%:*}" - echo " - $name" - done | sort - echo "" - fi - echo "Download extra voices with: /agent-vibes:provider download" - exit 1 - fi - else - # ElevenLabs voice lookup - # Check if input is a number - if [[ "$VOICE_NAME" =~ ^[0-9]+$ ]]; then - # Get voice array - VOICE_ARRAY=() - for voice in "${!VOICES[@]}"; do - VOICE_ARRAY+=("$voice") - done - - # Sort the array - IFS=$'\n' SORTED_VOICES=($(sort <<<"${VOICE_ARRAY[*]}")) - unset IFS - - # Get voice by number (adjust for 0-based index) - INDEX=$((VOICE_NAME - 1)) - - if [[ $INDEX -ge 0 && $INDEX -lt ${#SORTED_VOICES[@]} ]]; then - VOICE_NAME="${SORTED_VOICES[$INDEX]}" - FOUND="${SORTED_VOICES[$INDEX]}" - else - echo "❌ Invalid number. Please choose between 1 and ${#SORTED_VOICES[@]}" - exit 1 - fi - else - # Check if voice exists (case-insensitive) - FOUND="" - for voice in "${!VOICES[@]}"; do - if [[ "${voice,,}" == "${VOICE_NAME,,}" ]]; then - FOUND="$voice" - break - fi - done - fi - - if [[ -z "$FOUND" ]]; then - echo "❌ Unknown voice: $VOICE_NAME" - echo "" - echo "Available voices:" - for voice in "${!VOICES[@]}"; do - echo " - $voice" - done | sort - exit 1 - fi - fi - - echo "$FOUND" > "$VOICE_FILE" - echo "✅ Voice switched to: $FOUND" - - # Show voice ID only for ElevenLabs voices - if [[ "$ACTIVE_PROVIDER" != "piper" ]] && [[ -n "${VOICES[$FOUND]}" ]]; then - echo "🎤 Voice ID: ${VOICES[$FOUND]}" - fi - - # Have the new voice introduce itself (unless silent mode) - if [[ "$SILENT_MODE" != "true" ]]; then - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - PLAY_TTS="$SCRIPT_DIR/play-tts.sh" - if [ -x "$PLAY_TTS" ]; then - "$PLAY_TTS" "Hi, I'm $FOUND. I'll be your voice assistant moving forward." "$FOUND" > /dev/null 2>&1 & - fi - - echo "" - echo "💡 Tip: To hear automatic TTS narration, enable the Agent Vibes output style:" - echo " /output-style Agent Vibes" - fi - ;; - - get) - if [ -f "$VOICE_FILE" ]; then - cat "$VOICE_FILE" - else - echo "Cowboy Bob" - fi - ;; - - whoami) - echo "🎤 Current Voice Configuration" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - - # Get active TTS provider - PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt" - if [[ ! -f "$PROVIDER_FILE" ]]; then - PROVIDER_FILE="$HOME/.claude/tts-provider.txt" - fi - - if [ -f "$PROVIDER_FILE" ]; then - ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE") - if [[ "$ACTIVE_PROVIDER" == "elevenlabs" ]]; then - echo "Provider: ElevenLabs (Premium AI)" - elif [[ "$ACTIVE_PROVIDER" == "piper" ]]; then - echo "Provider: Piper TTS (Free, Offline)" - else - echo "Provider: $ACTIVE_PROVIDER" - fi - else - # Default to ElevenLabs if no provider file - echo "Provider: ElevenLabs (Premium AI)" - fi - - # Get current voice - if [ -f "$VOICE_FILE" ]; then - CURRENT_VOICE=$(cat "$VOICE_FILE") - else - CURRENT_VOICE="Cowboy Bob" - fi - echo "Voice: $CURRENT_VOICE" - - # Get current sentiment (priority) - if [ -f "$HOME/.claude/tts-sentiment.txt" ]; then - SENTIMENT=$(cat "$HOME/.claude/tts-sentiment.txt") - echo "Sentiment: $SENTIMENT (active)" - - # Also show personality if set - if [ -f "$HOME/.claude/tts-personality.txt" ]; then - PERSONALITY=$(cat "$HOME/.claude/tts-personality.txt") - echo "Personality: $PERSONALITY (overridden by sentiment)" - fi - else - # No sentiment, check personality - if [ -f "$HOME/.claude/tts-personality.txt" ]; then - PERSONALITY=$(cat "$HOME/.claude/tts-personality.txt") - echo "Personality: $PERSONALITY (active)" - else - echo "Personality: normal" - fi - fi - - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - ;; - - list-simple) - # Simple list for AI to parse and display - # Get active provider - PROVIDER_FILE="$CLAUDE_DIR/tts-provider.txt" - if [[ ! -f "$PROVIDER_FILE" ]]; then - PROVIDER_FILE="$HOME/.claude/tts-provider.txt" - fi - - ACTIVE_PROVIDER="elevenlabs" # default - if [ -f "$PROVIDER_FILE" ]; then - ACTIVE_PROVIDER=$(cat "$PROVIDER_FILE") - fi - - if [[ "$ACTIVE_PROVIDER" == "piper" ]]; then - # List downloaded Piper voices - if [[ -f "$SCRIPT_DIR/piper-voice-manager.sh" ]]; then - source "$SCRIPT_DIR/piper-voice-manager.sh" - VOICE_DIR=$(get_voice_storage_dir) - for onnx_file in "$VOICE_DIR"/*.onnx; do - if [[ -f "$onnx_file" ]]; then - basename "$onnx_file" .onnx - fi - done | sort - fi - else - # List ElevenLabs voices - for voice in "${!VOICES[@]}"; do - echo "$voice" - done | sort - fi - ;; - - replay) - # Replay recent TTS audio from history - # Use project-local directory with same logic as play-tts.sh - if [[ -n "$CLAUDE_PROJECT_DIR" ]]; then - AUDIO_DIR="$CLAUDE_PROJECT_DIR/.claude/audio" - else - # Fallback: try to find .claude directory in current path - CURRENT_DIR="$PWD" - while [[ "$CURRENT_DIR" != "/" ]]; do - if [[ -d "$CURRENT_DIR/.claude" ]]; then - AUDIO_DIR="$CURRENT_DIR/.claude/audio" - break - fi - CURRENT_DIR=$(dirname "$CURRENT_DIR") - done - # Final fallback to global if no project .claude found - if [[ -z "$AUDIO_DIR" ]]; then - AUDIO_DIR="$HOME/.claude/audio" - fi - fi - - # Default to replay last audio (N=1) - N="${2:-1}" - - # Validate N is a number - if ! [[ "$N" =~ ^[0-9]+$ ]]; then - echo "❌ Invalid argument. Please use a number (1-10)" - echo "Usage: /agent-vibes:replay [N]" - echo " N=1 - Last audio (default)" - echo " N=2 - Second-to-last" - echo " N=3 - Third-to-last" - exit 1 - fi - - # Check bounds - if [[ $N -lt 1 || $N -gt 10 ]]; then - echo "❌ Number out of range. Please choose 1-10" - exit 1 - fi - - # Get list of audio files sorted by time (newest first) - if [[ ! -d "$AUDIO_DIR" ]]; then - echo "❌ No audio history found" - echo "Audio files are stored in: $AUDIO_DIR" - exit 1 - fi - - # Get the Nth most recent file - AUDIO_FILE=$(ls -t "$AUDIO_DIR"/tts-*.mp3 2>/dev/null | sed -n "${N}p") - - if [[ -z "$AUDIO_FILE" ]]; then - TOTAL=$(ls -t "$AUDIO_DIR"/tts-*.mp3 2>/dev/null | wc -l) - echo "❌ Audio #$N not found in history" - echo "Total audio files available: $TOTAL" - exit 1 - fi - - echo "🔊 Replaying audio #$N:" - echo " File: $(basename "$AUDIO_FILE")" - echo " Path: $AUDIO_FILE" - - # Play the audio file in background - (paplay "$AUDIO_FILE" 2>/dev/null || aplay "$AUDIO_FILE" 2>/dev/null || mpg123 "$AUDIO_FILE" 2>/dev/null) & - ;; - - *) - echo "Usage: voice-manager.sh [list|switch|get|replay|whoami] [voice_name]" - echo "" - echo "Commands:" - echo " list - List all available voices" - echo " switch - Switch to a different voice" - echo " get - Get current voice name" - echo " replay [N] - Replay Nth most recent audio (default: 1)" - echo " whoami - Show current voice and personality" - exit 1 - ;; -esac \ No newline at end of file diff --git a/.claude/hooks/voices-config.sh b/.claude/hooks/voices-config.sh deleted file mode 100755 index 3e45def1..00000000 --- a/.claude/hooks/voices-config.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/bin/bash -# -# File: .claude/hooks/voices-config.sh -# -# AgentVibes - Finally, your AI Agents can Talk Back! Text-to-Speech WITH personality for AI Assistants! -# Website: https://agentvibes.org -# Repository: https://github.com/paulpreibisch/AgentVibes -# -# Co-created by Paul Preibisch with Claude AI -# Copyright (c) 2025 Paul Preibisch -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# DISCLAIMER: This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND, -# express or implied, including but not limited to the warranties of -# merchantability, fitness for a particular purpose and noninfringement. -# In no event shall the authors or copyright holders be liable for any claim, -# damages or other liability, whether in an action of contract, tort or -# otherwise, arising from, out of or in connection with the software or the -# use or other dealings in the software. -# -# --- -# -# @fileoverview ElevenLabs Voice Configuration - Single source of truth for voice ID mappings -# @context Maps human-readable voice names to ElevenLabs API voice IDs for consistency -# @architecture Associative array (bash hash map) sourced by multiple scripts -# @dependencies None (pure data structure) -# @entrypoints Sourced by voice-manager.sh, play-tts-elevenlabs.sh, and personality managers -# @patterns Centralized configuration, DRY principle for voice mappings -# @related voice-manager.sh, play-tts-elevenlabs.sh, personality/*.md files - -declare -A VOICES=( - ["Amy"]="bhJUNIXWQQ94l8eI2VUf" - ["Antoni"]="ErXwobaYiN019PkySvjV" - ["Archer"]="L0Dsvb3SLTyegXwtm47J" - ["Aria"]="TC0Zp7WVFzhA8zpTlRqV" - ["Bella"]="EXAVITQu4vr4xnSDxMaL" - ["Burt Reynolds"]="4YYIPFl9wE5c4L2eu2Gb" - ["Charlotte"]="XB0fDUnXU5powFXDhCwa" - ["Cowboy Bob"]="KTPVrSVAEUSJRClDzBw7" - ["Demon Monster"]="vfaqCOvlrKi4Zp7C2IAm" - ["Domi"]="AZnzlk1XvdvUeBnXmlld" - ["Dr. Von Fusion"]="yjJ45q8TVCrtMhEKurxY" - ["Drill Sergeant"]="vfaqCOvlrKi4Zp7C2IAm" - ["Grandpa Spuds Oxley"]="NOpBlnGInO9m6vDvFkFC" - ["Grandpa Werthers"]="MKlLqCItoCkvdhrxgtLv" - ["Jessica Anne Bogart"]="flHkNRp1BlvT73UL6gyz" - ["Juniper"]="aMSt68OGf4xUZAnLpTU8" - ["Lutz Laugh"]="9yzdeviXkFddZ4Oz8Mok" - ["Matilda"]="XrExE9yKIg1WjnnlVkGX" - ["Matthew Schmitz"]="0SpgpJ4D3MpHCiWdyTg3" - ["Michael"]="U1Vk2oyatMdYs096Ety7" - ["Ms. Walker"]="DLsHlh26Ugcm6ELvS0qi" - ["Northern Terry"]="wo6udizrrtpIxWGp2qJk" - ["Pirate Marshal"]="PPzYpIqttlTYA83688JI" - ["Rachel"]="21m00Tcm4TlvDq8ikWAM" - ["Ralf Eisend"]="A9evEp8yGjv4c3WsIKuY" - ["Tiffany"]="6aDn1KB0hjpdcocrUkmq" - ["Tom"]="DYkrAHD8iwork3YSUBbs" -) \ No newline at end of file