feat(transcription): [2f388f42] integrate prompt database and AI insights
Implements the core functionality for the AI-powered analysis of meeting transcripts in the Transcription Tool. This commit introduces a new 'AI Insights' feature that allows users to generate various summaries and analyses from a transcript on demand. - Creates a to manage and version different AI prompts for tasks like generating meeting minutes, extracting action items, and creating sales summaries. - Adds a new responsible for orchestrating the analysis process: fetching the transcript, calling the Gemini API with the appropriate prompt, and caching the results in the database. - Extends the FastAPI backend with a new endpoint to trigger the insight generation. - Updates the React frontend () with a new 'AI Insights' panel, including buttons to trigger the analyses and a modal to display the results. - Updates the documentation () to reflect the new features, API endpoints, and version.
This commit is contained in:
@@ -11,6 +11,7 @@ from datetime import datetime
|
||||
from .config import settings
|
||||
from .database import init_db, get_db, Meeting, TranscriptChunk, AnalysisResult, SessionLocal
|
||||
from .services.orchestrator import process_meeting_task
|
||||
from .services.insights_service import generate_insight
|
||||
|
||||
# Initialize FastAPI App
|
||||
app = FastAPI(
|
||||
@@ -42,7 +43,8 @@ def list_meetings(db: Session = Depends(get_db)):
|
||||
@app.get("/api/meetings/{meeting_id}")
|
||||
def get_meeting(meeting_id: int, db: Session = Depends(get_db)):
|
||||
meeting = db.query(Meeting).options(
|
||||
joinedload(Meeting.chunks)
|
||||
joinedload(Meeting.chunks),
|
||||
joinedload(Meeting.analysis_results) # Eager load analysis results
|
||||
).filter(Meeting.id == meeting_id).first()
|
||||
|
||||
if not meeting:
|
||||
@@ -99,6 +101,26 @@ async def upload_audio(
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
class InsightRequest(BaseModel):
|
||||
insight_type: str
|
||||
|
||||
@app.post("/api/meetings/{meeting_id}/insights")
|
||||
def create_insight(meeting_id: int, payload: InsightRequest, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Triggers the generation of a specific insight (e.g., meeting minutes, action items).
|
||||
If the insight already exists, it returns the stored result.
|
||||
Otherwise, it generates, stores, and returns the new insight.
|
||||
"""
|
||||
try:
|
||||
insight = generate_insight(db, meeting_id, payload.insight_type)
|
||||
return insight
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
# For unexpected errors, return a generic 500 error
|
||||
print(f"ERROR: Unexpected error in create_insight: {e}")
|
||||
raise HTTPException(status_code=500, detail="An internal error occurred while generating the insight.")
|
||||
|
||||
class RenameRequest(BaseModel):
|
||||
old_name: str
|
||||
new_name: str
|
||||
|
||||
113
transcription-tool/backend/prompt_library.py
Normal file
113
transcription-tool/backend/prompt_library.py
Normal file
@@ -0,0 +1,113 @@
|
||||
|
||||
MEETING_MINUTES_PROMPT = """
|
||||
You are a professional assistant specialized in summarizing meeting transcripts.
|
||||
Your task is to create a formal and structured protocol (Meeting Minutes) from the provided transcript.
|
||||
|
||||
Please analyze the following transcript and generate the Meeting Minutes in German.
|
||||
|
||||
**Transcript:**
|
||||
---
|
||||
{transcript_text}
|
||||
---
|
||||
|
||||
**Instructions for the Meeting Minutes:**
|
||||
|
||||
1. **Header:**
|
||||
* Start with a clear title: "Meeting-Protokoll".
|
||||
* Add a placeholder for the meeting date: "Datum: [Datum des Meetings]".
|
||||
|
||||
2. **Agenda Items:**
|
||||
* Identify the main topics discussed.
|
||||
* Structure the protocol using these topics as headlines (e.g., "Tagesordnungspunkt 1: [Thema]").
|
||||
|
||||
3. **Key Discussions & Decisions:**
|
||||
* For each agenda item, summarize the key points of the discussion.
|
||||
* Clearly list all decisions that were made. Use a format like "**Entscheidung:** ...".
|
||||
|
||||
4. **Action Items (Next Steps):**
|
||||
* Extract all clear tasks or action items.
|
||||
* For each action item, identify the responsible person (Owner) and the deadline, if mentioned.
|
||||
* Present the action items in a clear list under a headline "Nächste Schritte / Action Items". Use the format: "- [Aufgabe] (Verantwortlich: [Person], Fällig bis: [Datum/unbestimmt])".
|
||||
|
||||
5. **General Tone & Language:**
|
||||
* The protocol must be written in formal, professional German.
|
||||
* Be concise and focus on the essential information (discussions, decisions, tasks).
|
||||
* Do not invent information that is not present in the transcript.
|
||||
|
||||
Please provide the output in Markdown format.
|
||||
"""
|
||||
|
||||
ACTION_ITEMS_PROMPT = """
|
||||
You are a highly efficient assistant focused on productivity and task management.
|
||||
Your goal is to extract all actionable tasks (Action Items) from a meeting transcript.
|
||||
|
||||
Please analyze the following transcript and list all tasks.
|
||||
|
||||
**Transcript:**
|
||||
---
|
||||
{transcript_text}
|
||||
---
|
||||
|
||||
**Instructions for the Action Item List:**
|
||||
|
||||
1. **Extraction:**
|
||||
* Carefully read the entire transcript and identify every statement that constitutes a task, a to-do, or a commitment to do something.
|
||||
* Ignore general discussions, opinions, and status updates. Focus only on future actions.
|
||||
|
||||
2. **Format:**
|
||||
* Present the extracted tasks as a bulleted list.
|
||||
* For each task, clearly state:
|
||||
* **What** needs to be done.
|
||||
* **Who** is responsible for it (Owner).
|
||||
* **When** it should be completed by (Due Date), if mentioned.
|
||||
|
||||
3. **Output Structure:**
|
||||
* Use the following format for each item: `- [Task Description] (Owner: [Person's Name], Due: [Date/unspecified])`
|
||||
* If the owner or due date is not explicitly mentioned, use "[unbestimmt]".
|
||||
* The list should be titled "Action Item Liste".
|
||||
|
||||
4. **Language:**
|
||||
* The output should be in German.
|
||||
|
||||
Please provide the output in Markdown format.
|
||||
"""
|
||||
|
||||
SALES_SUMMARY_PROMPT = """
|
||||
You are a Senior Sales Manager analyzing a meeting transcript from a client conversation.
|
||||
Your objective is to create a concise, rollenbasierte Zusammenfassung for the sales team. The summary should highlight key information relevant to closing a deal.
|
||||
|
||||
Please analyze the following transcript and generate a Sales Summary.
|
||||
|
||||
**Transcript:**
|
||||
---
|
||||
{transcript_text}
|
||||
---
|
||||
|
||||
**Instructions for the Sales Summary:**
|
||||
|
||||
1. **Customer Needs & Pain Points:**
|
||||
* Identify and list the core problems, challenges, and needs expressed by the client.
|
||||
* What are their primary business goals?
|
||||
|
||||
2. **Buying Signals:**
|
||||
* Extract any phrases or questions that indicate a strong interest in the product/service (e.g., questions about price, implementation, specific features).
|
||||
|
||||
3. **Key Decision-Makers:**
|
||||
* Identify the people in the meeting who seem to have the most influence on the purchasing decision. Note their role or title if mentioned.
|
||||
|
||||
4. **Budget & Timeline:**
|
||||
* Note any mentions of budget, pricing expectations, or the timeline for their decision-making process.
|
||||
|
||||
5. **Next Steps (from a Sales Perspective):**
|
||||
* What are the immediate next actions the sales team needs to take to move this deal forward? (e.g., "Send proposal," "Schedule demo for the technical team").
|
||||
|
||||
**Output Format:**
|
||||
* Use clear headings for each section (e.g., "Kundenbedürfnisse & Pain Points", "Kaufsignale").
|
||||
* Use bullet points for lists.
|
||||
* The language should be direct, actionable, and in German.
|
||||
|
||||
Please provide the output in Markdown format.
|
||||
"""
|
||||
|
||||
# You can add more prompts here for other analysis types.
|
||||
# For example, a prompt for a technical summary, a marketing summary, etc.
|
||||
110
transcription-tool/backend/services/insights_service.py
Normal file
110
transcription-tool/backend/services/insights_service.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import sys
|
||||
import os
|
||||
from sqlalchemy.orm import Session
|
||||
from .. import database
|
||||
from .. import prompt_library
|
||||
|
||||
# Add project root to path to allow importing from 'helpers'
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')))
|
||||
from helpers import call_gemini_flash
|
||||
|
||||
def _format_transcript(chunks: list[database.TranscriptChunk]) -> str:
|
||||
"""
|
||||
Formats the transcript chunks into a single, human-readable string.
|
||||
Example: "[00:00:01] Speaker A: Hello world."
|
||||
"""
|
||||
full_transcript = []
|
||||
# Sort chunks by their index to ensure correct order
|
||||
sorted_chunks = sorted(chunks, key=lambda c: c.chunk_index)
|
||||
|
||||
for chunk in sorted_chunks:
|
||||
if not chunk.json_content:
|
||||
continue
|
||||
|
||||
for item in chunk.json_content:
|
||||
# json_content can be a list of dicts
|
||||
if isinstance(item, dict):
|
||||
speaker = item.get('speaker', 'Unknown')
|
||||
start_time = item.get('start', 0)
|
||||
text = item.get('line', '')
|
||||
|
||||
# Format timestamp from seconds to HH:MM:SS
|
||||
hours, remainder = divmod(int(start_time), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
timestamp = f"{hours:02}:{minutes:02}:{seconds:02}"
|
||||
|
||||
full_transcript.append(f"[{timestamp}] {speaker}: {text}")
|
||||
|
||||
return "\n".join(full_transcript)
|
||||
|
||||
def get_prompt_by_type(insight_type: str) -> str:
|
||||
"""
|
||||
Returns the corresponding prompt from the prompt_library based on the type.
|
||||
"""
|
||||
if insight_type == "meeting_minutes":
|
||||
return prompt_library.MEETING_MINUTES_PROMPT
|
||||
elif insight_type == "action_items":
|
||||
return prompt_library.ACTION_ITEMS_PROMPT
|
||||
elif insight_type == "sales_summary":
|
||||
return prompt_library.SALES_SUMMARY_PROMPT
|
||||
else:
|
||||
raise ValueError(f"Unknown insight type: {insight_type}")
|
||||
|
||||
def generate_insight(db: Session, meeting_id: int, insight_type: str) -> database.AnalysisResult:
|
||||
"""
|
||||
Generates a specific insight for a meeting, stores it, and returns it.
|
||||
Checks for existing analysis to avoid re-generating.
|
||||
"""
|
||||
# 1. Check if the insight already exists
|
||||
existing_insight = db.query(database.AnalysisResult).filter(
|
||||
database.AnalysisResult.meeting_id == meeting_id,
|
||||
database.AnalysisResult.prompt_key == insight_type
|
||||
).first()
|
||||
|
||||
if existing_insight:
|
||||
return existing_insight
|
||||
|
||||
# 2. Get the meeting and its transcript
|
||||
meeting = db.query(database.Meeting).filter(database.Meeting.id == meeting_id).first()
|
||||
if not meeting:
|
||||
raise ValueError(f"Meeting with id {meeting_id} not found.")
|
||||
|
||||
if not meeting.chunks:
|
||||
raise ValueError(f"Meeting with id {meeting_id} has no transcript chunks.")
|
||||
|
||||
# 3. Format the transcript and select the prompt
|
||||
transcript_text = _format_transcript(meeting.chunks)
|
||||
if not transcript_text.strip():
|
||||
raise ValueError(f"Transcript for meeting {meeting_id} is empty.")
|
||||
|
||||
prompt_template = get_prompt_by_type(insight_type)
|
||||
final_prompt = prompt_template.format(transcript_text=transcript_text)
|
||||
|
||||
# 4. Call the AI model
|
||||
# Update meeting status
|
||||
meeting.status = "ANALYZING"
|
||||
db.commit()
|
||||
|
||||
try:
|
||||
generated_text = call_gemini_flash(prompt=final_prompt, temperature=0.5)
|
||||
|
||||
# 5. Store the new insight
|
||||
new_insight = database.AnalysisResult(
|
||||
meeting_id=meeting_id,
|
||||
prompt_key=insight_type,
|
||||
result_text=generated_text
|
||||
)
|
||||
db.add(new_insight)
|
||||
|
||||
meeting.status = "COMPLETED"
|
||||
db.commit()
|
||||
db.refresh(new_insight)
|
||||
|
||||
return new_insight
|
||||
|
||||
except Exception as e:
|
||||
meeting.status = "ERROR"
|
||||
db.commit()
|
||||
# Log the error properly in a real application
|
||||
print(f"Error generating insight for meeting {meeting_id}: {e}")
|
||||
raise
|
||||
@@ -1,10 +1,19 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Upload, FileText, Clock, CheckCircle2, Loader2, AlertCircle, Trash2, ArrowLeft, Copy, Edit3, X, Scissors, Users } from 'lucide-react'
|
||||
import { Upload, FileText, Clock, CheckCircle2, Loader2, AlertCircle, Trash2, ArrowLeft, Copy, Edit3, X, Scissors, Users, Wand2 } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const API_BASE = '/tr/api'
|
||||
|
||||
// --- INTERFACES ---
|
||||
|
||||
interface AnalysisResult {
|
||||
id: number;
|
||||
prompt_key: string;
|
||||
result_text: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface TranscriptMessage {
|
||||
time: string
|
||||
display_time: string
|
||||
@@ -30,8 +39,29 @@ interface Meeting {
|
||||
duration_seconds?: number
|
||||
created_at: string
|
||||
chunks?: Chunk[]
|
||||
analysis_results?: AnalysisResult[]
|
||||
}
|
||||
|
||||
// --- MODAL COMPONENT ---
|
||||
|
||||
const InsightModal = ({ title, content, onClose, onCopy }: { title: string, content: string, onClose: () => void, onCopy: (text: string) => void }) => (
|
||||
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4 backdrop-blur-sm animate-fade-in">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] flex flex-col border border-slate-200 dark:border-slate-800">
|
||||
<header className="p-4 border-b border-slate-200 dark:border-slate-800 flex justify-between items-center flex-shrink-0">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2"><Wand2 className="h-5 w-5 text-blue-500" /> {title}</h2>
|
||||
<div className='flex items-center gap-2'>
|
||||
<button onClick={() => onCopy(content)} className="flex items-center gap-2 text-sm px-3 py-1.5 bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-md font-medium transition-colors"><Copy className='h-4 w-4'/> Copy</button>
|
||||
<button onClick={onClose} className="p-1.5 text-slate-500 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-full"><X className="h-5 w-5" /></button>
|
||||
</div>
|
||||
</header>
|
||||
<main className="p-6 overflow-auto">
|
||||
<pre className="whitespace-pre-wrap font-sans text-sm leading-relaxed">{content}</pre>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
export default function App() {
|
||||
const [view, setView] = useState<'list' | 'detail'>('list')
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null)
|
||||
@@ -48,6 +78,12 @@ export default function App() {
|
||||
const [editValue, setEditValue] = useState("")
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
|
||||
// Insight State
|
||||
const [showInsightModal, setShowInsightModal] = useState(false)
|
||||
const [insightLoading, setInsightLoading] = useState(false)
|
||||
const [insightResult, setInsightResult] = useState('')
|
||||
const [insightTitle, setInsightTitle] = useState('')
|
||||
|
||||
const fetchMeetings = async () => {
|
||||
try {
|
||||
const res = await axios.get(`${API_BASE}/meetings`)
|
||||
@@ -102,6 +138,39 @@ export default function App() {
|
||||
} catch (e) { alert("Delete failed") }
|
||||
}
|
||||
|
||||
// --- AI INSIGHTS ---
|
||||
|
||||
const handleGenerateInsight = async (insightType: string, title: string) => {
|
||||
if (!detailMeeting) return;
|
||||
setInsightLoading(true);
|
||||
setInsightTitle(title);
|
||||
|
||||
try {
|
||||
// First, check if result already exists
|
||||
const existing = detailMeeting.analysis_results?.find(r => r.prompt_key === insightType);
|
||||
if (existing) {
|
||||
setInsightResult(existing.result_text);
|
||||
setShowInsightModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If not, generate it
|
||||
const res = await axios.post(`${API_BASE}/meetings/${detailMeeting.id}/insights`, {
|
||||
insight_type: insightType
|
||||
});
|
||||
setInsightResult(res.data.result_text);
|
||||
setShowInsightModal(true);
|
||||
// Refresh detail view to get the new analysis result in the meeting object
|
||||
fetchDetail(detailMeeting.id);
|
||||
} catch(e) {
|
||||
console.error("Failed to generate insight", e);
|
||||
alert(`Error: Could not generate ${title}.`);
|
||||
} finally {
|
||||
setInsightLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- GLOBAL SPEAKER MANAGEMENT ---
|
||||
|
||||
const getUniqueSpeakers = () => {
|
||||
@@ -235,6 +304,7 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-200 font-sans">
|
||||
{showInsightModal && <InsightModal title={insightTitle} content={insightResult} onClose={() => setShowInsightModal(false)} onCopy={text => navigator.clipboard.writeText(text)} />}
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
<button
|
||||
onClick={() => { setView('list'); setSelectedId(null); setDetailMeeting(null); }}
|
||||
@@ -287,6 +357,20 @@ export default function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI INSIGHTS PANEL */}
|
||||
<div className="mb-6 bg-purple-50 dark:bg-purple-900/20 p-4 rounded-xl border border-purple-100 dark:border-purple-800">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Wand2 className="h-5 w-5 text-purple-600 dark:text-purple-300" />
|
||||
<h3 className="font-bold text-purple-800 dark:text-purple-200">AI Insights</h3>
|
||||
{insightLoading && <Loader2 className="h-4 w-4 animate-spin text-purple-500" />}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<button disabled={insightLoading} onClick={() => handleGenerateInsight('meeting_minutes', 'Meeting Protocol')} className="text-sm font-medium px-4 py-2 bg-white dark:bg-slate-800 border dark:border-slate-700 rounded-lg shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 disabled:opacity-50 transition-colors">Generate Protocol</button>
|
||||
<button disabled={insightLoading} onClick={() => handleGenerateInsight('action_items', 'Action Items')} className="text-sm font-medium px-4 py-2 bg-white dark:bg-slate-800 border dark:border-slate-700 rounded-lg shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 disabled:opacity-50 transition-colors">Extract Action Items</button>
|
||||
<button disabled={insightLoading} onClick={() => handleGenerateInsight('sales_summary', 'Sales Summary')} className="text-sm font-medium px-4 py-2 bg-white dark:bg-slate-800 border dark:border-slate-700 rounded-lg shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700/50 disabled:opacity-50 transition-colors">Sales Summary</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isProcessing && (
|
||||
<div className="fixed inset-0 bg-white/50 dark:bg-black/50 z-50 flex items-center justify-center backdrop-blur-sm">
|
||||
<div className="bg-white dark:bg-slate-800 p-6 rounded-xl shadow-2xl flex items-center gap-4">
|
||||
|
||||
Reference in New Issue
Block a user