Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Phase 3 (Synthesis): - deepdive_synthesis.py: LLM-powered briefing generation - Supports OpenAI (gpt-4o-mini) and Anthropic (claude-3-haiku) - Fallback to keyword summary if LLM unavailable - Intelligence briefing format: Headlines, Deep Dive, Bottom Line Phase 4 (TTS): - TTS integration in orchestrator - Converts markdown to speech-friendly text - Configurable provider (openai/elevenlabs/piper) Phase 5 (Delivery): - Enhanced delivery.py with --text and --chat-id/--bot-token overrides - Supports text-only and audio+text delivery - Full Telegram Bot API integration Orchestrator: - Complete 5-phase pipeline - --dry-run mode for testing - State management in ~/the-nexus/deepdive_state/ - Error handling with fallbacks Progresses #830 to implementation-ready status
187 lines
6.6 KiB
Python
187 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""deepdive_delivery.py — Phase 5: Telegram voice message delivery.
|
|
|
|
Issue: #830 (the-nexus)
|
|
Delivers synthesized audio briefing as Telegram voice message.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
import urllib.request
|
|
|
|
|
|
class TelegramDeliveryAdapter:
|
|
"""Deliver audio briefing via Telegram bot as voice message."""
|
|
|
|
def __init__(self, bot_token: str, chat_id: str):
|
|
self.bot_token = bot_token
|
|
self.chat_id = chat_id
|
|
self.api_base = f"https://api.telegram.org/bot{bot_token}"
|
|
|
|
def _api_post(self, method: str, data: dict, files: dict = None):
|
|
"""Call Telegram Bot API."""
|
|
import urllib.request
|
|
import urllib.parse
|
|
|
|
url = f"{self.api_base}/{method}"
|
|
|
|
if files:
|
|
# Multipart form for file uploads
|
|
boundary = "----DeepDiveBoundary"
|
|
body_parts = []
|
|
|
|
for key, value in data.items():
|
|
body_parts.append(f'--{boundary}\r\nContent-Disposition: form-data; name="{key}"\r\n\r\n{value}\r\n')
|
|
|
|
for key, (filename, content) in files.items():
|
|
body_parts.append(
|
|
f'--{boundary}\r\n'
|
|
f'Content-Disposition: form-data; name="{key}"; filename="{filename}"\r\n'
|
|
f'Content-Type: audio/mpeg\r\n\r\n'
|
|
)
|
|
body_parts.append(content)
|
|
body_parts.append(f'\r\n')
|
|
|
|
body_parts.append(f'--{boundary}--\r\n')
|
|
|
|
body = b""
|
|
for part in body_parts:
|
|
if isinstance(part, str):
|
|
body += part.encode()
|
|
else:
|
|
body += part
|
|
|
|
req = urllib.request.Request(url, data=body, method="POST")
|
|
req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}")
|
|
else:
|
|
body = urllib.parse.urlencode(data).encode()
|
|
req = urllib.request.Request(url, data=body, method="POST")
|
|
req.add_header("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
return json.loads(resp.read().decode())
|
|
except urllib.error.HTTPError as e:
|
|
error_body = e.read().decode()
|
|
raise RuntimeError(f"Telegram API error: {e.code} - {error_body}")
|
|
|
|
def send_voice(self, audio_path: Path, caption: str = None) -> dict:
|
|
"""Send audio file as voice message."""
|
|
audio_bytes = audio_path.read_bytes()
|
|
|
|
files = {"voice": (audio_path.name, audio_bytes)}
|
|
data = {"chat_id": self.chat_id}
|
|
if caption:
|
|
data["caption"] = caption[:1024] # Telegram caption limit
|
|
|
|
result = self._api_post("sendVoice", data, files)
|
|
|
|
if not result.get("ok"):
|
|
raise RuntimeError(f"Telegram send failed: {result}")
|
|
|
|
return result
|
|
|
|
def send_text_preview(self, text: str) -> dict:
|
|
"""Send text summary before voice (optional)."""
|
|
data = {
|
|
"chat_id": self.chat_id,
|
|
"text": text[:4096] # Telegram message limit
|
|
}
|
|
return self._api_post("sendMessage", data)
|
|
|
|
|
|
def load_config():
|
|
"""Load Telegram configuration from environment."""
|
|
token = os.environ.get("DEEPDIVE_TELEGRAM_BOT_TOKEN") or os.environ.get("TELEGRAM_BOT_TOKEN")
|
|
chat_id = os.environ.get("DEEPDIVE_TELEGRAM_CHAT_ID") or os.environ.get("TELEGRAM_CHAT_ID")
|
|
|
|
if not token:
|
|
raise RuntimeError(
|
|
"Telegram bot token required. Set DEEPDIVE_TELEGRAM_BOT_TOKEN or TELEGRAM_BOT_TOKEN"
|
|
)
|
|
if not chat_id:
|
|
raise RuntimeError(
|
|
"Telegram chat ID required. Set DEEPDIVE_TELEGRAM_CHAT_ID or TELEGRAM_CHAT_ID"
|
|
)
|
|
|
|
return token, chat_id
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Deep Dive Delivery Pipeline")
|
|
parser.add_argument("--audio", "-a", help="Path to audio file (MP3)")
|
|
parser.add_argument("--text", "-t", help="Text message to send")
|
|
parser.add_argument("--caption", "-c", help="Caption for voice message")
|
|
parser.add_argument("--preview-text", help="Optional text preview sent before voice")
|
|
parser.add_argument("--bot-token", help="Telegram bot token (overrides env)")
|
|
parser.add_argument("--chat-id", help="Telegram chat ID (overrides env)")
|
|
parser.add_argument("--dry-run", action="store_true", help="Validate config without sending")
|
|
args = parser.parse_args()
|
|
|
|
# Load config
|
|
try:
|
|
if args.bot_token and args.chat_id:
|
|
token, chat_id = args.bot_token, args.chat_id
|
|
else:
|
|
token, chat_id = load_config()
|
|
except RuntimeError as e:
|
|
print(f"[ERROR] {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Validate input
|
|
if not args.audio and not args.text:
|
|
print("[ERROR] Either --audio or --text required", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if args.dry_run:
|
|
print(f"[DRY RUN] Config valid")
|
|
print(f" Bot: {token[:10]}...")
|
|
print(f" Chat: {chat_id}")
|
|
if args.audio:
|
|
audio_path = Path(args.audio)
|
|
print(f" Audio: {audio_path} ({audio_path.stat().st_size} bytes)")
|
|
if args.text:
|
|
print(f" Text: {args.text[:100]}...")
|
|
sys.exit(0)
|
|
|
|
# Deliver
|
|
adapter = TelegramDeliveryAdapter(token, chat_id)
|
|
|
|
# Send text if provided
|
|
if args.text:
|
|
print("[DELIVERY] Sending text message...")
|
|
result = adapter.send_text_preview(args.text)
|
|
message_id = result["result"]["message_id"]
|
|
print(f"[DELIVERY] Text sent! Message ID: {message_id}")
|
|
|
|
# Send audio if provided
|
|
if args.audio:
|
|
audio_path = Path(args.audio)
|
|
if not audio_path.exists():
|
|
print(f"[ERROR] Audio file not found: {audio_path}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if args.preview_text:
|
|
print("[DELIVERY] Sending text preview...")
|
|
adapter.send_text_preview(args.preview_text)
|
|
|
|
print(f"[DELIVERY] Sending voice message: {audio_path}...")
|
|
result = adapter.send_voice(audio_path, args.caption)
|
|
|
|
message_id = result["result"]["message_id"]
|
|
print(f"[DELIVERY] Voice sent! Message ID: {message_id}")
|
|
|
|
print(json.dumps({
|
|
"success": True,
|
|
"message_id": message_id,
|
|
"chat_id": chat_id,
|
|
"audio_size_bytes": audio_path.stat().st_size
|
|
}))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|