Spaces:
Running on Zero
Running on Zero
ryota commited on
Commit ·
7ff69e0
1
Parent(s): 018fcd7
商談用途に切り替え、HuggingFace Spaces で動くようにする
Browse files- Dockerfile を追加。ffmpeg とモデルを箱に焼き込むので起動が速い
- APP_PASSWORD による簡易認証を追加(未設定なら素通し=ローカル用)
- HOST/PORT を環境変数で切り替え(コンテナでは 0.0.0.0:7860)
- 公開先でパスワード未設定のときは画面に警告を出す
- 既定の語彙を介護から商談に変更
- README を Spaces の手順と料金表に書き直し
- .dockerignore +6 -0
- Dockerfile +35 -0
- README.md +131 -101
- app.py +40 -3
- index.html +11 -2
- pipeline.py +3 -3
.dockerignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.git/
|
| 5 |
+
.claude/
|
| 6 |
+
出力/
|
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HuggingFace Spaces / その他のコンテナ環境で動かすための定義。
|
| 2 |
+
# 画面もモデルもこの1つの箱に入るので、どこに置いても同じ挙動になる。
|
| 3 |
+
FROM python:3.12-slim
|
| 4 |
+
|
| 5 |
+
# 音声の変換に ffmpeg(と長さを測る ffprobe)が要る
|
| 6 |
+
RUN apt-get update \
|
| 7 |
+
&& apt-get install -y --no-install-recommends ffmpeg \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Spaces は root で動かさない決まりなので、一般ユーザーを作る
|
| 11 |
+
RUN useradd -m -u 1000 user
|
| 12 |
+
USER user
|
| 13 |
+
ENV HOME=/home/user \
|
| 14 |
+
PATH=/home/user/.local/bin:$PATH \
|
| 15 |
+
HF_HOME=/home/user/.cache/huggingface
|
| 16 |
+
|
| 17 |
+
WORKDIR /home/user/app
|
| 18 |
+
|
| 19 |
+
COPY --chown=user requirements.txt ./
|
| 20 |
+
RUN pip install --no-cache-dir --upgrade pip \
|
| 21 |
+
&& pip install --no-cache-dir torch torchaudio --index-url https://download.pytorch.org/whl/cpu \
|
| 22 |
+
&& pip install --no-cache-dir -r requirements.txt
|
| 23 |
+
|
| 24 |
+
# 文字起こしモデルを箱の中に焼いておく。
|
| 25 |
+
# こうしないと、起動のたびに約1.6GBを取りに行って数分待たされる。
|
| 26 |
+
RUN python -c "from faster_whisper import WhisperModel; WhisperModel('large-v3-turbo', device='cpu', compute_type='int8')"
|
| 27 |
+
|
| 28 |
+
COPY --chown=user . ./
|
| 29 |
+
|
| 30 |
+
# コンテナの外から届くようにする(自分のPCで動かすときは app.py の既定=127.0.0.1)
|
| 31 |
+
ENV HOST=0.0.0.0 \
|
| 32 |
+
PORT=7860
|
| 33 |
+
|
| 34 |
+
EXPOSE 7860
|
| 35 |
+
CMD ["python", "app.py"]
|
README.md
CHANGED
|
@@ -1,74 +1,99 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
録音ZIPを投げると、指定秒数以上のファイルだけを文字起こしし、話者ごとに分けてCSVにする
|
| 4 |
-
音声・文字起こし結果ともに外部へ送信しない(モデルの初回ダウンロードのみ通信あり)。
|
| 5 |
|
| 6 |
-
- 文字起こし: Whisper large-v3 系(
|
| 7 |
-
- 話者分離: pyannote.audio 3.1
|
| 8 |
-
-
|
|
|
|
|
|
|
| 9 |
|
| 10 |
---
|
| 11 |
|
| 12 |
-
## 1.
|
| 13 |
|
| 14 |
-
###
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
winget install Gyan.FFmpeg
|
| 20 |
-
```
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
|
| 28 |
-
|
| 29 |
-
.\setup.ps1
|
| 30 |
-
```
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
|
|
|
|
| 36 |
|
| 37 |
-
1.
|
| 38 |
-
2. 次の2つのページを開き、それぞれ利用条件に同意する
|
| 39 |
-
- https://huggingface.co/pyannote/speaker-diarization-3.1
|
| 40 |
-
- https://huggingface.co/pyannote/segmentation-3.0
|
| 41 |
-
3. https://huggingface.co/settings/tokens で read 権限のトークンを作成
|
| 42 |
|
| 43 |
-
|
| 44 |
|
| 45 |
-
```
|
| 46 |
-
|
|
|
|
| 47 |
```
|
| 48 |
|
| 49 |
-
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
---
|
| 56 |
|
| 57 |
-
## 2.
|
| 58 |
|
| 59 |
-
|
| 60 |
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
```
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
|
| 70 |
-
|
| 71 |
-
(`C:\Users\<ユーザー名>\.cache\huggingface`)。
|
| 72 |
|
| 73 |
---
|
| 74 |
|
|
@@ -87,11 +112,11 @@ setx HF_TOKEN "hf_xxxxxxxxxxxx"
|
|
| 87 |
```
|
| 88 |
文字起こし_<ID>/
|
| 89 |
├── 書き起こし/
|
| 90 |
-
│ ├──
|
| 91 |
-
│ └──
|
| 92 |
├── データ/
|
| 93 |
-
│ ├──
|
| 94 |
-
│ └──
|
| 95 |
├── 録音ごと.xlsx ← 1録音=1シート+先頭に一覧シート
|
| 96 |
├── 処理結果一覧.csv ← 何が処理され何が除外されたかの記録
|
| 97 |
└── 全発話.csv ← 横断で検索・集計したいとき用
|
|
@@ -100,41 +125,16 @@ setx HF_TOKEN "hf_xxxxxxxxxxxx"
|
|
| 100 |
画面上でも録音ごとの折りたたみカードになっていて、1本だけコピーしたり、
|
| 101 |
その録音のテキスト/CSVだけを個別に落とすこともできる。
|
| 102 |
|
| 103 |
-
**書き起こしテキスト(.txt)の形**
|
| 104 |
-
|
| 105 |
-
```
|
| 106 |
-
────────────────────────────────────────
|
| 107 |
-
訪問_田中様.m4a
|
| 108 |
-
────────────────────────────────────────
|
| 109 |
-
録音日時 2026-08-12 10:32:04
|
| 110 |
-
録音長 00:06:52(412秒)
|
| 111 |
-
話者 SPEAKER_00、SPEAKER_01
|
| 112 |
-
状態 書き起こし済み
|
| 113 |
-
|
| 114 |
-
00:00:03 SPEAKER_00
|
| 115 |
-
今日の体温は36度4分でした
|
| 116 |
-
|
| 117 |
-
00:00:12 SPEAKER_01
|
| 118 |
-
ありがとう、変わりないね
|
| 119 |
-
|
| 120 |
-
00:04:41 SPEAKER_00 ※要確認(Whisperの定型幻聴に一致)
|
| 121 |
-
ご視聴ありがとうございました
|
| 122 |
-
|
| 123 |
-
────────────────────────────────────────
|
| 124 |
-
全 3 発話 / 要確認 1 件
|
| 125 |
-
```
|
| 126 |
-
|
| 127 |
**CSVの列**(録音ごとのCSV)
|
| 128 |
|
| 129 |
| 列 | 内容 |
|
| 130 |
|---|---|
|
| 131 |
| 開始 / 終了 | 発話のタイムコード |
|
| 132 |
-
| 話者 | SPEAKER_00, SPEAKER_01 … |
|
| 133 |
| 発話内容 | 書き起こし |
|
| 134 |
| 要確認 | 誤認識の疑いがある行に印 |
|
| 135 |
| 備考 | 疑わしいと判定した理由 |
|
| 136 |
|
| 137 |
-
`全発話.csv` はこれの先頭に「ファイル名・録音日時・録音長(秒)」が付く。
|
| 138 |
Excelで文字化けしないようBOM付きUTF-8で出力している。
|
| 139 |
|
| 140 |
30秒未満で除外したファイルは発話CSVには出ないが、`処理結果一覧.csv` と
|
|
@@ -142,63 +142,93 @@ Excelで文字化けしないようBOM付きUTF-8で出力している。
|
|
| 142 |
|
| 143 |
---
|
| 144 |
|
| 145 |
-
## 4.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
**話者の名前は自動では分からない。** `SPEAKER_00` のような番号が振られるだけで、
|
| 148 |
-
ど
|
| 149 |
-
話者人数を事前に指定すると精度が目に見えて上がるので、1対1の訪問なら「2」を入れる。
|
| 150 |
|
| 151 |
-
**精度は録音環境に強く依存する。** レコーダーを机に置いた1対1の
|
| 152 |
-
複数人が同時に話す、
|
| 153 |
「完成品」ではなく「人が直す前提の下書き」として設計してある。
|
| 154 |
|
| 155 |
**Whisperは無音区間で幻の文を作る。** 「ご視聴ありがとうございました」のような定型句が典型。
|
| 156 |
検出した行は削除せず「要確認」を立てるようにした。勝手に消すと、
|
| 157 |
本当の発話まで消えたときに気づけないため。
|
| 158 |
|
| 159 |
-
**
|
| 160 |
-
|
| 161 |
-
GPU搭載機で動かせば自動で `large-v3` が既定になり、速度も数倍になる。
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
---
|
| 174 |
|
| 175 |
-
##
|
| 176 |
|
| 177 |
```
|
| 178 |
-
|
| 179 |
-
├── app.py Webサーバー(アップロード・進捗・
|
| 180 |
├── pipeline.py ZIP展開 → 長さフィルタ → 文字起こし → 話者分離 → 統合
|
| 181 |
├── index.html 画面
|
| 182 |
-
├──
|
| 183 |
-
├──
|
|
|
|
| 184 |
├── requirements.txt
|
| 185 |
└── README.md
|
| 186 |
```
|
| 187 |
|
| 188 |
処理の中身を変えたいときは `pipeline.py` を見る。
|
| 189 |
-
|
| 190 |
-
`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
---
|
| 193 |
|
| 194 |
-
##
|
| 195 |
|
| 196 |
| 症状 | 対処 |
|
| 197 |
|---|---|
|
| 198 |
-
|
|
|
|
|
|
|
|
| 199 |
| 話者分離モデルが読めない | HuggingFaceの2ページ両方で条件に同意したか、トークンが read 権限か確認 |
|
| 200 |
| 日本語ファイル名が化ける | Windows製ZIPのCP932は自動判別済み。それでも化けるなら元のZIPの作り方を確認 |
|
| 201 |
-
| 極端に遅い | 精度を `turbo` に落とす、
|
| 202 |
| 発話が検出されない | 録音の音量が小さい可能性。`ffmpeg -i 元ファイル -af loudnorm 正規化後.wav` で正規化してから再投入 |
|
| 203 |
-
|
|
| 204 |
-
| モデル取得で WinError 1314 | HuggingFaceキャッシュのシンボリックリンクに管理者権限が要るのが原因。`pipeline.py` で実体コピーに切り替え済み。それでも出るなら `C:\Users\<ユーザー名>\.cache\huggingface` の該当モデルフォルダを消して再実行 |
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: 商談文字起こし
|
| 3 |
+
emoji: 🎙️
|
| 4 |
+
colorFrom: gray
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: false
|
| 9 |
+
short_description: 商談録音のZIPを文字起こしして話者ごとにCSVにする
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# 商談文字起こし
|
| 13 |
|
| 14 |
+
録音ZIPを投げると、指定秒数以上のファイルだけを文字起こしし、話者ごとに分けてCSVにするWebアプリ。
|
|
|
|
| 15 |
|
| 16 |
+
- 文字起こし: Whisper large-v3 系(faster-whisper。GPUがあれば自動で使う)
|
| 17 |
+
- 話者分離: pyannote.audio 3.1(任意。使うにはHuggingFaceのトークンが要る)
|
| 18 |
+
- 動く場所: HuggingFace Spaces(URLでどのPCからでも)/ 自分のPC(ローカル完結)
|
| 19 |
+
|
| 20 |
+
同じコードが両方で動く。違いは環境変数だけ。
|
| 21 |
|
| 22 |
---
|
| 23 |
|
| 24 |
+
## 1. HuggingFace Spaces に置く(URLで使う)
|
| 25 |
|
| 26 |
+
### 1-1. Space を作る
|
| 27 |
|
| 28 |
+
1. https://huggingface.co/new-space を開く
|
| 29 |
+
2. **Space name** に `shodan-mojiokoshi` など。**Owner** は個人でもOrganizationでも可
|
| 30 |
+
3. **License** は任意。**Space SDK** は **Docker** → **Blank** を選ぶ
|
| 31 |
+
4. **Public** を選ぶ(理由は下記)
|
| 32 |
+
5. **Create Space**
|
| 33 |
|
| 34 |
+
**Public と Private の選び方**
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
| | 誰が使えるか | 向き不向き |
|
| 37 |
+
|---|---|---|
|
| 38 |
+
| Public + アプリのパスワード(推奨) | URLとパスワードを知っている人。**HuggingFaceのアカウントは不要** | 社内の誰でもURLを開けばいい。ソースコードは公開されるが、録音や結果は公開されない |
|
| 39 |
+
| Private | Space の所有者と Organization メンバーだけ。**全員にHuggingFaceアカウントが必要** | 社内の全員をHuggingFaceのOrganizationに招く前提なら |
|
| 40 |
|
| 41 |
+
「どのパソコンからでもURLで」を優先するなら **Public + 次のパスワード設定**が実用的です。
|
| 42 |
+
その場合、**パスワード設定は必須**と考えてください。
|
| 43 |
|
| 44 |
+
### 1-2. パスワードを設定する(必ず先に)
|
| 45 |
|
| 46 |
+
Space の **Settings** → **Variables and secrets** → **New secret**
|
|
|
|
|
|
|
| 47 |
|
| 48 |
+
| Name | Value |
|
| 49 |
+
|---|---|
|
| 50 |
+
| `APP_PASSWORD` | 好きなパスワード |
|
| 51 |
+
| `APP_USER` | ログイン名(省略すると `spin`) |
|
| 52 |
+
| `HF_TOKEN` | 話者分離を使うときだけ。read権限のトークン |
|
| 53 |
|
| 54 |
+
`APP_PASSWORD` を設定しないと、**URLを知っている人は誰でも使えます**(その状態のときは画面に赤い警告が出ます)。
|
| 55 |
+
Public にするなら、コードを送る前にここを設定しておく。
|
| 56 |
|
| 57 |
+
### 1-3. コードを送る
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
+
Space のページに出ている git のURLを使って、このフォルダから送る。
|
| 60 |
|
| 61 |
+
```bash
|
| 62 |
+
git remote add space https://huggingface.co/spaces/<オーナー名>/<Space名>
|
| 63 |
+
git push space main
|
| 64 |
```
|
| 65 |
|
| 66 |
+
初回はパスワードの代わりに **HuggingFace のアクセストークン(write権限)** を聞かれる。
|
| 67 |
|
| 68 |
+
送ると Space が自動でビルドを始める。**初回は15〜20分かかる**(Whisperのモデルを箱に焼き込むため)。
|
| 69 |
+
Building → Running に変われば完成。以後 `git push space main` するたびに自動で更新される。
|
| 70 |
+
|
| 71 |
+
GitHubにも置きたい場合は、リモートを2つ持たせればいい。
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
git remote add origin https://github.com/<アカウント>/<リポジトリ>.git
|
| 75 |
+
git push origin main
|
| 76 |
+
```
|
| 77 |
|
| 78 |
---
|
| 79 |
|
| 80 |
+
## 2. 速度と料金
|
| 81 |
|
| 82 |
+
Spaces の無料枠は 2 vCPU。**動くが、速くはない。**
|
| 83 |
|
| 84 |
+
| ハードウェア | 料金 | 1時間の商談を文字起こしする時間 |
|
| 85 |
+
|---|---|---|
|
| 86 |
+
| CPU Basic(無料・初期設定) | ¥0 | 1.5〜3時間 |
|
| 87 |
+
| CPU Upgrade(8 vCPU) | $0.03/時 | 30〜45分 |
|
| 88 |
+
| Nvidia T4 small | $0.40/時 | 3〜5分 |
|
| 89 |
|
| 90 |
+
有料ハードウェアは **Settings → Sleep time** で「◯分使われなければ寝る」を設定できる。
|
| 91 |
+
寝ている間は課金されないので、月に数時間しか使わないなら、有料でも実際の請求は数十円〜数百円で収まる。
|
|
|
|
| 92 |
|
| 93 |
+
**まず無料で始めて、遅くて使い物にならなければ上げる**のが安全。ハードウェアの変更は Settings から数クリックで、コードの変更は要らない
|
| 94 |
+
(ただしGPUを使う場合は `Dockerfile` の torch を CUDA版に差し替える必要があるので、そのときは声をかけてください)。
|
| 95 |
|
| 96 |
+
無料枠は **48時間使われないと寝る**。次に開いた人は起動を数分待つことになる。
|
|
|
|
| 97 |
|
| 98 |
---
|
| 99 |
|
|
|
|
| 112 |
```
|
| 113 |
文字起こし_<ID>/
|
| 114 |
├── 書き起こし/
|
| 115 |
+
│ ├── 001_A社_初回訪問.txt ← 人が読む用。そのまま議事録に貼れる
|
| 116 |
+
│ └── 002_B社_見積提示.txt
|
| 117 |
├── データ/
|
| 118 |
+
│ ├── 001_A社_初回訪問.csv ← システム取り込み用
|
| 119 |
+
│ └── 002_B社_見積提示.csv
|
| 120 |
├── 録音ごと.xlsx ← 1録音=1シート+先頭に一覧シート
|
| 121 |
├── 処理結果一覧.csv ← 何が処理され何が除外されたかの記録
|
| 122 |
└── 全発話.csv ← 横断で検索・集計したいとき用
|
|
|
|
| 125 |
画面上でも録音ごとの折りたたみカードになっていて、1本だけコピーしたり、
|
| 126 |
その録音のテキスト/CSVだけを個別に落とすこともできる。
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
**CSVの列**(録音ごとのCSV)
|
| 129 |
|
| 130 |
| 列 | 内容 |
|
| 131 |
|---|---|
|
| 132 |
| 開始 / 終了 | 発話のタイムコード |
|
| 133 |
+
| 話者 | SPEAKER_00, SPEAKER_01 …(話者分離オフなら空) |
|
| 134 |
| 発話内容 | 書き起こし |
|
| 135 |
| 要確認 | 誤認識の疑いがある行に印 |
|
| 136 |
| 備考 | 疑わしいと判定した理由 |
|
| 137 |
|
|
|
|
| 138 |
Excelで文字化けしないようBOM付きUTF-8で出力している。
|
| 139 |
|
| 140 |
30秒未満で除外したファイルは発話CSVには出ないが、`処理結果一覧.csv` と
|
|
|
|
| 142 |
|
| 143 |
---
|
| 144 |
|
| 145 |
+
## 4. 話者分離を使う(任意)
|
| 146 |
+
|
| 147 |
+
商談で「どちらが話したか」を分けたいときに使う。設定し���くても文字起こしは動く(話者列が空になるだけ)。
|
| 148 |
+
|
| 149 |
+
1. https://huggingface.co/pyannote/speaker-diarization-3.1 と
|
| 150 |
+
https://huggingface.co/pyannote/segmentation-3.0 の**両方**で利用条件に同意する
|
| 151 |
+
2. https://huggingface.co/settings/tokens で read 権限のトークンを作る
|
| 152 |
+
3. Space の Secret に `HF_TOKEN` として登録する(自分のPCで動かすなら `setx HF_TOKEN "hf_..."`)
|
| 153 |
+
|
| 154 |
+
1対1の商談なら「話者の人数」に `2` を入れると精度が上がる。
|
| 155 |
+
ただし**話者分離は文字起こしと同じかそれ以上の時間がかかる**ので、無料枠では現実的でないことが多い。
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## 5. 知っておくべき制約
|
| 160 |
|
| 161 |
**話者の名前は自動では分からない。** `SPEAKER_00` のような番号が振られるだけで、
|
| 162 |
+
どちらが自社でどちらが先方かは人が対応付ける必要がある。
|
|
|
|
| 163 |
|
| 164 |
+
**精度は録音環境に強く依存する。** レコーダーを机に置いた1対1の商談なら実用的だが、
|
| 165 |
+
複数人が同時に話す、店内の騒音が大きい環境では話者の切り替わりを取りこぼす。
|
| 166 |
「完成品」ではなく「人が直す前提の下書き」として設計してある。
|
| 167 |
|
| 168 |
**Whisperは無音区間で幻の文を作る。** 「ご視聴ありがとうございました」のような定型句が典型。
|
| 169 |
検出した行は削除せず「要確認」を立てるようにした。勝手に消すと、
|
| 170 |
本当の発話まで消えたときに気づけないため。
|
| 171 |
|
| 172 |
+
**録音は相手の同意を得てから。** 商談相手との会話を録音すること自体は違法ではないが、
|
| 173 |
+
文字起こしを社外に出す、AIの学習に使うといった話になると別問題になる。運用ルールは決めておく。
|
|
|
|
| 174 |
|
| 175 |
+
---
|
| 176 |
+
|
| 177 |
+
## 6. 自分のPCで動かす(ローカル完結)
|
| 178 |
+
|
| 179 |
+
外に一切出したくない録音は、こちらを使う。
|
| 180 |
|
| 181 |
+
```powershell
|
| 182 |
+
.\setup.ps1 # 初回のみ。Python環境を作る
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
ffmpeg が要る。PowerShell で `winget install Gyan.FFmpeg`。
|
| 186 |
+
|
| 187 |
+
起動は `start.cmd` をダブルクリック。`127.0.0.1:8000` にのみ待ち受けるので、
|
| 188 |
+
同じPC以外からはアクセスできない。パスワードも掛からない(掛けたい場合は `APP_PASSWORD` を設定する)。
|
| 189 |
|
| 190 |
---
|
| 191 |
|
| 192 |
+
## 7. ファイル構成
|
| 193 |
|
| 194 |
```
|
| 195 |
+
spinthoughts/
|
| 196 |
+
├── app.py Webサーバー(アップロード・認証・進捗・出力)
|
| 197 |
├── pipeline.py ZIP展開 → 長さフィルタ → 文字起こし → 話者分離 → 統合
|
| 198 |
├── index.html 画面
|
| 199 |
+
├── Dockerfile Spaces / コンテナ用
|
| 200 |
+
├── setup.ps1 自分のPCで動かすための初回セットアップ
|
| 201 |
+
├── start.cmd 自分のPCでの起動
|
| 202 |
├── requirements.txt
|
| 203 |
└── README.md
|
| 204 |
```
|
| 205 |
|
| 206 |
処理の中身を変えたいときは `pipeline.py` を見る。
|
| 207 |
+
`DEFAULT_PROMPT` に自社名・��品名・業界用語を足すと、固有名詞の精度が上がる。
|
| 208 |
+
`HALLUCINATION_PATTERNS` には、自分の環境で出やすい誤認識を足せる。
|
| 209 |
+
|
| 210 |
+
### 環境変数
|
| 211 |
+
|
| 212 |
+
| 名前 | 既定 | 用途 |
|
| 213 |
+
|---|---|---|
|
| 214 |
+
| `APP_PASSWORD` | 空(=認証なし) | 設定すると閲覧にパスワードが要る |
|
| 215 |
+
| `APP_USER` | `spin` | ログイン名 |
|
| 216 |
+
| `HF_TOKEN` | 空 | 話者分離に使う |
|
| 217 |
+
| `HOST` | `127.0.0.1` | コンテナでは `0.0.0.0` |
|
| 218 |
+
| `PORT` | `8000` | Spaces では `7860` |
|
| 219 |
+
| `HOSTED` | 空 | 設定すると画面の説明文が「サーバー処理」向けに変わる |
|
| 220 |
|
| 221 |
---
|
| 222 |
|
| 223 |
+
## 8. うまくいかないとき
|
| 224 |
|
| 225 |
| 症状 | 対処 |
|
| 226 |
|---|---|
|
| 227 |
+
| Space が Build error | ログの最後を見る。多くは requirements.txt の綴りかネットワーク。再ビルドで直ることもある |
|
| 228 |
+
| Space が寝ている | 無料枠は48時間で寝る。開いて数分待つ |
|
| 229 |
+
| パスワードを聞かれない | `APP_PASSWORD` が未設定。Settings → Secrets を確認 |
|
| 230 |
| 話者分離モデルが読めない | HuggingFaceの2ページ両方で条件に同意したか、トークンが read 権限か確認 |
|
| 231 |
| 日本語ファイル名が化ける | Windows製ZIPのCP932は自動判別済み。それでも化けるなら元のZIPの作り方を確認 |
|
| 232 |
+
| 極端に遅い | 精度を `turbo` に落とす、話者分離をオフにする、ハードウェアを上げる |
|
| 233 |
| 発話が検出されない | 録音の音量が小さい可能性。`ffmpeg -i 元ファイル -af loudnorm 正規化後.wav` で正規化してから再投入 |
|
| 234 |
+
| ローカルでモデル取得が WinError 1314 | HuggingFaceキャッシュのシンボリックリンク。実体コピーに切替済み。残骸が残っていたら `.cache\huggingface` の該当フォルダを消して再実行 |
|
|
|
app.py
CHANGED
|
@@ -11,6 +11,8 @@
|
|
| 11 |
|
| 12 |
from __future__ import annotations
|
| 13 |
|
|
|
|
|
|
|
| 14 |
import os
|
| 15 |
import shutil
|
| 16 |
import tempfile
|
|
@@ -19,8 +21,8 @@ import traceback
|
|
| 19 |
import uuid
|
| 20 |
from pathlib import Path
|
| 21 |
|
| 22 |
-
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
| 23 |
-
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
| 24 |
|
| 25 |
import pipeline as pl
|
| 26 |
|
|
@@ -32,6 +34,33 @@ app = FastAPI(title="録音文字起こし")
|
|
| 32 |
JOBS: dict[str, dict] = {}
|
| 33 |
LOCK = threading.Lock()
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
def update(job_id: str, **fields) -> None:
|
| 37 |
with LOCK:
|
|
@@ -122,6 +151,8 @@ def environment() -> JSONResponse:
|
|
| 122 |
"gpu": pl.gpu_label(),
|
| 123 |
"defaultModel": pl.default_model_size(),
|
| 124 |
"tokenFromEnv": bool(os.environ.get("HF_TOKEN")),
|
|
|
|
|
|
|
| 125 |
"defaultPrompt": pl.DEFAULT_PROMPT,
|
| 126 |
})
|
| 127 |
|
|
@@ -245,4 +276,10 @@ def download_one(job_id: str, index: int, kind: str) -> FileResponse:
|
|
| 245 |
if __name__ == "__main__":
|
| 246 |
import uvicorn
|
| 247 |
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
+
import base64
|
| 15 |
+
import hmac
|
| 16 |
import os
|
| 17 |
import shutil
|
| 18 |
import tempfile
|
|
|
|
| 21 |
import uuid
|
| 22 |
from pathlib import Path
|
| 23 |
|
| 24 |
+
from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile
|
| 25 |
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
| 26 |
|
| 27 |
import pipeline as pl
|
| 28 |
|
|
|
|
| 34 |
JOBS: dict[str, dict] = {}
|
| 35 |
LOCK = threading.Lock()
|
| 36 |
|
| 37 |
+
# インターネットに置くときは APP_PASSWORD を設定する。
|
| 38 |
+
# 未設定なら素通し(自分のPCで動かすときはこれで困らない)。
|
| 39 |
+
APP_USER = os.environ.get("APP_USER", "spin")
|
| 40 |
+
APP_PASSWORD = os.environ.get("APP_PASSWORD", "")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
@app.middleware("http")
|
| 44 |
+
async def require_password(request: Request, call_next):
|
| 45 |
+
if not APP_PASSWORD:
|
| 46 |
+
return await call_next(request)
|
| 47 |
+
|
| 48 |
+
header = request.headers.get("authorization", "")
|
| 49 |
+
if header.startswith("Basic "):
|
| 50 |
+
try:
|
| 51 |
+
user, _, password = base64.b64decode(header[6:]).decode("utf-8").partition(":")
|
| 52 |
+
# 文字列比較の時間差から答えが漏れないように compare_digest を使う
|
| 53 |
+
if hmac.compare_digest(user, APP_USER) and hmac.compare_digest(password, APP_PASSWORD):
|
| 54 |
+
return await call_next(request)
|
| 55 |
+
except Exception:
|
| 56 |
+
pass
|
| 57 |
+
|
| 58 |
+
return Response(
|
| 59 |
+
"パスワードが必要です。",
|
| 60 |
+
status_code=401,
|
| 61 |
+
headers={"WWW-Authenticate": 'Basic realm="spinthoughts"'},
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
|
| 65 |
def update(job_id: str, **fields) -> None:
|
| 66 |
with LOCK:
|
|
|
|
| 151 |
"gpu": pl.gpu_label(),
|
| 152 |
"defaultModel": pl.default_model_size(),
|
| 153 |
"tokenFromEnv": bool(os.environ.get("HF_TOKEN")),
|
| 154 |
+
"passwordSet": bool(APP_PASSWORD),
|
| 155 |
+
"hosted": bool(os.environ.get("HOSTED")),
|
| 156 |
"defaultPrompt": pl.DEFAULT_PROMPT,
|
| 157 |
})
|
| 158 |
|
|
|
|
| 276 |
if __name__ == "__main__":
|
| 277 |
import uvicorn
|
| 278 |
|
| 279 |
+
# 既定は 127.0.0.1(自分のPCからしか開けない)。
|
| 280 |
+
# コンテナで動かすときだけ HOST=0.0.0.0 を渡す。
|
| 281 |
+
uvicorn.run(
|
| 282 |
+
app,
|
| 283 |
+
host=os.environ.get("HOST", "127.0.0.1"),
|
| 284 |
+
port=int(os.environ.get("PORT", "8000")),
|
| 285 |
+
)
|
index.html
CHANGED
|
@@ -248,7 +248,7 @@
|
|
| 248 |
|
| 249 |
<header class="masthead">
|
| 250 |
<h1>録音文字起こし</h1>
|
| 251 |
-
<p>ZIPの中から一定の長さ以上の録音だけを選び、話者ごとに書き起こしてCSVにします。
|
| 252 |
<div class="chips" id="chips"></div>
|
| 253 |
</header>
|
| 254 |
|
|
@@ -339,6 +339,11 @@ let poller = null;
|
|
| 339 |
/* ---------- environment ---------- */
|
| 340 |
fetch("/environment").then(r => r.json()).then(env => {
|
| 341 |
$("prompt").value = env.defaultPrompt || "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
if (env.defaultModel) $("modelSize").value = env.defaultModel;
|
| 343 |
if (!env.gpu) {
|
| 344 |
$("modelHint").textContent =
|
|
@@ -354,7 +359,11 @@ fetch("/environment").then(r => r.json()).then(env => {
|
|
| 354 |
env.tokenFromEnv
|
| 355 |
? { text: "HF_TOKEN 設定済み", cls: "ok" }
|
| 356 |
: { text: "HF_TOKEN 未設定", cls: "" },
|
| 357 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
$("chips").innerHTML = chips
|
| 359 |
.map(c => `<span class="chip ${c.cls}">${c.text}</span>`)
|
| 360 |
.join("");
|
|
|
|
| 248 |
|
| 249 |
<header class="masthead">
|
| 250 |
<h1>録音文字起こし</h1>
|
| 251 |
+
<p id="lede">ZIPの中から一定の長さ以上の録音だけを選び、話者ごとに書き起こしてCSVにします。</p>
|
| 252 |
<div class="chips" id="chips"></div>
|
| 253 |
</header>
|
| 254 |
|
|
|
|
| 339 |
/* ---------- environment ---------- */
|
| 340 |
fetch("/environment").then(r => r.json()).then(env => {
|
| 341 |
$("prompt").value = env.defaultPrompt || "";
|
| 342 |
+
$("lede").textContent = env.hosted
|
| 343 |
+
? "ZIPの中から一定の長さ以上の録音だけを選び、話者ごとに書き起こしてCSVにします。"
|
| 344 |
+
+ "音声はサーバー上で処理され、処理が終わると消えます。"
|
| 345 |
+
: "ZIPの中から一定の長さ以上の録音だけを選び、話者ごとに書き起こしてCSVにします。"
|
| 346 |
+
+ "音声はこのPCの外に出ません。";
|
| 347 |
if (env.defaultModel) $("modelSize").value = env.defaultModel;
|
| 348 |
if (!env.gpu) {
|
| 349 |
$("modelHint").textContent =
|
|
|
|
| 359 |
env.tokenFromEnv
|
| 360 |
? { text: "HF_TOKEN 設定済み", cls: "ok" }
|
| 361 |
: { text: "HF_TOKEN 未設定", cls: "" },
|
| 362 |
+
// 公開先でパスワードを掛け忘れると、URLを知る全員が使えてしまう
|
| 363 |
+
env.hosted && !env.passwordSet
|
| 364 |
+
? { text: "パスワード未設定 — URLを知る人は誰でも使えます", cls: "bad" }
|
| 365 |
+
: null,
|
| 366 |
+
].filter(Boolean);
|
| 367 |
$("chips").innerHTML = chips
|
| 368 |
.map(c => `<span class="chip ${c.cls}">${c.text}</span>`)
|
| 369 |
.join("");
|
pipeline.py
CHANGED
|
@@ -50,9 +50,9 @@ MLX_MODELS = {
|
|
| 50 |
}
|
| 51 |
|
| 52 |
DEFAULT_PROMPT = (
|
| 53 |
-
"
|
| 54 |
-
"
|
| 55 |
-
"
|
| 56 |
)
|
| 57 |
|
| 58 |
|
|
|
|
| 50 |
}
|
| 51 |
|
| 52 |
DEFAULT_PROMPT = (
|
| 53 |
+
"商談の記録です。御社、弊社、お見積り、御見積、初期費用、月額、ランニングコスト、"
|
| 54 |
+
"導入、稟議、決裁、リード、ヒアリング、課題感、費用対効果、他社比較、相見積もり、"
|
| 55 |
+
"契約、締結、納期、トライアル、といった言葉が出てきます。"
|
| 56 |
)
|
| 57 |
|
| 58 |
|