Skip to content

Commit 1e93f42

Browse files
committed
feat: Add Groq LLM service adapter and integrate with existing LLM command structure
- Implement GroqService for interacting with Groq's API, including model fetching and text generation. - Update LLM service module to include Groq as a provider. - Enhance OpenAIService for better model context window estimation. - Create Tauri commands for managing LLM services, including fetching models and generating insights. - Introduce prompt templates for various insight types to standardize prompt generation. - Add frontend API bindings for generating insights and managing prompts.
1 parent f464f7a commit 1e93f42

28 files changed

Lines changed: 3099 additions & 85 deletions
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
-- Prompt templates table for customizable LLM prompts
2+
CREATE TABLE IF NOT EXISTS prompt_templates (
3+
id INTEGER PRIMARY KEY AUTOINCREMENT,
4+
insight_type TEXT NOT NULL CHECK(insight_type IN ('summary', 'action_item', 'key_point', 'decision')),
5+
name TEXT NOT NULL,
6+
prompt_text TEXT NOT NULL,
7+
is_default BOOLEAN NOT NULL DEFAULT 0,
8+
is_active BOOLEAN NOT NULL DEFAULT 0,
9+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
10+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
11+
UNIQUE(insight_type, name)
12+
);
13+
14+
CREATE INDEX idx_prompt_templates_type ON prompt_templates(insight_type);
15+
CREATE INDEX idx_prompt_templates_active ON prompt_templates(is_active);
16+
17+
-- Insert default templates for each insight type
18+
INSERT INTO prompt_templates (insight_type, name, prompt_text, is_default, is_active) VALUES
19+
('summary', 'Default Summary', 'You are an expert meeting summarizer. Analyze the following meeting transcript and create a concise summary.
20+
21+
Meeting Transcript:
22+
{transcript}
23+
24+
{context}
25+
26+
Create a clear, concise summary in 3-5 bullet points covering:
27+
- Main topics discussed
28+
- Key decisions or conclusions
29+
- Important highlights
30+
31+
Format your response as a bulleted list with each point starting with "- ".', 1, 1),
32+
33+
('action_item', 'Default Action Items', 'You are an expert at extracting action items from meetings. Analyze the following meeting transcript and identify all actionable tasks.
34+
35+
Meeting Transcript:
36+
{transcript}
37+
38+
{context}
39+
40+
Extract all action items, decisions requiring follow-up, and tasks mentioned. For each action item, provide:
41+
- The specific task or action
42+
- Who is responsible (if mentioned)
43+
- Any deadlines or timeframes (if mentioned)
44+
45+
Format each action item on a separate line starting with "- ". Be specific and actionable.', 1, 1),
46+
47+
('key_point', 'Default Key Points', 'You are an expert at identifying key discussion points from meetings. Analyze the following meeting transcript and extract the most important points.
48+
49+
Meeting Transcript:
50+
{transcript}
51+
52+
{context}
53+
54+
Identify 3-7 key points, insights, or important statements from the meeting. Focus on:
55+
- Critical information shared
56+
- Important questions raised
57+
- Significant agreements or disagreements
58+
- Notable insights or revelations
59+
60+
Format your response as a bulleted list with each point starting with "- ".', 1, 1),
61+
62+
('decision', 'Default Decisions', 'You are an expert at identifying decisions made in meetings. Analyze the following meeting transcript and extract all decisions.
63+
64+
Meeting Transcript:
65+
{transcript}
66+
67+
{context}
68+
69+
Identify all explicit or implicit decisions made during the meeting. For each decision, provide:
70+
- What was decided
71+
- The rationale or context (if provided)
72+
- Who made or approved the decision (if mentioned)
73+
74+
Format each decision on a separate line starting with "- ". Focus on concrete decisions, not just discussions.', 1, 1);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- Model configuration overrides table
2+
-- Allows users to customize model-specific settings like context window
3+
CREATE TABLE IF NOT EXISTS model_overrides (
4+
id INTEGER PRIMARY KEY AUTOINCREMENT,
5+
provider TEXT NOT NULL, -- 'openai', 'anthropic', 'google', 'groq'
6+
model_id TEXT NOT NULL, -- Model identifier (e.g., 'gpt-4', 'claude-3-opus-20240229')
7+
context_window INTEGER, -- User-configured context window (overrides default)
8+
notes TEXT, -- User notes about this model
9+
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
10+
updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
11+
UNIQUE(provider, model_id)
12+
);
13+
14+
CREATE INDEX idx_model_overrides_provider ON model_overrides(provider);
15+
CREATE INDEX idx_model_overrides_model ON model_overrides(model_id);
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- Add speaker_label to transcripts table for diarization support
2+
-- This allows storing speaker identification directly on transcript segments
3+
ALTER TABLE transcripts ADD COLUMN speaker_label TEXT;
4+
5+
CREATE INDEX IF NOT EXISTS idx_transcripts_speaker_label ON transcripts(speaker_label);

apps/desktop/src-tauri/src/adapters/services/asr/deepgram.rs

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,35 @@ impl DeepgramService {
5151
.await
5252
.map_err(|e| AppError::Transcription(format!("Failed to read audio file: {}", e)))?;
5353

54+
// Log WAV file details
55+
if buffer.len() > 44 {
56+
// WAV header is 44 bytes - check if this looks like a valid WAV
57+
let is_wav = &buffer[0..4] == b"RIFF" && &buffer[8..12] == b"WAVE";
58+
println!(
59+
">>> WAV file check: is_valid_wav={}, total_bytes={}",
60+
is_wav,
61+
buffer.len()
62+
);
63+
64+
if is_wav {
65+
// Parse basic WAV info
66+
let audio_format = u16::from_le_bytes([buffer[20], buffer[21]]);
67+
let num_channels = u16::from_le_bytes([buffer[22], buffer[23]]);
68+
let sample_rate =
69+
u32::from_le_bytes([buffer[24], buffer[25], buffer[26], buffer[27]]);
70+
let bits_per_sample = u16::from_le_bytes([buffer[34], buffer[35]]);
71+
72+
println!(">>> WAV format: audio_format={}, channels={}, sample_rate={}, bits_per_sample={}",
73+
audio_format, num_channels, sample_rate, bits_per_sample);
74+
} else {
75+
println!("!!! WARNING: File doesn't have valid WAV header!");
76+
}
77+
}
78+
5479
// Build query parameters
5580
let mut url = format!("{}/listen", DEEPGRAM_API_BASE);
5681
let mut params = vec![
82+
("model", "nova-2-meeting"), // Use Nova-2-meeting optimized for meetings
5783
("punctuate", "true"),
5884
(
5985
"diarize",
@@ -78,6 +104,11 @@ impl DeepgramService {
78104

79105
url = format!("{}?{}", url, query_string);
80106

107+
println!(">>> Sending request to Deepgram API: {}", url);
108+
println!(">>> Audio file size: {} bytes", buffer.len());
109+
log::info!("Sending request to Deepgram API: {}", url);
110+
log::info!("Audio file size: {} bytes", buffer.len());
111+
81112
// Send request
82113
let response = self
83114
.client
@@ -87,22 +118,39 @@ impl DeepgramService {
87118
.body(buffer)
88119
.send()
89120
.await
90-
.map_err(|e| AppError::Transcription(format!("Deepgram request failed: {}", e)))?;
121+
.map_err(|e| {
122+
log::error!("Deepgram HTTP request failed: {}", e);
123+
AppError::Transcription(format!("Deepgram request failed: {}", e))
124+
})?;
91125

92-
if !response.status().is_success() {
93-
let status = response.status();
126+
let status = response.status();
127+
println!(">>> Deepgram API response status: {}", status);
128+
log::info!("Deepgram API response status: {}", status);
129+
130+
if !status.is_success() {
94131
let error_text = response.text().await.unwrap_or_default();
132+
println!("!!! Deepgram API error ({}): {}", status, error_text);
133+
log::error!("Deepgram API error response: {}", error_text);
95134
return Err(AppError::Transcription(format!(
96135
"Deepgram API error ({}): {}",
97136
status, error_text
98137
)));
99138
}
100139

101140
let deepgram_response: DeepgramResponse = response.json().await.map_err(|e| {
141+
println!("!!! Failed to parse Deepgram JSON response: {}", e);
142+
log::error!("Failed to parse Deepgram JSON response: {}", e);
102143
AppError::Transcription(format!("Failed to parse Deepgram response: {}", e))
103144
})?;
104145

105-
self.parse_deepgram_response(deepgram_response)
146+
println!(">>> Successfully parsed Deepgram JSON response");
147+
println!(">>> Channels: {}", deepgram_response.results.channels.len());
148+
149+
let result = self.parse_deepgram_response(deepgram_response)?;
150+
println!(">>> Parsed into {} segments", result.segments.len());
151+
println!(">>> Transcript text length: {} chars", result.text.len());
152+
153+
Ok(result)
106154
}
107155

108156
/// Parse Deepgram response into our TranscriptionResult format
@@ -118,8 +166,20 @@ impl DeepgramService {
118166
let text = alternative.transcript.clone();
119167
let confidence = Some(alternative.confidence);
120168

169+
println!(">>> Transcript text from Deepgram: {} chars", text.len());
170+
println!(">>> Has utterances: {}", alternative.utterances.is_some());
171+
println!(">>> Has words: {}", alternative.words.is_some());
172+
173+
if let Some(ref utterances) = alternative.utterances {
174+
println!(">>> Utterances count: {}", utterances.len());
175+
}
176+
if let Some(ref words) = alternative.words {
177+
println!(">>> Words count: {}", words.len());
178+
}
179+
121180
// Parse utterances with speaker labels
122181
let segments = if let Some(utterances) = &alternative.utterances {
182+
println!(">>> Using utterances for segments");
123183
utterances
124184
.iter()
125185
.map(|utt| TranscriptionSegment {
@@ -131,6 +191,7 @@ impl DeepgramService {
131191
})
132192
.collect()
133193
} else if let Some(words) = &alternative.words {
194+
println!(">>> Using words fallback for segments");
134195
// Fallback: group words by speaker if utterances not available
135196
let mut segments = Vec::new();
136197
let mut current_speaker = None;
@@ -193,16 +254,24 @@ impl DeepgramService {
193254

194255
segments
195256
} else {
257+
println!(">>> No utterances or words - using fallback single segment");
196258
// No diarization - single segment
197-
vec![TranscriptionSegment {
198-
text: text.clone(),
199-
start_ms: 0,
200-
end_ms: (response.metadata.duration * 1000.0) as i64,
201-
speaker_label: None,
202-
confidence,
203-
}]
259+
if text.is_empty() {
260+
println!("!!! WARNING: Transcript text is empty!");
261+
vec![]
262+
} else {
263+
vec![TranscriptionSegment {
264+
text: text.clone(),
265+
start_ms: 0,
266+
end_ms: (response.metadata.duration * 1000.0) as i64,
267+
speaker_label: None,
268+
confidence,
269+
}]
270+
}
204271
};
205272

273+
println!(">>> Final segments count: {}", segments.len());
274+
206275
Ok(TranscriptionResult {
207276
text,
208277
segments,
@@ -246,6 +315,7 @@ impl TranscriptionServicePort for DeepgramService {
246315
// Build query parameters
247316
let mut url = format!("{}/listen", DEEPGRAM_API_BASE);
248317
let mut params = vec![
318+
("model", "nova-2-meeting"), // Use Nova-2-meeting optimized for meetings
249319
("punctuate", "true"),
250320
(
251321
"diarize",

0 commit comments

Comments
 (0)