diff --git a/src/components/player/Player.tsx b/src/components/player/Player.tsx
index 5fde65b..8d1ceb2 100644
--- a/src/components/player/Player.tsx
+++ b/src/components/player/Player.tsx
@@ -19,7 +19,6 @@ import {
import { soundsService } from '@/lib/api/services/sounds'
import { PLAYER_EVENTS, playerEvents } from '@/lib/events'
import { cn } from '@/lib/utils'
-import { formatDuration } from '@/utils/format-duration'
import {
ArrowRight,
ArrowRightToLine,
diff --git a/src/components/tts/CreateTTSDialog.tsx b/src/components/tts/CreateTTSDialog.tsx
index 0f02f78..053f320 100644
--- a/src/components/tts/CreateTTSDialog.tsx
+++ b/src/components/tts/CreateTTSDialog.tsx
@@ -15,6 +15,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
+import { Combobox, type ComboboxOption } from '@/components/ui/combobox'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { Switch } from '@/components/ui/switch'
@@ -24,12 +25,12 @@ import { Loader2, Mic } from 'lucide-react'
import { ttsService, type TTSProvider } from '@/lib/api/services/tts'
import { TTS_EVENTS, ttsEvents } from '@/lib/events'
import { toast } from 'sonner'
+import { getSortedLanguages, getLanguageDisplayName } from '@/lib/constants/gtts-languages'
interface FormData {
text: string
provider: string
language?: string
- tld?: string
slow?: boolean
}
@@ -49,10 +50,16 @@ export function CreateTTSDialog({ open, onOpenChange }: CreateTTSDialogProps) {
text: '',
provider: 'gtts',
language: 'en',
- tld: 'com',
slow: false,
})
+ // Prepare language options for combobox
+ const languageOptions: ComboboxOption[] = getSortedLanguages().map((lang) => ({
+ value: lang.code,
+ label: getLanguageDisplayName(lang),
+ searchValue: `${lang.name} ${lang.region || ''} ${lang.code}`.toLowerCase()
+ }))
+
// Load providers when dialog opens
useEffect(() => {
if (open && !providers) {
@@ -74,13 +81,13 @@ export function CreateTTSDialog({ open, onOpenChange }: CreateTTSDialogProps) {
const generateTTS = async (data: FormData) => {
try {
setIsGenerating(true)
- const { text, provider, language, tld, slow } = data
+ const { text, provider, language, slow } = data
const response = await ttsService.generateTTS({
text,
provider,
options: {
...(language && { lang: language }),
- ...(tld && { tld }),
+ tld: 'com', // Always use .com TLD
...(slow !== undefined && { slow }),
},
})
@@ -134,7 +141,6 @@ export function CreateTTSDialog({ open, onOpenChange }: CreateTTSDialogProps) {
text: '',
provider: 'gtts',
language: 'en',
- tld: 'com',
slow: false,
})
setFormErrors({})
@@ -220,42 +226,16 @@ export function CreateTTSDialog({ open, onOpenChange }: CreateTTSDialogProps) {
<>
-
-
-
-
-
-
+ options={languageOptions}
+ placeholder="Select language..."
+ searchPlaceholder="Search languages..."
+ emptyMessage="No language found."
+ />
- Different domains may have different voice characteristics
+ Choose from {languageOptions.length} supported languages including regional variants
diff --git a/src/components/tts/TTSList.tsx b/src/components/tts/TTSList.tsx
index e2a5214..813a994 100644
--- a/src/components/tts/TTSList.tsx
+++ b/src/components/tts/TTSList.tsx
@@ -2,7 +2,6 @@ import { useState, useEffect, useCallback } from 'react'
import { ttsService, type TTSResponse } from '@/lib/api/services/tts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
-import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -31,7 +30,7 @@ export function TTSList() {
setIsLoading(true)
setError(null)
const data = await ttsService.getTTSHistory({ limit })
- setTTSHistory(data)
+ setTTSHistory(data.tts)
} catch (err) {
setError('Failed to load TTS history')
console.error('Failed to fetch TTS history:', err)
diff --git a/src/components/tts/TTSTable.tsx b/src/components/tts/TTSTable.tsx
index cf41ed3..317c607 100644
--- a/src/components/tts/TTSTable.tsx
+++ b/src/components/tts/TTSTable.tsx
@@ -1,4 +1,4 @@
-import { AlertCircle, Calendar, CheckCircle, Clock, Loader, Mic, Trash2, Volume2, XCircle } from 'lucide-react'
+import { Calendar, CheckCircle, Clock, Loader, Mic, Trash2, Volume2, XCircle } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
diff --git a/src/contexts/SocketContext.tsx b/src/contexts/SocketContext.tsx
index a20ae59..1b84097 100644
--- a/src/contexts/SocketContext.tsx
+++ b/src/contexts/SocketContext.tsx
@@ -162,8 +162,6 @@ export function SocketProvider({ children }: SocketProviderProps) {
// Listen for TTS status updates
newSocket.on('tts_completed', data => {
- const { tts_id, sound_id } = data
-
// Emit local event for other components to listen to
ttsEvents.emit(TTS_EVENTS.TTS_COMPLETED, data)
@@ -173,7 +171,7 @@ export function SocketProvider({ children }: SocketProviderProps) {
})
newSocket.on('tts_failed', data => {
- const { tts_id, error } = data
+ const { error } = data
// Emit local event for other components to listen to
ttsEvents.emit(TTS_EVENTS.TTS_FAILED, data)
diff --git a/src/lib/api/services/tts.ts b/src/lib/api/services/tts.ts
index 66e7e85..ec80455 100644
--- a/src/lib/api/services/tts.ts
+++ b/src/lib/api/services/tts.ts
@@ -98,6 +98,11 @@ export const ttsService = {
bValue = new Date(bValue as string).getTime()
}
+ // Handle null values
+ if (aValue === null && bValue === null) return 0
+ if (aValue === null) return 1
+ if (bValue === null) return -1
+
const comparison = aValue > bValue ? 1 : -1
return params.sort_order === 'asc' ? comparison : -comparison
})
diff --git a/src/lib/constants/gtts-languages.ts b/src/lib/constants/gtts-languages.ts
new file mode 100644
index 0000000..ef30f24
--- /dev/null
+++ b/src/lib/constants/gtts-languages.ts
@@ -0,0 +1,115 @@
+export interface LanguageOption {
+ code: string
+ name: string
+ region?: string
+}
+
+export const GTTS_LANGUAGES: LanguageOption[] = [
+ { code: 'af', name: 'Afrikaans' },
+ { code: 'ar', name: 'Arabic' },
+ { code: 'bg', name: 'Bulgarian' },
+ { code: 'bn', name: 'Bengali' },
+ { code: 'bs', name: 'Bosnian' },
+ { code: 'ca', name: 'Catalan' },
+ { code: 'cs', name: 'Czech' },
+ { code: 'cy', name: 'Welsh' },
+ { code: 'da', name: 'Danish' },
+ { code: 'de', name: 'German' },
+ { code: 'el', name: 'Greek' },
+ { code: 'en', name: 'English', region: 'United States' },
+ { code: 'en-au', name: 'English', region: 'Australia' },
+ { code: 'en-ca', name: 'English', region: 'Canada' },
+ { code: 'en-gb', name: 'English', region: 'UK' },
+ { code: 'en-ie', name: 'English', region: 'Ireland' },
+ { code: 'en-in', name: 'English', region: 'India' },
+ { code: 'en-ng', name: 'English', region: 'Nigeria' },
+ { code: 'en-nz', name: 'English', region: 'New Zealand' },
+ { code: 'en-ph', name: 'English', region: 'Philippines' },
+ { code: 'en-za', name: 'English', region: 'South Africa' },
+ { code: 'en-tz', name: 'English', region: 'Tanzania' },
+ { code: 'en-uk', name: 'English', region: 'United Kingdom' },
+ { code: 'en-us', name: 'English', region: 'United States' },
+ { code: 'eo', name: 'Esperanto' },
+ { code: 'es', name: 'Spanish', region: 'Spain' },
+ { code: 'es-es', name: 'Spanish', region: 'Spain' },
+ { code: 'es-mx', name: 'Spanish', region: 'Mexico' },
+ { code: 'es-us', name: 'Spanish', region: 'United States' },
+ { code: 'et', name: 'Estonian' },
+ { code: 'eu', name: 'Basque' },
+ { code: 'fa', name: 'Persian' },
+ { code: 'fi', name: 'Finnish' },
+ { code: 'fr', name: 'French', region: 'France' },
+ { code: 'fr-ca', name: 'French', region: 'Canada' },
+ { code: 'fr-fr', name: 'French', region: 'France' },
+ { code: 'ga', name: 'Irish' },
+ { code: 'gu', name: 'Gujarati' },
+ { code: 'he', name: 'Hebrew' },
+ { code: 'hi', name: 'Hindi' },
+ { code: 'hr', name: 'Croatian' },
+ { code: 'hu', name: 'Hungarian' },
+ { code: 'hy', name: 'Armenian' },
+ { code: 'id', name: 'Indonesian' },
+ { code: 'is', name: 'Icelandic' },
+ { code: 'it', name: 'Italian' },
+ { code: 'ja', name: 'Japanese' },
+ { code: 'jw', name: 'Javanese' },
+ { code: 'ka', name: 'Georgian' },
+ { code: 'kk', name: 'Kazakh' },
+ { code: 'km', name: 'Khmer' },
+ { code: 'kn', name: 'Kannada' },
+ { code: 'ko', name: 'Korean' },
+ { code: 'la', name: 'Latin' },
+ { code: 'lv', name: 'Latvian' },
+ { code: 'mk', name: 'Macedonian' },
+ { code: 'ml', name: 'Malayalam' },
+ { code: 'mr', name: 'Marathi' },
+ { code: 'ms', name: 'Malay' },
+ { code: 'mt', name: 'Maltese' },
+ { code: 'my', name: 'Myanmar (Burmese)' },
+ { code: 'ne', name: 'Nepali' },
+ { code: 'nl', name: 'Dutch' },
+ { code: 'no', name: 'Norwegian' },
+ { code: 'pa', name: 'Punjabi' },
+ { code: 'pl', name: 'Polish' },
+ { code: 'pt', name: 'Portuguese', region: 'Brazil' },
+ { code: 'pt-br', name: 'Portuguese', region: 'Brazil' },
+ { code: 'pt-pt', name: 'Portuguese', region: 'Portugal' },
+ { code: 'ro', name: 'Romanian' },
+ { code: 'ru', name: 'Russian' },
+ { code: 'si', name: 'Sinhala' },
+ { code: 'sk', name: 'Slovak' },
+ { code: 'sl', name: 'Slovenian' },
+ { code: 'sq', name: 'Albanian' },
+ { code: 'sr', name: 'Serbian' },
+ { code: 'su', name: 'Sundanese' },
+ { code: 'sv', name: 'Swedish' },
+ { code: 'sw', name: 'Swahili' },
+ { code: 'ta', name: 'Tamil' },
+ { code: 'te', name: 'Telugu' },
+ { code: 'th', name: 'Thai' },
+ { code: 'tl', name: 'Filipino' },
+ { code: 'tr', name: 'Turkish' },
+ { code: 'uk', name: 'Ukrainian' },
+ { code: 'ur', name: 'Urdu' },
+ { code: 'vi', name: 'Vietnamese' },
+ { code: 'yo', name: 'Yoruba' },
+ { code: 'zh', name: 'Chinese (Mandarin)' },
+ { code: 'zh-cn', name: 'Chinese', region: 'China' },
+ { code: 'zh-tw', name: 'Chinese', region: 'Taiwan' },
+ { code: 'zu', name: 'Zulu' }
+]
+
+export function getLanguageDisplayName(lang: LanguageOption): string {
+ if (lang.region) {
+ return `${lang.name} (${lang.region}) - ${lang.code}`
+ }
+ return `${lang.name} - ${lang.code}`
+}
+
+export function getSortedLanguages(): LanguageOption[] {
+ return [...GTTS_LANGUAGES].sort((a, b) => {
+ const aDisplay = getLanguageDisplayName(a)
+ const bDisplay = getLanguageDisplayName(b)
+ return aDisplay.localeCompare(bDisplay)
+ })
+}
\ No newline at end of file