feat: Meeting transcription app with WhisperX and Llama #1

Open
naomi wants to merge 17 commits from feat/prototype into main
5 changed files with 50 additions and 9 deletions
Showing only changes of commit 9efda8ded6 - Show all commits
+21 -2
View File
@@ -470,13 +470,26 @@ async fn get_remaining_audio(
} }
} }
/// Extract title from the summary text.
fn extract_title_from_summary(summary: &str) -> Option<String> {
// Look for **Title:** pattern and extract the line
if let Some(title_start) = summary.find("**Title:**") {
let title_line_start = title_start + "**Title:**".len();
if let Some(title_end) = summary[title_line_start..].find("\n") {
let title = summary[title_line_start..title_line_start + title_end].trim();
return Some(title.to_string());
}
}
None
}
/// Generate a summary from a transcript. /// Generate a summary from a transcript.
#[tauri::command] #[tauri::command]
async fn summarize( async fn summarize(
state: State<'_, AppState>, state: State<'_, AppState>,
app_handle: tauri::AppHandle, app_handle: tauri::AppHandle,
transcript: String, transcript: String,
) -> Result<String, String> { ) -> Result<(String, Option<String>), String> {
let logs = Arc::clone(&state.logs); let logs = Arc::clone(&state.logs);
emit_log(&app_handle, &logs, "[Summary] Generating summary..."); emit_log(&app_handle, &logs, "[Summary] Generating summary...");
@@ -498,7 +511,13 @@ async fn summarize(
emit_log(&app_handle, &logs, &format!("[Summary] Generated {} character summary", summary.len())); emit_log(&app_handle, &logs, &format!("[Summary] Generated {} character summary", summary.len()));
Ok(summary) // Extract title from the summary
let title = extract_title_from_summary(&summary);
if let Some(ref t) = title {
emit_log(&app_handle, &logs, &format!("[Summary] Extracted title: {}", t));
}
Ok((summary, title))
} }
/// Get backend logs. /// Get backend logs.
+1
View File
@@ -88,6 +88,7 @@ impl LlamaSummarizer {
"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n\ "<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n\
You are a helpful assistant that creates structured meeting summaries. \ You are a helpful assistant that creates structured meeting summaries. \
Format your response using the following template:\n\n\ Format your response using the following template:\n\n\
**Title:** A concise, descriptive title for the meeting (5-10 words max).\n\n\
**Summary:** A high level overview of the meeting.\n\n\ **Summary:** A high level overview of the meeting.\n\n\
**Key decisions:** Any important resolutions that the meeting reached.\n\n\ **Key decisions:** Any important resolutions that the meeting reached.\n\n\
**Action Items:**\n\ **Action Items:**\n\
+2
View File
@@ -33,6 +33,7 @@ pub struct StoredRecording {
pub duration: f64, pub duration: f64,
pub transcript_segments: Vec<StoredTranscriptSegment>, pub transcript_segments: Vec<StoredTranscriptSegment>,
pub summary: Option<String>, pub summary: Option<String>,
pub title: Option<String>,
} }
/// Storage manager for recordings. /// Storage manager for recordings.
@@ -160,6 +161,7 @@ mod tests {
}, },
], ],
summary: Some("Test summary".to_string()), summary: Some("Test summary".to_string()),
title: Some("Test Meeting".to_string()),
}; };
// Save it // Save it
+10 -2
View File
@@ -663,9 +663,17 @@ body {
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
.transcript-title {
font-weight: 600;
font-size: 0.9375rem;
line-height: 1.3;
margin-bottom: 0.25rem;
}
.transcript-time { .transcript-time {
font-weight: 500; font-weight: 400;
font-size: 0.875rem; font-size: 0.8125rem;
color: var(--text-secondary);
} }
.transcript-status { .transcript-status {
+16 -5
View File
@@ -27,6 +27,7 @@ interface StoredRecording {
duration: number; duration: number;
transcript_segments: StoredTranscriptSegment[]; transcript_segments: StoredTranscriptSegment[];
summary: string | null; summary: string | null;
title: string | null;
} }
interface Recording { interface Recording {
@@ -35,6 +36,7 @@ interface Recording {
duration: number; duration: number;
transcriptSegments: TranscriptSegment[]; transcriptSegments: TranscriptSegment[];
summary: string | null; summary: string | null;
title: string | null;
isGeneratingSummary: boolean; isGeneratingSummary: boolean;
summaryProgress?: number; summaryProgress?: number;
} }
@@ -73,6 +75,7 @@ function App() {
duration: stored.duration, duration: stored.duration,
transcriptSegments: stored.transcript_segments, transcriptSegments: stored.transcript_segments,
summary: stored.summary, summary: stored.summary,
title: stored.title,
isGeneratingSummary: false, isGeneratingSummary: false,
summaryProgress: undefined, summaryProgress: undefined,
}); });
@@ -84,6 +87,7 @@ function App() {
duration: recording.duration, duration: recording.duration,
transcript_segments: recording.transcriptSegments, transcript_segments: recording.transcriptSegments,
summary: recording.summary, summary: recording.summary,
title: recording.title,
}); });
// Cleanup timers and listeners on unmount // Cleanup timers and listeners on unmount
@@ -242,6 +246,7 @@ function App() {
duration: 0, duration: 0,
transcriptSegments: [], transcriptSegments: [],
summary: null, summary: null,
title: null,
isGeneratingSummary: false, isGeneratingSummary: false,
}; };
setActiveRecording(newRecording); setActiveRecording(newRecording);
@@ -366,14 +371,15 @@ function App() {
try { try {
// Progress will be updated via events from the backend // Progress will be updated via events from the backend
const summaryResult = await invoke<string>("summarize", { transcript: fullTranscript }); const [summaryResult, titleResult] = await invoke<[string, string | null]>("summarize", { transcript: fullTranscript });
// Update the recording with the summary // Update the recording with the summary and title
const updatedRecording = recordings.find(r => r.id === recordingId); const updatedRecording = recordings.find(r => r.id === recordingId);
if (updatedRecording) { if (updatedRecording) {
const recordingWithSummary = { const recordingWithSummary = {
...updatedRecording, ...updatedRecording,
summary: summaryResult, summary: summaryResult,
title: titleResult,
isGeneratingSummary: false, isGeneratingSummary: false,
summaryProgress: 100 summaryProgress: 100
}; };
@@ -598,9 +604,14 @@ function App() {
className="transcript-content" className="transcript-content"
onClick={() => setSelectedRecordingId(recording.id)} onClick={() => setSelectedRecordingId(recording.id)}
> >
<div className="transcript-time"> <div className="transcript-title">
{recording.timestamp.toLocaleTimeString()} - {formatDuration(recording.duration)} {recording.title || `${recording.timestamp.toLocaleTimeString()} - ${formatDuration(recording.duration)}`}
</div> </div>
{recording.title && (
<div className="transcript-time">
{recording.timestamp.toLocaleString()} {formatDuration(recording.duration)}
</div>
)}
<div className="transcript-status"> <div className="transcript-status">
{recording.summary ? '✓ Summary' : recording.isGeneratingSummary ? '⏳ Summarizing...' : ''} {recording.summary ? '✓ Summary' : recording.isGeneratingSummary ? '⏳ Summarizing...' : ''}
</div> </div>
@@ -654,7 +665,7 @@ function App() {
{displayedRecording && ( {displayedRecording && (
<div className="transcript-details"> <div className="transcript-details">
<div className="transcript-header"> <div className="transcript-header">
<h2>Transcript from {displayedRecording.timestamp.toLocaleString()}</h2> <h2>{displayedRecording.title || `Transcript from ${displayedRecording.timestamp.toLocaleString()}`}</h2>
<div className="transcript-actions"> <div className="transcript-actions">
<button <button
className="secondary-button" className="secondary-button"