Skip to content

Commit 136c069

Browse files
authored
Merge pull request #11 from intellwe/feat/sum
FEAT: Implement AI-powered summary for transcripts
2 parents 849d0ba + 8b68f22 commit 136c069

5 files changed

Lines changed: 235 additions & 46 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ A modern web application for transcribing multi-speaker audio with automatic spe
1818
- **Interactive UI**: Animated, responsive interface with modern design elements
1919
- **Timestamp Display**: See exactly when each segment was spoken
2020
- **Animated Segments**: Transcript segments fade in with a smooth animation
21+
- **AI-Powered Summaries**: Generate concise summaries of transcripts to quickly understand key points
2122

2223
### Text-to-Speech
2324
- **Voice Selection**: Choose from multiple premium AI voices
@@ -44,8 +45,9 @@ A modern web application for transcribing multi-speaker audio with automatic spe
4445
- **Copy to Clipboard**: One-click copy of the entire transcript
4546
- **Download**: Save transcripts locally in your preferred format
4647
- **Share**: Share transcripts directly from the application
48+
- **Summary Export**: Download or copy AI-generated summaries of conversations
4749

4850
## Technology
49-
Built with React, TypeScript, and Tailwind CSS, Wedio App provides a seamless user experience with a modern, responsive design. The application leverages advanced AI APIs for speech recognition, speaker diarization, text-to-speech conversion, and voice transformation.
51+
Built with React, TypeScript, and Tailwind CSS, Wedio App provides a seamless user experience with a modern, responsive design. The application leverages advanced AI APIs for speech recognition, speaker diarization, text-to-speech conversion, voice transformation, and content summarization.
5052

5153
<p align="center">Made with ❤️ by Riana Azad</p>

src/components/ExportOptions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const ExportOptions: React.FC<ExportOptionsProps> = ({ transcript }) => {
9393
};
9494

9595
return (
96-
<div className="bg-gray-800 rounded-xl p-4 border border-gray-700 mb-8">
96+
<div className="bg-gray-800 rounded-xl p-8 border border-gray-700 mb-4">
9797
<h3 className="text-lg font-medium text-gray-200 mb-3">Export Options</h3>
9898

9999
<div className="flex flex-wrap gap-3 mb-4">

src/components/TextToSpeech.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ const TextToSpeech: React.FC<TextToSpeechProps> = ({ transcript, apiKey }) => {
191191
}, [audioUrl]);
192192

193193
return (
194-
<div className="bg-gray-800 rounded-xl p-4 border border-gray-700 mb-8">
194+
<div className="bg-gray-800 rounded-xl p-8 border border-gray-700 mb-8">
195195
<div className="flex items-center justify-between mb-4">
196196
<h3 className="text-lg font-medium text-gray-200 flex items-center">
197197
<Volume2 className="h-5 w-5 mr-2 text-blue-400" />

src/components/TranscriptDisplay.tsx

Lines changed: 36 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { TranscriptResponse } from '../types';
33
import { User, Clock } from 'lucide-react';
44
import ExportOptions from './ExportOptions';
55
import TextToSpeech from './TextToSpeech';
6+
import TranscriptSummary from './TranscriptSummary';
67

78
interface TranscriptDisplayProps {
89
transcript: TranscriptResponse | null;
@@ -109,53 +110,45 @@ const TranscriptDisplay: React.FC<TranscriptDisplayProps> = ({ transcript, apiKe
109110
};
110111

111112
return (
112-
<div className="w-full max-w-3xl mx-auto mt-12 animate-fade-in" ref={containerRef}>
113-
<div className="flex items-center justify-between mb-6">
114-
<h2 className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-blue-500">
115-
Transcript
116-
</h2>
117-
118-
<div className="flex space-x-4">
119-
<div className="px-3 py-1 bg-gray-800 rounded-full text-sm text-gray-300 flex items-center">
120-
<div className="w-2 h-2 rounded-full bg-green-500 mr-2 animate-pulse"></div>
121-
{transcript.language_code.toUpperCase()}
122-
<span className="ml-1 text-green-400">
123-
{Math.round(transcript.language_probability * 100)}%
124-
</span>
125-
</div>
126-
127-
<div className="px-3 py-1 bg-gray-800 rounded-full text-sm text-gray-300 flex items-center">
128-
<User className="h-3 w-3 mr-1 text-blue-400" />
129-
{Object.keys(speakerColors).length} speakers
130-
</div>
113+
<div ref={containerRef} className="w-full max-w-4xl mx-auto">
114+
{!transcript ? (
115+
<div className="text-center py-20">
116+
<p className="text-gray-400 text-lg">Upload an audio file or record to see your transcript here</p>
131117
</div>
132-
</div>
133-
134-
{transcript && <ExportOptions transcript={transcript} />}
135-
136-
{transcript && <TextToSpeech transcript={transcript} apiKey={apiKey} />}
137-
138-
<div className="space-y-4">
139-
{groupedBySpeaker.map((segment, index) => (
140-
<div key={index} className="flex transcript-segment opacity-0">
141-
<div className="w-20 flex-shrink-0 text-sm text-gray-400 pt-1 flex items-center">
142-
<Clock className="h-3 w-3 mr-1" />
143-
{formatTime(segment.startTime)}
144-
</div>
145-
<div className={`flex-grow p-4 rounded-lg border bg-gradient-to-r bg-opacity-10 ${speakerColors[segment.speaker]}`}>
146-
<div className="font-medium mb-2 flex items-center">
147-
<div className="h-6 w-6 rounded-full bg-gray-700 flex items-center justify-center mr-2">
148-
<User className="h-3 w-3 text-white" />
118+
) : (
119+
<>
120+
<h2 className="text-2xl font-bold mb-6 text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
121+
Transcript
122+
</h2>
123+
124+
<div className="space-y-4">
125+
<ExportOptions transcript={transcript} />
126+
127+
<TranscriptSummary transcript={transcript} apiKey={apiKey} />
128+
129+
<TextToSpeech transcript={transcript} apiKey={apiKey} />
130+
{groupedBySpeaker.map((segment, index) => (
131+
<div key={index} className="flex transcript-segment opacity-0">
132+
<div className="w-20 flex-shrink-0 text-sm text-gray-400 pt-1 flex items-center">
133+
<Clock className="h-3 w-3 mr-1" />
134+
{formatTime(segment.startTime)}
135+
</div>
136+
<div className={`flex-grow p-4 rounded-lg border bg-gradient-to-r bg-opacity-10 ${speakerColors[segment.speaker]}`}>
137+
<div className="font-medium mb-2 flex items-center">
138+
<div className="h-6 w-6 rounded-full bg-gray-700 flex items-center justify-center mr-2">
139+
<User className="h-3 w-3 text-white" />
140+
</div>
141+
<span className="bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-300">
142+
{segment.speaker.replace('speaker_', 'Speaker ')}
143+
</span>
144+
</div>
145+
<p className="text-gray-200 leading-relaxed">{segment.text}</p>
149146
</div>
150-
<span className="bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-300">
151-
{segment.speaker.replace('speaker_', 'Speaker ')}
152-
</span>
153147
</div>
154-
<p className="text-gray-200 leading-relaxed">{segment.text}</p>
155-
</div>
148+
))}
156149
</div>
157-
))}
158-
</div>
150+
</>
151+
)}
159152
</div>
160153
);
161154
};
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import React, { useState } from 'react';
2+
import axios from 'axios';
3+
import { FileText, Loader2, Copy, Download } from 'lucide-react';
4+
import { TranscriptResponse } from '../types';
5+
6+
interface TranscriptSummaryProps {
7+
transcript: TranscriptResponse;
8+
apiKey?: string;
9+
}
10+
11+
const TranscriptSummary: React.FC<TranscriptSummaryProps> = ({ transcript, apiKey }) => {
12+
const [isGenerating, setIsGenerating] = useState(false);
13+
const [summary, setSummary] = useState<string | null>(null);
14+
const [error, setError] = useState<string | null>(null);
15+
const [copied, setCopied] = useState(false);
16+
17+
const openAiApiKey = import.meta.env.VITE_OPENAI_API_KEY;
18+
19+
const generateSummary = async () => {
20+
if (!openAiApiKey) {
21+
setError('OpenAI API key is required for summary generation');
22+
return;
23+
}
24+
25+
setIsGenerating(true);
26+
setError(null);
27+
28+
try {
29+
const transcriptText = formatTranscriptForAI();
30+
31+
const response = await axios.post(
32+
'https://api.openai.com/v1/chat/completions',
33+
{
34+
model: 'gpt-3.5-turbo',
35+
messages: [
36+
{
37+
role: 'system',
38+
content: 'You are a helpful assistant that summarizes conversations. Create a concise summary that captures the main points, decisions, and action items from the transcript.'
39+
},
40+
{
41+
role: 'user',
42+
content: `Please summarize this transcript: ${transcriptText}`
43+
}
44+
],
45+
max_tokens: 500,
46+
temperature: 0.7
47+
},
48+
{
49+
headers: {
50+
'Content-Type': 'application/json',
51+
'Authorization': `Bearer ${openAiApiKey}`
52+
}
53+
}
54+
);
55+
56+
const generatedSummary = response.data.choices[0].message.content.trim();
57+
setSummary(generatedSummary);
58+
} catch (err: any) {
59+
console.error('Error generating summary:', err);
60+
setError(`Failed to generate summary: ${err.message || 'Unknown error'}`);
61+
} finally {
62+
setIsGenerating(false);
63+
}
64+
};
65+
66+
const formatTranscriptForAI = () => {
67+
let result = '';
68+
let currentSpeaker = '';
69+
let currentText = '';
70+
71+
transcript.words.forEach((word, index) => {
72+
if (word.type === 'word') {
73+
if (word.speaker_id !== currentSpeaker) {
74+
// New speaker, add previous speaker's text
75+
if (currentSpeaker) {
76+
result += `${currentSpeaker.replace('speaker_', 'Speaker ')}: ${currentText.trim()}\n`;
77+
}
78+
currentSpeaker = word.speaker_id;
79+
currentText = word.text;
80+
} else {
81+
currentText += ' ' + word.text;
82+
}
83+
}
84+
85+
// Handle last word
86+
if (index === transcript.words.length - 1 && currentSpeaker) {
87+
result += `${currentSpeaker.replace('speaker_', 'Speaker ')}: ${currentText.trim()}\n`;
88+
}
89+
});
90+
91+
return result;
92+
};
93+
94+
const copyToClipboard = () => {
95+
if (summary) {
96+
navigator.clipboard.writeText(summary);
97+
setCopied(true);
98+
setTimeout(() => setCopied(false), 2000);
99+
}
100+
};
101+
102+
const downloadSummary = () => {
103+
if (!summary) return;
104+
105+
const blob = new Blob([summary], { type: 'text/plain' });
106+
const url = URL.createObjectURL(blob);
107+
const a = document.createElement('a');
108+
a.href = url;
109+
a.download = 'transcript-summary.txt';
110+
document.body.appendChild(a);
111+
a.click();
112+
document.body.removeChild(a);
113+
URL.revokeObjectURL(url);
114+
};
115+
116+
return (
117+
<div className="w-full max-w-4xl mx-auto bg-gray-800 p-8 rounded-xl shadow-xl border border-gray-700 transform hover:scale-[1.01] transition-all duration-300 mt-8">
118+
<h2 className="text-xl font-semibold mb-4 text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500 flex items-center">
119+
<FileText className="mr-2 h-5 w-5" />
120+
AI Transcript Summary
121+
</h2>
122+
123+
{!summary ? (
124+
<div className="mb-4">
125+
<p className="text-gray-300 mb-4">
126+
Generate an AI-powered summary of your transcript to quickly understand the key points of the conversation.
127+
</p>
128+
129+
<button
130+
onClick={generateSummary}
131+
disabled={isGenerating}
132+
className={`w-full py-3 px-4 rounded-md font-medium ${
133+
isGenerating
134+
? 'bg-gray-700 text-gray-500 cursor-not-allowed'
135+
: 'bg-gradient-to-r from-blue-600 to-purple-600 text-white hover:from-blue-700 hover:to-purple-700 transform hover:translate-y-[-2px] hover:shadow-lg'
136+
} transition-all duration-300 flex items-center justify-center`}
137+
>
138+
{isGenerating ? (
139+
<>
140+
<Loader2 className="animate-spin mr-2 h-5 w-5" />
141+
Generating Summary...
142+
</>
143+
) : (
144+
'Generate AI Summary'
145+
)}
146+
</button>
147+
</div>
148+
) : (
149+
<div className="mb-4">
150+
<div className="bg-gray-700/50 p-4 rounded-lg border border-gray-600 mb-4">
151+
<div className="prose prose-invert max-w-none">
152+
{summary.split('\n').map((paragraph, index) => (
153+
paragraph ? <p key={index} className="mb-2 text-gray-200">{paragraph}</p> : null
154+
))}
155+
</div>
156+
</div>
157+
158+
<div className="flex flex-wrap gap-3">
159+
<button
160+
onClick={copyToClipboard}
161+
className="px-4 py-2 bg-gray-700 rounded-md text-white flex items-center hover:bg-gray-600 transition-colors flex-grow"
162+
>
163+
<Copy className="h-4 w-4 mr-2" />
164+
{copied ? 'Copied!' : 'Copy to Clipboard'}
165+
</button>
166+
167+
<button
168+
onClick={downloadSummary}
169+
className="px-4 py-2 bg-gray-700 rounded-md text-white flex items-center hover:bg-gray-600 transition-colors flex-grow"
170+
>
171+
<Download className="h-4 w-4 mr-2" />
172+
Download Summary
173+
</button>
174+
175+
<button
176+
onClick={() => setSummary(null)}
177+
className="px-4 py-2 bg-gray-700 rounded-md text-white flex items-center hover:bg-gray-600 transition-colors"
178+
>
179+
Regenerate
180+
</button>
181+
</div>
182+
</div>
183+
)}
184+
185+
{error && (
186+
<div className="mb-4 p-3 bg-red-900/30 text-red-400 border border-red-800 rounded-md text-sm">
187+
{error}
188+
</div>
189+
)}
190+
</div>
191+
);
192+
};
193+
194+
export default TranscriptSummary;

0 commit comments

Comments
 (0)