Files
talk2me/static/js/src/speakerManager.ts
Adolfo Delorenzo dc3e67e17b Add multi-speaker support for group conversations
Features:
- Speaker management system with unique IDs and colors
- Visual speaker selection with avatars and color coding
- Automatic language detection per speaker
- Real-time translation for all speakers' languages
- Conversation history with speaker attribution
- Export conversation as text file
- Persistent speaker data in localStorage

UI Components:
- Speaker toolbar with add/remove controls
- Active speaker indicators
- Conversation view with color-coded messages
- Settings toggle for multi-speaker mode
- Mobile-responsive speaker buttons

Technical Implementation:
- SpeakerManager class handles all speaker operations
- Automatic translation to all active languages
- Conversation entries with timestamps
- Translation caching per language
- Clean separation of original vs translated text
- Support for up to 8 concurrent speakers

User Experience:
- Click to switch active speaker
- Visual feedback for active speaker
- Conversation flows naturally with colors
- Export feature for meeting minutes
- Clear conversation history option
- Seamless single/multi speaker mode switching

This enables group conversations where each participant can speak
in their native language and see translations in real-time.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-02 23:39:15 -06:00

270 lines
8.1 KiB
TypeScript

// Speaker management for multi-speaker support
export interface Speaker {
id: string;
name: string;
language: string;
color: string;
avatar?: string;
isActive: boolean;
lastActiveTime?: number;
}
export interface SpeakerTranscription {
speakerId: string;
text: string;
language: string;
timestamp: number;
}
export interface ConversationEntry {
id: string;
speakerId: string;
originalText: string;
originalLanguage: string;
translations: Map<string, string>; // languageCode -> translatedText
timestamp: number;
audioUrl?: string;
}
export class SpeakerManager {
private static instance: SpeakerManager;
private speakers: Map<string, Speaker> = new Map();
private conversation: ConversationEntry[] = [];
private activeSpeakerId: string | null = null;
private maxConversationLength = 100;
// Predefined colors for speakers
private speakerColors = [
'#007bff', '#28a745', '#dc3545', '#ffc107',
'#17a2b8', '#6f42c1', '#e83e8c', '#fd7e14'
];
private constructor() {
this.loadFromLocalStorage();
}
static getInstance(): SpeakerManager {
if (!SpeakerManager.instance) {
SpeakerManager.instance = new SpeakerManager();
}
return SpeakerManager.instance;
}
// Add a new speaker
addSpeaker(name: string, language: string): Speaker {
const id = this.generateSpeakerId();
const colorIndex = this.speakers.size % this.speakerColors.length;
const speaker: Speaker = {
id,
name,
language,
color: this.speakerColors[colorIndex],
isActive: false,
avatar: this.generateAvatar(name)
};
this.speakers.set(id, speaker);
this.saveToLocalStorage();
return speaker;
}
// Update speaker
updateSpeaker(id: string, updates: Partial<Speaker>): void {
const speaker = this.speakers.get(id);
if (speaker) {
Object.assign(speaker, updates);
this.saveToLocalStorage();
}
}
// Remove speaker
removeSpeaker(id: string): void {
this.speakers.delete(id);
if (this.activeSpeakerId === id) {
this.activeSpeakerId = null;
}
this.saveToLocalStorage();
}
// Get all speakers
getAllSpeakers(): Speaker[] {
return Array.from(this.speakers.values());
}
// Get speaker by ID
getSpeaker(id: string): Speaker | undefined {
return this.speakers.get(id);
}
// Set active speaker
setActiveSpeaker(id: string | null): void {
// Deactivate all speakers
this.speakers.forEach(speaker => {
speaker.isActive = false;
});
// Activate selected speaker
if (id && this.speakers.has(id)) {
const speaker = this.speakers.get(id)!;
speaker.isActive = true;
speaker.lastActiveTime = Date.now();
this.activeSpeakerId = id;
} else {
this.activeSpeakerId = null;
}
this.saveToLocalStorage();
}
// Get active speaker
getActiveSpeaker(): Speaker | null {
return this.activeSpeakerId ? this.speakers.get(this.activeSpeakerId) || null : null;
}
// Add conversation entry
addConversationEntry(
speakerId: string,
originalText: string,
originalLanguage: string
): ConversationEntry {
const entry: ConversationEntry = {
id: this.generateEntryId(),
speakerId,
originalText,
originalLanguage,
translations: new Map(),
timestamp: Date.now()
};
this.conversation.push(entry);
// Limit conversation length
if (this.conversation.length > this.maxConversationLength) {
this.conversation.shift();
}
this.saveToLocalStorage();
return entry;
}
// Add translation to conversation entry
addTranslation(entryId: string, language: string, translatedText: string): void {
const entry = this.conversation.find(e => e.id === entryId);
if (entry) {
entry.translations.set(language, translatedText);
this.saveToLocalStorage();
}
}
// Get conversation for a specific language
getConversationInLanguage(language: string): Array<{
speakerId: string;
speakerName: string;
speakerColor: string;
text: string;
timestamp: number;
isOriginal: boolean;
}> {
return this.conversation.map(entry => {
const speaker = this.speakers.get(entry.speakerId);
const isOriginal = entry.originalLanguage === language;
const text = isOriginal ?
entry.originalText :
entry.translations.get(language) || `[Translating from ${entry.originalLanguage}...]`;
return {
speakerId: entry.speakerId,
speakerName: speaker?.name || 'Unknown',
speakerColor: speaker?.color || '#666',
text,
timestamp: entry.timestamp,
isOriginal
};
});
}
// Get full conversation history
getFullConversation(): ConversationEntry[] {
return [...this.conversation];
}
// Clear conversation
clearConversation(): void {
this.conversation = [];
this.saveToLocalStorage();
}
// Generate unique speaker ID
private generateSpeakerId(): string {
return `speaker_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// Generate unique entry ID
private generateEntryId(): string {
return `entry_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// Generate avatar initials
private generateAvatar(name: string): string {
const parts = name.trim().split(' ');
if (parts.length >= 2) {
return parts[0][0].toUpperCase() + parts[1][0].toUpperCase();
}
return name.substr(0, 2).toUpperCase();
}
// Save to localStorage
private saveToLocalStorage(): void {
try {
const data = {
speakers: Array.from(this.speakers.entries()),
conversation: this.conversation.map(entry => ({
...entry,
translations: Array.from(entry.translations.entries())
})),
activeSpeakerId: this.activeSpeakerId
};
localStorage.setItem('speakerData', JSON.stringify(data));
} catch (error) {
console.error('Failed to save speaker data:', error);
}
}
// Load from localStorage
private loadFromLocalStorage(): void {
try {
const saved = localStorage.getItem('speakerData');
if (saved) {
const data = JSON.parse(saved);
// Restore speakers
if (data.speakers) {
this.speakers = new Map(data.speakers);
}
// Restore conversation with Map translations
if (data.conversation) {
this.conversation = data.conversation.map((entry: any) => ({
...entry,
translations: new Map(entry.translations || [])
}));
}
// Restore active speaker
this.activeSpeakerId = data.activeSpeakerId || null;
}
} catch (error) {
console.error('Failed to load speaker data:', error);
}
}
// Export conversation as text
exportConversation(language: string): string {
const entries = this.getConversationInLanguage(language);
return entries.map(entry =>
`[${new Date(entry.timestamp).toLocaleTimeString()}] ${entry.speakerName}: ${entry.text}`
).join('\n');
}
}