Teleprompter.js 3.35 KB
import React from 'react'
import styled from 'styled-components'
import stringSimilarity from 'string-similarity'

const StyledTeleprompter = styled.div`
  font-size: 6rem;
  width: 100%;
  height: 35rem;
  scroll-behavior: smooth;
  overflow: auto;
  display: block;
  margin-bottom: 1rem;
`
// Userκ°€ μ§€κΈˆ λ§ν•˜κ³  μžˆλŠ” 단어 style
const Interim = styled.div`
  background: rgb(0, 0, 0, 0.25);
  color: white;
  flex: 0 0 auto;
  padding: 0.5rem;
  border-radius: 1rem;
  display: inline-block;
`

// Script λ¬Έμžμ—΄ 처리 ["I", "am", "happy"] -> "iamhappy"
const onlyWord = (word) => 
  word
    .trim()                                 // λ¬Έμžμ—΄ μ’Œμš°μ—μ„œ 곡백 제거
    .toLocaleLowerCase()                    // μ•ŒνŒŒλ²³ μ†Œλ¬Έμžλ‘œ λ³€ν™˜
    .replace(/[^κ°€-νž£γ„±-γ…Žγ…-γ…£a-z]/gi, '') // μ •κ·œμ‹μ„ μ΄μš©ν•΄ ν•œκΈ€ λ˜λŠ” μ•ŒνŒŒλ²³μ΄ μ•„λ‹Œ 문자 빈칸으둜 λ³€ν™˜

export default function Teleprompter({ words, progress, listening, onChange }) {
  const recog = React.useRef(null)
  const scrollRef = React.useRef(null)
  const [ results, setResults ] = React.useState('')

  React.useEffect(() => {
    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition
    recog.current = new SpeechRecognition()
    recog.current.continuous = true
    recog.current.interimResults = true
  }, [])

  React.useEffect(() => {
    if (listening) {
      recog.current.start()
    }
    else {
      recog.current.stop()
    }
  }, [listening])

  React.useEffect(() => {
    const handleResult = ({ results }) => {
      const interim = Array.from(results)
        .filter(r => !r.isFinal)
        .map(r => r[0].transcript)
        .join(' ')
      setResults(interim)

      const newIndex = interim
        .split(' ')
        .reduce((memo, word) => {
          if ( memo >= words.length) {
            return memo
          }
          const similarity = stringSimilarity.compareTwoStrings(
            onlyWord(word),
            onlyWord(words[memo])
          )
          memo +=
            similarity > 0.3  // μœ μ‚¬λ„ 민감도 μ„€μ •
              ? 1
              : 0
          return memo
        }, progress)
      if ( newIndex > progress && newIndex <= words.length ) {
        onChange(newIndex)
      }
    }
    recog.current.addEventListener(
      'result',
      handleResult
    )
    return () => {
      recog.current.removeEventListener(
        'result',
        handleResult
      )
    }
  }, [onChange, progress, words])

  React.useEffect(() => {
    /* eslint-disable no-unused-expressions */
    scrollRef.current
      .querySelector(
        `[data-index='${
          progress + 8  // ν˜„μž¬ μ§„ν–‰ μƒνƒœμ— 따라 Scroll μ„€μ •
        }']`
      )
      ?.scrollIntoView({
        behavior: 'smooth',
        block: 'nearest',
        inline: 'start'
      })
  }, [progress])

  return (
    <>
      <StyledTeleprompter ref={scrollRef}>
        {words.map((word, i) => (
          <span
            key={`${word}:${i}`}
            data-index={i}
            style={{
              color:
                i < progress
                  ? '#000'  // 아직 읽지 μ•Šμ€ wordλŠ” 흰색
                  : '#ccc'  // 읽은 wordλŠ” κ²€μ€μƒ‰μœΌλ‘œ λ³€κ²½
            }}
          >
            {word}{' '}
          </span>
        ))}
      </StyledTeleprompter>
      {results && ( <Interim>{results}</Interim> )}
    </>
  )
}