Реалізація Web Speech API (розпізнавання/синтез мовлення) на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Web Speech API (розпізнавання/синтез мовлення) на сайті
Середня
~2-3 робочих дні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Web Speech API (розпізнавання та синтез мовлення) на сайті

Web Speech API складається з двох незалежних частин: SpeechRecognition (мовлення → текст) та SpeechSynthesis (текст → мовлення). Перша потрібна для голосового управління, диктовки, голосового пошуку. Друга — для озвучування контенту, сповіщень, доступності.

Підтримка браузерами

SpeechRecognition: Chrome/Edge (з префіксом webkit), Android Chrome. Firefox та Safari — без підтримки. Для продакшену потрібен fallback на серверне ASR (Whisper/Deepgram).

SpeechSynthesis: усі сучасні браузери, включаючи Safari iOS.

Розпізнавання мовлення

const SpeechRecognition =
  window.SpeechRecognition || (window as any).webkitSpeechRecognition

interface UseSpeechRecognitionOptions {
  lang?: string
  continuous?: boolean       // Безперервна запис vs одна фраза
  interimResults?: boolean   // Проміжні результати в реальному часі
  onResult: (transcript: string, isFinal: boolean) => void
  onError?: (error: string) => void
}

function useSpeechRecognition({
  lang = 'uk-UA',
  continuous = false,
  interimResults = true,
  onResult,
  onError,
}: UseSpeechRecognitionOptions) {
  const recognitionRef = useRef<SpeechRecognition | null>(null)
  const [isListening, setIsListening] = useState(false)
  const [isSupported] = useState(() => 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window)

  function start() {
    if (!isSupported) {
      onError?.('Браузер не підтримує розпізнавання мовлення')
      return
    }

    const recognition = new SpeechRecognition()
    recognition.lang = lang
    recognition.continuous = continuous
    recognition.interimResults = interimResults
    recognition.maxAlternatives = 1

    recognition.onstart = () => setIsListening(true)
    recognition.onend = () => setIsListening(false)

    recognition.onresult = (event: SpeechRecognitionEvent) => {
      let finalTranscript = ''
      let interimTranscript = ''

      for (let i = event.resultIndex; i < event.results.length; i++) {
        const transcript = event.results[i][0].transcript
        if (event.results[i].isFinal) {
          finalTranscript += transcript
        } else {
          interimTranscript += transcript
        }
      }

      if (finalTranscript) {
        onResult(finalTranscript.trim(), true)
      } else if (interimTranscript) {
        onResult(interimTranscript.trim(), false)
      }
    }

    recognition.onerror = (event: SpeechRecognitionErrorEvent) => {
      const messages: Record<string, string> = {
        'not-allowed': 'Доступ до мікрофону запрещено',
        'no-speech': 'Мовлення не обнаружено',
        'network': 'Помилка мережі під час розпізнавання',
        'audio-capture': 'Мікрофон недоступен',
      }
      onError?.(messages[event.error] ?? event.error)
      setIsListening(false)
    }

    recognitionRef.current = recognition
    recognition.start()
  }

  function stop() {
    recognitionRef.current?.stop()
    recognitionRef.current = null
  }

  return { isListening, isSupported, start, stop }
}

Компонент голосової диктовки

function VoiceDictation({ onChange }: { onChange: (text: string) => void }) {
  const [transcript, setTranscript] = useState('')
  const [interim, setInterim] = useState('')

  const { isListening, isSupported, start, stop } = useSpeechRecognition({
    lang: 'uk-UA',
    continuous: true,
    interimResults: true,
    onResult: (text, isFinal) => {
      if (isFinal) {
        setTranscript((prev) => {
          const next = prev + (prev ? ' ' : '') + text
          onChange(next)
          return next
        })
        setInterim('')
      } else {
        setInterim(text)
      }
    },
    onError: (err) => console.warn('Speech error:', err),
  })

  if (!isSupported) {
    return <p className="text-sm text-gray-500">Голосовий ввід недоступен у цьому браузері</p>
  }

  return (
    <div className="border rounded-lg p-3">
      <div className="min-h-[80px] text-sm">
        <span>{transcript}</span>
        {interim && <span className="text-gray-400 italic"> {interim}</span>}
      </div>
      <div className="flex gap-2 mt-2 border-t pt-2">
        <button
          onClick={isListening ? stop : start}
          className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm ${
            isListening
              ? 'bg-red-100 text-red-700'
              : 'bg-blue-100 text-blue-700'
          }`}
        >
          {isListening ? (
            <>
              <span className="w-2 h-2 bg-red-500 rounded-full animate-pulse" />
              Стоп
            </>
          ) : (
            'Говорити'
          )}
        </button>
        <button
          onClick={() => { setTranscript(''); setInterim(''); onChange('') }}
          className="text-sm text-gray-500 hover:text-gray-700"
        >
          Очистити
        </button>
      </div>
    </div>
  )
}

Голосові команди

function useVoiceCommands(commands: Record<string, () => void>) {
  const { start, stop, isListening } = useSpeechRecognition({
    lang: 'uk-UA',
    continuous: true,
    interimResults: false,
    onResult: (transcript) => {
      const lower = transcript.toLowerCase().trim()
      for (const [phrase, action] of Object.entries(commands)) {
        if (lower.includes(phrase)) {
          action()
          break
        }
      }
    },
  })

  return { start, stop, isListening }
}

// Використання
const { start } = useVoiceCommands({
  'наступний слайд': () => goToSlide(current + 1),
  'попередній слайд': () => goToSlide(current - 1),
  'перший слайд': () => goToSlide(0),
  'повний екран': () => document.documentElement.requestFullscreen(),
})

Синтез мовлення (Text-to-Speech)

class TextToSpeech {
  private synth = window.speechSynthesis
  private currentUtterance: SpeechSynthesisUtterance | null = null

  speak(text: string, options: {
    lang?: string
    rate?: number    // 0.1–10, за замовчуванням 1
    pitch?: number   // 0–2, за замовчуванням 1
    volume?: number  // 0–1
    voiceName?: string
    onEnd?: () => void
  } = {}) {
    this.stop()

    const utterance = new SpeechSynthesisUtterance(text)
    utterance.lang = options.lang ?? 'uk-UA'
    utterance.rate = options.rate ?? 1
    utterance.pitch = options.pitch ?? 1
    utterance.volume = options.volume ?? 1

    if (options.voiceName) {
      const voices = this.synth.getVoices()
      const voice = voices.find((v) => v.name === options.voiceName)
      if (voice) utterance.voice = voice
    }

    if (options.onEnd) utterance.onend = options.onEnd

    // Chrome workaround: довгий текст обрізається близько 15 секунд
    utterance.onboundary = (event) => {
      if (event.name === 'sentence') {
        // Періодично "будимо" синтезатор
        this.synth.pause()
        this.synth.resume()
      }
    }

    this.currentUtterance = utterance
    this.synth.speak(utterance)
  }

  stop() {
    this.synth.cancel()
    this.currentUtterance = null
  }

  pause() { this.synth.pause() }
  resume() { this.synth.resume() }

  getVoices(): SpeechSynthesisVoice[] {
    return this.synth.getVoices().filter((v) => v.lang.startsWith('uk'))
  }
}

Fallback: Whisper API для серйозного ASR

Коли браузерного ASR недостатньо (низька якість, відсутність підтримки Firefox/Safari):

async function transcribeWithWhisper(audioBlob: Blob): Promise<string> {
  const formData = new FormData()
  formData.append('file', audioBlob, 'audio.webm')
  formData.append('model', 'whisper-1')
  formData.append('language', 'uk')

  const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
    body: formData,
  })

  const data = await response.json()
  return data.text
}

Запис через MediaRecorder API, отправка на /api/transcribe, яка проксирует в Whisper — ключ не утікає на фронт.

Що ми робимо

Визначаємо сценарій: голосовий пошук, диктовка, команди управління, TTS для доступності. Реалізуємо відповідну частину API, додаємо fallback (Whisper для ASR, браузерний TTS везде працює). Тестуємо на різних браузерах, враховуємо політику автоплея.

Строк: голосовий пошук або диктовка — 1–2 дні. Голосові команди + TTS — 2–3 дні. З Whisper fallback — плюс 1 день.