Course EN
← back to chapter

Notas de reunión

Pega una audio URL de una reunión o nota de voz y obtén un resumen de una línea más

Showcase — Notas de reunión

Pega una audio URL de una reunión o nota de voz y obtén un resumen de una línea más puntos de acción. Es la forma de la mayoría de las features de audio reales: speech to text, luego un modelo de texto hace el razonamiento. OpenAI lo hace en dos pasos (transcribir, luego resumir); Gemini entiende audio de forma nativa y lo hace en una sola llamada — ambos aterrizan en el mismo lugar.

Córrelo

bash bootstrap-secrets.sh              # reads ../../../../.env, writes secrets/
docker compose up --build              # default: PROVIDER=openai

Abre http://localhost:3000. Gemini: PROVIDER=gemini docker compose up --build.

Qué hay aquí

  • backend/ai_openai.py — transcribe, luego resume con un modelo de chat.
  • backend/ai_gemini.py — una sola llamada multimodal: audio adentro, resumen afuera.
  • frontend/app/page.tsx — caja de URL + resumen y transcripción.

Detenlo

docker compose down

Ejecútalo en tu máquina

Descarga el proyecto como ZIP y córrelo con Docker. Levanta un backend FastAPI y un frontend Next.js en localhost:3000.

Descargar meeting-notes.zip

unzip meeting-notes.zip
cd meeting-notes
bash bootstrap-secrets.sh   # one-time: pulls API keys into ./secrets
docker compose up --build   # default provider: openai
# or:  PROVIDER=gemini docker compose up --build

Escribe algo, elige un proveedor y ejecuta el mismo código de Código contra la API real. Requiere iniciar sesión.


  

Los mismos módulos que ejecuta el botón Run. El proyecto completo (frontend, Dockerfile, compose) está en el ZIP, pestaña README.

backend/ai_openai.py

"""Showcase 3 (OpenAI): audio to meeting notes.

Two steps chained: transcribe the audio, then summarize the transcript into a
one-line recap plus action items. This is the shape of most real audio features
— speech to text, then an ordinary text model does the thinking on the words.
"""
import urllib.request

from openai import OpenAI

_client = OpenAI()

_STT_MODEL = "gpt-4o-mini-transcribe"
_CHAT_MODEL = "gpt-5.4-nano"


def run(audio_url: str) -> str:
    url = audio_url.strip()
    if not url.startswith("http"):
        return "Paste a public audio URL (http/https) of a meeting or voice note."
    data = urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})).read()
    name = url.split("/")[-1].split("?")[0] or "audio.mp3"
    transcript = _client.audio.transcriptions.create(model=_STT_MODEL, file=(name, data)).text

    summary = _client.responses.create(
        model=_CHAT_MODEL,
        instructions=(
            "Summarize this meeting transcript: first one sentence of overall "
            "summary, then a bulleted list of action items with an owner in "
            "parentheses if one is named. Be concise."
        ),
        input=[{"role": "user", "content": transcript}],
    )
    return f"{summary.output_text}\n\n---\nTranscript:\n{transcript}"

backend/ai_gemini.py

"""Showcase 3 (Gemini): audio to meeting notes.

Gemini understands audio natively, so this is ONE call: hand it the audio and
ask for the summary directly. (OpenAI does it in two steps — transcribe, then
summarize — which is the more common pattern; both land in the same place.)
"""
import os
import urllib.request

from google import genai
from google.genai import types

_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

_MODEL = "gemini-3.1-flash-lite"

_MIME = {"mp3": "audio/mpeg", "wav": "audio/wav", "m4a": "audio/mp4", "ogg": "audio/ogg", "flac": "audio/flac"}


def _mime(url: str) -> str:
    ext = url.lower().split("?")[0].rsplit(".", 1)[-1]
    return _MIME.get(ext, "audio/mpeg")


def run(audio_url: str) -> str:
    url = audio_url.strip()
    if not url.startswith("http"):
        return "Paste a public audio URL (http/https) of a meeting or voice note."
    data = urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})).read()
    response = _client.models.generate_content(
        model=_MODEL,
        contents=[
            types.Part.from_bytes(data=data, mime_type=_mime(url)),
            "Summarize this meeting audio: first one sentence of overall summary, "
            "then a bulleted list of action items with an owner in parentheses if "
            "one is named. Be concise.",
        ],
    )
    return response.text or ""

Archivos del proyecto

  • .gitignore
  • README.es.md
  • README.md
  • backend/Dockerfile
  • backend/ai_gemini.py
  • backend/ai_openai.py
  • backend/main.py
  • backend/requirements.txt
  • bootstrap-secrets.sh
  • docker-compose.yml
  • frontend/Dockerfile
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/tsconfig.json