{"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":[]}