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.
Files changed (125) hide show
  1. package/README.md +452 -451
  2. package/container/Dockerfile +127 -127
  3. package/container/Dockerfile.base +109 -109
  4. package/container/Dockerfile.power +17 -17
  5. package/container/agent-package.json +8 -8
  6. package/container/build.sh +54 -54
  7. package/docs/ROADMAP.md +3 -4
  8. package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
  9. package/docs/auth/dashboard.md +28 -28
  10. package/docs/auth/overview.md +109 -109
  11. package/docs/auth/whatsapp.md +173 -173
  12. package/docs/authoring-profiles.md +174 -174
  13. package/docs/configuration.md +54 -54
  14. package/docs/container-lifecycle.md +349 -349
  15. package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
  16. package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
  17. package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
  18. package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
  19. package/docs/deployment.md +199 -199
  20. package/docs/extensions.md +375 -375
  21. package/docs/graceful-shutdown.md +62 -62
  22. package/docs/kb-distillation.md +77 -77
  23. package/docs/media/overview.md +140 -140
  24. package/docs/media/whatsapp.md +171 -171
  25. package/docs/memory.md +137 -137
  26. package/docs/permissions.md +217 -217
  27. package/docs/pipeline.md +228 -228
  28. package/docs/prd-chat-memory.md +76 -76
  29. package/docs/prd-config-load.md +82 -82
  30. package/docs/rate-limiting.md +229 -229
  31. package/docs/runbooks/publish-checklist.md +9 -1
  32. package/docs/scheduler.md +288 -288
  33. package/docs/setup-discord.md +100 -100
  34. package/docs/setup-slack.md +119 -119
  35. package/docs/setup-whatsapp.md +94 -94
  36. package/docs/subagents.md +166 -166
  37. package/docs/web-search.md +62 -62
  38. package/examples/extensions/README.md +12 -12
  39. package/examples/extensions/charts/index.ts +13 -13
  40. package/examples/extensions/charts/skill/SKILL.md +98 -98
  41. package/examples/extensions/gws/README.md +52 -52
  42. package/examples/extensions/gws/skill/SKILL.md +57 -57
  43. package/examples/extensions/gws/skill/references/calendar.md +101 -101
  44. package/examples/extensions/gws/skill/references/docs.md +65 -65
  45. package/examples/extensions/gws/skill/references/drive.md +79 -79
  46. package/examples/extensions/gws/skill/references/gmail.md +85 -85
  47. package/examples/extensions/gws/skill/references/sheets.md +60 -60
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -728
  49. package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
  50. package/examples/extensions/pdf/skill/SKILL.md +314 -314
  51. package/examples/extensions/pdf/skill/forms.md +294 -294
  52. package/examples/extensions/pdf/skill/reference.md +611 -611
  53. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
  54. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
  55. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
  56. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
  57. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
  58. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
  59. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
  60. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
  61. package/examples/extensions/permission-guard/index.ts +65 -65
  62. package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
  63. package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
  64. package/examples/extensions/pinchtab/skill/references/api.md +297 -297
  65. package/examples/extensions/pinchtab/skill/references/env.md +45 -45
  66. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
  67. package/examples/extensions/tradestation/host/refresh.ts +102 -102
  68. package/examples/extensions/tradestation/index.ts +153 -153
  69. package/examples/extensions/tradestation/skill/SKILL.md +67 -67
  70. package/examples/extensions/voice-synth/index.ts +94 -94
  71. package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
  72. package/examples/extensions/voice-transcribe/requirements.txt +8 -8
  73. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
  74. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
  75. package/examples/extensions/yahoo-mail/cli/package.json +13 -13
  76. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
  77. package/package.json +106 -106
  78. package/resources/agents/explore.md +50 -50
  79. package/resources/agents/worker.md +24 -24
  80. package/resources/connection-env-vars.json +25 -25
  81. package/resources/pi-extensions/subagent/agents.ts +126 -126
  82. package/resources/pi-extensions/subagent/index.ts +964 -964
  83. package/resources/profiles/coding/AGENTS.md +43 -43
  84. package/resources/profiles/coding/mercury-profile.yaml +15 -15
  85. package/resources/profiles/general/AGENTS.md +31 -31
  86. package/resources/profiles/general/mercury-profile.yaml +15 -15
  87. package/resources/profiles/research/AGENTS.md +40 -40
  88. package/resources/profiles/research/mercury-profile.yaml +15 -15
  89. package/resources/skills/config/SKILL.md +25 -25
  90. package/resources/skills/context/SKILL.md +33 -33
  91. package/resources/skills/conversation-recap/SKILL.md +19 -19
  92. package/resources/skills/mutes/SKILL.md +31 -31
  93. package/resources/skills/permissions/SKILL.md +19 -19
  94. package/resources/skills/preferences/SKILL.md +31 -31
  95. package/resources/skills/recall/SKILL.md +24 -24
  96. package/resources/skills/roles/SKILL.md +18 -18
  97. package/resources/skills/spaces/SKILL.md +18 -18
  98. package/resources/skills/tasks/SKILL.md +45 -45
  99. package/resources/templates/AGENTS.md +157 -157
  100. package/resources/templates/env.template +38 -38
  101. package/resources/templates/mercury.example.yaml +99 -99
  102. package/src/agent/container-entry.ts +1 -1
  103. package/src/agent/container-runner.ts +1345 -1346
  104. package/src/cli/mercury.ts +39 -7
  105. package/src/cli/mrctl.ts +636 -636
  106. package/src/config-file.ts +540 -540
  107. package/src/config.ts +339 -339
  108. package/src/core/api.ts +125 -125
  109. package/src/core/caller-token.ts +101 -101
  110. package/src/core/permissions.ts +228 -228
  111. package/src/core/profiles.ts +271 -271
  112. package/src/core/routes/capability.ts +70 -70
  113. package/src/core/routes/index.ts +15 -15
  114. package/src/core/runtime.ts +1530 -1530
  115. package/src/dashboard/index.html +729 -729
  116. package/src/extensions/api.ts +273 -273
  117. package/src/extensions/loader.ts +286 -286
  118. package/src/extensions/types.ts +517 -517
  119. package/src/main.ts +605 -605
  120. package/docs/pending-updates/applicative-profiles.md +0 -15
  121. package/docs/pending-updates/caller-bound-capability-token.md +0 -15
  122. package/docs/pending-updates/dm-auto-space.md +0 -15
  123. /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
  124. /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
  125. /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
+ }