mercury-agent 0.5.0 → 0.5.2
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/README.md +452 -451
- package/container/Dockerfile +127 -127
- package/container/Dockerfile.base +109 -109
- package/container/Dockerfile.power +17 -17
- package/container/agent-package.json +8 -8
- package/container/build.sh +54 -54
- package/docs/ROADMAP.md +3 -4
- package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
- package/docs/auth/dashboard.md +28 -28
- package/docs/auth/overview.md +109 -109
- package/docs/auth/whatsapp.md +173 -173
- package/docs/authoring-profiles.md +174 -174
- package/docs/configuration.md +54 -54
- package/docs/container-lifecycle.md +349 -349
- package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
- package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
- package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
- package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
- package/docs/deployment.md +199 -199
- package/docs/extensions.md +375 -375
- package/docs/graceful-shutdown.md +62 -62
- package/docs/kb-distillation.md +77 -77
- package/docs/media/overview.md +140 -140
- package/docs/media/whatsapp.md +171 -171
- package/docs/memory.md +137 -137
- package/docs/permissions.md +217 -217
- package/docs/pipeline.md +228 -228
- package/docs/prd-chat-memory.md +76 -76
- package/docs/prd-config-load.md +82 -82
- package/docs/rate-limiting.md +229 -229
- package/docs/runbooks/publish-checklist.md +9 -1
- package/docs/scheduler.md +288 -288
- package/docs/setup-discord.md +100 -100
- package/docs/setup-slack.md +119 -119
- package/docs/setup-whatsapp.md +94 -94
- package/docs/subagents.md +166 -166
- package/docs/web-search.md +62 -62
- package/examples/extensions/README.md +12 -12
- package/examples/extensions/charts/index.ts +13 -13
- package/examples/extensions/charts/skill/SKILL.md +98 -98
- package/examples/extensions/gws/README.md +52 -52
- package/examples/extensions/gws/skill/SKILL.md +57 -57
- package/examples/extensions/gws/skill/references/calendar.md +101 -101
- package/examples/extensions/gws/skill/references/docs.md +65 -65
- package/examples/extensions/gws/skill/references/drive.md +79 -79
- package/examples/extensions/gws/skill/references/gmail.md +85 -85
- package/examples/extensions/gws/skill/references/sheets.md +60 -60
- package/examples/extensions/napkin/skill/SKILL.md +728 -728
- package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
- package/examples/extensions/pdf/skill/SKILL.md +314 -314
- package/examples/extensions/pdf/skill/forms.md +294 -294
- package/examples/extensions/pdf/skill/reference.md +611 -611
- package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
- package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
- package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
- package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
- package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
- package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
- package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
- package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
- package/examples/extensions/permission-guard/index.ts +65 -65
- package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
- package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
- package/examples/extensions/pinchtab/skill/references/api.md +297 -297
- package/examples/extensions/pinchtab/skill/references/env.md +45 -45
- package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
- package/examples/extensions/tradestation/host/refresh.ts +102 -102
- package/examples/extensions/tradestation/index.ts +153 -153
- package/examples/extensions/tradestation/skill/SKILL.md +67 -67
- package/examples/extensions/voice-synth/index.ts +94 -94
- package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
- package/examples/extensions/voice-transcribe/requirements.txt +8 -8
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
- package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
- package/examples/extensions/yahoo-mail/cli/package.json +13 -13
- package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
- package/package.json +106 -106
- package/resources/agents/explore.md +50 -50
- package/resources/agents/worker.md +24 -24
- package/resources/connection-env-vars.json +25 -25
- package/resources/pi-extensions/subagent/agents.ts +126 -126
- package/resources/pi-extensions/subagent/index.ts +964 -964
- package/resources/profiles/coding/AGENTS.md +43 -43
- package/resources/profiles/coding/mercury-profile.yaml +15 -15
- package/resources/profiles/general/AGENTS.md +31 -31
- package/resources/profiles/general/mercury-profile.yaml +15 -15
- package/resources/profiles/research/AGENTS.md +40 -40
- package/resources/profiles/research/mercury-profile.yaml +15 -15
- package/resources/skills/config/SKILL.md +25 -25
- package/resources/skills/context/SKILL.md +33 -33
- package/resources/skills/conversation-recap/SKILL.md +19 -19
- package/resources/skills/mutes/SKILL.md +31 -31
- package/resources/skills/permissions/SKILL.md +19 -19
- package/resources/skills/preferences/SKILL.md +31 -31
- package/resources/skills/recall/SKILL.md +24 -24
- package/resources/skills/roles/SKILL.md +18 -18
- package/resources/skills/spaces/SKILL.md +18 -18
- package/resources/skills/tasks/SKILL.md +45 -45
- package/resources/templates/AGENTS.md +157 -157
- package/resources/templates/env.template +38 -38
- package/resources/templates/mercury.example.yaml +99 -99
- package/src/agent/container-entry.ts +1 -1
- package/src/agent/container-runner.ts +1345 -1346
- package/src/cli/mercury.ts +39 -7
- package/src/cli/mrctl.ts +636 -636
- package/src/config-file.ts +540 -540
- package/src/config.ts +339 -339
- package/src/core/api.ts +125 -125
- package/src/core/caller-token.ts +101 -101
- package/src/core/permissions.ts +228 -228
- package/src/core/profiles.ts +271 -271
- package/src/core/routes/capability.ts +70 -70
- package/src/core/routes/index.ts +15 -15
- package/src/core/runtime.ts +1530 -1530
- package/src/dashboard/index.html +729 -729
- package/src/extensions/api.ts +273 -273
- package/src/extensions/loader.ts +286 -286
- package/src/extensions/types.ts +517 -517
- package/src/main.ts +605 -605
- package/docs/pending-updates/applicative-profiles.md +0 -15
- package/docs/pending-updates/caller-bound-capability-token.md +0 -15
- package/docs/pending-updates/dm-auto-space.md +0 -15
- /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
- /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
- /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: voice-synth
|
|
3
|
-
description: Cloud text-to-speech (Google or Azure) for English and Hebrew — on-demand via mrctl or optional auto voice attachments per space.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Voice synthesis (TTS)
|
|
7
|
-
|
|
8
|
-
**`mrctl tts synthesize` is available in this environment.** When the user asks for a voice message, audio reply, or TTS, you MUST run the command below. Do not assume it is unavailable — always try it and report any error verbatim.
|
|
9
|
-
|
|
10
|
-
**Do not paste** `mrctl`, shell snippets, or JSON tool-call blobs into your **visible** reply — use the **bash tool** only for the command. The user sees normal text plus the audio attachment.
|
|
11
|
-
|
|
12
|
-
## How to send a voice message
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
mrctl tts synthesize --text "Your spoken reply here" --out outbox/reply.mp3
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
This writes an MP3 file to `outbox/` which Mercury automatically attaches to your reply. Do **not** create `.txt` files or use misleading names — chat apps decide how to render attachments from the file extension and MIME type.
|
|
19
|
-
|
|
20
|
-
Optional flags:
|
|
21
|
-
|
|
22
|
-
- `--language` — `auto` (default), `he-IL`, or `en-US`. `auto` picks Hebrew if the text contains Hebrew script.
|
|
23
|
-
- `--provider` — `google`, `azure`, or `auto` (host default).
|
|
24
|
-
|
|
25
|
-
Requires the caller to have **`tts.synthesize`** permission (admins have it by default).
|
|
26
|
-
|
|
27
|
-
### Telegram / WhatsApp delivery
|
|
28
|
-
|
|
29
|
-
**Telegram** uses `sendAudio` for MP3 (in-chat player) and `sendVoice` for OGG voice notes. **WhatsApp** treats audio as a voice note when the filename matches **`voice-*.ogg`** (case-insensitive); otherwise it sends as normal audio (`ptt: false`).
|
|
30
|
-
|
|
31
|
-
## Automatic mode (optional)
|
|
32
|
-
|
|
33
|
-
| Setting | Behavior |
|
|
34
|
-
|--------|----------|
|
|
35
|
-
| **`voice-synth.mode=on_demand`** (default) | TTS runs only when you call `mrctl tts synthesize`. |
|
|
36
|
-
| **`voice-synth.mode=auto`** | The host attaches a TTS MP3 to every assistant reply automatically. |
|
|
37
|
-
|
|
38
|
-
Set with `mrctl config set voice-synth.mode on_demand` or `auto`.
|
|
1
|
+
---
|
|
2
|
+
name: voice-synth
|
|
3
|
+
description: Cloud text-to-speech (Google or Azure) for English and Hebrew — on-demand via mrctl or optional auto voice attachments per space.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Voice synthesis (TTS)
|
|
7
|
+
|
|
8
|
+
**`mrctl tts synthesize` is available in this environment.** When the user asks for a voice message, audio reply, or TTS, you MUST run the command below. Do not assume it is unavailable — always try it and report any error verbatim.
|
|
9
|
+
|
|
10
|
+
**Do not paste** `mrctl`, shell snippets, or JSON tool-call blobs into your **visible** reply — use the **bash tool** only for the command. The user sees normal text plus the audio attachment.
|
|
11
|
+
|
|
12
|
+
## How to send a voice message
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
mrctl tts synthesize --text "Your spoken reply here" --out outbox/reply.mp3
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This writes an MP3 file to `outbox/` which Mercury automatically attaches to your reply. Do **not** create `.txt` files or use misleading names — chat apps decide how to render attachments from the file extension and MIME type.
|
|
19
|
+
|
|
20
|
+
Optional flags:
|
|
21
|
+
|
|
22
|
+
- `--language` — `auto` (default), `he-IL`, or `en-US`. `auto` picks Hebrew if the text contains Hebrew script.
|
|
23
|
+
- `--provider` — `google`, `azure`, or `auto` (host default).
|
|
24
|
+
|
|
25
|
+
Requires the caller to have **`tts.synthesize`** permission (admins have it by default).
|
|
26
|
+
|
|
27
|
+
### Telegram / WhatsApp delivery
|
|
28
|
+
|
|
29
|
+
**Telegram** uses `sendAudio` for MP3 (in-chat player) and `sendVoice` for OGG voice notes. **WhatsApp** treats audio as a voice note when the filename matches **`voice-*.ogg`** (case-insensitive); otherwise it sends as normal audio (`ptt: false`).
|
|
30
|
+
|
|
31
|
+
## Automatic mode (optional)
|
|
32
|
+
|
|
33
|
+
| Setting | Behavior |
|
|
34
|
+
|--------|----------|
|
|
35
|
+
| **`voice-synth.mode=on_demand`** (default) | TTS runs only when you call `mrctl tts synthesize`. |
|
|
36
|
+
| **`voice-synth.mode=auto`** | The host attaches a TTS MP3 to every assistant reply automatically. |
|
|
37
|
+
|
|
38
|
+
Set with `mrctl config set voice-synth.mode on_demand` or `auto`.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Local transcription (voice-transcribe.provider=local). Install on the Mercury host:
|
|
2
|
-
# pip install -r requirements.txt
|
|
3
|
-
transformers>=4.40.0
|
|
4
|
-
torch>=2.0.0
|
|
5
|
-
# Optional local engine when voice-transcribe.local_engine=faster_whisper (CTranslate2 models on Hub).
|
|
6
|
-
faster-whisper>=1.0.0
|
|
7
|
-
# Ships a platform ffmpeg binary when the `ffmpeg` command is missing (Telegram .ogg / Opus).
|
|
8
|
-
imageio-ffmpeg>=0.4.9
|
|
1
|
+
# Local transcription (voice-transcribe.provider=local). Install on the Mercury host:
|
|
2
|
+
# pip install -r requirements.txt
|
|
3
|
+
transformers>=4.40.0
|
|
4
|
+
torch>=2.0.0
|
|
5
|
+
# Optional local engine when voice-transcribe.local_engine=faster_whisper (CTranslate2 models on Hub).
|
|
6
|
+
faster-whisper>=1.0.0
|
|
7
|
+
# Ships a platform ffmpeg binary when the `ffmpeg` command is missing (Telegram .ogg / Opus).
|
|
8
|
+
imageio-ffmpeg>=0.4.9
|
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Transcribe a single audio file; print one JSON line to stdout: {\"text\":\"...\"} or {\"error\":\"...\"}."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import argparse
|
|
7
|
-
import json
|
|
8
|
-
import os
|
|
9
|
-
import shutil
|
|
10
|
-
import sys
|
|
11
|
-
import tempfile
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def _ensure_ffmpeg_on_path() -> None:
|
|
15
|
-
"""
|
|
16
|
-
Transformers and faster-whisper decode via ffmpeg on PATH.
|
|
17
|
-
imageio-ffmpeg ships a versioned .exe (e.g. ffmpeg-win-x86_64-v7.1.exe). On Windows,
|
|
18
|
-
Popen([...]) without shell=True will not run a .bat shim — it needs a real `ffmpeg.exe`
|
|
19
|
-
on PATH. We hardlink (or copy) the vendored binary to `<temp>/mercury-voice-ffmpeg/ffmpeg.exe`.
|
|
20
|
-
"""
|
|
21
|
-
if shutil.which("ffmpeg"):
|
|
22
|
-
return
|
|
23
|
-
try:
|
|
24
|
-
import imageio_ffmpeg
|
|
25
|
-
except ImportError:
|
|
26
|
-
return
|
|
27
|
-
src = imageio_ffmpeg.get_ffmpeg_exe()
|
|
28
|
-
if not src or not os.path.isfile(src):
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
shim_root = os.path.join(tempfile.gettempdir(), "mercury-voice-ffmpeg")
|
|
32
|
-
os.makedirs(shim_root, exist_ok=True)
|
|
33
|
-
dst = os.path.join(shim_root, "ffmpeg.exe" if sys.platform == "win32" else "ffmpeg")
|
|
34
|
-
|
|
35
|
-
if not os.path.exists(dst):
|
|
36
|
-
try:
|
|
37
|
-
os.link(src, dst)
|
|
38
|
-
except OSError:
|
|
39
|
-
try:
|
|
40
|
-
shutil.copy2(src, dst)
|
|
41
|
-
except OSError:
|
|
42
|
-
return
|
|
43
|
-
elif sys.platform != "win32":
|
|
44
|
-
try:
|
|
45
|
-
os.chmod(dst, 0o755)
|
|
46
|
-
except OSError:
|
|
47
|
-
pass
|
|
48
|
-
|
|
49
|
-
os.environ["PATH"] = f"{shim_root}{os.pathsep}{os.environ.get('PATH', '')}"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _asr_device_torch():
|
|
53
|
-
"""Resolve torch device for Transformers. Env: MERCURY_VOICE_ASR_DEVICE=cpu|cuda|auto (default auto)."""
|
|
54
|
-
import torch
|
|
55
|
-
|
|
56
|
-
raw = (os.environ.get("MERCURY_VOICE_ASR_DEVICE") or "").strip().lower()
|
|
57
|
-
if not raw or raw == "auto":
|
|
58
|
-
# Windows often reports CUDA but inference fails (drivers, OOM); CPU is the reliable default.
|
|
59
|
-
if sys.platform == "win32":
|
|
60
|
-
raw = "cpu"
|
|
61
|
-
else:
|
|
62
|
-
raw = "cuda" if torch.cuda.is_available() else "cpu"
|
|
63
|
-
if raw in ("cpu", "-1"):
|
|
64
|
-
return torch.device("cpu")
|
|
65
|
-
if raw in ("cuda", "gpu", "cuda:0", "0"):
|
|
66
|
-
if torch.cuda.is_available():
|
|
67
|
-
return torch.device("cuda:0")
|
|
68
|
-
return torch.device("cpu")
|
|
69
|
-
return torch.device("cpu")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def _cuda_available() -> bool:
|
|
73
|
-
try:
|
|
74
|
-
import torch
|
|
75
|
-
|
|
76
|
-
return bool(torch.cuda.is_available())
|
|
77
|
-
except ImportError:
|
|
78
|
-
return False
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _fw_device_str() -> str:
|
|
82
|
-
"""faster-whisper device string: cpu | cuda. Same MERCURY_VOICE_ASR_DEVICE semantics as Transformers."""
|
|
83
|
-
raw = (os.environ.get("MERCURY_VOICE_ASR_DEVICE") or "").strip().lower()
|
|
84
|
-
if not raw or raw == "auto":
|
|
85
|
-
if sys.platform == "win32":
|
|
86
|
-
return "cpu"
|
|
87
|
-
return "cuda" if _cuda_available() else "cpu"
|
|
88
|
-
if raw in ("cpu", "-1"):
|
|
89
|
-
return "cpu"
|
|
90
|
-
if raw in ("cuda", "gpu", "cuda:0", "0"):
|
|
91
|
-
return "cuda" if _cuda_available() else "cpu"
|
|
92
|
-
return "cpu"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _fw_compute_type(device: str) -> str:
|
|
96
|
-
override = (os.environ.get("MERCURY_VOICE_FW_COMPUTE_TYPE") or "").strip()
|
|
97
|
-
if override:
|
|
98
|
-
return override
|
|
99
|
-
return "int8" if device == "cuda" else "float32"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _fw_language() -> str | None:
|
|
103
|
-
lang = (os.environ.get("MERCURY_VOICE_LANGUAGE") or "").strip()
|
|
104
|
-
return lang if lang else None
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _run_transformers(audio_path: str, model_id: str) -> str:
|
|
108
|
-
try:
|
|
109
|
-
from transformers import pipeline
|
|
110
|
-
except ImportError as e:
|
|
111
|
-
raise ImportError(f"transformers import failed: {e}") from e
|
|
112
|
-
|
|
113
|
-
_ensure_ffmpeg_on_path()
|
|
114
|
-
device = _asr_device_torch()
|
|
115
|
-
pipe = pipeline(
|
|
116
|
-
"automatic-speech-recognition",
|
|
117
|
-
model=model_id,
|
|
118
|
-
device=device,
|
|
119
|
-
)
|
|
120
|
-
result = pipe(audio_path, return_timestamps=True)
|
|
121
|
-
if isinstance(result, dict):
|
|
122
|
-
return (result.get("text") or "").strip()
|
|
123
|
-
return str(result).strip()
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def _run_faster_whisper(audio_path: str, model_id: str) -> str:
|
|
127
|
-
try:
|
|
128
|
-
from faster_whisper import WhisperModel
|
|
129
|
-
except ImportError as e:
|
|
130
|
-
raise ImportError(
|
|
131
|
-
"faster-whisper is required when local_engine=faster_whisper. "
|
|
132
|
-
"Install: pip install faster-whisper"
|
|
133
|
-
) from e
|
|
134
|
-
|
|
135
|
-
_ensure_ffmpeg_on_path()
|
|
136
|
-
device = _fw_device_str()
|
|
137
|
-
compute_type = _fw_compute_type(device)
|
|
138
|
-
lang = _fw_language()
|
|
139
|
-
|
|
140
|
-
model = WhisperModel(model_id, device=device, compute_type=compute_type)
|
|
141
|
-
kwargs: dict = {}
|
|
142
|
-
if lang:
|
|
143
|
-
kwargs["language"] = lang
|
|
144
|
-
segments, _info = model.transcribe(audio_path, **kwargs)
|
|
145
|
-
parts: list[str] = []
|
|
146
|
-
for seg in segments:
|
|
147
|
-
parts.append(seg.text)
|
|
148
|
-
return "".join(parts).strip()
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def main() -> None:
|
|
152
|
-
ap = argparse.ArgumentParser(description="ASR for Mercury voice-transcribe (local provider)")
|
|
153
|
-
ap.add_argument("--audio", required=True, help="Path to audio file on disk")
|
|
154
|
-
ap.add_argument("--model", required=True, help="Hugging Face model id")
|
|
155
|
-
ap.add_argument(
|
|
156
|
-
"--local-engine",
|
|
157
|
-
choices=("transformers", "faster_whisper"),
|
|
158
|
-
default="transformers",
|
|
159
|
-
help="Local ASR backend (default: transformers)",
|
|
160
|
-
)
|
|
161
|
-
args = ap.parse_args()
|
|
162
|
-
|
|
163
|
-
try:
|
|
164
|
-
if args.local_engine == "faster_whisper":
|
|
165
|
-
text = _run_faster_whisper(args.audio, args.model)
|
|
166
|
-
else:
|
|
167
|
-
text = _run_transformers(args.audio, args.model)
|
|
168
|
-
except ImportError as e:
|
|
169
|
-
print(json.dumps({"error": str(e)}))
|
|
170
|
-
sys.exit(2)
|
|
171
|
-
except Exception as e:
|
|
172
|
-
print(json.dumps({"error": str(e)}))
|
|
173
|
-
sys.exit(1)
|
|
174
|
-
|
|
175
|
-
print(json.dumps({"text": text}))
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if __name__ == "__main__":
|
|
179
|
-
main()
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Transcribe a single audio file; print one JSON line to stdout: {\"text\":\"...\"} or {\"error\":\"...\"}."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _ensure_ffmpeg_on_path() -> None:
|
|
15
|
+
"""
|
|
16
|
+
Transformers and faster-whisper decode via ffmpeg on PATH.
|
|
17
|
+
imageio-ffmpeg ships a versioned .exe (e.g. ffmpeg-win-x86_64-v7.1.exe). On Windows,
|
|
18
|
+
Popen([...]) without shell=True will not run a .bat shim — it needs a real `ffmpeg.exe`
|
|
19
|
+
on PATH. We hardlink (or copy) the vendored binary to `<temp>/mercury-voice-ffmpeg/ffmpeg.exe`.
|
|
20
|
+
"""
|
|
21
|
+
if shutil.which("ffmpeg"):
|
|
22
|
+
return
|
|
23
|
+
try:
|
|
24
|
+
import imageio_ffmpeg
|
|
25
|
+
except ImportError:
|
|
26
|
+
return
|
|
27
|
+
src = imageio_ffmpeg.get_ffmpeg_exe()
|
|
28
|
+
if not src or not os.path.isfile(src):
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
shim_root = os.path.join(tempfile.gettempdir(), "mercury-voice-ffmpeg")
|
|
32
|
+
os.makedirs(shim_root, exist_ok=True)
|
|
33
|
+
dst = os.path.join(shim_root, "ffmpeg.exe" if sys.platform == "win32" else "ffmpeg")
|
|
34
|
+
|
|
35
|
+
if not os.path.exists(dst):
|
|
36
|
+
try:
|
|
37
|
+
os.link(src, dst)
|
|
38
|
+
except OSError:
|
|
39
|
+
try:
|
|
40
|
+
shutil.copy2(src, dst)
|
|
41
|
+
except OSError:
|
|
42
|
+
return
|
|
43
|
+
elif sys.platform != "win32":
|
|
44
|
+
try:
|
|
45
|
+
os.chmod(dst, 0o755)
|
|
46
|
+
except OSError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
os.environ["PATH"] = f"{shim_root}{os.pathsep}{os.environ.get('PATH', '')}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _asr_device_torch():
|
|
53
|
+
"""Resolve torch device for Transformers. Env: MERCURY_VOICE_ASR_DEVICE=cpu|cuda|auto (default auto)."""
|
|
54
|
+
import torch
|
|
55
|
+
|
|
56
|
+
raw = (os.environ.get("MERCURY_VOICE_ASR_DEVICE") or "").strip().lower()
|
|
57
|
+
if not raw or raw == "auto":
|
|
58
|
+
# Windows often reports CUDA but inference fails (drivers, OOM); CPU is the reliable default.
|
|
59
|
+
if sys.platform == "win32":
|
|
60
|
+
raw = "cpu"
|
|
61
|
+
else:
|
|
62
|
+
raw = "cuda" if torch.cuda.is_available() else "cpu"
|
|
63
|
+
if raw in ("cpu", "-1"):
|
|
64
|
+
return torch.device("cpu")
|
|
65
|
+
if raw in ("cuda", "gpu", "cuda:0", "0"):
|
|
66
|
+
if torch.cuda.is_available():
|
|
67
|
+
return torch.device("cuda:0")
|
|
68
|
+
return torch.device("cpu")
|
|
69
|
+
return torch.device("cpu")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _cuda_available() -> bool:
|
|
73
|
+
try:
|
|
74
|
+
import torch
|
|
75
|
+
|
|
76
|
+
return bool(torch.cuda.is_available())
|
|
77
|
+
except ImportError:
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _fw_device_str() -> str:
|
|
82
|
+
"""faster-whisper device string: cpu | cuda. Same MERCURY_VOICE_ASR_DEVICE semantics as Transformers."""
|
|
83
|
+
raw = (os.environ.get("MERCURY_VOICE_ASR_DEVICE") or "").strip().lower()
|
|
84
|
+
if not raw or raw == "auto":
|
|
85
|
+
if sys.platform == "win32":
|
|
86
|
+
return "cpu"
|
|
87
|
+
return "cuda" if _cuda_available() else "cpu"
|
|
88
|
+
if raw in ("cpu", "-1"):
|
|
89
|
+
return "cpu"
|
|
90
|
+
if raw in ("cuda", "gpu", "cuda:0", "0"):
|
|
91
|
+
return "cuda" if _cuda_available() else "cpu"
|
|
92
|
+
return "cpu"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _fw_compute_type(device: str) -> str:
|
|
96
|
+
override = (os.environ.get("MERCURY_VOICE_FW_COMPUTE_TYPE") or "").strip()
|
|
97
|
+
if override:
|
|
98
|
+
return override
|
|
99
|
+
return "int8" if device == "cuda" else "float32"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _fw_language() -> str | None:
|
|
103
|
+
lang = (os.environ.get("MERCURY_VOICE_LANGUAGE") or "").strip()
|
|
104
|
+
return lang if lang else None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _run_transformers(audio_path: str, model_id: str) -> str:
|
|
108
|
+
try:
|
|
109
|
+
from transformers import pipeline
|
|
110
|
+
except ImportError as e:
|
|
111
|
+
raise ImportError(f"transformers import failed: {e}") from e
|
|
112
|
+
|
|
113
|
+
_ensure_ffmpeg_on_path()
|
|
114
|
+
device = _asr_device_torch()
|
|
115
|
+
pipe = pipeline(
|
|
116
|
+
"automatic-speech-recognition",
|
|
117
|
+
model=model_id,
|
|
118
|
+
device=device,
|
|
119
|
+
)
|
|
120
|
+
result = pipe(audio_path, return_timestamps=True)
|
|
121
|
+
if isinstance(result, dict):
|
|
122
|
+
return (result.get("text") or "").strip()
|
|
123
|
+
return str(result).strip()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _run_faster_whisper(audio_path: str, model_id: str) -> str:
|
|
127
|
+
try:
|
|
128
|
+
from faster_whisper import WhisperModel
|
|
129
|
+
except ImportError as e:
|
|
130
|
+
raise ImportError(
|
|
131
|
+
"faster-whisper is required when local_engine=faster_whisper. "
|
|
132
|
+
"Install: pip install faster-whisper"
|
|
133
|
+
) from e
|
|
134
|
+
|
|
135
|
+
_ensure_ffmpeg_on_path()
|
|
136
|
+
device = _fw_device_str()
|
|
137
|
+
compute_type = _fw_compute_type(device)
|
|
138
|
+
lang = _fw_language()
|
|
139
|
+
|
|
140
|
+
model = WhisperModel(model_id, device=device, compute_type=compute_type)
|
|
141
|
+
kwargs: dict = {}
|
|
142
|
+
if lang:
|
|
143
|
+
kwargs["language"] = lang
|
|
144
|
+
segments, _info = model.transcribe(audio_path, **kwargs)
|
|
145
|
+
parts: list[str] = []
|
|
146
|
+
for seg in segments:
|
|
147
|
+
parts.append(seg.text)
|
|
148
|
+
return "".join(parts).strip()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def main() -> None:
|
|
152
|
+
ap = argparse.ArgumentParser(description="ASR for Mercury voice-transcribe (local provider)")
|
|
153
|
+
ap.add_argument("--audio", required=True, help="Path to audio file on disk")
|
|
154
|
+
ap.add_argument("--model", required=True, help="Hugging Face model id")
|
|
155
|
+
ap.add_argument(
|
|
156
|
+
"--local-engine",
|
|
157
|
+
choices=("transformers", "faster_whisper"),
|
|
158
|
+
default="transformers",
|
|
159
|
+
help="Local ASR backend (default: transformers)",
|
|
160
|
+
)
|
|
161
|
+
args = ap.parse_args()
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
if args.local_engine == "faster_whisper":
|
|
165
|
+
text = _run_faster_whisper(args.audio, args.model)
|
|
166
|
+
else:
|
|
167
|
+
text = _run_transformers(args.audio, args.model)
|
|
168
|
+
except ImportError as e:
|
|
169
|
+
print(json.dumps({"error": str(e)}))
|
|
170
|
+
sys.exit(2)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(json.dumps({"error": str(e)}))
|
|
173
|
+
sys.exit(1)
|
|
174
|
+
|
|
175
|
+
print(json.dumps({"text": text}))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: voice-transcribe
|
|
3
|
-
description: Voice notes are transcribed to text before the agent runs (local Python or Hugging Face Inference API).
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Voice transcription
|
|
7
|
-
|
|
8
|
-
When a user sends a **voice note** or **audio** attachment, Mercury runs the `voice-transcribe` extension **before** the container starts. The transcript is appended to the user message as a `[Voice transcript]` block. You receive normal text — you do not need to read or play audio files for intent.
|
|
9
|
-
|
|
10
|
-
## Configuration (per space)
|
|
11
|
-
|
|
12
|
-
| Key | Purpose |
|
|
13
|
-
|-----|---------|
|
|
14
|
-
| `voice-transcribe.provider` | `local` (default) or `api` |
|
|
15
|
-
| `voice-transcribe.model` | Hugging Face model id (see below) |
|
|
16
|
-
| `voice-transcribe.local_engine` | Local only: `transformers` (default) or `faster_whisper` (CTranslate2 Hub repos) |
|
|
17
|
-
|
|
18
|
-
### Local provider (`local`)
|
|
19
|
-
|
|
20
|
-
Runs `scripts/transcribe.py` on the **Mercury host**.
|
|
21
|
-
|
|
22
|
-
**Transformers (default)** — `voice-transcribe.local_engine=transformers` uses [Transformers](https://huggingface.co/docs/transformers) `pipeline("automatic-speech-recognition", model=...)`. Use standard PyTorch Whisper / compatible Hub ids (e.g. [mike249/whisper-tiny-he-2](https://huggingface.co/mike249/whisper-tiny-he-2)).
|
|
23
|
-
|
|
24
|
-
**Faster-Whisper** — set `voice-transcribe.local_engine=faster_whisper` and [faster-whisper](https://github.com/SYSTRAN/faster-whisper) loads **CTranslate2** checkpoints from the Hub. Set `voice-transcribe.model` to a **CT2** repo (not plain PyTorch Whisper weights). Examples for Hebrew: [ivrit-ai/faster-whisper-v2-d4](https://huggingface.co/ivrit-ai/faster-whisper-v2-d4), [ivrit-ai/whisper-large-v3-turbo-ct2](https://huggingface.co/ivrit-ai/whisper-large-v3-turbo-ct2).
|
|
25
|
-
|
|
26
|
-
1. Install on the same machine that runs Mercury:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
pip install -r /path/to/.mercury/extensions/voice-transcribe/requirements.txt
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Or: `pip install "transformers>=4.40.0" "torch>=2.0.0" "faster-whisper>=1.0.0" "imageio-ffmpeg>=0.4.9"`
|
|
33
|
-
|
|
34
|
-
**Telegram voice** is usually `.ogg` (Opus). Both backends shell out to an executable named `ffmpeg`; `transcribe.py` uses `imageio-ffmpeg`’s binary and exposes it as `ffmpeg` / `ffmpeg.exe` on `PATH` (hardlink or copy under `%TEMP%\\mercury-voice-ffmpeg` on Windows). Installing system ffmpeg on `PATH` also works (e.g. `winget install Gyan.FFmpeg`).
|
|
35
|
-
|
|
36
|
-
2. Optional host `.env`:
|
|
37
|
-
- `MERCURY_VOICE_PYTHON` — Python executable (default: `python` on Windows, `python3` elsewhere)
|
|
38
|
-
- `MERCURY_VOICE_TRANSCRIBE_TIMEOUT_MS` — subprocess timeout in ms (default `300000`)
|
|
39
|
-
- `MERCURY_VOICE_ASR_DEVICE` — `cpu`, `cuda`, or `auto`. Used by **both** local engines. On Windows the default is **CPU** (CUDA often looks available but fails at inference). Set `cuda` if you have a working GPU stack.
|
|
40
|
-
- `MERCURY_VOICE_FW_COMPUTE_TYPE` — **Faster-Whisper only**: e.g. `int8`, `float16`, `float32`. If unset: `int8` on CUDA, `float32` on CPU.
|
|
41
|
-
- `MERCURY_VOICE_LANGUAGE` — **Faster-Whisper only**: ISO code passed to `transcribe(..., language=...)` (e.g. `he`). If unset, language is auto-detected.
|
|
42
|
-
|
|
43
|
-
Hub messages like *Xet Storage… hf_xet* are warnings only, not the cause of failures.
|
|
44
|
-
|
|
45
|
-
First run downloads the model into the Hugging Face cache (size depends on the model). **Each voice note spawns a new Python process**, so the first transcription after Mercury starts can be slow while the model loads (no in-process reuse yet).
|
|
46
|
-
|
|
47
|
-
### API provider (`api`)
|
|
48
|
-
|
|
49
|
-
POSTs audio to `https://api-inference.huggingface.co/models/<model>`. Requires `MERCURY_HF_TOKEN` on the host. Pick a model that has an [Inference Provider](https://huggingface.co/docs/api-inference) on its Hub page (e.g. `openai/whisper-large-v3`). Hebrew-tuned models such as [mike249/whisper-tiny-he-2](https://huggingface.co/mike249/whisper-tiny-he-2) often have **no** hosted provider — use **`local`** for those. CT2 / Faster-Whisper Hub repos are for the **local** engine only, not this API.
|
|
50
|
-
|
|
51
|
-
## RBAC
|
|
52
|
-
|
|
53
|
-
Only callers with the `voice-transcribe` permission (default: admin + member) get transcription. Others keep the raw message without an appended transcript.
|
|
1
|
+
---
|
|
2
|
+
name: voice-transcribe
|
|
3
|
+
description: Voice notes are transcribed to text before the agent runs (local Python or Hugging Face Inference API).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Voice transcription
|
|
7
|
+
|
|
8
|
+
When a user sends a **voice note** or **audio** attachment, Mercury runs the `voice-transcribe` extension **before** the container starts. The transcript is appended to the user message as a `[Voice transcript]` block. You receive normal text — you do not need to read or play audio files for intent.
|
|
9
|
+
|
|
10
|
+
## Configuration (per space)
|
|
11
|
+
|
|
12
|
+
| Key | Purpose |
|
|
13
|
+
|-----|---------|
|
|
14
|
+
| `voice-transcribe.provider` | `local` (default) or `api` |
|
|
15
|
+
| `voice-transcribe.model` | Hugging Face model id (see below) |
|
|
16
|
+
| `voice-transcribe.local_engine` | Local only: `transformers` (default) or `faster_whisper` (CTranslate2 Hub repos) |
|
|
17
|
+
|
|
18
|
+
### Local provider (`local`)
|
|
19
|
+
|
|
20
|
+
Runs `scripts/transcribe.py` on the **Mercury host**.
|
|
21
|
+
|
|
22
|
+
**Transformers (default)** — `voice-transcribe.local_engine=transformers` uses [Transformers](https://huggingface.co/docs/transformers) `pipeline("automatic-speech-recognition", model=...)`. Use standard PyTorch Whisper / compatible Hub ids (e.g. [mike249/whisper-tiny-he-2](https://huggingface.co/mike249/whisper-tiny-he-2)).
|
|
23
|
+
|
|
24
|
+
**Faster-Whisper** — set `voice-transcribe.local_engine=faster_whisper` and [faster-whisper](https://github.com/SYSTRAN/faster-whisper) loads **CTranslate2** checkpoints from the Hub. Set `voice-transcribe.model` to a **CT2** repo (not plain PyTorch Whisper weights). Examples for Hebrew: [ivrit-ai/faster-whisper-v2-d4](https://huggingface.co/ivrit-ai/faster-whisper-v2-d4), [ivrit-ai/whisper-large-v3-turbo-ct2](https://huggingface.co/ivrit-ai/whisper-large-v3-turbo-ct2).
|
|
25
|
+
|
|
26
|
+
1. Install on the same machine that runs Mercury:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install -r /path/to/.mercury/extensions/voice-transcribe/requirements.txt
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or: `pip install "transformers>=4.40.0" "torch>=2.0.0" "faster-whisper>=1.0.0" "imageio-ffmpeg>=0.4.9"`
|
|
33
|
+
|
|
34
|
+
**Telegram voice** is usually `.ogg` (Opus). Both backends shell out to an executable named `ffmpeg`; `transcribe.py` uses `imageio-ffmpeg`’s binary and exposes it as `ffmpeg` / `ffmpeg.exe` on `PATH` (hardlink or copy under `%TEMP%\\mercury-voice-ffmpeg` on Windows). Installing system ffmpeg on `PATH` also works (e.g. `winget install Gyan.FFmpeg`).
|
|
35
|
+
|
|
36
|
+
2. Optional host `.env`:
|
|
37
|
+
- `MERCURY_VOICE_PYTHON` — Python executable (default: `python` on Windows, `python3` elsewhere)
|
|
38
|
+
- `MERCURY_VOICE_TRANSCRIBE_TIMEOUT_MS` — subprocess timeout in ms (default `300000`)
|
|
39
|
+
- `MERCURY_VOICE_ASR_DEVICE` — `cpu`, `cuda`, or `auto`. Used by **both** local engines. On Windows the default is **CPU** (CUDA often looks available but fails at inference). Set `cuda` if you have a working GPU stack.
|
|
40
|
+
- `MERCURY_VOICE_FW_COMPUTE_TYPE` — **Faster-Whisper only**: e.g. `int8`, `float16`, `float32`. If unset: `int8` on CUDA, `float32` on CPU.
|
|
41
|
+
- `MERCURY_VOICE_LANGUAGE` — **Faster-Whisper only**: ISO code passed to `transcribe(..., language=...)` (e.g. `he`). If unset, language is auto-detected.
|
|
42
|
+
|
|
43
|
+
Hub messages like *Xet Storage… hf_xet* are warnings only, not the cause of failures.
|
|
44
|
+
|
|
45
|
+
First run downloads the model into the Hugging Face cache (size depends on the model). **Each voice note spawns a new Python process**, so the first transcription after Mercury starts can be slow while the model loads (no in-process reuse yet).
|
|
46
|
+
|
|
47
|
+
### API provider (`api`)
|
|
48
|
+
|
|
49
|
+
POSTs audio to `https://api-inference.huggingface.co/models/<model>`. Requires `MERCURY_HF_TOKEN` on the host. Pick a model that has an [Inference Provider](https://huggingface.co/docs/api-inference) on its Hub page (e.g. `openai/whisper-large-v3`). Hebrew-tuned models such as [mike249/whisper-tiny-he-2](https://huggingface.co/mike249/whisper-tiny-he-2) often have **no** hosted provider — use **`local`** for those. CT2 / Faster-Whisper Hub repos are for the **local** engine only, not this API.
|
|
50
|
+
|
|
51
|
+
## RBAC
|
|
52
|
+
|
|
53
|
+
Only callers with the `voice-transcribe` permission (default: admin + member) get transcription. Others keep the raw message without an appended transcript.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ymail",
|
|
3
|
-
"version": "1.0.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"ymail": "./ymail.mjs"
|
|
8
|
-
},
|
|
9
|
-
"dependencies": {
|
|
10
|
-
"imapflow": "^1.0.171",
|
|
11
|
-
"nodemailer": "^8.0.5"
|
|
12
|
-
}
|
|
13
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ymail",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ymail": "./ymail.mjs"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"imapflow": "^1.0.171",
|
|
11
|
+
"nodemailer": "^8.0.5"
|
|
12
|
+
}
|
|
13
|
+
}
|