(summary) introduce summary logic for meeting transcripts

Implement summarization functionality that processes completed meeting
transcripts to generate concise summaries.

First draft base on a simple recursive agentic scenario.
Observability and evaluation will be added in the next PRs.
This commit is contained in:
lebaudantoine
2025-09-09 22:16:21 +02:00
committed by aleb_the_flash
parent 9fd264ae0e
commit 849f8ac08c
9 changed files with 187 additions and 1 deletions

View File

@@ -119,6 +119,7 @@ run-backend: ## start only the backend application and all needed services
run-summary: ## start only the summary application and all needed services
@$(COMPOSE) up --force-recreate -d celery-summary-transcribe
@$(COMPOSE) up --force-recreate -d celery-summary-summarize
.PHONY: run-summary
run:

View File

@@ -258,3 +258,19 @@ services:
- redis-summary
- app-summary-dev
- minio
celery-summary-summarize:
container_name: celery-summary-summarize
build:
context: ./src/summary
dockerfile: Dockerfile
target: production
command: celery -A summary.core.celery_worker worker --pool=solo --loglevel=debug -Q summarize-queue
env_file:
- env.d/development/summary
volumes:
- ./src/summary:/app
depends_on:
- redis-summary
- app-summary-dev
- minio

View File

@@ -12,6 +12,10 @@ WHISPERX_BASE_URL="https://configure-your-url.com"
WHISPERX_ASR_MODEL="large-v2"
WHISPERX_API_KEY="your-secret-key"
LLM_BASE_URL="https://configure-your-url.com"
LLM_API_KEY="dev-apikey"
LLM_MODEL="Qwen/Qwen2.5-Coder-32B-Instruct-AWQ"
WEBHOOK_API_TOKEN="secret"
WEBHOOK_URL="https://configure-your-url.com"

View File

@@ -148,6 +148,9 @@ summary:
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
@@ -180,6 +183,9 @@ celery:
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1

View File

@@ -155,6 +155,9 @@ summary:
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
@@ -188,6 +191,9 @@ celery:
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1

View File

@@ -175,6 +175,9 @@ summary:
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
@@ -207,6 +210,9 @@ celery:
WHISPERX_API_KEY: your-secret-value
WHISPERX_BASE_URL: https://configure-your-url.com
WHISPERX_ASR_MODEL: large-v2
LLM_BASE_URL: https://configure-your-url.com
LLM_API_KEY: your-secret-value
LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
WEBHOOK_API_TOKEN: password
WEBHOOK_URL: https://www.mock-impress.com/webhook/
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1

View File

@@ -21,6 +21,14 @@ from urllib3.util import Retry
from summary.core.analytics import MetadataManager, get_analytics
from summary.core.config import get_settings
from summary.core.prompt import (
PROMPT_SYSTEM_CLEANING,
PROMPT_SYSTEM_NEXT_STEP,
PROMPT_SYSTEM_PART,
PROMPT_SYSTEM_PLAN,
PROMPT_SYSTEM_TLDR,
PROMPT_USER_PART,
)
settings = get_settings()
analytics = get_analytics()
@@ -94,6 +102,39 @@ def create_retry_session():
return session
class LLMException(Exception):
"""LLM call failed."""
class LLMService:
"""Service for performing calls to the LLM configured in the settings."""
def __init__(self):
"""Init the LLMService once."""
self._client = openai.OpenAI(
base_url=settings.llm_base_url, api_key=settings.llm_api_key
)
def call(self, system_prompt: str, user_prompt: str):
"""Call the LLM service.
Takes a system prompt and a user prompt, and returns the LLM's response
Returns None if the call fails.
"""
try:
response = self._client.chat.completions.create(
model=settings.llm_model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
)
return response.choices[0].message.content
except Exception as e:
logger.error("LLM call failed: %s", e)
raise LLMException("LLM call failed.") from e
def format_segments(transcription_data):
"""Format transcription segments from WhisperX into a readable conversation format.
@@ -277,4 +318,77 @@ def process_audio_transcribe_summarize_v2(
metadata_manager.capture(task_id, settings.posthog_event_success)
# TODO - integrate summarize the transcript and create a new document.
if analytics.is_feature_enabled("summary-enabled", distinct_id=sub):
logger.info("Queuing summary generation task.")
summarize_transcription.apply_async(
args=[formatted_transcription, email, sub, title],
queue=settings.summarize_queue,
)
else:
logger.info("Summary generation not enabled for this user.")
@celery.task(
bind=True,
autoretry_for=[LLMException, Exception],
max_retries=settings.celery_max_retries,
queue=settings.summarize_queue,
)
def summarize_transcription(self, transcript: str, email: str, sub: str, title: str):
"""Generate a summary from the provided transcription text.
This Celery task performs the following operations:
1. Uses an LLM to generate a TL;DR summary of the transcription.
2. Breaks the transcription into parts and summarizes each part.
3. Cleans up the combined summary
4. Generates next steps.
5. Sends the final summary via webhook.
"""
logger.info("Starting summarization task")
llm_service = LLMService()
tldr = llm_service.call(PROMPT_SYSTEM_TLDR, transcript)
logger.info("TLDR generated")
parts = llm_service.call(PROMPT_SYSTEM_PLAN, transcript)
logger.info("Plan generated")
parts = parts.split("\n")
parts = [x for x in parts if x.strip() != ""]
logger.info("Empty parts removed")
parts_summarized = []
for part in parts:
prompt_user_part = PROMPT_USER_PART.format(part=part, transcript=transcript)
logger.info("Summarizing part: %s", part)
parts_summarized.append(llm_service.call(PROMPT_SYSTEM_PART, prompt_user_part))
logger.info("Parts summarized")
raw_summary = "\n\n".join(parts_summarized)
next_steps = llm_service.call(PROMPT_SYSTEM_NEXT_STEP, transcript)
logger.info("Next steps generated")
cleaned_summary = llm_service.call(PROMPT_SYSTEM_CLEANING, raw_summary)
logger.info("Summary cleaned")
summary = tldr + "\n\n" + cleaned_summary + "\n\n" + next_steps
data = {
"title": settings.summary_title_template.format(
title=title,
),
"content": summary,
"email": email,
"sub": sub,
}
logger.debug("Submitting webhook to %s", settings.webhook_url)
response = post_with_retries(settings.webhook_url, data)
logger.info("Webhook submitted successfully. Status: %s", response.status_code)
logger.debug("Response body: %s", response.text)

View File

@@ -25,6 +25,7 @@ class Settings(BaseSettings):
celery_max_retries: int = 1
transcribe_queue: str = "transcribe-queue"
summarize_queue: str = "summarize-queue"
# Minio settings
aws_storage_bucket_name: str
@@ -38,6 +39,9 @@ class Settings(BaseSettings):
whisperx_base_url: str = "https://api.openai.com/v1"
whisperx_asr_model: str = "whisper-1"
whisperx_max_retries: int = 0
llm_base_url: str
llm_api_key: str
llm_model: str
# Webhook-related settings
webhook_max_retries: int = 2
@@ -51,6 +55,7 @@ class Settings(BaseSettings):
document_title_template: Optional[str] = (
'Réunion "{room}" du {room_recording_date} à {room_recording_time}'
)
summary_title_template: Optional[str] = "Résumé de {title}"
# Sentry
sentry_is_enabled: bool = False

View File

@@ -0,0 +1,28 @@
# ruff: noqa
PROMPT_SYSTEM_TLDR = """Tu es un agent dont le rôle est de créer un TL;DR (résumé très concis) d'un compte rendu de réunion. Tu utiliseras un style synthétique, administratif, à la troisième personne, sans affect. Tu recevras en entrée le transcript. Ta tâche est de rédiger un résumé concis et structuré, en te concentrant uniquement sur les informations essentielles et pertinentes. Tu répondras en un paragraphe structuré (3 à 6 phrases), sans rien ajouter d'autre. Tu répondras dans le format suivant sans rien ajouter d'autre:
### Résumé TL;DR
[Résumé concis et structuré]"""
PROMPT_SYSTEM_PLAN = """Ta tâche est de diviser le contenu du transcript en sujets concrets correspondant aux grands axes discutés durant la réunion. Ne crée pas de catégories génériques. Les titres doivent être courts, précis et représentatifs des échanges. Veille à ce que chaque sujet soit distinct et quaucun thème ne soit répété. Tu te limiteras à 5 ou 6 sujets maximum.
L'introduction, ordre du jour, conclusion, etc. seront rajoutés a posteriori. Tu répondras dans le format suivant sans rien ajouter d'autre:
"Titre du sujet 1
Titre du sujet 2
Titre du sujet 3
..."
"""
PROMPT_SYSTEM_PART = """Tu es un agent dont le rôle est de créer une partie du résumé d'un compte rendu de réunion. Tu utiliseras un style synthétique, administratif, à la troisième personne, sans affect. Tu recevras en entrée le transcript, et le titre du sujet correspondant. Ta tâche est de rédiger un résumé concis de cette partie et uniquement cette partie, en te concentrant uniquement sur les informations essentielles et pertinentes. Le résumé de chaque partie doit tenir en 4 à 6 phrases maximum, sans entrer dans les détails mineurs. Tu répondras dans le format suivant :
### Titre du sujet [Traduire ce titre selon la langue du transcript]
[Résumé concis et structuré de la partie du transcript]
"""
PROMPT_USER_PART = """Titre de la partie à résumer : {part}
Transcript complet :
{transcript}"""
PROMPT_SYSTEM_CLEANING = """Tu es un agent dont le rôle est de nettoyer un résumé de compte rendu de réunion. Tu recevras en entrée le résumé brut, potentiellement avec des erreurs de formatage, des incohérences ou des redondances. Ta tâche est de corriger les erreurs de formatage, d'améliorer la clarté et la cohérence du texte, et de t'assurer que le résumé est bien structuré et facile à lire. Ton but principal est de retirer les redondances et les répétitions. Assure la cohérence entre les titres et homogénéise le style décriture entre les parties. Supprime les doublons dinformations entre les parties si présents. Si certaines parties sont plus secondaires, tu peux les fusionner ou les réduire en 1 à 2 phrases. Mets en avant les points centraux qui ont fait lobjet de décisions ou dactions. Tu répondras uniquement avec le résumé sans rien ajouter d'autre"""
PROMPT_SYSTEM_NEXT_STEP = """Tu es un agent dont le rôle est d'extraire les prochaines étapes d'un transcript de réunion. Tu utiliseras un style synthétique, administratif, à la troisième personne, sans affect. Tu recevras en entrée le transcript. Ta tâche est d'identifier et de lister toutes les actions à entreprendre, en indiquant la ou les personnes assignées et en précisant les échéances si elles sont mentionnées. Ne retiens que les actions concrètes et à venir. Ignore les remarques générales ou les constats sans suite. Les actions doivent suivre ce format strict :
### Prochaines étapes
- [ ] [Action à effectuer] Assignée à : [Nom], Échéance : [Date si mentionnée]"""