From fd51349dc647139ab77b1ca2619379ce70267e9c Mon Sep 17 00:00:00 2001 From: Thomas Howe Date: Fri, 25 Sep 2026 18:46:11 -0400 Subject: [PATCH] fix(wtf_transcribe): resolve dialog mediatype instead of always defaulting dialog_mimetype() only read the legacy "mimetype" key, but the conserver's read-compat layer renames it to "mediatype" and modern adapters write "mediatype" directly, so the multipart upload to vfun always fell back to audio/wav regardless of the real format (MP3, Opus, etc.). Renamed to dialog_mediatype() with a resolution order: mediatype, legacy mimetype, guessed from the filename extension, else audio/wav default. Kept dialog_mimetype as a backward-compatible alias. Grepped conserver/links, common, and api for other dialog mimetype reads; the only other non-test hit is api/api.py's DialogEntry.mimetype field validator, which validates incoming legacy API input rather than reading an existing dialog for a content-type, so it was left alone. Co-Authored-By: Claude Opus 5.5 --- conserver/links/wtf_transcribe/__init__.py | 34 ++++++++++++++++--- .../test_wtf_transcribe_unit.py | 24 +++++++++++-- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/conserver/links/wtf_transcribe/__init__.py b/conserver/links/wtf_transcribe/__init__.py index 7323a35..685e83a 100644 --- a/conserver/links/wtf_transcribe/__init__.py +++ b/conserver/links/wtf_transcribe/__init__.py @@ -25,6 +25,7 @@ import json import logging +import mimetypes import requests from typing import Optional, Dict, Any @@ -180,8 +181,33 @@ def dialog_to_audio_binary(dialog, url_timeout): def dialog_filename(dialog, dialog_index): return dialog.get("filename", f"audio_{dialog_index}.wav") -def dialog_mimetype(dialog): - return dialog.get("mimetype", "audio/wav") +def dialog_mediatype(dialog): + """Resolve a dialog's media type. + + Prefers the modern ``mediatype`` field, then falls back to the legacy + ``mimetype`` field (in case a caller passes a pre-compat dialog), then + derives it from the dialog's filename extension, and finally defaults + to audio/wav. + """ + mediatype = dialog.get("mediatype") + if mediatype: + return mediatype + + mimetype = dialog.get("mimetype") + if mimetype: + return mimetype + + filename = dialog.get("filename") + if filename: + guessed_type, _ = mimetypes.guess_type(filename) + if guessed_type and (guessed_type.startswith("audio/") or guessed_type.startswith("video/")): + return guessed_type + + return "audio/wav" + + +# Backwards-compatible alias; prefer dialog_mediatype for new code. +dialog_mimetype = dialog_mediatype def build_vfun_headers(api_key): headers = {} @@ -202,8 +228,8 @@ def maybe_decode_double_encoded_json(response_json): def send_audio_to_vfun(audio_binary, dialog, dialog_index, vfun_server_url, api_key, diarize, language, vfun_timeout): filename = dialog_filename(dialog, dialog_index) - mimetype = dialog_mimetype(dialog) - files = {"file-binary": (filename, audio_binary, mimetype)} + mediatype = dialog_mediatype(dialog) + files = {"file-binary": (filename, audio_binary, mediatype)} headers = build_vfun_headers(api_key) data = build_vfun_data(diarize, language) response = requests.post( diff --git a/conserver/links/wtf_transcribe/test_wtf_transcribe_unit.py b/conserver/links/wtf_transcribe/test_wtf_transcribe_unit.py index 85e3e02..2102ca6 100644 --- a/conserver/links/wtf_transcribe/test_wtf_transcribe_unit.py +++ b/conserver/links/wtf_transcribe/test_wtf_transcribe_unit.py @@ -10,7 +10,7 @@ analysis_dialog_index, analysis_is_wtf_transcription, dialog_filename, - dialog_mimetype, + dialog_mediatype, dialog_to_audio_binary, dialog_to_binary, dialog_to_index, @@ -63,7 +63,27 @@ def test_analysis_and_dialog_helpers_cover_transcribed_and_recording_checks(): def test_file_and_remote_loaders_handle_success_and_failure(): assert is_file_url("file:///tmp/audio.wav") is True assert dialog_filename({}, 3) == "audio_3.wav" - assert dialog_mimetype({}) == "audio/wav" + assert dialog_mediatype({}) == "audio/wav" + + +def test_dialog_mediatype_resolution_order(): + # Modern adapters write "mediatype"; it takes priority. + assert dialog_mediatype({"mediatype": "audio/mpeg"}) == "audio/mpeg" + + # Legacy "mimetype" is honoured when "mediatype" is absent. + assert dialog_mediatype({"mimetype": "audio/mpeg"}) == "audio/mpeg" + + # "mediatype" wins over a legacy "mimetype" when both are present. + assert dialog_mediatype({"mediatype": "audio/mpeg", "mimetype": "audio/wav"}) == "audio/mpeg" + + # Neither field present: derive from the filename extension. + assert dialog_mediatype({"filename": "clip.ogg"}) == "audio/ogg" + + # A non-audio/video guess from the filename doesn't count; fall through to default. + assert dialog_mediatype({"filename": "clip.txt"}) == "audio/wav" + + # Nothing at all gives the audio/wav default. + assert dialog_mediatype({}) == "audio/wav" with patch("builtins.open", mock_open(read_data=b"file-bytes")): assert maybe_load_file_url("file:///tmp/audio.wav") == b"file-bytes"