feat: initial discord meeting bot
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from helpers import summarize_error
|
||||
|
||||
load_dotenv()
|
||||
|
||||
OPENROUTER_BASE = "https://openrouter.ai/api/v1"
|
||||
|
||||
|
||||
def _api_key() -> str:
|
||||
return os.getenv("OPENROUTER_API_KEY", "")
|
||||
|
||||
|
||||
def _auth_headers() -> dict[str, str]:
|
||||
api_key = _api_key()
|
||||
if not api_key:
|
||||
raise ValueError("OPENROUTER_API_KEY not set")
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
|
||||
|
||||
async def transcribe(audio_path: str) -> str:
|
||||
"""Send a WAV file to OpenRouter's whisper model and return transcript text."""
|
||||
async with httpx.AsyncClient(timeout=300) as client:
|
||||
with open(audio_path, "rb") as audio_file:
|
||||
files = {
|
||||
"file": (os.path.basename(audio_path), audio_file, "audio/wav"),
|
||||
"model": (None, "openai/whisper-large-v3"),
|
||||
}
|
||||
resp = await client.post(
|
||||
f"{OPENROUTER_BASE}/audio/transcriptions",
|
||||
headers=_auth_headers(),
|
||||
files=files,
|
||||
)
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except httpx.HTTPStatusError as exc:
|
||||
detail = summarize_error(_safe_json(resp), fallback=resp.text)
|
||||
raise RuntimeError(f"OpenRouter transcription failed: {detail}") from exc
|
||||
|
||||
data = resp.json()
|
||||
text = data.get("text", "")
|
||||
if not text.strip():
|
||||
raise RuntimeError("OpenRouter transcription returned empty text")
|
||||
return text.strip()
|
||||
|
||||
|
||||
async def summarize(transcript: str) -> str:
|
||||
"""Send transcript to DeepSeek via OpenRouter and return structured meeting summary."""
|
||||
prompt = f"""You are a meeting summarizer. Given the following meeting transcript, produce a concise, well-structured summary with:
|
||||
|
||||
1. **Overview** — 2-3 sentences covering what was discussed
|
||||
2. **Key Decisions** — decisions that were made
|
||||
3. **Action Items** — who needs to do what (use bullet points)
|
||||
4. **Next Steps / Deadlines** — any dates, timelines, or follow-ups mentioned
|
||||
|
||||
If speaker labels are present, preserve them where helpful in action items.
|
||||
|
||||
Transcript:
|
||||
{transcript[:16000]}"""
|
||||
|
||||
body = {
|
||||
"model": "@preset/cheap-deepseek-v4-flash",
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": 1500,
|
||||
}
|
||||
|
||||
headers = _auth_headers()
|
||||
headers["Content-Type"] = "application/json"
|
||||
|
||||
async with httpx.AsyncClient(timeout=180) as client:
|
||||
resp = await client.post(
|
||||
f"{OPENROUTER_BASE}/chat/completions",
|
||||
headers=headers,
|
||||
json=body,
|
||||
)
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except httpx.HTTPStatusError as exc:
|
||||
detail = summarize_error(_safe_json(resp), fallback=resp.text)
|
||||
raise RuntimeError(f"OpenRouter summarization failed: {detail}") from exc
|
||||
|
||||
data = resp.json()
|
||||
try:
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError) as exc:
|
||||
raise RuntimeError("Unexpected OpenRouter summarization response shape") from exc
|
||||
|
||||
if not isinstance(content, str) or not content.strip():
|
||||
raise RuntimeError("OpenRouter summarization returned empty content")
|
||||
return content.strip()
|
||||
|
||||
|
||||
def _safe_json(resp: httpx.Response) -> Any:
|
||||
try:
|
||||
return resp.json()
|
||||
except Exception:
|
||||
return None
|
||||
Reference in New Issue
Block a user