davinci-resolve-mcp 2.65.0 → 2.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +1 -1
- package/docs/guides/media-analysis-guide.md +40 -11
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/media_analysis.py +171 -86
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
|
|
4
4
|
|
|
5
|
+
## What's New in v2.66.0
|
|
6
|
+
|
|
7
|
+
Generalizes the HTTP transcription backend introduced in v2.65.0 into a
|
|
8
|
+
configuration-driven registry, so additional local, network, or cloud-backed
|
|
9
|
+
adapters no longer require MCP source changes. Contributed in PR #97 by
|
|
10
|
+
@double2tea.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **Pluggable HTTP transcription providers** (PR #97, @double2tea) — the
|
|
15
|
+
MLX-specific router backend is replaced by an ordered registry of HTTP
|
|
16
|
+
transcription providers registered via
|
|
17
|
+
`DAVINCI_RESOLVE_MCP_TRANSCRIPTION_HTTP_PROVIDERS` (a JSON array). Each entry
|
|
18
|
+
requires `id` and `base_url`; optional adapter fields cover `label`, `model`,
|
|
19
|
+
`health_path`, `transcribe_path`, `health_field`, `health_value`, `headers`,
|
|
20
|
+
`request_body`, `field_map`, and `response_field`. Configured providers are
|
|
21
|
+
selected as stable `http:<id>` backend names and preferred in transcription
|
|
22
|
+
capability ordering. Auth headers are sent on health and transcription
|
|
23
|
+
requests but kept out of capability reports, and malformed configuration
|
|
24
|
+
fails fast. Response handling now accepts a transcript object, a JSON-encoded
|
|
25
|
+
transcript string, or plain text under the configured `response_field`.
|
|
26
|
+
Audiobox is documented as one adapter example rather than a core requirement.
|
|
27
|
+
|
|
28
|
+
### Removed
|
|
29
|
+
|
|
30
|
+
- The `DAVINCI_RESOLVE_MCP_MLX_AUDIO_URL` / `DAVINCI_RESOLVE_MCP_MLX_AUDIO_MODEL`
|
|
31
|
+
environment variables added in v2.65.0 are superseded by the generic provider
|
|
32
|
+
registry above. To keep an Audiobox/MLX router, register it as a provider:
|
|
33
|
+
`[{"id":"audiobox-local","base_url":"http://127.0.0.1:8000","request_body":{"provider":"mlx"}}]`.
|
|
34
|
+
|
|
35
|
+
### Validation
|
|
36
|
+
|
|
37
|
+
- `tests/test_media_analysis.py` and the analysis caps/runs/store suites pass
|
|
38
|
+
(153 on the merged tree); static checks and drift guards pass.
|
|
39
|
+
- No DaVinci Resolve scripting behavior changed: the change is confined to the
|
|
40
|
+
stdlib HTTP transcription path and is gated behind an env var. Live Resolve
|
|
41
|
+
validation not required.
|
|
42
|
+
|
|
5
43
|
## What's New in v2.65.0
|
|
6
44
|
|
|
7
45
|
Bundles two community contributions from @double2tea: an optional HTTP
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
|
@@ -79,21 +79,50 @@ which ffmpeg && ffmpeg -version 2>&1 | head -1
|
|
|
79
79
|
which whisper 2>/dev/null || which whisper-cpp 2>/dev/null || python3 -c "import whisper" 2>/dev/null
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
into the Resolve MCP Python environment
|
|
82
|
+
One or more HTTP transcription adapters can be registered without installing a
|
|
83
|
+
transcription module into the Resolve MCP Python environment. Providers are
|
|
84
|
+
selected as `http:<id>`, in configuration order:
|
|
84
85
|
|
|
85
86
|
```bash
|
|
86
|
-
export
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
export DAVINCI_RESOLVE_MCP_TRANSCRIPTION_HTTP_PROVIDERS='[
|
|
88
|
+
{
|
|
89
|
+
"id": "audiobox-local",
|
|
90
|
+
"label": "Audiobox local",
|
|
91
|
+
"base_url": "http://127.0.0.1:8000",
|
|
92
|
+
"model": "mlx-community/Qwen3-ASR-1.7B-8bit",
|
|
93
|
+
"request_body": {"provider": "mlx"}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "studio-asr",
|
|
97
|
+
"label": "Studio ASR",
|
|
98
|
+
"base_url": "https://asr.example.com",
|
|
99
|
+
"headers": {"Authorization": "Bearer replace-me"},
|
|
100
|
+
"health_path": "/ready",
|
|
101
|
+
"transcribe_path": "/v1/transcribe",
|
|
102
|
+
"field_map": {"audio": "input_path"},
|
|
103
|
+
"response_field": "result"
|
|
104
|
+
}
|
|
105
|
+
]'
|
|
89
106
|
```
|
|
90
107
|
|
|
91
|
-
|
|
92
|
-
`
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
Each provider requires `id` and `base_url`. Optional fields are `label`,
|
|
109
|
+
`model`, `health_path`, `transcribe_path`, `health_field`, `health_value`,
|
|
110
|
+
`headers`, `request_body`, `field_map`, and `response_field`. Authentication
|
|
111
|
+
headers are used for health and transcription requests but are omitted from
|
|
112
|
+
capability reports.
|
|
113
|
+
|
|
114
|
+
The default adapter contract uses `GET /health` with `{"status":"ok"}` and
|
|
115
|
+
`POST /stt`. Its JSON request includes `audio`, `output_path`, `format`,
|
|
116
|
+
`verbose`, `allow_download`, and optional `model` and `language` fields.
|
|
117
|
+
`request_body` adds adapter-specific values, while `field_map` renames standard
|
|
118
|
+
request fields. The response may return a transcript object, a JSON-encoded
|
|
119
|
+
transcript string, or plain text under `response_field` (default
|
|
120
|
+
`transcript`). The normalized transcript object uses `text` and `segments`.
|
|
121
|
+
|
|
122
|
+
[Audiobox MLX Audio Router](https://github.com/double2tea/Audiobox) is one
|
|
123
|
+
adapter implementation, not a built-in backend requirement. Other local,
|
|
124
|
+
network, or cloud-backed adapters can be added through configuration without
|
|
125
|
+
changing MCP source code. Model download behavior follows the existing
|
|
97
126
|
`allow_model_download` transcription option.
|
|
98
127
|
|
|
99
128
|
FFprobe is required. If missing:
|
package/install.py
CHANGED
|
@@ -36,7 +36,7 @@ from src.utils.update_check import (
|
|
|
36
36
|
|
|
37
37
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
|
-
VERSION = "2.
|
|
39
|
+
VERSION = "2.66.0"
|
|
40
40
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
41
41
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
42
42
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -81,7 +81,7 @@ if not logging.getLogger().handlers:
|
|
|
81
81
|
handlers=[logging.StreamHandler()],
|
|
82
82
|
)
|
|
83
83
|
|
|
84
|
-
VERSION = "2.
|
|
84
|
+
VERSION = "2.66.0"
|
|
85
85
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
86
86
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
87
87
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
|
@@ -23,6 +23,7 @@ import sys
|
|
|
23
23
|
import threading
|
|
24
24
|
import time
|
|
25
25
|
import urllib.error
|
|
26
|
+
import urllib.parse
|
|
26
27
|
import urllib.request
|
|
27
28
|
from datetime import datetime, timezone
|
|
28
29
|
from pathlib import Path
|
|
@@ -357,8 +358,8 @@ ANALYSIS_DIR_NAME = "davinci-resolve-mcp-analysis"
|
|
|
357
358
|
HIDDEN_ANALYSIS_DIR_NAME = ".davinci-resolve-mcp-analysis"
|
|
358
359
|
ANALYSIS_VERSION = "0.2"
|
|
359
360
|
|
|
360
|
-
|
|
361
|
-
|
|
361
|
+
HTTP_TRANSCRIPTION_PROVIDERS_ENV = "DAVINCI_RESOLVE_MCP_TRANSCRIPTION_HTTP_PROVIDERS"
|
|
362
|
+
HTTP_TRANSCRIPTION_BACKEND_PREFIX = "http:"
|
|
362
363
|
ANALYSIS_INDEX_FILENAME = "index.sqlite"
|
|
363
364
|
ANALYSIS_REGISTRY_FILENAME = "analysis_registry.json"
|
|
364
365
|
ANALYSIS_INDEX_SCHEMA_VERSION = 1
|
|
@@ -1611,46 +1612,112 @@ def install_plan_for(tool_name: str, platform_id: Optional[str] = None) -> Dict[
|
|
|
1611
1612
|
}
|
|
1612
1613
|
|
|
1613
1614
|
|
|
1614
|
-
def
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
"configured": False,
|
|
1621
|
-
"url_env": MLX_AUDIO_ROUTER_URL_ENV,
|
|
1622
|
-
"model_env": MLX_AUDIO_ROUTER_MODEL_ENV,
|
|
1623
|
-
}
|
|
1624
|
-
if not router_url.startswith(("http://", "https://")):
|
|
1625
|
-
return {
|
|
1626
|
-
"available": False,
|
|
1627
|
-
"configured": True,
|
|
1628
|
-
"url": router_url,
|
|
1629
|
-
"model": model,
|
|
1630
|
-
"error": f"{MLX_AUDIO_ROUTER_URL_ENV} must use http:// or https://",
|
|
1631
|
-
}
|
|
1615
|
+
def _http_provider_endpoint(provider: Dict[str, Any], path_key: str) -> str:
|
|
1616
|
+
path = str(provider[path_key]).strip()
|
|
1617
|
+
if path.startswith(("http://", "https://")):
|
|
1618
|
+
return path
|
|
1619
|
+
return f"{provider['base_url']}/{path.lstrip('/')}"
|
|
1620
|
+
|
|
1632
1621
|
|
|
1622
|
+
def _load_http_transcription_providers(env: Dict[str, str]) -> Tuple[List[Dict[str, Any]], Optional[str]]:
|
|
1623
|
+
raw = (env.get(HTTP_TRANSCRIPTION_PROVIDERS_ENV) or "").strip()
|
|
1624
|
+
if not raw:
|
|
1625
|
+
return [], None
|
|
1633
1626
|
try:
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1627
|
+
entries = json.loads(raw)
|
|
1628
|
+
except json.JSONDecodeError as exc:
|
|
1629
|
+
return [], f"{HTTP_TRANSCRIPTION_PROVIDERS_ENV} is not valid JSON: {exc}"
|
|
1630
|
+
if not isinstance(entries, list):
|
|
1631
|
+
return [], f"{HTTP_TRANSCRIPTION_PROVIDERS_ENV} must be a JSON array"
|
|
1632
|
+
|
|
1633
|
+
providers: List[Dict[str, Any]] = []
|
|
1634
|
+
seen_ids = set()
|
|
1635
|
+
for index, entry in enumerate(entries):
|
|
1636
|
+
if not isinstance(entry, dict):
|
|
1637
|
+
return [], f"HTTP transcription provider at index {index} must be an object"
|
|
1638
|
+
provider_id = str(entry.get("id") or "").strip()
|
|
1639
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}", provider_id):
|
|
1640
|
+
return [], f"HTTP transcription provider at index {index} has an invalid id"
|
|
1641
|
+
if provider_id in seen_ids:
|
|
1642
|
+
return [], f"Duplicate HTTP transcription provider id: {provider_id}"
|
|
1643
|
+
seen_ids.add(provider_id)
|
|
1644
|
+
|
|
1645
|
+
base_url = str(entry.get("base_url") or "").strip().rstrip("/")
|
|
1646
|
+
parsed_url = urllib.parse.urlparse(base_url)
|
|
1647
|
+
if parsed_url.scheme not in {"http", "https"} or not parsed_url.netloc:
|
|
1648
|
+
return [], f"HTTP transcription provider '{provider_id}' base_url must use http:// or https://"
|
|
1649
|
+
headers = entry.get("headers") or {}
|
|
1650
|
+
request_body = entry.get("request_body") or {}
|
|
1651
|
+
field_map = entry.get("field_map") or {}
|
|
1652
|
+
if not isinstance(headers, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in headers.items()):
|
|
1653
|
+
return [], f"HTTP transcription provider '{provider_id}' headers must be a string map"
|
|
1654
|
+
if not isinstance(request_body, dict):
|
|
1655
|
+
return [], f"HTTP transcription provider '{provider_id}' request_body must be an object"
|
|
1656
|
+
if not isinstance(field_map, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in field_map.items()):
|
|
1657
|
+
return [], f"HTTP transcription provider '{provider_id}' field_map must be a string map"
|
|
1658
|
+
|
|
1659
|
+
label = str(entry.get("label") or provider_id).strip() or provider_id
|
|
1660
|
+
providers.append({
|
|
1661
|
+
"id": provider_id,
|
|
1662
|
+
"label": label,
|
|
1663
|
+
"base_url": base_url,
|
|
1664
|
+
"model": str(entry.get("model") or "").strip() or None,
|
|
1665
|
+
"health_path": str(entry.get("health_path") or "/health").strip(),
|
|
1666
|
+
"transcribe_path": str(entry.get("transcribe_path") or "/stt").strip(),
|
|
1667
|
+
"health_field": str(entry.get("health_field") or "status").strip(),
|
|
1668
|
+
"health_value": entry.get("health_value", "ok"),
|
|
1669
|
+
"response_field": str(entry.get("response_field", "transcript")).strip(),
|
|
1670
|
+
"headers": dict(headers),
|
|
1671
|
+
"request_body": dict(request_body),
|
|
1672
|
+
"field_map": dict(field_map),
|
|
1673
|
+
})
|
|
1674
|
+
return providers, None
|
|
1675
|
+
|
|
1676
|
+
|
|
1677
|
+
def _detect_http_transcription_providers(env: Dict[str, str]) -> Dict[str, Any]:
|
|
1678
|
+
providers, config_error = _load_http_transcription_providers(env)
|
|
1679
|
+
if config_error:
|
|
1640
1680
|
return {
|
|
1641
1681
|
"available": False,
|
|
1642
1682
|
"configured": True,
|
|
1643
|
-
"
|
|
1644
|
-
"
|
|
1645
|
-
"
|
|
1683
|
+
"config_env": HTTP_TRANSCRIPTION_PROVIDERS_ENV,
|
|
1684
|
+
"error": config_error,
|
|
1685
|
+
"providers": [],
|
|
1646
1686
|
}
|
|
1647
1687
|
|
|
1688
|
+
detected = []
|
|
1689
|
+
for provider in providers:
|
|
1690
|
+
public = {
|
|
1691
|
+
"id": provider["id"],
|
|
1692
|
+
"label": provider["label"],
|
|
1693
|
+
"url": provider["base_url"],
|
|
1694
|
+
"model": provider["model"],
|
|
1695
|
+
"backend": f"{HTTP_TRANSCRIPTION_BACKEND_PREFIX}{provider['id']}",
|
|
1696
|
+
}
|
|
1697
|
+
try:
|
|
1698
|
+
request = urllib.request.Request(
|
|
1699
|
+
_http_provider_endpoint(provider, "health_path"),
|
|
1700
|
+
headers=provider["headers"],
|
|
1701
|
+
method="GET",
|
|
1702
|
+
)
|
|
1703
|
+
with urllib.request.urlopen(request, timeout=1.0) as response:
|
|
1704
|
+
payload = json.loads(response.read().decode("utf-8"))
|
|
1705
|
+
if not isinstance(payload, dict):
|
|
1706
|
+
raise ValueError("health response must be a JSON object")
|
|
1707
|
+
if payload.get(provider["health_field"]) != provider["health_value"]:
|
|
1708
|
+
raise ValueError(
|
|
1709
|
+
f"health field '{provider['health_field']}' did not equal the configured ready value"
|
|
1710
|
+
)
|
|
1711
|
+
public.update({"available": True, "api_version": payload.get("api_version")})
|
|
1712
|
+
except (OSError, ValueError) as exc:
|
|
1713
|
+
public.update({"available": False, "error": str(exc)})
|
|
1714
|
+
detected.append(public)
|
|
1715
|
+
|
|
1648
1716
|
return {
|
|
1649
|
-
"available":
|
|
1650
|
-
"configured":
|
|
1651
|
-
"
|
|
1652
|
-
"
|
|
1653
|
-
"api_version": payload.get("api_version"),
|
|
1717
|
+
"available": any(provider["available"] for provider in detected),
|
|
1718
|
+
"configured": bool(providers),
|
|
1719
|
+
"config_env": HTTP_TRANSCRIPTION_PROVIDERS_ENV,
|
|
1720
|
+
"providers": detected,
|
|
1654
1721
|
}
|
|
1655
1722
|
|
|
1656
1723
|
|
|
@@ -1666,7 +1733,7 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
|
|
1666
1733
|
mlx_whisper = importlib.util.find_spec("mlx_whisper") is not None
|
|
1667
1734
|
cv2 = importlib.util.find_spec("cv2") is not None
|
|
1668
1735
|
provider = env.get("DAVINCI_RESOLVE_MCP_VISION_PROVIDER")
|
|
1669
|
-
|
|
1736
|
+
http_transcription = _detect_http_transcription_providers(env)
|
|
1670
1737
|
|
|
1671
1738
|
sync_events = detect_sync_event_capabilities()
|
|
1672
1739
|
|
|
@@ -1702,7 +1769,7 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
|
|
1702
1769
|
"whisper_cli": _tool_entry("whisper_cli", bool(whisper_cli), {"path": whisper_cli}),
|
|
1703
1770
|
"whisper_cpp": _tool_entry("whisper_cpp", bool(whisper_cpp), {"path": whisper_cpp}),
|
|
1704
1771
|
"mlx_whisper": _tool_entry("mlx_whisper", bool(mlx_whisper), {"python_module": "mlx_whisper"}),
|
|
1705
|
-
"
|
|
1772
|
+
"http_transcription": http_transcription,
|
|
1706
1773
|
"opencv": _tool_entry("opencv", bool(cv2), {"python_module": "cv2"}),
|
|
1707
1774
|
"ollama_embeddings": _tool_entry(
|
|
1708
1775
|
"ollama_embeddings",
|
|
@@ -1722,10 +1789,13 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
|
|
1722
1789
|
},
|
|
1723
1790
|
"embeddings": embedding_caps,
|
|
1724
1791
|
"transcription": {
|
|
1725
|
-
"available": bool(
|
|
1792
|
+
"available": bool(http_transcription["available"] or whisper_cli or whisper_cpp or mlx_whisper),
|
|
1726
1793
|
"backends": [
|
|
1794
|
+
provider["backend"]
|
|
1795
|
+
for provider in http_transcription["providers"]
|
|
1796
|
+
if provider["available"]
|
|
1797
|
+
] + [
|
|
1727
1798
|
name for name, available in (
|
|
1728
|
-
("mlx_audio_router", mlx_audio_router["available"]),
|
|
1729
1799
|
("whisper_cli", bool(whisper_cli)),
|
|
1730
1800
|
("whisper_cpp", bool(whisper_cpp)),
|
|
1731
1801
|
("mlx_whisper", bool(mlx_whisper)),
|
|
@@ -1781,7 +1851,7 @@ def install_guidance(capabilities: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
1781
1851
|
missing["transcription"] = {
|
|
1782
1852
|
"required_for": ["transcription analysis", "default Resolve media analysis"],
|
|
1783
1853
|
"options": [
|
|
1784
|
-
f"Configure
|
|
1854
|
+
f"Configure one or more HTTP providers with {HTTP_TRANSCRIPTION_PROVIDERS_ENV}",
|
|
1785
1855
|
"Install/configure whisper CLI",
|
|
1786
1856
|
"Install/configure whisper-cpp",
|
|
1787
1857
|
"Install mlx-whisper on supported Apple Silicon systems",
|
|
@@ -3986,33 +4056,17 @@ def _transcribe_with_mlx_whisper(path: str, artifacts: Dict[str, Any], transcrip
|
|
|
3986
4056
|
return payload
|
|
3987
4057
|
|
|
3988
4058
|
|
|
3989
|
-
def
|
|
4059
|
+
def _transcribe_with_http_provider(
|
|
3990
4060
|
path: str,
|
|
3991
4061
|
artifacts: Dict[str, Any],
|
|
3992
4062
|
transcription: Dict[str, Any],
|
|
4063
|
+
provider: Dict[str, Any],
|
|
3993
4064
|
) -> Dict[str, Any]:
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
or os.environ.get(MLX_AUDIO_ROUTER_URL_ENV)
|
|
3997
|
-
or ""
|
|
3998
|
-
).strip().rstrip("/")
|
|
3999
|
-
if not router_url:
|
|
4000
|
-
return {
|
|
4001
|
-
"success": False,
|
|
4002
|
-
"status": "skipped",
|
|
4003
|
-
"backend": "mlx_audio_router",
|
|
4004
|
-
"reason": f"Set {MLX_AUDIO_ROUTER_URL_ENV} to the MLX Audio Router base URL.",
|
|
4005
|
-
}
|
|
4006
|
-
|
|
4007
|
-
model = str(
|
|
4008
|
-
transcription.get("model")
|
|
4009
|
-
or os.environ.get(MLX_AUDIO_ROUTER_MODEL_ENV)
|
|
4010
|
-
or ""
|
|
4011
|
-
).strip()
|
|
4065
|
+
backend = f"{HTTP_TRANSCRIPTION_BACKEND_PREFIX}{provider['id']}"
|
|
4066
|
+
model = str(transcription.get("model") or provider.get("model") or "").strip()
|
|
4012
4067
|
transcript_json = artifacts.get("transcript_json") or artifacts["analysis_json"]
|
|
4013
4068
|
output_base = os.path.splitext(transcript_json)[0]
|
|
4014
|
-
|
|
4015
|
-
"provider": "mlx",
|
|
4069
|
+
canonical_payload = {
|
|
4016
4070
|
"audio": path,
|
|
4017
4071
|
"output_path": output_base,
|
|
4018
4072
|
"format": "json",
|
|
@@ -4020,11 +4074,19 @@ def _transcribe_with_mlx_audio_router(
|
|
|
4020
4074
|
"allow_download": _coerce_bool(transcription.get("allow_model_download"), default=False),
|
|
4021
4075
|
}
|
|
4022
4076
|
if model:
|
|
4023
|
-
|
|
4077
|
+
canonical_payload["model"] = model
|
|
4078
|
+
if transcription.get("language"):
|
|
4079
|
+
canonical_payload["language"] = transcription["language"]
|
|
4080
|
+
request_payload = dict(provider.get("request_body") or {})
|
|
4081
|
+
field_map = provider.get("field_map") or {}
|
|
4082
|
+
for key, value in canonical_payload.items():
|
|
4083
|
+
request_payload[field_map.get(key, key)] = value
|
|
4084
|
+
headers = {"Content-Type": "application/json"}
|
|
4085
|
+
headers.update(provider.get("headers") or {})
|
|
4024
4086
|
request = urllib.request.Request(
|
|
4025
|
-
|
|
4087
|
+
_http_provider_endpoint(provider, "transcribe_path"),
|
|
4026
4088
|
data=json.dumps(request_payload).encode("utf-8"),
|
|
4027
|
-
headers=
|
|
4089
|
+
headers=headers,
|
|
4028
4090
|
method="POST",
|
|
4029
4091
|
)
|
|
4030
4092
|
try:
|
|
@@ -4034,41 +4096,46 @@ def _transcribe_with_mlx_audio_router(
|
|
|
4034
4096
|
detail = exc.read().decode("utf-8", errors="replace").strip()
|
|
4035
4097
|
return {
|
|
4036
4098
|
"success": False,
|
|
4037
|
-
"backend":
|
|
4038
|
-
"
|
|
4099
|
+
"backend": backend,
|
|
4100
|
+
"provider": provider["label"],
|
|
4101
|
+
"error": detail or f"HTTP transcription provider returned HTTP {exc.code}",
|
|
4039
4102
|
}
|
|
4040
4103
|
except (OSError, ValueError) as exc:
|
|
4041
|
-
return {"success": False, "backend": "
|
|
4104
|
+
return {"success": False, "backend": backend, "provider": provider["label"], "error": str(exc)}
|
|
4042
4105
|
|
|
4043
4106
|
if not isinstance(response_payload, dict):
|
|
4044
4107
|
return {
|
|
4045
4108
|
"success": False,
|
|
4046
|
-
"backend":
|
|
4047
|
-
"
|
|
4048
|
-
|
|
4049
|
-
raw_transcript = response_payload.get("transcript")
|
|
4050
|
-
if not isinstance(raw_transcript, str) or not raw_transcript.strip():
|
|
4051
|
-
return {
|
|
4052
|
-
"success": False,
|
|
4053
|
-
"backend": "mlx_audio_router",
|
|
4054
|
-
"error": "MLX Audio Router response did not include a JSON transcript.",
|
|
4109
|
+
"backend": backend,
|
|
4110
|
+
"provider": provider["label"],
|
|
4111
|
+
"error": "HTTP transcription response must be a JSON object.",
|
|
4055
4112
|
}
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
except json.JSONDecodeError as exc:
|
|
4113
|
+
response_field = provider.get("response_field")
|
|
4114
|
+
if response_field and response_field not in response_payload:
|
|
4059
4115
|
return {
|
|
4060
4116
|
"success": False,
|
|
4061
|
-
"backend":
|
|
4062
|
-
"
|
|
4117
|
+
"backend": backend,
|
|
4118
|
+
"provider": provider["label"],
|
|
4119
|
+
"error": f"HTTP transcription response did not include '{response_field}'.",
|
|
4063
4120
|
}
|
|
4121
|
+
raw_transcript = response_payload[response_field] if response_field else response_payload
|
|
4122
|
+
if isinstance(raw_transcript, str):
|
|
4123
|
+
try:
|
|
4124
|
+
raw = json.loads(raw_transcript)
|
|
4125
|
+
except json.JSONDecodeError:
|
|
4126
|
+
raw = {"text": raw_transcript, "segments": []}
|
|
4127
|
+
else:
|
|
4128
|
+
raw = raw_transcript
|
|
4064
4129
|
if not isinstance(raw, dict):
|
|
4065
4130
|
return {
|
|
4066
4131
|
"success": False,
|
|
4067
|
-
"backend":
|
|
4068
|
-
"
|
|
4132
|
+
"backend": backend,
|
|
4133
|
+
"provider": provider["label"],
|
|
4134
|
+
"error": "HTTP transcription payload must resolve to a JSON object or text string.",
|
|
4069
4135
|
}
|
|
4070
4136
|
|
|
4071
|
-
payload = _normalize_transcript_payload(raw,
|
|
4137
|
+
payload = _normalize_transcript_payload(raw, backend, transcription.get("language"))
|
|
4138
|
+
payload["provider"] = provider["label"]
|
|
4072
4139
|
if response_payload.get("model") or model:
|
|
4073
4140
|
payload["model"] = response_payload.get("model") or model
|
|
4074
4141
|
_write_transcript_artifacts(payload, artifacts)
|
|
@@ -4116,8 +4183,20 @@ def _transcribe(path: str, artifacts: Dict[str, Any], options: Dict[str, Any], c
|
|
|
4116
4183
|
payload = {"success": True, "backend": backend, "language": transcription.get("language", "unknown"), "segments": segments, "text": " ".join(s.get("text", "") for s in segments)}
|
|
4117
4184
|
_write_transcript_artifacts(payload, artifacts)
|
|
4118
4185
|
return payload
|
|
4119
|
-
if backend
|
|
4120
|
-
|
|
4186
|
+
if isinstance(backend, str) and backend.startswith(HTTP_TRANSCRIPTION_BACKEND_PREFIX):
|
|
4187
|
+
providers, config_error = _load_http_transcription_providers(os.environ)
|
|
4188
|
+
if config_error:
|
|
4189
|
+
return {"success": False, "backend": backend, "error": config_error}
|
|
4190
|
+
provider_id = backend[len(HTTP_TRANSCRIPTION_BACKEND_PREFIX):]
|
|
4191
|
+
provider = next((item for item in providers if item["id"] == provider_id), None)
|
|
4192
|
+
if provider is None:
|
|
4193
|
+
return {
|
|
4194
|
+
"success": False,
|
|
4195
|
+
"status": "skipped",
|
|
4196
|
+
"backend": backend,
|
|
4197
|
+
"reason": f"HTTP transcription provider '{provider_id}' is not configured.",
|
|
4198
|
+
}
|
|
4199
|
+
return _transcribe_with_http_provider(path, artifacts, transcription, provider)
|
|
4121
4200
|
if backend in {"whisper_cli", "mlx_whisper"}:
|
|
4122
4201
|
if not _coerce_bool(transcription.get("allow_model_download"), default=False):
|
|
4123
4202
|
return {
|
|
@@ -4159,7 +4238,13 @@ def _transcribe(path: str, artifacts: Dict[str, Any], options: Dict[str, Any], c
|
|
|
4159
4238
|
# original branches below so behaviour stays identical for those.
|
|
4160
4239
|
if result is not None and result.get("status") != "fallthrough":
|
|
4161
4240
|
return result
|
|
4162
|
-
if
|
|
4241
|
+
if (
|
|
4242
|
+
backend in {"mock", "local_mock", "whisper_cli", "mlx_whisper"}
|
|
4243
|
+
or (
|
|
4244
|
+
isinstance(backend, str)
|
|
4245
|
+
and backend.startswith(HTTP_TRANSCRIPTION_BACKEND_PREFIX)
|
|
4246
|
+
)
|
|
4247
|
+
):
|
|
4163
4248
|
return result if result is not None else {"success": False, "backend": backend}
|
|
4164
4249
|
elif backend == "whisper_cpp":
|
|
4165
4250
|
if not transcription.get("model_path"):
|