|
|
import os, json, re, datetime |
|
|
from typing import Dict, List |
|
|
from transformers import pipeline |
|
|
from faster_whisper import WhisperModel |
|
|
|
|
|
|
|
|
_SUMMARIZER = None |
|
|
_EXTRACTOR = None |
|
|
_WHISPER = None |
|
|
|
|
|
def get_summarizer(): |
|
|
global _SUMMARIZER |
|
|
if _SUMMARIZER is None: |
|
|
_SUMMARIZER = pipeline("summarization", model="facebook/bart-large-cnn") |
|
|
return _SUMMARIZER |
|
|
|
|
|
def get_extractor(): |
|
|
"""Flan-T5 used for JSON-style action/decision extraction via text2text pipeline.""" |
|
|
global _EXTRACTOR |
|
|
if _EXTRACTOR is None: |
|
|
_EXTRACTOR = pipeline("text2text-generation", model="google/flan-t5-large", max_new_tokens=256) |
|
|
return _EXTRACTOR |
|
|
|
|
|
def get_whisper(device: str = "auto"): |
|
|
global _WHISPER |
|
|
if _WHISPER is None: |
|
|
|
|
|
_WHISPER = WhisperModel("Systran/faster-whisper-small", device=device, compute_type="int8") |
|
|
return _WHISPER |
|
|
|
|
|
|
|
|
def transcribe_audio(audio_path: str) -> str: |
|
|
model = get_whisper() |
|
|
segments, info = model.transcribe(audio_path, beam_size=1) |
|
|
text = " ".join(seg.text.strip() for seg in segments) |
|
|
return text.strip() |
|
|
|
|
|
def _chunk(text: str, max_chars: int) -> List[str]: |
|
|
parts, buf, size = [], [], 0 |
|
|
import re as _re |
|
|
for sent in _re.split(r'(?<=[\.!\?])\s+', text): |
|
|
if size + len(sent) > max_chars and buf: |
|
|
parts.append(" ".join(buf)); buf, size = [], 0 |
|
|
buf.append(sent); size += len(sent) + 1 |
|
|
if buf: parts.append(" ".join(buf)) |
|
|
return parts |
|
|
|
|
|
def summarize(text: str) -> str: |
|
|
summarizer = get_summarizer() |
|
|
chunks = _chunk(text, 2200) |
|
|
partials = [summarizer(ch, do_sample=False)[0]["summary_text"] for ch in chunks] |
|
|
merged = " ".join(partials) |
|
|
final = summarizer(merged, do_sample=False, max_length=200, min_length=60)[0]["summary_text"] |
|
|
return final |
|
|
|
|
|
def extract_actions_decisions(text: str) -> Dict[str, List[str]]: |
|
|
prompt = f"""You are a meeting note-taking assistant. |
|
|
From the transcript below, extract: |
|
|
1) a concise list of "Action Items" (who does what, use infinitive verb, include deadline if mentioned) |
|
|
2) a list of "Decisions" (short statements) |
|
|
|
|
|
Return strict JSON with this shape: |
|
|
{{"actions": ["...","..."], "decisions": ["...","..."]}} |
|
|
|
|
|
Transcript: |
|
|
{text[:7000]} |
|
|
""" |
|
|
gen = get_extractor() |
|
|
out = gen(prompt)[0]["generated_text"] |
|
|
try: |
|
|
data = json.loads(out) |
|
|
actions = [s.strip() for s in data.get("actions", []) if s.strip()] |
|
|
decisions = [s.strip() for s in data.get("decisions", []) if s.strip()] |
|
|
return {"actions": actions, "decisions": decisions} |
|
|
except Exception: |
|
|
|
|
|
actions, decisions = [], [] |
|
|
for line in text.splitlines(): |
|
|
if re.search(r"(?i)\b(action|todo|to do):", line): |
|
|
actions.append(re.sub(r"(?i)^.*?:\s*", "", line).strip()) |
|
|
if re.search(r"(?i)\b(decision|decisions):", line): |
|
|
decisions.append(re.sub(r"(?i)^.*?:\s*", "", line).strip()) |
|
|
return {"actions": actions, "decisions": decisions} |
|
|
|
|
|
def make_minutes_md(title: str, summary: str, actions: List[str], decisions: List[str]) -> str: |
|
|
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") |
|
|
lines = [ |
|
|
f"# {title} β Minutes", |
|
|
f"_Generated on {now}_", |
|
|
"", |
|
|
"## Summary", |
|
|
summary.strip() if summary else "β", |
|
|
"", |
|
|
"## Action Items", |
|
|
*[f"- [ ] {a}" for a in (actions or ["β"])], |
|
|
"", |
|
|
"## Decisions", |
|
|
*[f"- {d}" for d in (decisions or ["β"])], |
|
|
"", |
|
|
] |
|
|
return "\n".join(lines) |
|
|
|