From 2b65fb00ee1c8454b0dddb7f91b751ccfe7228e5 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Sat, 24 Jan 2026 20:24:51 +0300 Subject: [PATCH] stiff --- mnemo_cards_admin/src/api/voiceService.ts | 5 +- .../src/components/CardVoicesManager.tsx | 138 ++++++++++++++++-- .../src/components/ui/audio-upload.tsx | 28 ++-- .../lib/api/v2/media_api_v2.dart | 4 +- .../lib/storage/minio_service.dart | 33 ++++- 5 files changed, 182 insertions(+), 26 deletions(-) diff --git a/mnemo_cards_admin/src/api/voiceService.ts b/mnemo_cards_admin/src/api/voiceService.ts index 6e68373..d691f9c 100644 --- a/mnemo_cards_admin/src/api/voiceService.ts +++ b/mnemo_cards_admin/src/api/voiceService.ts @@ -12,12 +12,14 @@ export const voiceServiceApi = { * @param text Text to convert to speech * @param voice Voice identifier (optional, defaults to 'alloy') * @param language Language code (optional) + * @param instructions Instructions for speech generation (optional) * @returns Object ID and presigned URL */ async generateTTS( text: string, voice?: string, - language?: string + language?: string, + instructions?: string ): Promise { const response = await adminApiClient.post( '/api/v2/media/generate-tts', @@ -25,6 +27,7 @@ export const voiceServiceApi = { text, voice: voice || 'alloy', ...(language && { language }), + ...(instructions && { instructions }), } ) diff --git a/mnemo_cards_admin/src/components/CardVoicesManager.tsx b/mnemo_cards_admin/src/components/CardVoicesManager.tsx index 08c4997..58a8254 100644 --- a/mnemo_cards_admin/src/components/CardVoicesManager.tsx +++ b/mnemo_cards_admin/src/components/CardVoicesManager.tsx @@ -5,7 +5,17 @@ import { voicesApi, type VoiceDto } from '@/api/voices' import { AudioUpload } from '@/components/ui/audio-upload' import { Button } from '@/components/ui/button' import { Label } from '@/components/ui/label' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' import { Badge } from '@/components/ui/badge' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' import { X, Plus, Music, Play, Pause, Volume2, Loader2 } from 'lucide-react' import type { AxiosError } from 'axios' import { voiceServiceApi } from '@/api/voiceService' @@ -25,7 +35,15 @@ export function CardVoicesManager({ const [showAddForm, setShowAddForm] = useState(false) const [newAudio, setNewAudio] = useState(undefined) const [newLanguage, setNewLanguage] = useState('es') + const [newInstructions, setNewInstructions] = useState('') const [isGeneratingTTS, setIsGeneratingTTS] = useState(false) + + // Dialog state for TTS generation + const [showTTSDialog, setShowTTSDialog] = useState(false) + const [dialogText, setDialogText] = useState('') + const [dialogVoice, setDialogVoice] = useState('alloy') + const [dialogLanguage, setDialogLanguage] = useState('es') + const [dialogInstructions, setDialogInstructions] = useState('pronunciar la frase claramente') // Load voices for the card const { data: voicesData, isLoading, refetch } = useQuery({ @@ -66,6 +84,7 @@ export function CardVoicesManager({ setShowAddForm(false) setNewAudio(undefined) setNewLanguage('es') + setNewInstructions('') }, onError: (error: unknown) => { console.error('❌ CardVoicesManager: addVoiceMutation.onError', error) @@ -95,30 +114,46 @@ export function CardVoicesManager({ } } - // Generate TTS audio for card - const handleGenerateTTS = async () => { + // Open TTS generation dialog + const handleOpenTTSDialog = () => { if (!cardOriginal || !cardOriginal.trim()) { toast.error('Card text is required for TTS generation') return } + + // Initialize dialog with default values + setDialogText(cardOriginal.trim()) + setDialogVoice('alloy') + setDialogLanguage('es') + setDialogInstructions('pronunciar la frase claramente') + setShowTTSDialog(true) + } + + // Generate TTS audio for card (called from dialog) + const handleConfirmGenerateTTS = async () => { + if (!dialogText || !dialogText.trim()) { + toast.error('Text is required for TTS generation') + return + } if (!cardId) { toast.error('Card ID is required') return } + setShowTTSDialog(false) setIsGeneratingTTS(true) try { - const text = cardOriginal.trim() - // Use default voice 'alloy' for TTS generation - // Language (newLanguage) is used for both TTS generation and saving the voice to the card - const voice = 'alloy' + const text = dialogText.trim() + const voice = dialogVoice || 'alloy' + const language = dialogLanguage || 'es' + const instructions = dialogInstructions || undefined - console.log('🔄 CardVoicesManager: Requesting TTS generation', { text, voice, language: newLanguage }) + console.log('🔄 CardVoicesManager: Requesting TTS generation', { text, voice, language, instructions }) // Generate TTS via backend API // Backend calls voice-service (which handles caching) and saves to MinIO - const ttsResponse = await voiceServiceApi.generateTTS(text, voice, newLanguage) + const ttsResponse = await voiceServiceApi.generateTTS(text, voice, language, instructions) console.log('✅ CardVoicesManager: TTS generated and saved to MinIO', { objectId: ttsResponse.objectId }) @@ -126,7 +161,7 @@ export function CardVoicesManager({ // Add voice to card using the objectId from backend await addVoiceMutation.mutateAsync({ voiceUrl: ttsResponse.objectId, - language: newLanguage || 'es', + language: language, }) } catch (error) { console.error('❌ CardVoicesManager: TTS generation failed', error) @@ -152,7 +187,7 @@ export function CardVoicesManager({ type="button" variant="outline" size="sm" - onClick={handleGenerateTTS} + onClick={handleOpenTTSDialog} disabled={disabled || isGeneratingTTS} > {isGeneratingTTS ? ( @@ -225,6 +260,7 @@ export function CardVoicesManager({ setShowAddForm(false) setNewAudio(undefined) setNewLanguage('es') + setNewInstructions('') }} disabled={disabled || addVoiceMutation.isPending} > @@ -235,6 +271,88 @@ export function CardVoicesManager({ )} + {/* TTS Generation Dialog */} + + + + Generate TTS Audio + + Configure the text-to-speech generation parameters + + +
+
+ +