Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:05:52 +08:00
commit db12a906d2
62 changed files with 27669 additions and 0 deletions

207
hooks/post_tool_use_elevenlabs.py Executable file
View 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()