Spaces:
Sleeping
Sleeping
File size: 12,109 Bytes
e12db0b 95dcd0c e12db0b 95dcd0c e12db0b 95dcd0c e12db0b 95dcd0c e12db0b 95dcd0c e12db0b 95dcd0c e12db0b 95dcd0c e12db0b 95dcd0c e12db0b 38097c0 e12db0b 38097c0 e12db0b 38097c0 e12db0b 38097c0 e12db0b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | #!/usr/bin/env python
"""
GritAI β Audio β Notes (Project #7 of "30 AI Projects in 15 Days")
Upload a recording or record from the mic -> a clean TRANSCRIPT with per-minute [mm:ss] timestamps
(quotable by the minute) -> a short SUMMARY + a list of ACTION ITEMS (task / owner / due).
FREE tier: faster-whisper ("small") on HF ZeroGPU for transcription + one Claude (haiku) call with
forced tool-use for schema-valid {summary, action_items}. PRO tier ("GritAI Studio"): long meetings,
SPEAKER attribution (who said what), a searchable/quotable archive, and auto-generated briefing docs --
the composition of several 30-in-15 rungs, assembled later. Shown here as a waitlist.
Two-stage pipeline: transcription runs inside @spaces.GPU (GPU held only for that); the summarization
Claude call runs after, on CPU, so the GPU is released before the network call.
"""
import os
import gradio as gr
ON_SPACE = bool(os.environ.get("SPACE_ID")) # True when running on a Hugging Face Space
WHISPER_SIZE = "base" # int8 on CPU β fast + reliable for short clips, no CUDA libs needed
CLAUDE_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5")
MAX_TRANSCRIPT_CHARS = 24000 # keep the summarize call comfortably in-context
_MODEL = None
def _get_model():
"""Construct the faster-whisper model on CPU (int8), cached. Runs anywhere β no GPU / CUDA libs."""
global _MODEL
if _MODEL is None:
from faster_whisper import WhisperModel
_MODEL = WhisperModel(WHISPER_SIZE, device="cpu", compute_type="int8")
return _MODEL
# Warm the model at startup on the Space so the first request isn't slow.
if ON_SPACE:
try:
_get_model()
print("[startup] faster-whisper (base, cpu/int8) loaded", flush=True)
except Exception as _e:
print(f"[startup] whisper load failed (will retry on first call): {_e}", flush=True)
def _mmss(seconds):
seconds = int(seconds or 0)
return f"{seconds // 60:02d}:{seconds % 60:02d}"
def _transcribe(audio_path):
try:
model = _get_model()
except ImportError:
raise gr.Error(
"ποΈ Local UI preview β transcription runs on the Space (faster-whisper isn't installed here). "
"The controls and Studio waitlist are live."
)
segments, info = model.transcribe(audio_path, vad_filter=True, beam_size=5)
rows = [(seg.start, seg.end, seg.text.strip()) for seg in segments]
return rows, getattr(info, "language", "?")
NOTES_TOOL = {
"name": "emit_notes",
"description": "Return a concise summary and the action items from a meeting/voice-memo transcript.",
"input_schema": {
"type": "object",
"properties": {
"summary": {"type": "string", "description": "3-5 sentence plain-English summary."},
"action_items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"task": {"type": "string"},
"owner": {"type": "string", "description": "Person responsible, or 'unassigned'."},
"due": {"type": "string", "description": "Due date/'when', or 'not stated'."},
},
"required": ["task"],
},
},
},
"required": ["summary", "action_items"],
},
}
def _summarize(transcript):
key = os.environ.get("ANTHROPIC_API_KEY")
if not key:
return "_(Set ANTHROPIC_API_KEY on the Space to get the summary + action items.)_", ""
try:
import anthropic
client = anthropic.Anthropic(api_key=key)
msg = client.messages.create(
model=CLAUDE_MODEL,
max_tokens=1024,
tools=[NOTES_TOOL],
tool_choice={"type": "tool", "name": "emit_notes"},
messages=[{"role": "user", "content":
"Summarize this transcript and extract the action items.\n\n" + transcript[:MAX_TRANSCRIPT_CHARS]}],
)
data = next((b.input for b in msg.content if getattr(b, "type", "") == "tool_use"), {})
except Exception as e:
return f"_(Summary unavailable: {type(e).__name__})_", ""
summary = data.get("summary", "").strip()
items = data.get("action_items", []) or []
if items:
lines = []
for it in items:
task = (it.get("task") or "").strip()
owner = (it.get("owner") or "unassigned").strip()
due = (it.get("due") or "not stated").strip()
lines.append(f"- **{task}** β _{owner}_ Β· {due}")
actions_md = "\n".join(lines)
else:
actions_md = "_No action items found._"
return summary or "_(no summary)_", actions_md
def process(audio_path, progress=gr.Progress(track_tqdm=True)):
if not audio_path:
raise gr.Error("Upload a file or record from the mic first.")
rows, lang = _transcribe(audio_path)
if not rows:
raise gr.Error("No speech detected in that audio.")
transcript = "\n".join(f"[{_mmss(s)}] {text}" for s, e, text in rows if text)
summary, actions_md = _summarize(transcript)
return transcript, summary, actions_md
# ----------------------------------------------------------------------------- UI
THEME = gr.themes.Base(
primary_hue=gr.themes.colors.orange, neutral_hue=gr.themes.colors.slate,
font=[gr.themes.GoogleFont("IBM Plex Sans"), "system-ui", "sans-serif"],
font_mono=[gr.themes.GoogleFont("IBM Plex Mono"), "monospace"],
).set(
body_background_fill="#0a0b0e", body_text_color="#eef0f4", body_text_color_subdued="#9aa1b0",
background_fill_primary="#111318", background_fill_secondary="#0e1015",
block_background_fill="#111318", block_border_color="#20242e",
block_label_background_fill="#0e1015", block_label_text_color="#9aa1b0", block_title_text_color="#eef0f4",
border_color_primary="#20242e", border_color_accent="#c9451f",
input_background_fill="#0e1015", input_border_color="#2b303c", input_border_color_focus="#c9451f",
button_primary_background_fill="#ff5a2a", button_primary_background_fill_hover="#ff7d54",
button_primary_text_color="#140a06", button_primary_border_color="#c9451f",
button_secondary_background_fill="#15181f", button_secondary_text_color="#eef0f4",
button_secondary_border_color="#2b303c",
color_accent="#ff5a2a", color_accent_soft="#2b1810", slider_color="#ff5a2a",
checkbox_background_color_selected="#ff5a2a", checkbox_border_color_focus="#c9451f",
)
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Archivo:wght@700;800;900&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap');
.gradio-container{background:#0a0b0e !important}
.gradio-container::before{content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
background:repeating-linear-gradient(90deg,transparent 0 63px,rgba(255,255,255,.018) 63px 64px),
repeating-linear-gradient(0deg,transparent 0 63px,rgba(255,255,255,.018) 63px 64px),
radial-gradient(120% 70% at 50% -10%,#141821 0%,#0a0b0e 55%)}
.main,.contain{position:relative;z-index:1}
#ghead{display:flex;align-items:center;gap:14px;padding:6px 2px 4px}
#ghead .co{font-family:'Archivo',sans-serif;font-weight:900;font-size:19px;letter-spacing:.5px;color:#eef0f4;line-height:1}
#ghead .co b{color:#ff5a2a}
#ghead .sub{font-family:'IBM Plex Mono',monospace;font-size:10.5px;letter-spacing:.28em;text-transform:uppercase;color:#5f6675;margin-top:5px}
#ghead .tags{margin-left:auto;display:flex;gap:8px}
#ghead .tag{font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:#9aa1b0;border:1px solid #2b303c;border-radius:2px;padding:5px 9px}
#ghead .tag.hot{color:#ff5a2a;border-color:#c9451f}
#ghero h1{font-family:'Archivo',sans-serif;font-weight:800;font-size:clamp(26px,4.2vw,40px);line-height:1.05;letter-spacing:-.02em;margin:14px 0 4px;color:#eef0f4}
#ghero h1 .g{color:#ff5a2a}
#ghero p{color:#9aa1b0;margin:0;font-size:14.5px}
footer{visibility:hidden}
button.selected{color:#ff5a2a !important}
.gritai-foot{text-align:center;color:#5f6675;font-size:.8rem;margin-top:16px;font-family:'IBM Plex Mono',monospace;letter-spacing:.05em}
.gritai-foot a{color:#9aa1b0;text-decoration:none}
.pro-card{border:1px solid #2b303c;border-left:2px solid #ff5a2a;border-radius:4px;padding:16px 18px;background:#111318}
.pro-card h3{color:#eef0f4}
"""
with gr.Blocks(theme=THEME, css=CSS, title="GritAI Β· Audio β Notes") as demo:
gr.HTML('''<div id="ghead">
<div style="width:34px;height:34px;flex:none"><svg width="34" height="34" viewBox="0 0 38 38" fill="none"><rect x="1" y="1" width="36" height="36" rx="2" stroke="#2b303c"/><path d="M27 12.5A9 9 0 1 0 28 22H19" stroke="#ff5a2a" stroke-width="2.4" stroke-linecap="square"/><rect x="10.5" y="10.5" width="3" height="3" fill="#ff5a2a"/></svg></div>
<div><div class="co">GRIT<b>AI</b></div><div class="sub">Audio → Notes</div></div>
<div class="tags"><span class="tag hot">▸ Live</span><span class="tag">SDVOSB</span><span class="tag">Lawton OK</span></div>
</div>
<div id="ghero"><h1>Audio in. <span class="g">Notes</span> out.</h1>
<p>Timestamped transcript + summary + action items · quotable by the minute · Project #7 of 30 in 15 days.</p></div>''')
with gr.Tab("Transcribe & summarize (free)"):
with gr.Row():
with gr.Column(scale=2):
audio = gr.Audio(sources=["upload", "microphone"], type="filepath", label="Audio")
go = gr.Button("Transcribe βΆ", variant="primary")
gr.Markdown("<small>Best on short clips (a few minutes). Long meetings + speaker labels "
"are the Pro tier.</small>")
with gr.Column(scale=3):
summary = gr.Markdown(label="Summary")
actions = gr.Markdown(label="Action items")
transcript = gr.Textbox(label="Transcript ( [mm:ss] per line )", lines=16, show_copy_button=True)
go.click(process, inputs=[audio], outputs=[transcript, summary, actions], api_name="transcribe")
with gr.Tab("GritAI Studio (Pro)"):
gr.Markdown(
"<div class='pro-card'>\n\n"
"### π Beyond the free demo\n"
"The free tier transcribes short clips and pulls a summary + action items. **GritAI Studio** "
"turns audio into an actual knowledge base:\n\n"
"- **Long meetings** β hours, not minutes.\n"
"- **Speaker attribution** β who said what, quotable by **minute _and_ speaker**.\n"
"- **Searchable & quotable archive** β find any moment across every recording, with citations.\n"
"- **Auto-briefings** β one-click briefing docs generated from the transcript.\n\n"
"*Built on our own stack β the composition of several GritAI tools into one briefing room.*\n\n"
"</div>"
)
with gr.Row():
email = gr.Textbox(label="Email", placeholder="you@team.com", scale=3)
use_case = gr.Textbox(label="What would you use it for?", placeholder="e.g. weekly stand-ups", scale=4)
join = gr.Button("Join the Studio waitlist", variant="primary")
waitlist_msg = gr.Markdown(visible=False)
def _join(email_v, use_case_v):
if not email_v or "@" not in email_v:
return gr.update(value="β οΈ Please enter a valid email.", visible=True)
return gr.update(value="β
You're on the list β we'll reach out when GritAI Studio opens. Thanks!",
visible=True)
join.click(_join, inputs=[email, use_case], outputs=waitlist_msg)
gr.HTML("<div class='gritai-foot'>faster-whisper + Claude · "
"<a href='https://gritai.solutions'>GRITAI SOLUTIONS</a> · 30-in-15 №7</div>")
if __name__ == "__main__":
demo.queue(max_size=20).launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))
|