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 の手順と料金表に書き直し

Files changed (6) hide show
  1. .dockerignore +6 -0
  2. Dockerfile +35 -0
  3. README.md +131 -101
  4. app.py +40 -3
  5. index.html +11 -2
  6. 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
- # 録音文字起こし(Windows・ローカル完結)
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- 録音ZIPを投げると、指定秒数以上のファイルだけを文字起こしし、話者ごとに分けてCSVにするローカルWebアプリ。
4
- 音声・文字起こし結果ともに外部へ送信しない(モデルの初回ダウンロードのみ通信あり)。
5
 
6
- - 文字起こし: Whisper large-v3 系(Windows は faster-whisper、Mac の Apple Silicon は MLX
7
- - 話者分離: pyannote.audio 3.1
8
- - 費用: 0円。すべてオープンソース+ローカル実行
 
 
9
 
10
  ---
11
 
12
- ## 1. セットアップ
13
 
14
- ### ffmpeg
15
 
16
- PowerShell :
 
 
 
 
17
 
18
- ```powershell
19
- winget install Gyan.FFmpeg
20
- ```
21
 
22
- インストール直後は PATH に載っていないことが、このアプリは winget の置場も自分で探すのでそのまま動く。
 
 
 
23
 
24
- ### Python環境
 
25
 
26
- Python 3.12使う。フォルダの中で PowerShell を開き:
27
 
28
- ```powershell
29
- .\setup.ps1
30
- ```
31
 
32
- 仮想環境 `.venv` を作り、torch(CPU版)と残りのライブラリを入れる。数分かかる。
33
- `python` ではなく `.venv\Scripts\python.exe` を使う点に注意(PATH 上の python.exe は Store のスタブ)。
 
 
 
34
 
35
- ### HuggingFaceトークン話者分離必要
 
36
 
37
- 1. https://huggingface.co/join でアカウント作成(無料)
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
- クンは環境変数入れおくと毎回入力が不要になる。PowerShell で一度だけ:
44
 
45
- ```powershell
46
- setx HF_TOKEN "hf_xxxxxxxxxxxx"
 
47
  ```
48
 
49
- 設定後PowerShell開き直す
50
 
51
- **両方のページで同意しないと動かない。** ここを飛ばすとモデル読みみで失敗する
52
- 話者分離をオフれば、トークン無しでも文字起こしだけは使えるその場合は話者列が空
53
- (区間が無いことを誤認識と数えて全行に「要確認」が付かないようにしてある)。
 
 
 
 
 
 
54
 
55
  ---
56
 
57
- ## 2. 起動
58
 
59
- エクスプローラーで `start.cmd` をダブルクリックするブラウザく。
60
 
61
- コマンからする場合:
 
 
 
 
62
 
63
- ```powershell
64
- .\.venv\Scripts\python.exe app.py
65
- ```
66
 
67
- `127.0.0.1:8000` のみ待ち受けるので、同じPC以外からはアセスない
68
- 別のポートを使いたいときは `$env:PORT=8030` のよう指定する。
69
 
70
- 初回Whisper話者分離のモデルを取得するた数分かかる。2回目以降はキャッシュから読む
71
- (`C:\Users\<ユーザー名>\.cache\huggingface`)。
72
 
73
  ---
74
 
@@ -87,11 +112,11 @@ setx HF_TOKEN "hf_xxxxxxxxxxxx"
87
  ```
88
  文字起こし_<ID>/
89
  ├── 書き起こし/
90
- │ ├── 001_訪問_田中様.txt ← 人が読む用。そのまま録に貼れる
91
- │ └── 002_訪問_佐藤様.txt
92
  ├── データ/
93
- │ ├── 001_訪問_田中様.csv ← システム取り込み用
94
- │ └── 002_訪問_佐藤様.csv
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
- **このPCCPU処理になる。** NVIDIAGPUが無いため、文字起こしも者分離もCPUで回
160
- 精度の既定 `large-v3` ではく `large-v3-turbo` してあそのため。
161
- GPU搭載機で動かせば自動で `large-v3` が既定になり、速度も数倍になる。
162
 
163
- **処理時間の目安(このPC / Ryzen 5 7530U・CPU処理)**
164
- 実測: `large-v3-turbo`・話者分離オフで、合計91秒の録音3本がモデル読み込み込み102秒。
165
- モデル読み込み(起動後の最初の1回だけ40秒前後)を除くと、録音1秒あたり0.6〜0.7秒。
166
- つまり1時間の録音で文字起こし35〜45分程度。話者分離を入れると同程度の時間が上乗せされる。
167
- `large-v3` を選ぶとさら数倍かかる。長時間の録音は夜間にまとめて流すのが現実的
168
 
169
- **要配慮個人情報の扱い。** 介護の録音は個人情報保護法の要配慮個人情報にあたる可能性が高い。
170
- ローカル完結にしてあるはそのためだが、出力CSVの保存場所、保持期間、
171
- アクセス権限は運用側で決める必要がある。作業用の一時ファイルは処理後に削除している。
 
 
 
 
 
172
 
173
  ---
174
 
175
- ## 5. ファイル構成
176
 
177
  ```
178
- koe-okoshi/
179
- ├── app.py Webサーバー(アップロード・進捗・CSV出力)
180
  ├── pipeline.py ZIP展開 → 長さフィルタ → 文字起こし → 話者分離 → 統合
181
  ├── index.html 画面
182
- ├── setup.ps1 初回セットアップ
183
- ├── start.cmd
 
184
  ├── requirements.txt
185
  └── README.md
186
  ```
187
 
188
  処理の中身を変えたいときは `pipeline.py` を見る。
189
- たとえば `HALLUCINATION_PATTERNS` に自分の環境で出やすい誤認識を足る。
190
- `DEFAULT_PROMPT` に事業所名や利用者様呼称を足すと固有名詞の精度が上がる。
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  ---
193
 
194
- ## 6. うまくいかないとき
195
 
196
  | 症状 | 対処 |
197
  |---|---|
198
- | ffmpeg見つからない | `winget install Gyan.FFmpeg` を実行し、PowerShellを開き |
 
 
199
  | 話者分離モデルが読めない | HuggingFaceの2ページ両方で条件に同意したか、トークンが read 権限か確認 |
200
  | 日本語ファイル名が化ける | Windows製ZIPのCP932は自動判別済み。それでも化けるなら元のZIPの作り方を確認 |
201
- | 極端に遅い | 精度を `turbo` に落とす、または話者分離をオフにする |
202
  | 発話が検出されない | 録音の音量が小さい可能性。`ffmpeg -i 元ファイル -af loudnorm 正規化後.wav` で正規化してから再投入 |
203
- | ト8000使用中 | `$env:PORT=8030; .\.venv\Scripts\python.exe app.py` |
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
+ | SpaceBuild 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
- uvicorn.run(app, host="127.0.0.1", port=int(os.environ.get("PORT", "8000")))
 
 
 
 
 
 
 
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にします。音声はこのPCの外に出ません。</p>
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
- "デイサービスケアマネ居宅介護支援モニタリング、ADL清拭、"
55
- "利用者様ご家族、といった言葉が出てきます。"
56
  )
57
 
58
 
 
50
  }
51
 
52
  DEFAULT_PROMPT = (
53
+ "商談の記録です。御社弊社お見積り御見積初期費用月額ランニングコスト、"
54
+ "導入稟議決裁、リード、ヒアリング、課題感費用対効果他社比較、相見積もり、"
55
+ "契約締結納期、トライアル、といった言葉が出てきます。"
56
  )
57
 
58