|
| 1 | +# RTZR STT Chapter Generator |
| 2 | + |
| 3 | +RTZR STT API로 음성 파일을 전사한 뒤, 오픈소스 문장 임베딩 모델로 챕터 경계를 자동 생성하는 Python 예제입니다. |
| 4 | +챕터 표시는 별도 LLM 없이, 실제 전사문에서 고른 대표 발화를 사용합니다. |
| 5 | + |
| 6 | +역할은 단순하게 나눕니다. |
| 7 | + |
| 8 | +- `transcribe.py`: RTZR STT API에 오디오 파일을 보내고 transcript JSON을 저장합니다. |
| 9 | +- `rtzr_openapi_client.py`: RTZR 공식 문서의 `RTZROpenAPIClient` 흐름을 분리한 클라이언트입니다. |
| 10 | +- `chapterize.py`: transcript의 문단을 문장 임베딩 모델로 비교해 C99-rank 방식으로 챕터 경계를 찾습니다. 챕터 표시는 Kiwi 형태소 분석기로 추출한 내부 키워드를 이용해 대표 발화를 고릅니다. |
| 11 | + |
| 12 | +## 1. Setup |
| 13 | + |
| 14 | +```bash |
| 15 | +cd stt-chapter-generator |
| 16 | +uv sync |
| 17 | +``` |
| 18 | + |
| 19 | +이 예제는 `pyproject.toml`에 의존성을 정의하고 `uv`로 실행 환경을 관리합니다. |
| 20 | +`.python-version`에는 기본 Python 버전으로 `3.12`를 명시했습니다. |
| 21 | +`uv sync`를 실행하면 `.venv`와 `uv.lock`을 기준으로 필요한 패키지가 설치됩니다. |
| 22 | + |
| 23 | +프로젝트 폴더의 `.env.example`을 복사해 `.env` 파일을 만들고 RTZR 키를 저장합니다. |
| 24 | + |
| 25 | +```bash |
| 26 | +cp .env.example .env |
| 27 | +``` |
| 28 | + |
| 29 | +```env |
| 30 | +RTZR_CLIENT_ID=... |
| 31 | +RTZR_CLIENT_SECRET=... |
| 32 | +``` |
| 33 | + |
| 34 | +실행할 때는 `uv run --env-file .env`를 사용해 `.env` 값을 함께 불러옵니다. |
| 35 | + |
| 36 | +```bash |
| 37 | +uv run --env-file .env -- python transcribe.py --help |
| 38 | +``` |
| 39 | + |
| 40 | +## 2. Transcribe |
| 41 | + |
| 42 | +분석할 음성 파일을 준비한 뒤 경로를 인자로 넘깁니다. |
| 43 | + |
| 44 | +```bash |
| 45 | +uv run --env-file .env -- python transcribe.py path/to/audio.wav \ |
| 46 | + --model-name whisper \ |
| 47 | + --language ko \ |
| 48 | + --use-paragraph-splitter \ |
| 49 | + --paragraph-max 40 \ |
| 50 | + --use-disfluency-filter |
| 51 | +``` |
| 52 | + |
| 53 | +결과는 기본적으로 아래 경로에 저장됩니다. |
| 54 | + |
| 55 | +```text |
| 56 | +data/transcripts/audio.transcript.json |
| 57 | +``` |
| 58 | + |
| 59 | +`--use-disfluency-filter`는 `음`, `아`, 반복 발화처럼 의미가 약한 구어체 표현을 줄이는 RTZR 옵션입니다. RTZR의 기본값은 켜짐입니다. 원본 구어체에 가까운 전사 결과와 비교하고 싶다면 아래처럼 끌 수 있습니다. |
| 60 | + |
| 61 | +```bash |
| 62 | +uv run --env-file .env -- python transcribe.py path/to/audio.wav \ |
| 63 | + --model-name whisper \ |
| 64 | + --language ko \ |
| 65 | + --use-paragraph-splitter \ |
| 66 | + --paragraph-max 40 \ |
| 67 | + --no-disfluency-filter \ |
| 68 | + --output data/transcripts/audio.raw.transcript.json |
| 69 | +``` |
| 70 | + |
| 71 | +## 3. Generate Chapters |
| 72 | + |
| 73 | +기본 실행은 문장 임베딩 모델과 C99-rank 경계 점수로 챕터 경계를 생성합니다. 이후 각 챕터 안에서 자주 등장하면서 전체 전사에서는 상대적으로 덜 흔한 명사 키워드를 내부적으로 고르고, 키워드와 가까운 실제 발화를 대표 발화로 표시합니다. |
| 74 | + |
| 75 | +챕터 수 상한은 전체 전사 글자 수를 기준으로 자동 계산됩니다. 기본값은 약 `1000`자마다 하나의 경계를 허용하되, 최소 `2`개에서 최대 `10`개의 경계까지만 선택합니다. 이후 경계 점수가 높은 지점을 우선적으로 고르고, 너무 가까운 위치가 반복해서 선택되지 않도록 최소 간격을 둡니다. |
| 76 | + |
| 77 | +```bash |
| 78 | +uv run python chapterize.py data/transcripts/audio.transcript.json |
| 79 | +``` |
| 80 | + |
| 81 | +결과: |
| 82 | + |
| 83 | +```text |
| 84 | +data/outputs/audio.chapters.json |
| 85 | +data/outputs/audio.chapters.md |
| 86 | +``` |
| 87 | + |
| 88 | +Markdown 결과를 바로 확인하려면: |
| 89 | + |
| 90 | +```bash |
| 91 | +cat data/outputs/audio.chapters.md |
| 92 | +``` |
| 93 | + |
| 94 | +## Fixed Defaults |
| 95 | + |
| 96 | +튜토리얼에서는 사용자가 경계 탐지 값을 직접 튜닝하지 않도록 주요 값을 코드 내부 기본값으로 고정했습니다. |
| 97 | + |
| 98 | +- 경계 간 최소 간격: `5`개 문단 |
| 99 | +- 경계 계산 window: `5` |
| 100 | +- 최대 경계 수: 전체 전사 글자 수 기준, `min(10, max(2, round(total_chars / 1000)))` |
| 101 | +- 경계 선택 기준: 경계 점수의 상대 순위가 `0.385` 이상인 후보 우선 선택 |
| 102 | +- C99-rank 반경: `3` |
| 103 | +- 챕터 표시 방식: 실제 전사문에서 고른 대표 발화 |
| 104 | +- 내부 키워드 개수: `8`개 |
| 105 | + |
| 106 | +사용자가 실행 시 바꿀 수 있는 옵션은 출력 위치를 정하는 `--output-dir`입니다. |
| 107 | + |
| 108 | +## Speech-Like Transcripts |
| 109 | + |
| 110 | +실제 음성 전사는 글보다 구어체 표현, 반복, 머뭇거림이 많습니다. 이 예제에서는 전사 단계에서 `--use-disfluency-filter`를 사용해 간투어를 줄이고, 챕터 경계는 정리된 전사 문단을 기준으로 계산합니다. |
| 111 | + |
| 112 | +다만 필터가 모든 구어체 문제를 해결하는 것은 아닙니다. 그래서 튜토리얼이나 실험에서는 같은 음성을 `--use-disfluency-filter`와 `--no-disfluency-filter`로 각각 전사한 뒤 챕터 결과를 비교해보는 것이 좋습니다. |
| 113 | + |
| 114 | +## Representative Text |
| 115 | + |
| 116 | +챕터 표시는 LLM으로 새 문장을 생성하지 않습니다. Kiwi 형태소 분석기로 일반 명사, 고유 명사, 외국어 토큰을 추출한 뒤 TF-IDF 점수로 챕터별 내부 키워드를 고릅니다. |
| 117 | + |
| 118 | +이 키워드는 출력에 직접 노출하지 않고, 챕터를 잘 대표하는 발화를 고르는 데만 사용합니다. 이 방식은 별도 로컬 LLM을 설치하지 않아도 되고 실행이 빠릅니다. 대신 사람이 쓴 제목처럼 자연스러운 문장을 만드는 방식이 아니라, 실제 전사에서 고른 대표 발화를 보여주는 방식입니다. |
| 119 | + |
| 120 | +## Example Output |
| 121 | + |
| 122 | +저장소에는 음성 파일과 전사 결과를 포함하지 않습니다. 아래는 국립민속박물관의 Creative Commons 라이선스 영상인 [\[전시라이브러리\] '그 겨울의 행복' 길상 특별전](https://www.youtube.com/watch?v=-VD6kS4d_kE)을 기준으로 한 실행 결과 형식 예시입니다. |
| 123 | + |
| 124 | +> 예제 영상 출처 |
| 125 | +> - 제목: [\[전시라이브러리\] '그 겨울의 행복' 길상 특별전](https://www.youtube.com/watch?v=-VD6kS4d_kE) |
| 126 | +> - 채널: 국립민속박물관 |
| 127 | +> - 라이선스: YouTube Creative Commons Attribution license (reuse allowed) |
| 128 | +
|
| 129 | +```md |
| 130 | +# Chapters: gilsang_winter_happiness |
| 131 | + |
| 132 | +- **00:00:12** |
| 133 | + - 대표 발화: 전시회 구성은 1부에서 길상과 행복의 의미를 환기시킨 후에 2부와 3부에서 본격적으로 길상의 모습을 살펴볼 수 있도록 했습니다. |
| 134 | +- **00:02:39** |
| 135 | + - 대표 발화: ...사는 열 가지인 십장생 또한 대표적인 장수의 상징입니다. 출세, 즉 과거에 합격하여 입신양명하고 부귀해지는 것 또한 옛 사람들이 꼽은 중요한 요소였습니다. |
| 136 | +- **00:05:11** |
| 137 | + - 대표 발화: ...가치에 대한 측면이었지만, 행복에는 즐거움, 만족감 같은 정서적인 측면도 있습니다. |
| 138 | +- **00:06:01** |
| 139 | + - 대표 발화: 전시 관람이라는 행위 자체가 행복한 경험이 되기를 바라는 취지에서 공간을 조성했고, 휴식과 관람이 조화를 이룰 수 있도록 구성하였습니다. |
| 140 | +``` |
| 141 | + |
| 142 | +## Models |
| 143 | + |
| 144 | +기본 모델: |
| 145 | + |
| 146 | +- Embedding: `google/embeddinggemma-300m` |
| 147 | + |
| 148 | +처음 실행할 때 Hugging Face에서 모델 파일을 내려받기 때문에 시간이 걸릴 수 있습니다. 한 번 받은 뒤에는 로컬 캐시를 사용합니다. |
| 149 | + |
| 150 | +`google/embeddinggemma-300m`은 Hugging Face에서 Google Gemma 사용 조건 동의가 필요할 수 있습니다. 처음 실행할 때 접근 권한 오류가 나면 Hugging Face에 로그인한 뒤 모델 페이지에서 라이선스 조건에 동의하고 다시 실행합니다. |
0 commit comments