Support whisper asr-webservice
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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", [])]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user