diff --git a/api-docs/docs.go b/api-docs/docs.go index ff55538a..842d5cfe 100644 --- a/api-docs/docs.go +++ b/api-docs/docs.go @@ -4363,6 +4363,9 @@ const docTemplate = `{ "description": { "type": "string" }, + "include_speaker_info": { + "type": "boolean" + }, "model": { "type": "string", "minLength": 1 @@ -4581,6 +4584,9 @@ const docTemplate = `{ "id": { "type": "string" }, + "include_speaker_info": { + "type": "boolean" + }, "model": { "type": "string" }, diff --git a/api-docs/swagger.json b/api-docs/swagger.json index 6d7d5e80..f5ddfdbb 100644 --- a/api-docs/swagger.json +++ b/api-docs/swagger.json @@ -4357,6 +4357,9 @@ "description": { "type": "string" }, + "include_speaker_info": { + "type": "boolean" + }, "model": { "type": "string", "minLength": 1 @@ -4575,6 +4578,9 @@ "id": { "type": "string" }, + "include_speaker_info": { + "type": "boolean" + }, "model": { "type": "string" }, diff --git a/api-docs/swagger.yaml b/api-docs/swagger.yaml index 24051b1f..4b43a4ca 100644 --- a/api-docs/swagger.yaml +++ b/api-docs/swagger.yaml @@ -358,6 +358,8 @@ definitions: properties: description: type: string + include_speaker_info: + type: boolean model: minLength: 1 type: string @@ -508,6 +510,8 @@ definitions: type: string id: type: string + include_speaker_info: + type: boolean model: type: string name: diff --git a/internal/api/summary_handlers.go b/internal/api/summary_handlers.go index df8a146f..ea13a90b 100644 --- a/internal/api/summary_handlers.go +++ b/internal/api/summary_handlers.go @@ -11,10 +11,11 @@ import ( ) type SummaryTemplateRequest struct { - Name string `json:"name" binding:"required,min=1"` - Description *string `json:"description"` - Model string `json:"model" binding:"required,min=1"` - Prompt string `json:"prompt" binding:"required,min=1"` + Name string `json:"name" binding:"required,min=1"` + Description *string `json:"description"` + Model string `json:"model" binding:"required,min=1"` + Prompt string `json:"prompt" binding:"required,min=1"` + IncludeSpeakerInfo *bool `json:"include_speaker_info"` } type SummarySettingsRequest struct { @@ -73,6 +74,9 @@ func (h *Handler) CreateSummaryTemplate(c *gin.Context) { CreatedAt: time.Now(), UpdatedAt: time.Now(), } + if req.IncludeSpeakerInfo != nil { + item.IncludeSpeakerInfo = *req.IncludeSpeakerInfo + } if err := h.summaryRepo.Create(c.Request.Context(), item); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create template"}) return @@ -135,6 +139,9 @@ func (h *Handler) UpdateSummaryTemplate(c *gin.Context) { item.Description = req.Description item.Model = req.Model item.Prompt = req.Prompt + if req.IncludeSpeakerInfo != nil { + item.IncludeSpeakerInfo = *req.IncludeSpeakerInfo + } item.UpdatedAt = time.Now() if err := h.summaryRepo.Update(c.Request.Context(), item); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update template"}) diff --git a/internal/models/summary.go b/internal/models/summary.go index ababd892..cfdca26e 100644 --- a/internal/models/summary.go +++ b/internal/models/summary.go @@ -9,13 +9,14 @@ import ( // SummaryTemplate represents a saved summarization prompt/template type SummaryTemplate struct { - ID string `json:"id" gorm:"primaryKey;type:varchar(36)"` - Name string `json:"name" gorm:"type:varchar(255);not null"` - Description *string `json:"description,omitempty" gorm:"type:text"` - Model string `json:"model" gorm:"type:varchar(255);not null;default:''"` - Prompt string `json:"prompt" gorm:"type:text;not null"` - CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` - UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` + ID string `json:"id" gorm:"primaryKey;type:varchar(36)"` + Name string `json:"name" gorm:"type:varchar(255);not null"` + Description *string `json:"description,omitempty" gorm:"type:text"` + Model string `json:"model" gorm:"type:varchar(255);not null;default:''"` + Prompt string `json:"prompt" gorm:"type:text;not null"` + IncludeSpeakerInfo bool `json:"include_speaker_info" gorm:"default:false"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` } func (st *SummaryTemplate) BeforeCreate(tx *gorm.DB) error { diff --git a/web/frontend/src/features/settings/components/SummaryTemplateDialog.tsx b/web/frontend/src/features/settings/components/SummaryTemplateDialog.tsx index d3752553..6ab2eb42 100644 --- a/web/frontend/src/features/settings/components/SummaryTemplateDialog.tsx +++ b/web/frontend/src/features/settings/components/SummaryTemplateDialog.tsx @@ -4,6 +4,7 @@ import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { useAuth } from "@/features/auth/hooks/useAuth"; import { FormField } from "@/components/transcription/FormHelpers"; import { Loader2 } from "lucide-react"; @@ -14,6 +15,7 @@ export interface SummaryTemplate { description?: string; model?: string; prompt: string; + include_speaker_info?: boolean; created_at?: string; updated_at?: string; } @@ -54,6 +56,7 @@ export function SummaryTemplateDialog({ open, onOpenChange, onSave, initial }: S const [description, setDescription] = useState(""); const [model, setModel] = useState(""); const [prompt, setPrompt] = useState(""); + const [includeSpeakerInfo, setIncludeSpeakerInfo] = useState(false); const [saving, setSaving] = useState(false); const [models, setModels] = useState([]); const { getAuthHeaders } = useAuth(); @@ -64,6 +67,7 @@ export function SummaryTemplateDialog({ open, onOpenChange, onSave, initial }: S setDescription(initial?.description || ""); setModel(initial?.model || ""); setPrompt(initial?.prompt || ""); + setIncludeSpeakerInfo(initial?.include_speaker_info || false); // Load models when dialog opens (async () => { try { @@ -89,7 +93,8 @@ export function SummaryTemplateDialog({ open, onOpenChange, onSave, initial }: S name: name.trim(), description: description.trim() || undefined, model: model.trim(), - prompt: prompt.trim() + prompt: prompt.trim(), + include_speaker_info: includeSpeakerInfo }); onOpenChange(false); } finally { @@ -176,6 +181,17 @@ Example: Summarize the following transcript into concise bullet points. Focus on key decisions, action items, and important discussion topics." /> + + {/* Include Speaker Info Toggle */} + + + {/* Footer */} diff --git a/web/frontend/src/features/settings/pages/SettingsPage.tsx b/web/frontend/src/features/settings/pages/SettingsPage.tsx index 9b70b52f..b4aa3edc 100644 --- a/web/frontend/src/features/settings/pages/SettingsPage.tsx +++ b/web/frontend/src/features/settings/pages/SettingsPage.tsx @@ -173,9 +173,9 @@ export function Settings() { const headers: HeadersInit = { 'Content-Type': 'application/json', ...getAuthHeaders() }; try { if (tpl.id) { - await fetch(`/api/v1/summaries/${tpl.id}`, { method: 'PUT', headers, body: JSON.stringify({ name: tpl.name, description: tpl.description, model: tpl.model, prompt: tpl.prompt }) }); + await fetch(`/api/v1/summaries/${tpl.id}`, { method: 'PUT', headers, body: JSON.stringify({ name: tpl.name, description: tpl.description, model: tpl.model, prompt: tpl.prompt, include_speaker_info: tpl.include_speaker_info }) }); } else { - await fetch('/api/v1/summaries', { method: 'POST', headers, body: JSON.stringify({ name: tpl.name, description: tpl.description, model: tpl.model, prompt: tpl.prompt }) }); + await fetch('/api/v1/summaries', { method: 'POST', headers, body: JSON.stringify({ name: tpl.name, description: tpl.description, model: tpl.model, prompt: tpl.prompt, include_speaker_info: tpl.include_speaker_info }) }); } } finally { // Invalidate cache to propagate changes diff --git a/web/frontend/src/features/transcription/components/audio-detail/SummaryDialog.tsx b/web/frontend/src/features/transcription/components/audio-detail/SummaryDialog.tsx index 809d4dfc..dceee3c5 100644 --- a/web/frontend/src/features/transcription/components/audio-detail/SummaryDialog.tsx +++ b/web/frontend/src/features/transcription/components/audio-detail/SummaryDialog.tsx @@ -29,10 +29,28 @@ import rehypeKatex from 'rehype-katex'; import rehypeHighlight from 'rehype-highlight'; import { useSummaryTemplates, useSummarizer, useExistingSummary } from "@/features/transcription/hooks/useTranscriptionSummary"; -import { useTranscript, useAudioDetail } from "@/features/transcription/hooks/useAudioDetail"; +import { useTranscript, useAudioDetail, type Transcript } from "@/features/transcription/hooks/useAudioDetail"; +import { useSpeakerMappings } from "@/features/transcription/hooks/useTranscriptionSpeakers"; import { Sparkles, Download, Copy, RefreshCw, ChevronDown, FileText } from "lucide-react"; +// Helper function to format transcript with speaker labels +function formatTranscriptWithSpeakers( + transcript: Transcript, + speakerMappings: Record +): string { + // If no segments, fall back to plain text + if (!transcript.segments || transcript.segments.length === 0) { + return transcript.text || ''; + } + + // Format each segment with speaker label + return transcript.segments.map(segment => { + const speaker = speakerMappings[segment.speaker || ''] || segment.speaker || 'UNKNOWN'; + return `[${speaker}] ${segment.text.trim()}`; + }).join('\n'); +} + interface SummaryDialogProps { audioId: string; isOpen: boolean; @@ -46,6 +64,7 @@ export function SummaryDialog({ audioId, isOpen, onClose, llmReady }: SummaryDia const { data: existingSummary, isLoading: summaryLoading } = useExistingSummary(audioId); const { data: transcript } = useTranscript(audioId, true); const { data: audioFile } = useAudioDetail(audioId); + const { data: speakerMappings = {} } = useSpeakerMappings(audioId, true); // Hooks const { generateSummary, isStreaming, streamContent, error } = useSummarizer(audioId); @@ -77,8 +96,12 @@ export function SummaryDialog({ audioId, isOpen, onClose, llmReady }: SummaryDia if (!selectedTemplate || !transcript) return; setShowOutput(true); - const transcriptText = transcript.text || ''; - generateSummary(selectedTemplate.id, selectedTemplate.model, selectedTemplate.prompt, transcriptText); + // Use formatted transcript with speaker info if template has it enabled + const includeSpeakers = selectedTemplate.include_speaker_info ?? false; + const transcriptText = includeSpeakers + ? formatTranscriptWithSpeakers(transcript, speakerMappings) + : (transcript.text || ''); + generateSummary(selectedTemplate.id, selectedTemplate.model, selectedTemplate.prompt, transcriptText, includeSpeakers); }; const handleCopy = async () => { diff --git a/web/frontend/src/features/transcription/hooks/useTranscriptionSummary.ts b/web/frontend/src/features/transcription/hooks/useTranscriptionSummary.ts index 72c3d8a8..e35a0ac9 100644 --- a/web/frontend/src/features/transcription/hooks/useTranscriptionSummary.ts +++ b/web/frontend/src/features/transcription/hooks/useTranscriptionSummary.ts @@ -7,6 +7,7 @@ export interface SummaryTemplate { name: string; model: string; prompt: string; + include_speaker_info?: boolean; } export function useSummaryTemplates() { @@ -46,12 +47,15 @@ export function useSummarizer(audioId: string) { const [streamContent, setStreamContent] = useState(""); const [error, setError] = useState(null); - const generateSummary = async (templateId: string, model: string, prompt: string, transcriptText: string) => { + const generateSummary = async (templateId: string, model: string, prompt: string, transcriptText: string, includeSpeakerInfo?: boolean) => { setIsStreaming(true); setStreamContent(""); setError(null); - const combinedContent = `Transcript:\n${transcriptText}\n\nInstructions:\n${prompt}`; + const transcriptLabel = includeSpeakerInfo + ? 'Transcript (with speaker labels - each line is prefixed with [SPEAKER_NAME]):' + : 'Transcript:'; + const combinedContent = `${transcriptLabel}\n${transcriptText}\n\nInstructions:\n${prompt}`; try { const res = await fetch('/api/v1/summarize', { diff --git a/web/project-site/public/api/swagger.json b/web/project-site/public/api/swagger.json index 6d7d5e80..f5ddfdbb 100644 --- a/web/project-site/public/api/swagger.json +++ b/web/project-site/public/api/swagger.json @@ -4357,6 +4357,9 @@ "description": { "type": "string" }, + "include_speaker_info": { + "type": "boolean" + }, "model": { "type": "string", "minLength": 1 @@ -4575,6 +4578,9 @@ "id": { "type": "string" }, + "include_speaker_info": { + "type": "boolean" + }, "model": { "type": "string" },