Initial commit
This commit is contained in:
207
hooks/post_tool_use_elevenlabs.py
Executable file
207
hooks/post_tool_use_elevenlabs.py
Executable file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "python-dotenv",
|
||||
# "openai",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import json
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def get_simple_summary(tool_name, tool_input, tool_response):
|
||||
"""
|
||||
Create a simple summary without LLM first
|
||||
"""
|
||||
if tool_name == "Task":
|
||||
# Extract task description
|
||||
task_desc = ""
|
||||
if "prompt" in tool_input:
|
||||
task_desc = tool_input['prompt']
|
||||
elif "description" in tool_input:
|
||||
task_desc = tool_input['description']
|
||||
|
||||
# Extract agent name if present
|
||||
if ':' in task_desc:
|
||||
parts = task_desc.split(':', 1)
|
||||
agent_name = parts[0].strip()
|
||||
task_detail = parts[1].strip() if len(parts) > 1 else ""
|
||||
# Shorten task detail
|
||||
if len(task_detail) > 30:
|
||||
task_detail = task_detail[:30] + "..."
|
||||
return f"{agent_name} completed {task_detail}"
|
||||
return "Agent task completed"
|
||||
|
||||
elif tool_name == "Write":
|
||||
file_path = tool_input.get("file_path", "")
|
||||
if file_path:
|
||||
file_name = Path(file_path).name
|
||||
return f"Created {file_name}"
|
||||
return "File created"
|
||||
|
||||
elif tool_name in ["Edit", "MultiEdit"]:
|
||||
file_path = tool_input.get("file_path", "")
|
||||
if file_path:
|
||||
file_name = Path(file_path).name
|
||||
return f"Updated {file_name}"
|
||||
return "File updated"
|
||||
|
||||
return f"{tool_name} completed"
|
||||
|
||||
def get_ai_summary(tool_name, tool_input, tool_response):
|
||||
"""
|
||||
Use OpenAI to create a better summary
|
||||
"""
|
||||
api_key = os.getenv("OPENAI_API_KEY")
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
try:
|
||||
from openai import OpenAI
|
||||
client = OpenAI(api_key=api_key)
|
||||
|
||||
# Build context
|
||||
context = f"Tool: {tool_name}\n"
|
||||
|
||||
if tool_name == "Task":
|
||||
task_desc = tool_input.get("prompt", tool_input.get("description", ""))
|
||||
context += f"Task: {task_desc}\n"
|
||||
if tool_response and "output" in tool_response:
|
||||
# Truncate output if too long
|
||||
output = str(tool_response["output"])[:200]
|
||||
context += f"Result: {output}\n"
|
||||
|
||||
elif tool_name == "Write":
|
||||
file_path = tool_input.get("file_path", "")
|
||||
context += f"File: {file_path}\n"
|
||||
context += "Action: Created new file\n"
|
||||
|
||||
elif tool_name in ["Edit", "MultiEdit"]:
|
||||
file_path = tool_input.get("file_path", "")
|
||||
context += f"File: {file_path}\n"
|
||||
context += "Action: Modified existing file\n"
|
||||
|
||||
prompt = f"""Create a 3-7 word summary of this tool completion for voice announcement.
|
||||
Be specific about what was accomplished.
|
||||
|
||||
{context}
|
||||
|
||||
Examples of good summaries:
|
||||
- "Created user authentication module"
|
||||
- "Updated API endpoints"
|
||||
- "Documentation generator built"
|
||||
- "Fixed validation errors"
|
||||
- "Database schema created"
|
||||
|
||||
Summary:"""
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-5-nano",
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
max_completion_tokens=15,
|
||||
)
|
||||
|
||||
summary = response.choices[0].message.content.strip()
|
||||
# Remove quotes if present
|
||||
summary = summary.strip('"').strip("'")
|
||||
return summary
|
||||
|
||||
except Exception as e:
|
||||
print(f"AI summary error: {e}", file=sys.stderr)
|
||||
return None
|
||||
|
||||
def announce_with_tts(summary):
|
||||
"""
|
||||
Use ElevenLabs Sarah voice for all announcements (high quality, consistent)
|
||||
Falls back to macOS say if ElevenLabs fails.
|
||||
"""
|
||||
script_dir = Path(__file__).parent
|
||||
tts_dir = script_dir / "utils" / "tts"
|
||||
elevenlabs_script = tts_dir / "elevenlabs_tts.py"
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["uv", "run", str(elevenlabs_script), summary],
|
||||
capture_output=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return "elevenlabs"
|
||||
else:
|
||||
# ElevenLabs failed, use macOS fallback
|
||||
subprocess.run(["say", summary], timeout=5)
|
||||
return "macos-fallback"
|
||||
except:
|
||||
# Last resort fallback
|
||||
try:
|
||||
subprocess.run(["say", summary], timeout=5)
|
||||
return "macos-fallback"
|
||||
except:
|
||||
return "none"
|
||||
|
||||
def main():
|
||||
try:
|
||||
# Read input
|
||||
input_data = json.load(sys.stdin)
|
||||
tool_name = input_data.get("tool_name", "")
|
||||
tool_input = input_data.get("tool_input", {})
|
||||
tool_response = input_data.get("tool_response", {})
|
||||
|
||||
# Skip certain tools
|
||||
if tool_name in ["TodoWrite", "Grep", "LS", "Bash", "Read", "Glob", "WebFetch", "WebSearch"]:
|
||||
sys.exit(0)
|
||||
|
||||
# Try AI summary first, fall back to simple summary
|
||||
summary = get_ai_summary(tool_name, tool_input, tool_response)
|
||||
if not summary:
|
||||
summary = get_simple_summary(tool_name, tool_input, tool_response)
|
||||
|
||||
# Announce with TTS (ElevenLabs or local)
|
||||
tts_method = announce_with_tts(summary)
|
||||
|
||||
# Log what we announced
|
||||
log_dir = os.path.join(os.getcwd(), "logs")
|
||||
if os.path.exists(log_dir):
|
||||
log_path = os.path.join(log_dir, "voice_announcements.json")
|
||||
logs = []
|
||||
if os.path.exists(log_path):
|
||||
try:
|
||||
with open(log_path, 'r') as f:
|
||||
logs = json.load(f)
|
||||
except:
|
||||
logs = []
|
||||
|
||||
logs.append({
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"tool": tool_name,
|
||||
"summary": summary,
|
||||
"ai_generated": bool(get_ai_summary(tool_name, tool_input, tool_response)),
|
||||
"tts_method": tts_method
|
||||
})
|
||||
|
||||
# Keep last 50
|
||||
logs = logs[-50:]
|
||||
|
||||
with open(log_path, 'w') as f:
|
||||
json.dump(logs, f, indent=2)
|
||||
|
||||
print(f"Announced via {tts_method}: {summary}")
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Hook error: {e}", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user