시각장애인을 위한 YouTube 동영상 정보 자동 음성 안내 확장 프로그램

틈메이러·2025년 4월 25일

포트폴리오

목록 보기
3/11
post-thumbnail

🍅 주제

시각장애인을 위한 YouTube 동영상 정보 자동 음성 안내 기능
깃허브링크


1. 개발 환경

  • 언어 & 런타임: Node.js, TypeScript
  • 번들러: Vite (MV3 Chrome Extension 템플릿)
  • 브라우저 & API: Chrome Extension MV3, Web Speech API (speechSynthesis)




2. 개발 기간

2025-04-24 ~ 현재 진행 중
(기능 개발이 어느 정도 끝나면 확장 프로그램 등록 예정)





3. 시스템 아키텍처(데이터 & 이벤트 흐름)

이 확장 프로그램은 크게 Content Script, Background Service Worker, 그리고 SpeechSynthesis 엔진 세 부분으로 나뉘어 동작합니다.


사용자 입력 → Content Script

사용자가 지정한 단축키(예: Shift + 마우스Hover)를 누르면, 브라우저가 현재 탭에 주입된 Content Script로 해당 이벤트를 전달합니다.
Content Script는 이 이벤트를 감지해 바로 동작을 시작할 준비를 합니다.


메타 정보 추출 → 메시지 발신

Content Script는 YouTube 플레이어 페이지의 DOM을 스캔해 제목, 채널명, 조회수, 업로드 날짜 요소를 찾아 텍스트로 파싱합니다.
파싱이 완료되면, 이 정보를 담은 메시지를 Background Service Worker로 전송합니다.


Background → TTS 요청 처리

Background Service WorkerContent Script로부터 받은 메시지를 받자마자 speechSynthesis.cancel()을 호출해 이전에 남아 있던 모든 TTS를 정리하고, 새로 받은 텍스트를 음성 합성 요청으로 넘깁니다.


SpeechSynthesis 엔진 → 음성 출력

Web Speech APIspeechSynthesis.speak()가 호출되면, 브라우저 내장 음성 엔진이 문장을 합성해 스피커로 출력합니다.
사용자는 화면을 보지 않고도 동영상의 핵심 메타정보를 청각으로 확인할 수 있습니다.


이렇게 각 컴포넌트가 메시지를 주고받으며 “이벤트 감지 → 정보 추출 → TTS 요청 → 음성 출력” 의 사이클을 명확하게 분리하여, 유지보수성과 확장성을 높였습니다.





4. 프로젝트 목적

  • 유튜브 영상 페이지에서 제목·채널명·업로드 날짜를 사용자가 지정한 단축키(예: Shift+마우스Hover)로 TTS 읽어주기
  • 시각적 피로 없이 청취로만 콘텐츠 메타 정보를 빠르게 확인




🍅 문제점 및 해결 방법

1. TTS가 화면 이동해도 계속 나오던 문제

영상 썸네일 → 재생 페이지로 이동해도 이전 TTS가 계속 실행됨

원인

Ctrl+Hover 키 조합이 클릭 동작과 충돌하여 의도치 않게 TTS가 발동


해결 방법

  • speechSynthesis.cancel() 호출로 기존 읽기 중단
  • chrome.webNavigation.onHistoryStateUpdated 이벤트 리스너 등록
  • 단축키를 Shift + 마우스Hover 등 다른 키로 변경
  • keydown/keyup 이벤트로 토글 상태 관리
  • Debounce 로직 추가로 빠른 연속 이벤트 방지

2. 숫자 읽기 부자연스러움 문제

조회수나 업로드 날짜 등의 숫자를 아라비아 숫자(하나, 둘, 셋...)로 읽어 부자연스러움
"1억""억", "1조""조"로 1이란 숫자를 읽지 않고 어색하게 읽음

원인

숫자를 한자식(일, 이, 삼...)으로 변환하지 않고 그대로 읽음
억/조 단위에서도 1(일)을 생략하는 기존 숫자 변환 로직


해결 방법

  • "1.2만", "4.1천" 같은 단위 표현은 parseKoreanUnitNumber()로 숫자로 환산 후 변환 ➡ ex) "1.2만""만이천", "4.1천""사천백"
  • 억/조 단위는 1이라도 '일억', '일조'처럼 반드시 '일'을 붙이도록 로직 수정
  • numberToKoreanSino() 함수를 만들어 숫자를 한자식으로 변환
export function numberToKoreanSino(num: number): string {
  if (num === 0) return '영';
  let res = '';
  for (const { v, str } of units) {
    const cnt = Math.floor(num / v);
    if (cnt > 0) {
      if (v >= 1e8) {  // 억(1e8), 조(1e12) 단위
        if (cnt === 1) {
          res += '일' + str;
        } else {
          res += numberToKoreanSino(cnt) + str;
        }
      } else if (v >= 10) {  // 십, 백, 천
        if (cnt > 1) {
          res += numberToKoreanSino(cnt);
        }
        res += str;
      } else {
        res += digits[cnt];
        res += str;
      }
      num %= v;
    }
  }
  return res;
}

3. 제목 대신 재생시간(1:24:39 등) 읽는 문제

일부 영상에서 제목 대신 재생시간(1, 24, 39 등)을 읽음

원인

  • 유튜브 썸네일에 표시되는 재생시간(1:24:39 등)innerText 기준으로 제목보다 먼저 노출됨
  • innerText.split('\n')로 줄 단위 분리 시, 가장 첫 줄을 제목으로 오인
  • 그 결과, 재생시간을 제목으로 잘못 읽는 현상 발생

해결 방법

시간 문자열인지 판별하는 정규식 함수 추가
제목이 아닌 재생시간만 포함된 텍스트를 필터링하기 위해 사용

function hasLiveBadge(card: HTMLElement): boolean {
  return !!card.querySelector('ytd-badge-supported-renderer p')?.textContent?.includes('실시간');
}

innerText의 비정형 구조에 의존하지 않고, 의미 있는 구조화된 요소(DOM) 위주로 접근


4. 웹페이지에서 동적으로 텍스트 읽어오기

기존에는 특정 요소를 하드코딩해서 고정된 위치에서만 제목, 채널명, 조회수 등을 읽음
ex) #info-strings, #metadata-line, yt-formatted-string#video-title 등 선택자를 하드코딩으로 직접 지정

원인

유튜브는 SPA 구조 + 지속적인 레이아웃 변경이 있어서 정된 셀렉터만 의존하면 구조가 바뀔 때마다 코드가 깨짐


해결 방법

카드(div#dismissible 등)의 전체 innerText를 줄 단위로 동적으로 읽어오기

(card.innerText || '').split(/\r?\n/).map(line => line.trim())

카드 전체 innerText를 가져온 뒤 줄(\n) 단위로 분리
줄들 중에서

  • "재생", "자동재생" 관련 텍스트 제거
  • 시간 포맷(예: 1:24) 제거
  • 너무 짧거나 숫자만 많은 줄 걸러내기

필터링된 줄 중에서

  • 첫 번째 줄을 제목으로
  • 두 번째 줄을 채널명으로 설정

조회수/업로드 날짜는 별도로 검색해서 파싱
추가로 MutationObserver를 이용해 동적으로 추가되는 카드에도 실시간 바인딩
SPA 이동 대응을 위해 yt-navigate-finish 이벤트로 초기화


5. 실시간 방송(LIVE) 영상 오인식 문제

  • 유튜브에는 라이브 영상에는 실시간이라는 뱃지가 영상 하단에 붙음
  • 해당 채널에서 현재 라이브 영상 중일 때에는 채널로고 부분에 라이브라는 뱃지가 붙음

원인

기존 코드에서는 innerText.includes('LIVE') 로만 판단하여 두 경우를 모두 실시간 방송 중으로 처리하는 문제가 발생함


해결 방법

  • 단순 텍스트가 아닌, 실시간 뱃지 DOM 요소가 존재하는지 여부로 판별
  • 유튜브에서 실시간 뱃지는 보통 ytd-badge-supported-renderer 내부에 포함됨
function hasLiveBadge(card: HTMLElement): boolean {
  return !!card.querySelector('ytd-badge-supported-renderer p')?.textContent?.includes('실시간');
}

profile
나는야 멋쟁이 토마토

0개의 댓글