From 3c530b6c9d58745a226f0c9d867db893de431785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=20J=20Barata=20Ribeiro?= <122732773+Barata-Ribeiro@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:01:40 -0300 Subject: [PATCH 1/3] feat: implement Text to Speech functionality with form validation and voice selection --- .../pages/utilities/text-to-speech.tsx | 190 ++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 app/components/pages/utilities/text-to-speech.tsx diff --git a/app/components/pages/utilities/text-to-speech.tsx b/app/components/pages/utilities/text-to-speech.tsx new file mode 100644 index 0000000..8f457ed --- /dev/null +++ b/app/components/pages/utilities/text-to-speech.tsx @@ -0,0 +1,190 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useEffect, useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { z } from 'zod/v4'; +import { Badge } from '~/components/ui/badge'; +import { Button } from '~/components/ui/button'; +import { Field, FieldContent, FieldError, FieldLabel } from '~/components/ui/field'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select'; +import { Slider } from '~/components/ui/slider'; +import { Textarea } from '~/components/ui/textarea'; + +const TextToSpeechSchema = z.object({ + text: z + .string() + .min(10, 'Text must be at least 10 characters long.') + .max(5000, 'Text exceeds maximum length of 5000 characters.'), + rate: z.number().min(0.5).max(2).default(1).optional(), + pitch: z.number().min(0).max(2).default(1).optional(), + voice: z.number().min(0).optional(), +}); + +type TextToSpeechType = z.infer; + +export default function TextToSpeech() { + const [voices, setVoices] = useState([]); + + useEffect(() => { + if (typeof globalThis === 'undefined') return; + + const s = globalThis.speechSynthesis; + if (!s) return; + + const loadVoices = () => { + const v = s.getVoices() ?? []; + setVoices(v); + }; + + loadVoices(); + + // In some browsers the voices are not ready on page load + const handler = () => loadVoices(); + s.addEventListener?.('voiceschanged', handler); + + return () => s.removeEventListener?.('voiceschanged', handler); + }, []); + + const form = useForm({ + resolver: zodResolver(TextToSpeechSchema), + defaultValues: { text: '', rate: 1, pitch: 1, voice: undefined }, + }); + + function onsubmit(data: TextToSpeechType) { + const s = globalThis.speechSynthesis; + if (!s) return; + + s.cancel?.(); + + const utter = new SpeechSynthesisUtterance(data.text); + utter.rate = data.rate ?? 1; + utter.pitch = data.pitch ?? 1; + + if (data.voice !== undefined && data.voice !== null) { + const idx = Number(data.voice); + + if (!Number.isNaN(idx) && voices[idx]) utter.voice = voices[idx]; + } + + s.speak?.(utter); + } + + function reset() { + form.reset(); + const s = globalThis.speechSynthesis; + s?.cancel?.(); + } + + return ( +
+ ( + + Text +