Upload 9 files
Browse files- CITATIONS.md +17 -0
- DEMO_SCRIPT.md +25 -0
- PROMPTS.md +19 -0
- README.md +49 -12
- USER_GUIDE.md +26 -0
- app.py +54 -0
- nlp_utils.py +101 -0
- requirements.txt +7 -0
- sample_transcript_en.txt +7 -0
CITATIONS.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CITATIONS
|
| 2 |
+
|
| 3 |
+
## Packages
|
| 4 |
+
- **gradio** (Apache 2.0)
|
| 5 |
+
- **transformers** (Apache 2.0) β Wolf et al. 2020
|
| 6 |
+
- **torch** (BSD-style)
|
| 7 |
+
- **sentencepiece** (Apache 2.0)
|
| 8 |
+
- **faster-whisper** (MIT)
|
| 9 |
+
- **numpy**, **tqdm** (BSD/MIT)
|
| 10 |
+
|
| 11 |
+
## Models (Hugging Face)
|
| 12 |
+
- `facebook/bart-large-cnn` β summarization (MIT)
|
| 13 |
+
- `google/flan-t5-large` β text generation/extraction (Apache 2.0)
|
| 14 |
+
- `Systran/faster-whisper-small` β transcription (MIT, multilingual)
|
| 15 |
+
|
| 16 |
+
## Data
|
| 17 |
+
- `data/sample_transcript_en.txt` β small synthetic example for testing.
|
DEMO_SCRIPT.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Demo Script (β€ 5 min) β MeetingNotes AI (EN)
|
| 2 |
+
|
| 3 |
+
0:00β0:20 β Hook
|
| 4 |
+
- Too many meetings, not enough time.
|
| 5 |
+
- MeetingNotes AI: audio/transcript β Summary + Action Items + Decisions + minutes.md
|
| 6 |
+
|
| 7 |
+
0:20β1:20 β Live demo
|
| 8 |
+
- Paste `data/sample_transcript_en.txt` or upload a short .mp3
|
| 9 |
+
- Click **Analyze**
|
| 10 |
+
- Show Summary + Actions + Decisions
|
| 11 |
+
|
| 12 |
+
1:20β2:20 β minutes.md
|
| 13 |
+
- Download / open the generated file
|
| 14 |
+
- Show the clean structure
|
| 15 |
+
|
| 16 |
+
2:20β3:30 β How it works
|
| 17 |
+
- Transcription: faster-whisper (small, multilingual)
|
| 18 |
+
- Summarization: BART CNN
|
| 19 |
+
- Extraction: Flan-T5 with a strict JSON prompt
|
| 20 |
+
|
| 21 |
+
3:30β4:30 β Value at scale
|
| 22 |
+
- Saves time, clarifies responsibilities, improves follow-up
|
| 23 |
+
|
| 24 |
+
4:30β5:00 β CTA
|
| 25 |
+
- Open-source, easy to deploy on Hugging Face Spaces
|
PROMPTS.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PROMPTS β MeetingNotes AI (EN)
|
| 2 |
+
|
| 3 |
+
## Summarization (BART via `pipeline("summarization")`)
|
| 4 |
+
- No custom prompt (default pipeline).
|
| 5 |
+
|
| 6 |
+
## Action Items & Decisions (Flan-T5)
|
| 7 |
+
Template used in `nlp_utils.py`:
|
| 8 |
+
```
|
| 9 |
+
You are a meeting note-taking assistant.
|
| 10 |
+
From the transcript below, extract:
|
| 11 |
+
1) a concise list of "Action Items" (who does what, use infinitive verb, include deadline if any)
|
| 12 |
+
2) a list of "Decisions" (short statements)
|
| 13 |
+
|
| 14 |
+
Return strict JSON with this shape:
|
| 15 |
+
{"actions": ["...","..."], "decisions": ["...","..."]}
|
| 16 |
+
|
| 17 |
+
Transcript:
|
| 18 |
+
{TRANSCRIPT}
|
| 19 |
+
```
|
README.md
CHANGED
|
@@ -1,12 +1,49 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MeetingNotes AI β Meeting Summarizer (EN)
|
| 2 |
+
|
| 3 |
+
**Goal:** Upload a **meeting audio** (mp3/wav) or paste a **transcript** and get:
|
| 4 |
+
- β
a clear **Summary**
|
| 5 |
+
- π§± **Action Items** (who does what, by when if stated)
|
| 6 |
+
- π§© **Decisions**
|
| 7 |
+
- ποΈ a ready-to-share **minutes.md**
|
| 8 |
+
|
| 9 |
+
**Tech**
|
| 10 |
+
- Transcription: `faster-whisper` (multilingual; works for English **and** French audio)
|
| 11 |
+
- Summarization: `facebook/bart-large-cnn`
|
| 12 |
+
- Extraction (actions/decisions): `google/flan-t5-large`
|
| 13 |
+
- UI: **Gradio**
|
| 14 |
+
|
| 15 |
+
## Quickstart (local)
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
python -m venv .venv
|
| 19 |
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
| 20 |
+
pip install -r requirements.txt
|
| 21 |
+
# (Optional) install ffmpeg for audio support:
|
| 22 |
+
# macOS: brew install ffmpeg
|
| 23 |
+
# Ubuntu/Debian: sudo apt-get install -y ffmpeg
|
| 24 |
+
python app.py
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## Deploy on Hugging Face Spaces (recommended)
|
| 28 |
+
1. Create a **Gradio** Space
|
| 29 |
+
2. Upload **all files** from this folder
|
| 30 |
+
3. Wait for the build to finish (it reads `requirements.txt`)
|
| 31 |
+
4. Test with a `.mp3/.wav` or paste a transcript
|
| 32 |
+
|
| 33 |
+
## Structure
|
| 34 |
+
```
|
| 35 |
+
MeetingNotes_AI_EN/
|
| 36 |
+
ββ app.py # Gradio UI (English)
|
| 37 |
+
ββ nlp_utils.py # Transcription + summarization + action/decision extraction
|
| 38 |
+
ββ requirements.txt
|
| 39 |
+
ββ PROMPTS.md # Prompts and tool-usage log
|
| 40 |
+
ββ CITATIONS.md # Packages & models used
|
| 41 |
+
ββ USER_GUIDE.md # User guide (English)
|
| 42 |
+
ββ DEMO_SCRIPT.md # β€ 5-min demo script (English)
|
| 43 |
+
ββ data/
|
| 44 |
+
β ββ sample_transcript_en.txt
|
| 45 |
+
ββ outputs/ # generated minutes.md
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
## License
|
| 49 |
+
MIT β 2025
|
USER_GUIDE.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# User Guide β MeetingNotes AI (EN)
|
| 2 |
+
|
| 3 |
+
## Run the app
|
| 4 |
+
- Local: see README (venv β pip install β `python app.py`)
|
| 5 |
+
- Hugging Face Spaces: upload all files and open the Space
|
| 6 |
+
|
| 7 |
+
## How to use
|
| 8 |
+
1. **Choose your input**:
|
| 9 |
+
- Upload a **meeting audio** (.mp3/.wav) β click **Analyze** to transcribe.
|
| 10 |
+
- OR paste a **transcript**.
|
| 11 |
+
|
| 12 |
+
2. **Outputs**:
|
| 13 |
+
- **Summary** (1β2 paragraphs)
|
| 14 |
+
- **Action Items** (list)
|
| 15 |
+
- **Decisions** (list)
|
| 16 |
+
- A downloadable **minutes.md** file
|
| 17 |
+
|
| 18 |
+
3. **Tips**:
|
| 19 |
+
- Prefer clean recordings for audio (less noise).
|
| 20 |
+
- Multiple speakers are fine; diarization is not enabled by default.
|
| 21 |
+
- You can edit the transcript and re-run the extraction.
|
| 22 |
+
|
| 23 |
+
## Troubleshooting
|
| 24 |
+
- If audio fails: ensure **ffmpeg** is available.
|
| 25 |
+
- If itβs slow on CPU: use a smaller Whisper model (tiny/base) or `flan-t5-base` in `nlp_utils.py`.
|
| 26 |
+
- Transcript-only flow works without ffmpeg.
|
app.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr, os, json
|
| 2 |
+
from nlp_utils import transcribe_audio, summarize, extract_actions_decisions, make_minutes_md
|
| 3 |
+
|
| 4 |
+
OUT_DIR = "outputs"
|
| 5 |
+
os.makedirs(OUT_DIR, exist_ok=True)
|
| 6 |
+
|
| 7 |
+
def process(audio_file, transcript_text, meeting_title):
|
| 8 |
+
text = ""
|
| 9 |
+
if audio_file is not None:
|
| 10 |
+
text = transcribe_audio(audio_file)
|
| 11 |
+
if transcript_text and transcript_text.strip():
|
| 12 |
+
extra = transcript_text.strip()
|
| 13 |
+
text = (text + "\n" + extra).strip() if text else extra
|
| 14 |
+
|
| 15 |
+
if not text or len(text) < 40:
|
| 16 |
+
return "Please upload audio OR paste a transcript (β₯ 40 characters).", "", [], [], None
|
| 17 |
+
|
| 18 |
+
resum = summarize(text)
|
| 19 |
+
ed = extract_actions_decisions(text)
|
| 20 |
+
actions = ed.get("actions", [])
|
| 21 |
+
decisions = ed.get("decisions", [])
|
| 22 |
+
|
| 23 |
+
title = meeting_title or "Meeting"
|
| 24 |
+
md = make_minutes_md(title, resum, actions, decisions)
|
| 25 |
+
md_path = os.path.join(OUT_DIR, "minutes.md")
|
| 26 |
+
with open(md_path, "w", encoding="utf-8") as f:
|
| 27 |
+
f.write(md)
|
| 28 |
+
|
| 29 |
+
actions_ht = [(a, "Action") for a in actions] if actions else []
|
| 30 |
+
decisions_ht = [(d, "Decision") for d in decisions] if decisions else []
|
| 31 |
+
|
| 32 |
+
return "Done β
", resum, actions_ht, decisions_ht, md_path
|
| 33 |
+
|
| 34 |
+
with gr.Blocks(title="MeetingNotes AI β Meeting Summarizer") as demo:
|
| 35 |
+
gr.Markdown("# MeetingNotes AI β Meeting Summarizer")
|
| 36 |
+
gr.Markdown("Upload **audio** or **paste a transcript**, then click **Analyze**. Multilingual audio supported (EN/FR).")
|
| 37 |
+
|
| 38 |
+
with gr.Row():
|
| 39 |
+
with gr.Column():
|
| 40 |
+
meeting_title = gr.Textbox(label="Meeting Title", value="Product Launch β Weekly")
|
| 41 |
+
audio = gr.Audio(label="Audio (mp3/wav)", sources=["upload"], type="filepath")
|
| 42 |
+
transcript = gr.Textbox(label="Transcript (optional if audio)", lines=10, placeholder="Paste hereβ¦")
|
| 43 |
+
btn = gr.Button("Analyze")
|
| 44 |
+
with gr.Column():
|
| 45 |
+
status = gr.Textbox(label="Status")
|
| 46 |
+
resume = gr.Textbox(label="Summary", lines=8)
|
| 47 |
+
actions = gr.HighlightedText(label="Action Items", combine_adjacent=True)
|
| 48 |
+
decisions = gr.HighlightedText(label="Decisions", combine_adjacent=True)
|
| 49 |
+
files = gr.File(label="Download minutes.md")
|
| 50 |
+
|
| 51 |
+
btn.click(process, inputs=[audio, transcript, meeting_title], outputs=[status, resume, actions, decisions, files])
|
| 52 |
+
|
| 53 |
+
if __name__ == "__main__":
|
| 54 |
+
demo.launch()
|
nlp_utils.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, json, re, datetime
|
| 2 |
+
from typing import Dict, List
|
| 3 |
+
from transformers import pipeline
|
| 4 |
+
from faster_whisper import WhisperModel
|
| 5 |
+
|
| 6 |
+
# --------- Lazy singletons ---------
|
| 7 |
+
_SUMMARIZER = None
|
| 8 |
+
_EXTRACTOR = None
|
| 9 |
+
_WHISPER = None
|
| 10 |
+
|
| 11 |
+
def get_summarizer():
|
| 12 |
+
global _SUMMARIZER
|
| 13 |
+
if _SUMMARIZER is None:
|
| 14 |
+
_SUMMARIZER = pipeline("summarization", model="facebook/bart-large-cnn")
|
| 15 |
+
return _SUMMARIZER
|
| 16 |
+
|
| 17 |
+
def get_extractor():
|
| 18 |
+
"""Flan-T5 used for JSON-style action/decision extraction via text2text pipeline."""
|
| 19 |
+
global _EXTRACTOR
|
| 20 |
+
if _EXTRACTOR is None:
|
| 21 |
+
_EXTRACTOR = pipeline("text2text-generation", model="google/flan-t5-large", max_new_tokens=256)
|
| 22 |
+
return _EXTRACTOR
|
| 23 |
+
|
| 24 |
+
def get_whisper(device: str = "auto"):
|
| 25 |
+
global _WHISPER
|
| 26 |
+
if _WHISPER is None:
|
| 27 |
+
# Small multilingual model: works for English + French audio
|
| 28 |
+
_WHISPER = WhisperModel("Systran/faster-whisper-small", device=device, compute_type="int8")
|
| 29 |
+
return _WHISPER
|
| 30 |
+
|
| 31 |
+
# --------- Core ---------
|
| 32 |
+
def transcribe_audio(audio_path: str) -> str:
|
| 33 |
+
model = get_whisper()
|
| 34 |
+
segments, info = model.transcribe(audio_path, beam_size=1)
|
| 35 |
+
text = " ".join(seg.text.strip() for seg in segments)
|
| 36 |
+
return text.strip()
|
| 37 |
+
|
| 38 |
+
def _chunk(text: str, max_chars: int) -> List[str]:
|
| 39 |
+
parts, buf, size = [], [], 0
|
| 40 |
+
import re as _re
|
| 41 |
+
for sent in _re.split(r'(?<=[\.!\?])\s+', text):
|
| 42 |
+
if size + len(sent) > max_chars and buf:
|
| 43 |
+
parts.append(" ".join(buf)); buf, size = [], 0
|
| 44 |
+
buf.append(sent); size += len(sent) + 1
|
| 45 |
+
if buf: parts.append(" ".join(buf))
|
| 46 |
+
return parts
|
| 47 |
+
|
| 48 |
+
def summarize(text: str) -> str:
|
| 49 |
+
summarizer = get_summarizer()
|
| 50 |
+
chunks = _chunk(text, 2200)
|
| 51 |
+
partials = [summarizer(ch, do_sample=False)[0]["summary_text"] for ch in chunks]
|
| 52 |
+
merged = " ".join(partials)
|
| 53 |
+
final = summarizer(merged, do_sample=False, max_length=200, min_length=60)[0]["summary_text"]
|
| 54 |
+
return final
|
| 55 |
+
|
| 56 |
+
def extract_actions_decisions(text: str) -> Dict[str, List[str]]:
|
| 57 |
+
prompt = f"""You are a meeting note-taking assistant.
|
| 58 |
+
From the transcript below, extract:
|
| 59 |
+
1) a concise list of "Action Items" (who does what, use infinitive verb, include deadline if mentioned)
|
| 60 |
+
2) a list of "Decisions" (short statements)
|
| 61 |
+
|
| 62 |
+
Return strict JSON with this shape:
|
| 63 |
+
{{"actions": ["...","..."], "decisions": ["...","..."]}}
|
| 64 |
+
|
| 65 |
+
Transcript:
|
| 66 |
+
{text[:7000]}
|
| 67 |
+
"""
|
| 68 |
+
gen = get_extractor()
|
| 69 |
+
out = gen(prompt)[0]["generated_text"]
|
| 70 |
+
try:
|
| 71 |
+
data = json.loads(out)
|
| 72 |
+
actions = [s.strip() for s in data.get("actions", []) if s.strip()]
|
| 73 |
+
decisions = [s.strip() for s in data.get("decisions", []) if s.strip()]
|
| 74 |
+
return {"actions": actions, "decisions": decisions}
|
| 75 |
+
except Exception:
|
| 76 |
+
# Fallback heuristic if JSON parsing fails
|
| 77 |
+
actions, decisions = [], []
|
| 78 |
+
for line in text.splitlines():
|
| 79 |
+
if re.search(r"(?i)\b(action|todo|to do):", line):
|
| 80 |
+
actions.append(re.sub(r"(?i)^.*?:\s*", "", line).strip())
|
| 81 |
+
if re.search(r"(?i)\b(decision|decisions):", line):
|
| 82 |
+
decisions.append(re.sub(r"(?i)^.*?:\s*", "", line).strip())
|
| 83 |
+
return {"actions": actions, "decisions": decisions}
|
| 84 |
+
|
| 85 |
+
def make_minutes_md(title: str, summary: str, actions: List[str], decisions: List[str]) -> str:
|
| 86 |
+
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
|
| 87 |
+
lines = [
|
| 88 |
+
f"# {title} β Minutes",
|
| 89 |
+
f"_Generated on {now}_",
|
| 90 |
+
"",
|
| 91 |
+
"## Summary",
|
| 92 |
+
summary.strip() if summary else "β",
|
| 93 |
+
"",
|
| 94 |
+
"## Action Items",
|
| 95 |
+
*[f"- [ ] {a}" for a in (actions or ["β"])],
|
| 96 |
+
"",
|
| 97 |
+
"## Decisions",
|
| 98 |
+
*[f"- {d}" for d in (decisions or ["β"])],
|
| 99 |
+
"",
|
| 100 |
+
]
|
| 101 |
+
return "\n".join(lines)
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44.0
|
| 2 |
+
transformers>=4.44.0
|
| 3 |
+
torch>=2.2.0
|
| 4 |
+
sentencepiece>=0.1.99
|
| 5 |
+
faster-whisper>=1.0.0
|
| 6 |
+
numpy>=1.26.4
|
| 7 |
+
tqdm>=4.66.4
|
sample_transcript_en.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[00:00] Alice: Welcome everyone. Goal: finalize the launch plan.
|
| 2 |
+
[00:15] Bob: We still need visuals for the campaign.
|
| 3 |
+
[00:30] Chloe: Design team will share a first draft on Wednesday.
|
| 4 |
+
[00:45] Alice: Decision: we keep the budget at $20k.
|
| 5 |
+
[01:00] Bob: Action: I'll contact the media agency today.
|
| 6 |
+
[01:15] Chloe: Action: I'll prepare a checklist for the product page.
|
| 7 |
+
[01:30] Alice: Next meeting Friday 10am. End.
|