use-browser-tts.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. /**
  2. * Browser Native TTS (Text-to-Speech) Hook
  3. * Uses Web Speech API for client-side text-to-speech
  4. * Completely free, no API key required
  5. */
  6. import { useState, useCallback, useRef, useEffect } from 'react';
  7. // Note: Window.SpeechSynthesis declaration is already in the global scope
  8. export interface UseBrowserTTSOptions {
  9. onStart?: () => void;
  10. onEnd?: () => void;
  11. onError?: (error: string) => void;
  12. rate?: number; // 0.1 to 10
  13. pitch?: number; // 0 to 2
  14. volume?: number; // 0 to 1
  15. lang?: string; // e.g., 'zh-CN', 'en-US'
  16. }
  17. export function useBrowserTTS(options: UseBrowserTTSOptions = {}) {
  18. const {
  19. onStart,
  20. onEnd,
  21. onError,
  22. rate = 1.0,
  23. pitch = 1.0,
  24. volume = 1.0,
  25. lang = 'zh-CN',
  26. } = options;
  27. const [isSpeaking, setIsSpeaking] = useState(false);
  28. const [isPaused, setIsPaused] = useState(false);
  29. const [availableVoices, setAvailableVoices] = useState<SpeechSynthesisVoice[]>([]);
  30. const utteranceRef = useRef<SpeechSynthesisUtterance | null>(null);
  31. // Load available voices
  32. useEffect(() => {
  33. if (typeof window === 'undefined' || !window.speechSynthesis) {
  34. return;
  35. }
  36. const loadVoices = () => {
  37. const voices = window.speechSynthesis.getVoices();
  38. setAvailableVoices(voices);
  39. };
  40. loadVoices();
  41. // Some browsers load voices asynchronously
  42. if (window.speechSynthesis.onvoiceschanged !== undefined) {
  43. window.speechSynthesis.onvoiceschanged = loadVoices;
  44. }
  45. return () => {
  46. if (window.speechSynthesis.onvoiceschanged !== undefined) {
  47. window.speechSynthesis.onvoiceschanged = null;
  48. }
  49. };
  50. }, []);
  51. const speak = useCallback(
  52. (text: string, voiceURI?: string) => {
  53. if (typeof window === 'undefined' || !window.speechSynthesis) {
  54. onError?.('浏览器不支持 Web Speech API');
  55. return;
  56. }
  57. // Cancel any ongoing speech
  58. window.speechSynthesis.cancel();
  59. const utterance = new SpeechSynthesisUtterance(text);
  60. utterance.rate = rate;
  61. utterance.pitch = pitch;
  62. utterance.volume = volume;
  63. utterance.lang = lang;
  64. // Set voice if specified
  65. if (voiceURI) {
  66. const voice = availableVoices.find((v) => v.voiceURI === voiceURI);
  67. if (voice) {
  68. utterance.voice = voice;
  69. }
  70. }
  71. utterance.onstart = () => {
  72. setIsSpeaking(true);
  73. setIsPaused(false);
  74. onStart?.();
  75. };
  76. utterance.onend = () => {
  77. setIsSpeaking(false);
  78. setIsPaused(false);
  79. utteranceRef.current = null;
  80. onEnd?.();
  81. };
  82. utterance.onerror = (event) => {
  83. setIsSpeaking(false);
  84. setIsPaused(false);
  85. utteranceRef.current = null;
  86. onError?.(event.error);
  87. };
  88. utterance.onpause = () => {
  89. setIsPaused(true);
  90. };
  91. utterance.onresume = () => {
  92. setIsPaused(false);
  93. };
  94. utteranceRef.current = utterance;
  95. window.speechSynthesis.speak(utterance);
  96. },
  97. [rate, pitch, volume, lang, availableVoices, onStart, onEnd, onError],
  98. );
  99. const pause = useCallback(() => {
  100. if (typeof window !== 'undefined' && window.speechSynthesis) {
  101. window.speechSynthesis.pause();
  102. }
  103. }, []);
  104. const resume = useCallback(() => {
  105. if (typeof window !== 'undefined' && window.speechSynthesis) {
  106. window.speechSynthesis.resume();
  107. }
  108. }, []);
  109. const cancel = useCallback(() => {
  110. if (typeof window !== 'undefined' && window.speechSynthesis) {
  111. window.speechSynthesis.cancel();
  112. setIsSpeaking(false);
  113. setIsPaused(false);
  114. utteranceRef.current = null;
  115. }
  116. }, []);
  117. return {
  118. speak,
  119. pause,
  120. resume,
  121. cancel,
  122. isSpeaking,
  123. isPaused,
  124. availableVoices,
  125. };
  126. }