Support whisper asr-webservice

This commit is contained in:
2026-07-27 09:53:03 -07:00
parent bd5593c3ed
commit fca9e874f3
5 changed files with 61 additions and 11 deletions
+13 -5
View File
@@ -9,9 +9,10 @@ The pipeline:
1. **acquire** — download audio/video via `yt-dlp` (or use a local video file)
2. **separate** — split vocals/instrumental with Demucs
3. **transcribe** — send the vocal stem to your own intranet OpenAI-compatible
Whisper server, as a sanity check that the supplied lyrics match the audio
and to locate vocal sections
3. **transcribe** — send the vocal stem to your own intranet Whisper server
(OpenAI-compatible, or [onerahmet/openai-whisper-asr-webservice](https://github.com/ahmetoner/whisper-asr-webservice)),
as a sanity check that the supplied lyrics match the audio and to locate
vocal sections
4. **align** — forced-align the *supplied* reference lyrics against the vocal
stem with wav2vec2 (torchaudio's MMS_FA) to get word timings
5. **pitch** — track pitch on the vocal stem with torchcrepe
@@ -45,8 +46,15 @@ $EDITOR config.toml # set whisper_url at minimum
```
See `config.example.toml` for every available field (`whisper_url`,
`whisper_api_key`, `whisper_model`, `output_dir`, `work_dir`, `creator`,
`demucs_model`, `crepe_model`, `device`, `bind_host`, `port`).
`whisper_api_key`, `whisper_model`, `whisper_api`, `output_dir`, `work_dir`,
`creator`, `demucs_model`, `crepe_model`, `device`, `bind_host`, `port`).
`whisper_api` selects the wire format: `"openai"` (default) speaks the
OpenAI-compatible `POST {whisper_url}/audio/transcriptions` API; `"asr-webservice"`
speaks [onerahmet/openai-whisper-asr-webservice](https://github.com/ahmetoner/whisper-asr-webservice)'s
`POST {whisper_url}/asr` API instead (point `whisper_url` at that server's base
URL with no `/v1` suffix — `whisper_model`/`whisper_api_key` are unused for
this backend, since the model is fixed by how that server was started).
## Docker
+2
View File
@@ -1,6 +1,8 @@
whisper_url = "http://whisper.intranet:9000/v1"
# whisper_api_key = ""
# whisper_model = "whisper-1"
# whisper_api = "openai" # or "asr-webservice" for onerahmet/openai-whisper-asr-webservice
# (set whisper_url without the /v1 suffix, e.g. "http://whisper.intranet:9000")
output_dir = "/data/output"
work_dir = "/data/work"
creator = "brad"
+1
View File
@@ -9,6 +9,7 @@ class Config:
whisper_url: str
whisper_api_key: str | None = None
whisper_model: str = "whisper-1"
whisper_api: str = "openai"
output_dir: Path = Path("output")
work_dir: Path = Path("work")
creator: str = "karaoke-maker"
+23 -6
View File
@@ -19,18 +19,35 @@ def transcribe(cfg: Config, audio_path: Path, language: str | None = None,
headers = {}
if cfg.whisper_api_key:
headers["Authorization"] = f"Bearer {cfg.whisper_api_key}"
data = {"model": cfg.whisper_model, "response_format": "verbose_json"}
iso_language = None
if language:
entry = LANGUAGES.get(language.lower())
data["language"] = entry.iso if entry else language
iso_language = entry.iso if entry else language
own = client is None
client = client or httpx.Client(timeout=600)
try:
with audio_path.open("rb") as f:
resp = client.post(
f"{cfg.whisper_url.rstrip('/')}/audio/transcriptions",
headers=headers, data=data,
files={"file": (audio_path.name, f, "audio/wav")})
if cfg.whisper_api == "asr-webservice":
# onerahmet/openai-whisper-asr-webservice: fixed /asr endpoint,
# file field is "audio_file", and language/output are query
# params rather than form fields - there is no "model" param,
# the model is fixed by how that server was started.
params = {"output": "json"}
if iso_language:
params["language"] = iso_language
resp = client.post(
f"{cfg.whisper_url.rstrip('/')}/asr",
headers=headers, params=params,
files={"audio_file": (audio_path.name, f, "audio/wav")})
else:
data = {"model": cfg.whisper_model, "response_format": "verbose_json"}
if iso_language:
data["language"] = iso_language
resp = client.post(
f"{cfg.whisper_url.rstrip('/')}/audio/transcriptions",
headers=headers, data=data,
files={"file": (audio_path.name, f, "audio/wav")})
resp.raise_for_status()
return [TranscriptSegment(s["start"], s["end"], s["text"].strip())
for s in resp.json().get("segments", [])]
+22
View File
@@ -36,3 +36,25 @@ def test_transcribe_maps_language_to_iso_code(tmp_path):
transcribe(cfg, audio, language="Japanese", client=client)
assert 'name="language"' in seen["body"]
assert "\r\n\r\nja\r\n" in seen["body"]
def test_transcribe_asr_webservice_hits_fixed_asr_endpoint(tmp_path):
audio = tmp_path / "v.wav"
audio.write_bytes(b"RIFF")
seen = {}
def handler(request: httpx.Request) -> httpx.Response:
seen["url"] = str(request.url)
seen["body"] = request.read().decode("utf-8", errors="replace")
return httpx.Response(200, json={
"text": "hello world",
"segments": [{"start": 0.5, "end": 2.0, "text": " hello world"}],
})
client = httpx.Client(transport=httpx.MockTransport(handler))
cfg = Config(whisper_url="http://w:9000", whisper_api="asr-webservice")
segs = transcribe(cfg, audio, language="Japanese", client=client)
assert segs == [TranscriptSegment(start=0.5, end=2.0, text="hello world")]
assert seen["url"] == "http://w:9000/asr?output=json&language=ja"
assert 'name="audio_file"' in seen["body"]
assert 'name="model"' not in seen["body"]