UNPKG

analytica-frontend-lib

Version:

Repositório público dos componentes utilizados nas plataformas da Analytica Ensino

1 lines 13.8 kB
{"version":3,"sources":["../src/hooks/useTTS.ts","../src/components/AccessibilityWidget/tts/WebSpeechProvider.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useAccessibilityStore } from '../store/accessibilityStore';\nimport { WebSpeechProvider } from '../components/AccessibilityWidget/tts/WebSpeechProvider';\nimport type {\n TTSProvider,\n TTSVoice,\n} from '../components/AccessibilityWidget/tts/types';\n\nexport interface UseTTSReturn {\n /** Indica se a síntese de voz está disponível neste navegador */\n isSupported: boolean;\n /** Vozes carregadas do provider (resolvidas após o primeiro mount) */\n voices: TTSVoice[];\n /**\n * Indica se há ao menos uma voz pt-BR/pt disponível. Útil para\n * mostrar aviso quando o usuário do SO não tem vozes em português.\n */\n hasPortugueseVoice: boolean;\n /** Lê o texto informado, respeitando rate/voiceId do store. */\n speak: (text: string) => void;\n pause: () => void;\n resume: () => void;\n stop: () => void;\n}\n\n/**\n * Hook que liga o `TTSProvider` (default: `WebSpeechProvider`) ao store\n * de acessibilidade — propaga eventos do provider para `ttsStatus` e\n * fornece comandos `speak/pause/resume/stop` que já aplicam as\n * preferências persistidas (`ttsRate`, `ttsVoiceId`).\n *\n * Aceita um provider custom para casos de TTS via backend (Azure,\n * Google etc.) sem alterar a UI.\n */\nexport const useTTS = (providerOverride?: TTSProvider | null): UseTTSReturn => {\n const setTTSStatus = useAccessibilityStore((s) => s.setTTSStatus);\n const ttsRate = useAccessibilityStore((s) => s.ttsRate);\n const ttsVoiceId = useAccessibilityStore((s) => s.ttsVoiceId);\n\n // Garante uma única instância do provider durante o ciclo de vida do hook.\n const provider = useMemo<TTSProvider>(\n () => providerOverride ?? new WebSpeechProvider(),\n [providerOverride]\n );\n\n const [voices, setVoices] = useState<TTSVoice[]>([]);\n\n // Mantemos as preferências em refs para que callbacks de eventos\n // (onstart/onend) leiam sempre o valor mais recente sem precisar\n // recriar o handler — evita resetar a fala quando o usuário ajusta\n // a velocidade no painel.\n const rateRef = useRef(ttsRate);\n const voiceIdRef = useRef(ttsVoiceId);\n rateRef.current = ttsRate;\n voiceIdRef.current = ttsVoiceId;\n\n /**\n * Marca se ESTA instância de `useTTS` foi quem iniciou a fala atual.\n *\n * Necessário porque vários componentes podem chamar `useTTS()`\n * independentemente (TTSController, TTSSection) e cada um cria seu\n * próprio `WebSpeechProvider`, mas todos compartilham o mesmo\n * `window.speechSynthesis` global. O `cancel()` é global — então,\n * sem essa guarda, o cleanup de uma instância (ex.: TTSSection\n * desmontando ao fechar o painel) cancelaria fala iniciada por outra\n * (ex.: click-to-read em andamento no TTSController).\n */\n const isActiveSpeakerRef = useRef(false);\n\n useEffect(() => {\n let mounted = true;\n if (provider.isSupported()) {\n provider\n .getVoices()\n .then((list) => {\n if (mounted) setVoices(list);\n })\n .catch(() => undefined);\n }\n return () => {\n mounted = false;\n // Só interrompe a síntese se foi essa instância que a iniciou.\n // Caso contrário, deixa o synth global em paz — outras instâncias\n // podem estar usando.\n if (isActiveSpeakerRef.current) {\n provider.stop();\n setTTSStatus('idle');\n isActiveSpeakerRef.current = false;\n }\n };\n }, [provider, setTTSStatus]);\n\n const hasPortugueseVoice = voices.some((v) =>\n v.lang.toLowerCase().startsWith('pt')\n );\n\n /**\n * IMPORTANTE: as funções abaixo são memoizadas com `useCallback` porque\n * são frequentemente passadas para `useEffect` deps em consumidores\n * (TTSController, etc.). Sem memoização, as referências mudam a cada\n * render e disparam cleanups indesejados — incluindo o `stop()` que\n * cancela a fala recém-iniciada (sintoma: ícone de áudio acende na aba\n * do browser mas você não ouve nada).\n */\n const speak = useCallback(\n (text: string) => {\n isActiveSpeakerRef.current = true;\n provider.speak(\n text,\n {\n rate: rateRef.current,\n voiceId: voiceIdRef.current ?? undefined,\n // Não fixamos `lang: 'pt-BR'` aqui — em sistemas sem voz pt-BR\n // instalada, o browser fica tentando achar uma e nunca emite\n // áudio. O provider usa o `voice.lang` quando há voz escolhida.\n },\n {\n onStart: () => setTTSStatus('speaking'),\n onEnd: () => {\n isActiveSpeakerRef.current = false;\n setTTSStatus('idle');\n },\n onPause: () => setTTSStatus('paused'),\n onResume: () => setTTSStatus('speaking'),\n onError: () => {\n isActiveSpeakerRef.current = false;\n setTTSStatus('idle');\n },\n }\n );\n },\n [provider, setTTSStatus]\n );\n\n const pause = useCallback(() => {\n provider.pause();\n setTTSStatus('paused');\n }, [provider, setTTSStatus]);\n\n const resume = useCallback(() => {\n provider.resume();\n setTTSStatus('speaking');\n }, [provider, setTTSStatus]);\n\n const stop = useCallback(() => {\n isActiveSpeakerRef.current = false;\n provider.stop();\n setTTSStatus('idle');\n }, [provider, setTTSStatus]);\n\n return {\n isSupported: provider.isSupported(),\n voices,\n hasPortugueseVoice,\n speak,\n pause,\n resume,\n stop,\n };\n};\n","/* eslint-disable no-undef -- DOM lib types (SpeechSynthesis, SpeechSynthesisVoice)\n são reconhecidas pelo TypeScript mas não pelo `globals.browser` do ESLint. */\nimport type {\n TTSProvider,\n TTSProviderEvents,\n TTSSpeakOptions,\n TTSVoice,\n} from './types';\n\n/**\n * Provider default usando a Web Speech API (`window.speechSynthesis`).\n *\n * Notas de compatibilidade:\n * - Disponível em Chrome, Edge, Firefox, Safari e iOS, mas a qualidade\n * das vozes varia muito por SO.\n * - Em Safari/iOS, `getVoices()` é assíncrono — voltamos vazio na\n * primeira chamada e populamos no evento `voiceschanged`. O método\n * `getVoices()` deste provider já trata isso retornando uma `Promise`.\n * - Em alguns SOs não há voz pt-BR pré-instalada — chamadores devem\n * exibir um aviso quando `getVoices()` não retornar voz nesse idioma.\n */\nexport class WebSpeechProvider implements TTSProvider {\n private readonly synth: SpeechSynthesis | null;\n private currentUtterance: SpeechSynthesisUtterance | null = null;\n\n constructor(synth: SpeechSynthesis | null = getSynth()) {\n this.synth = synth;\n }\n\n isSupported(): boolean {\n return this.synth !== null;\n }\n\n async getVoices(): Promise<TTSVoice[]> {\n if (!this.synth) return [];\n\n const synth = this.synth;\n const collect = (): TTSVoice[] =>\n synth.getVoices().map((v) => ({\n id: v.voiceURI,\n name: v.name,\n lang: v.lang,\n isLocal: v.localService,\n }));\n\n const initial = collect();\n if (initial.length > 0) return initial;\n\n // Safari/iOS: vozes carregam de forma assíncrona via 'voiceschanged'.\n return new Promise<TTSVoice[]>((resolve) => {\n const onChange = () => {\n synth.removeEventListener('voiceschanged', onChange);\n resolve(collect());\n };\n synth.addEventListener('voiceschanged', onChange);\n // Fallback: timeout para ambientes que nunca disparam o evento.\n setTimeout(() => {\n synth.removeEventListener('voiceschanged', onChange);\n resolve(collect());\n }, 1500);\n });\n }\n\n speak(\n text: string,\n options: TTSSpeakOptions = {},\n events: TTSProviderEvents = {}\n ): void {\n if (!this.synth) {\n events.onError?.('Síntese de voz não disponível neste navegador.');\n return;\n }\n const trimmed = text.trim();\n if (!trimmed) return;\n\n // Cancela qualquer fala em andamento (evita fila acumulando).\n this.synth.cancel();\n\n const utterance = new SpeechSynthesisUtterance(trimmed);\n utterance.rate = options.rate ?? 1;\n utterance.pitch = options.pitch ?? 1;\n\n // Resolução de voz/idioma:\n // 1. Se o usuário escolheu uma voz, usa ela e o `lang` dela\n // (para evitar mismatch — voz en-US com utterance.lang='pt-BR'\n // causa silêncio em alguns browsers).\n // 2. Se não escolheu mas pediu um lang específico via options,\n // procuramos uma voz que cubra esse lang. Se não houver,\n // deixamos `lang` em branco para o browser usar a voz default.\n const allVoices = this.synth.getVoices();\n let chosenVoice: SpeechSynthesisVoice | undefined;\n if (options.voiceId) {\n chosenVoice = allVoices.find((v) => v.voiceURI === options.voiceId);\n } else if (options.lang) {\n chosenVoice = allVoices.find((v) =>\n v.lang.toLowerCase().startsWith(options.lang!.toLowerCase())\n );\n }\n if (chosenVoice) {\n utterance.voice = chosenVoice;\n utterance.lang = chosenVoice.lang;\n }\n\n utterance.onstart = () => events.onStart?.();\n utterance.onend = () => {\n this.currentUtterance = null;\n events.onEnd?.();\n };\n utterance.onpause = () => events.onPause?.();\n utterance.onresume = () => events.onResume?.();\n utterance.onerror = (event) => {\n this.currentUtterance = null;\n events.onError?.(event.error || 'Erro desconhecido na síntese.');\n };\n\n this.currentUtterance = utterance;\n this.synth.speak(utterance);\n }\n\n pause(): void {\n this.synth?.pause();\n }\n\n resume(): void {\n this.synth?.resume();\n }\n\n stop(): void {\n this.synth?.cancel();\n this.currentUtterance = null;\n }\n}\n\nconst getSynth = (): SpeechSynthesis | null => {\n if (typeof globalThis === 'undefined') return null;\n const synth = (globalThis as unknown as { speechSynthesis?: SpeechSynthesis })\n .speechSynthesis;\n return synth ?? null;\n};\n"],"mappings":"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","names":[]}