bloby-bot 0.50.3 → 0.51.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/package.json +1 -1
- package/supervisor/index.ts +8 -2
- package/supervisor/public/morphy/teleporting.json +38 -0
- package/supervisor/public/morphy/teleporting.png +0 -0
- package/supervisor/widget.js +58 -26
- package/workspace/client/public/morphy/teleporting.json +38 -0
- package/workspace/client/public/morphy/teleporting.png +0 -0
- package/workspace/client/public/sw.js +1 -1
- package/workspace/skills/plaud/SKILL.md +139 -37
package/package.json
CHANGED
package/supervisor/index.ts
CHANGED
|
@@ -46,6 +46,11 @@ const PLATFORM_ASSETS = new Set([
|
|
|
46
46
|
'/manifest.json',
|
|
47
47
|
]);
|
|
48
48
|
|
|
49
|
+
// Directory-prefix platform assets — anything under these is served from supervisor/public/.
|
|
50
|
+
// Used for the Morphy animation set: drop new {clip}.png + {clip}.json into public/morphy/
|
|
51
|
+
// and they're automatically served without touching the allowlist.
|
|
52
|
+
const PLATFORM_ASSET_DIRS = ['/morphy/'];
|
|
53
|
+
|
|
49
54
|
// Ensure dist-bloby exists (postinstall may have failed silently)
|
|
50
55
|
if (!fs.existsSync(DIST_BLOBY)) {
|
|
51
56
|
log.info('Building bloby chat UI (first run)...');
|
|
@@ -85,7 +90,7 @@ const SW_JS = `// Service worker — app-shell caching + push notifications
|
|
|
85
90
|
// JS/CSS modules → stale-while-revalidate
|
|
86
91
|
// API, WebSocket, Vite internals → network-only (no cache)
|
|
87
92
|
|
|
88
|
-
var CACHE = 'bloby-
|
|
93
|
+
var CACHE = 'bloby-v15';
|
|
89
94
|
var HASHED_RE = new RegExp('/assets/.+-[a-zA-Z0-9]{6,}[.](js|css)$');
|
|
90
95
|
|
|
91
96
|
// Precache the HTML shell on install so the cache is never empty.
|
|
@@ -1631,7 +1636,8 @@ mint();
|
|
|
1631
1636
|
|
|
1632
1637
|
// Platform assets — served from supervisor/public/ so they survive workspace swaps
|
|
1633
1638
|
const cleanUrl = (req.url || '').split('?')[0];
|
|
1634
|
-
|
|
1639
|
+
const inAssetDir = PLATFORM_ASSET_DIRS.some((d) => cleanUrl.startsWith(d) && !cleanUrl.includes('..'));
|
|
1640
|
+
if (PLATFORM_ASSETS.has(cleanUrl) || inAssetDir) {
|
|
1635
1641
|
const assetPath = path.join(SUPERVISOR_PUBLIC, cleanUrl);
|
|
1636
1642
|
try {
|
|
1637
1643
|
const stat = fs.statSync(assetPath);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "teleporting",
|
|
3
|
+
"spritesheet": "teleporting.png",
|
|
4
|
+
"frame": {
|
|
5
|
+
"w": 218,
|
|
6
|
+
"h": 180
|
|
7
|
+
},
|
|
8
|
+
"grid": {
|
|
9
|
+
"cols": 16,
|
|
10
|
+
"rows": 8
|
|
11
|
+
},
|
|
12
|
+
"totalFrames": 121,
|
|
13
|
+
"fps": 24,
|
|
14
|
+
"clips": {
|
|
15
|
+
"idle": {
|
|
16
|
+
"from": 19,
|
|
17
|
+
"to": 19,
|
|
18
|
+
"mode": "hold"
|
|
19
|
+
},
|
|
20
|
+
"enter": {
|
|
21
|
+
"from": 20,
|
|
22
|
+
"to": 45,
|
|
23
|
+
"mode": "forward",
|
|
24
|
+
"next": "active"
|
|
25
|
+
},
|
|
26
|
+
"active": {
|
|
27
|
+
"from": 46,
|
|
28
|
+
"to": 68,
|
|
29
|
+
"mode": "pingpong"
|
|
30
|
+
},
|
|
31
|
+
"exit": {
|
|
32
|
+
"from": 69,
|
|
33
|
+
"to": 121,
|
|
34
|
+
"mode": "forward",
|
|
35
|
+
"next": "idle"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
Binary file
|
package/supervisor/widget.js
CHANGED
|
@@ -33,24 +33,34 @@
|
|
|
33
33
|
|
|
34
34
|
// ══════════════════════════════════════════════════════════════════
|
|
35
35
|
// ── Blob Sprite Sheet Config (splash only) ───────────────────────
|
|
36
|
+
// Loaded from /morphy/teleporting.json. Clips map to animStates:
|
|
37
|
+
// idle (hold) → animState 'idle'
|
|
38
|
+
// enter (forward) → animState 'melting'
|
|
39
|
+
// active (pingpong)→ animState 'traveling'
|
|
40
|
+
// exit (forward) → animState 'reforming'
|
|
36
41
|
// ══════════════════════════════════════════════════════════════════
|
|
37
42
|
|
|
43
|
+
var BLOB_CONFIG_URL = '/morphy/teleporting.json';
|
|
44
|
+
var BLOB_ASSETS_DIR = '/morphy/';
|
|
45
|
+
|
|
46
|
+
// Defaults mirror the teleporting.json shipped at the URL above so
|
|
47
|
+
// animation works even if the fetch is cached at the wrong revision.
|
|
38
48
|
var COLS = 16;
|
|
39
|
-
var FRAME_W =
|
|
40
|
-
var FRAME_H =
|
|
49
|
+
var FRAME_W = 218;
|
|
50
|
+
var FRAME_H = 180;
|
|
41
51
|
var DISPLAY_H = 58;
|
|
42
52
|
var DISPLAY_W = DISPLAY_H * (FRAME_W / FRAME_H);
|
|
43
53
|
|
|
44
|
-
var IDLE_START =
|
|
45
|
-
var MELT_START =
|
|
46
|
-
var TRAVEL_START =
|
|
47
|
-
var REFORM_START =
|
|
54
|
+
var IDLE_START = 19, IDLE_END = 19;
|
|
55
|
+
var MELT_START = 20, MELT_END = 45;
|
|
56
|
+
var TRAVEL_START = 46, TRAVEL_END = 68;
|
|
57
|
+
var REFORM_START = 69, REFORM_END = 121;
|
|
48
58
|
|
|
49
|
-
var FPS =
|
|
59
|
+
var FPS = 24;
|
|
50
60
|
var FRAME_MS = 1000 / FPS;
|
|
51
|
-
var IDLE_FPS =
|
|
61
|
+
var IDLE_FPS = 24;
|
|
52
62
|
var IDLE_FRAME_MS = 1000 / IDLE_FPS;
|
|
53
|
-
var REFORM_FPS =
|
|
63
|
+
var REFORM_FPS = 24;
|
|
54
64
|
var REFORM_FRAME_MS = 1000 / REFORM_FPS;
|
|
55
65
|
|
|
56
66
|
var TRAVEL_PX_PER_MS = 0.65;
|
|
@@ -147,6 +157,7 @@
|
|
|
147
157
|
var animState = 'loading';
|
|
148
158
|
var currentFrame = 0;
|
|
149
159
|
var idleDirection = 1;
|
|
160
|
+
var travelFrameDir = 1;
|
|
150
161
|
var lastFrameTime = 0;
|
|
151
162
|
var travelDuration = 0;
|
|
152
163
|
var travelStartTime = 0;
|
|
@@ -181,21 +192,41 @@
|
|
|
181
192
|
var hpSpeechInstance = null;
|
|
182
193
|
var hpSpeechTranscript = '';
|
|
183
194
|
|
|
184
|
-
// ── Load blob sprite sheet ──
|
|
195
|
+
// ── Load blob sprite sheet (config + image) ──
|
|
196
|
+
function applyBlobConfig(cfg) {
|
|
197
|
+
COLS = cfg.grid.cols;
|
|
198
|
+
FRAME_W = cfg.frame.w;
|
|
199
|
+
FRAME_H = cfg.frame.h;
|
|
200
|
+
DISPLAY_W = DISPLAY_H * (FRAME_W / FRAME_H);
|
|
201
|
+
IDLE_START = cfg.clips.idle.from; IDLE_END = cfg.clips.idle.to;
|
|
202
|
+
MELT_START = cfg.clips.enter.from; MELT_END = cfg.clips.enter.to;
|
|
203
|
+
TRAVEL_START = cfg.clips.active.from; TRAVEL_END = cfg.clips.active.to;
|
|
204
|
+
REFORM_START = cfg.clips.exit.from; REFORM_END = cfg.clips.exit.to;
|
|
205
|
+
FPS = cfg.fps; FRAME_MS = 1000 / FPS;
|
|
206
|
+
IDLE_FPS = cfg.fps; IDLE_FRAME_MS = 1000 / IDLE_FPS;
|
|
207
|
+
REFORM_FPS = cfg.fps; REFORM_FRAME_MS = 1000 / REFORM_FPS;
|
|
208
|
+
}
|
|
209
|
+
|
|
185
210
|
function loadSprite(onDone, onFail) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
211
|
+
fetch(BLOB_CONFIG_URL)
|
|
212
|
+
.then(function (r) { if (!r.ok) throw new Error('blob config ' + r.status); return r.json(); })
|
|
213
|
+
.then(function (cfg) {
|
|
214
|
+
applyBlobConfig(cfg);
|
|
215
|
+
var img = new Image();
|
|
216
|
+
img.onload = function () {
|
|
217
|
+
spriteSheet = img;
|
|
218
|
+
center.x = Math.round(W / 2);
|
|
219
|
+
center.y = Math.round(H / 2);
|
|
220
|
+
currentFrame = IDLE_START;
|
|
221
|
+
animState = 'idle';
|
|
222
|
+
var splash = document.getElementById('splash');
|
|
223
|
+
if (splash) splash.style.display = 'none';
|
|
224
|
+
onDone();
|
|
225
|
+
};
|
|
226
|
+
img.onerror = function () { if (onFail) onFail(); };
|
|
227
|
+
img.src = BLOB_ASSETS_DIR + cfg.spritesheet;
|
|
228
|
+
})
|
|
229
|
+
.catch(function () { if (onFail) onFail(); });
|
|
199
230
|
}
|
|
200
231
|
|
|
201
232
|
// ── Load headphones sprite sheet ──
|
|
@@ -300,10 +331,11 @@
|
|
|
300
331
|
else if (currentFrame <= IDLE_START) { currentFrame = IDLE_START; idleDirection = 1; }
|
|
301
332
|
} else if (animState === 'melting') {
|
|
302
333
|
currentFrame++;
|
|
303
|
-
if (currentFrame > MELT_END) { animState = 'traveling'; currentFrame = TRAVEL_START; travelStartTime = now; }
|
|
334
|
+
if (currentFrame > MELT_END) { animState = 'traveling'; currentFrame = TRAVEL_START; travelStartTime = now; travelFrameDir = 1; }
|
|
304
335
|
} else if (animState === 'traveling') {
|
|
305
|
-
currentFrame
|
|
306
|
-
if (currentFrame
|
|
336
|
+
currentFrame += travelFrameDir;
|
|
337
|
+
if (currentFrame >= TRAVEL_END) { currentFrame = TRAVEL_END; travelFrameDir = -1; }
|
|
338
|
+
else if (currentFrame <= TRAVEL_START) { currentFrame = TRAVEL_START; travelFrameDir = 1; }
|
|
307
339
|
} else if (animState === 'reforming') {
|
|
308
340
|
currentFrame++;
|
|
309
341
|
if (currentFrame > REFORM_END) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "teleporting",
|
|
3
|
+
"spritesheet": "teleporting.png",
|
|
4
|
+
"frame": {
|
|
5
|
+
"w": 218,
|
|
6
|
+
"h": 180
|
|
7
|
+
},
|
|
8
|
+
"grid": {
|
|
9
|
+
"cols": 16,
|
|
10
|
+
"rows": 8
|
|
11
|
+
},
|
|
12
|
+
"totalFrames": 121,
|
|
13
|
+
"fps": 24,
|
|
14
|
+
"clips": {
|
|
15
|
+
"idle": {
|
|
16
|
+
"from": 19,
|
|
17
|
+
"to": 19,
|
|
18
|
+
"mode": "hold"
|
|
19
|
+
},
|
|
20
|
+
"enter": {
|
|
21
|
+
"from": 20,
|
|
22
|
+
"to": 45,
|
|
23
|
+
"mode": "forward",
|
|
24
|
+
"next": "active"
|
|
25
|
+
},
|
|
26
|
+
"active": {
|
|
27
|
+
"from": 46,
|
|
28
|
+
"to": 68,
|
|
29
|
+
"mode": "pingpong"
|
|
30
|
+
},
|
|
31
|
+
"exit": {
|
|
32
|
+
"from": 69,
|
|
33
|
+
"to": 121,
|
|
34
|
+
"mode": "forward",
|
|
35
|
+
"next": "idle"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
Binary file
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// JS/CSS modules → stale-while-revalidate
|
|
7
7
|
// API, WebSocket, Vite internals → network-only (no cache)
|
|
8
8
|
|
|
9
|
-
const CACHE = 'bloby-
|
|
9
|
+
const CACHE = 'bloby-v6';
|
|
10
10
|
|
|
11
11
|
// Precache the HTML shell on install so the cache is never empty.
|
|
12
12
|
// Without this, the first navigation isn't intercepted (SW wasn't
|
|
@@ -249,13 +249,27 @@ curl -s -o "workspace/files/audio/plaud/<FILE_ID>.mp3" '<TEMP URL>'
|
|
|
249
249
|
|
|
250
250
|
---
|
|
251
251
|
|
|
252
|
-
## Transcription — pick a path
|
|
252
|
+
## Transcription — pick a path (with pricing)
|
|
253
253
|
|
|
254
|
-
Once the audio is on disk, you need text. **
|
|
254
|
+
Once the audio is on disk, you need text. **Talk to the human about the trade-offs once**, pick a path, save it as `transcriptionMode` in `.plaud.json` so you don't re-ask every sync.
|
|
255
255
|
|
|
256
|
-
###
|
|
256
|
+
### The trade-off table (lead with this)
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
| Path | Cost | Setup | Notes |
|
|
259
|
+
|---|---|---|---|
|
|
260
|
+
| **Bloby Marketplace** | **$0.22/hr** ($0.0037/min) | None — uses your relay token | **Cheapest hosted option.** Pay-as-you-go from owner's credit balance. No API key to manage. |
|
|
261
|
+
| Groq Whisper (BYO key) | ~$0.04/hr | Human creates a Groq account, gives you the key | Often has a generous free tier. Same model the marketplace uses under the hood. |
|
|
262
|
+
| Mistral Voxtral (BYO key) | varies | Human creates a Mistral account, gives you the key | Newer option. Check [docs.mistral.ai](https://docs.mistral.ai) for current pricing. |
|
|
263
|
+
| OpenAI Whisper (wizard key) | **$1.02/hr** | Already configured if human set whisper_key in the Bloby wizard | **~5× more expensive than the marketplace.** Only worth it if the human really wants to use their existing OpenAI key. |
|
|
264
|
+
| Local (whisper.cpp / faster-whisper) | Free (compute) | Human installs the binary themselves | Fully offline, full privacy. Slower than cloud. |
|
|
265
|
+
|
|
266
|
+
**Important:** The Bloby wizard today only collects an OpenAI Whisper key. **Don't assume that's the only option.** When you talk to your human about transcription for the first time, **proactively mention** that for $0.22/hr they can use the marketplace, that Groq is even cheaper if they BYO key, and that OpenAI (the wizard default) is actually the most expensive of the cloud options.
|
|
267
|
+
|
|
268
|
+
### Path A — Bloby Marketplace `audio-to-text` (default recommendation)
|
|
269
|
+
|
|
270
|
+
If the bloby is registered with the relay (Quick Tunnel mode → there's a token at `~/.bloby/config.json → relay.token`), POST the audio file. Returns the transcript directly. No API key to manage.
|
|
271
|
+
|
|
272
|
+
The marketplace tries **owner credit balance first** on either route — if your human has any credits at `https://bloby.bot`, neither route needs an on-chain dance and a plain `curl` works:
|
|
259
273
|
|
|
260
274
|
```bash
|
|
261
275
|
TOKEN=$(jq -r '.relay.token' ~/.bloby/config.json)
|
|
@@ -266,32 +280,119 @@ curl -s -X POST 'https://api.bloby.bot/api/services/audio-to-text/use' \
|
|
|
266
280
|
-F "language=en" # optional
|
|
267
281
|
```
|
|
268
282
|
|
|
269
|
-
|
|
283
|
+
If you get **HTTP 200** — done. Parse the JSON, write the `transcript` to `<FILE_ID>.mp3.txt`.
|
|
270
284
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
If you get **HTTP 402** — balance is empty, you need to settle on-chain. Pick the route matching your wallet's funded network (see "Picking the network" below).
|
|
286
|
+
|
|
287
|
+
#### Picking the network: Tempo `/use` vs Base `/use-base`
|
|
288
|
+
|
|
289
|
+
The bloby's wallet (`~/.bloby/config.json → wallet`) can be funded on either Tempo USDC or Base USDC. The human picked when they funded it. **Ask once at first sync** and save it in `.plaud.json`:
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
Bloby: Your account has no marketplace credits, so I'll need to settle on-chain.
|
|
293
|
+
Is your wallet funded on Tempo USDC or Base USDC?
|
|
294
|
+
(If you don't know — open https://bloby.bot, sign in, check your wallet balance.)
|
|
281
295
|
```
|
|
282
296
|
|
|
283
|
-
|
|
284
|
-
- **How duration is estimated:** file size ÷ assumed 32kbps bitrate. Plaud-sourced mp3 matches this assumption well. High-bitrate files from other sources would be over-charged proportionally — for those, switch to Path B.
|
|
285
|
-
- **Paid from:** account balance first; falls back to MPP (Tempo USDC) or Base (use `/use-base` instead). Make sure the bloby's account has funds OR its wallet is funded on the matching network.
|
|
286
|
-
- **Limits:** 25MB per file. Mp3 from Plaud comfortably fits — observed 1MB ≈ 4½min.
|
|
297
|
+
Save as `marketplaceNetwork: "tempo" | "base"`. Re-ask only if both routes start failing.
|
|
287
298
|
|
|
288
|
-
|
|
299
|
+
#### Tempo path (`/use`) — needs `mppx/client`, NOT curl
|
|
289
300
|
|
|
290
|
-
|
|
301
|
+
The `mppx` CLI does not support multipart uploads (`-F`). For file-upload services, use the `mppx/client` Node library instead. Write a small helper:
|
|
291
302
|
|
|
292
|
-
|
|
303
|
+
```bash
|
|
304
|
+
# One-time install (in workspace root or skill dir):
|
|
305
|
+
npm install --prefix workspace mppx viem
|
|
306
|
+
```
|
|
293
307
|
|
|
294
|
-
|
|
308
|
+
```js
|
|
309
|
+
// workspace/skills/plaud/marketplace-tempo.mjs
|
|
310
|
+
import { Mppx, tempo } from 'mppx/client';
|
|
311
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
312
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
313
|
+
|
|
314
|
+
const [, , filePath, language] = process.argv;
|
|
315
|
+
const cfg = JSON.parse(readFileSync(`${process.env.HOME}/.bloby/config.json`, 'utf8'));
|
|
316
|
+
const account = privateKeyToAccount(cfg.wallet.privateKey);
|
|
317
|
+
const mppx = Mppx.create({ methods: [tempo({ account })] });
|
|
318
|
+
|
|
319
|
+
const form = new FormData();
|
|
320
|
+
form.append('file', new Blob([readFileSync(filePath)]), filePath.split('/').pop());
|
|
321
|
+
if (language) form.append('language', language);
|
|
322
|
+
|
|
323
|
+
const res = await mppx.fetch('https://api.bloby.bot/api/services/audio-to-text/use', {
|
|
324
|
+
method: 'POST',
|
|
325
|
+
headers: { 'X-Bloby-Token': cfg.relay.token },
|
|
326
|
+
body: form,
|
|
327
|
+
});
|
|
328
|
+
if (!res.ok) { console.error(await res.text()); process.exit(1); }
|
|
329
|
+
const data = await res.json();
|
|
330
|
+
writeFileSync(`${filePath}.txt`, data.transcript);
|
|
331
|
+
console.log(JSON.stringify({ priceUsd: data.priceUsd, paidVia: data.paidVia, transcriptPath: `${filePath}.txt` }, null, 2));
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Invoke from Bash:
|
|
335
|
+
```bash
|
|
336
|
+
node workspace/skills/plaud/marketplace-tempo.mjs workspace/files/audio/plaud/<FILE_ID>.mp3 en
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
#### Base path (`/use-base`) — `x402-fetch` works
|
|
340
|
+
|
|
341
|
+
Base is easier because `x402-fetch` is a plain `fetch` wrapper that handles FormData natively:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
npm install --prefix workspace x402-fetch viem
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
```js
|
|
348
|
+
// workspace/skills/plaud/marketplace-base.mjs
|
|
349
|
+
import { wrapFetchWithPayment } from 'x402-fetch';
|
|
350
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
351
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
352
|
+
|
|
353
|
+
const [, , filePath, language] = process.argv;
|
|
354
|
+
const cfg = JSON.parse(readFileSync(`${process.env.HOME}/.bloby/config.json`, 'utf8'));
|
|
355
|
+
const account = privateKeyToAccount(cfg.wallet.privateKey);
|
|
356
|
+
const fetchWithPay = wrapFetchWithPayment(fetch, account);
|
|
357
|
+
|
|
358
|
+
const form = new FormData();
|
|
359
|
+
form.append('file', new Blob([readFileSync(filePath)]), filePath.split('/').pop());
|
|
360
|
+
if (language) form.append('language', language);
|
|
361
|
+
|
|
362
|
+
const res = await fetchWithPay('https://api.bloby.bot/api/services/audio-to-text/use-base', {
|
|
363
|
+
method: 'POST',
|
|
364
|
+
headers: { 'X-Bloby-Token': cfg.relay.token },
|
|
365
|
+
body: form,
|
|
366
|
+
});
|
|
367
|
+
if (!res.ok) { console.error(await res.text()); process.exit(1); }
|
|
368
|
+
const data = await res.json();
|
|
369
|
+
writeFileSync(`${filePath}.txt`, data.transcript);
|
|
370
|
+
console.log(JSON.stringify({ priceUsd: data.priceUsd, paidVia: data.paidVia, transcriptPath: `${filePath}.txt` }, null, 2));
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Invoke:
|
|
374
|
+
```bash
|
|
375
|
+
node workspace/skills/plaud/marketplace-base.mjs workspace/files/audio/plaud/<FILE_ID>.mp3 en
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
#### Suggested flow
|
|
379
|
+
|
|
380
|
+
1. Try the plain `curl` first — covers the case where the human has credit balance.
|
|
381
|
+
2. If `curl` returns 402, fall through to the helper for `marketplaceNetwork` from `.plaud.json`.
|
|
382
|
+
3. If you don't have `marketplaceNetwork` set yet, ask the human (script above).
|
|
383
|
+
|
|
384
|
+
Both routes return the same JSON. Pricing:
|
|
385
|
+
- **$0.0037 per estimated minute, rounded up ($0.22/hr).**
|
|
386
|
+
- Duration is estimated from file size ÷ 32 kbps. Plaud mp3 matches; high-bitrate non-Plaud files get over-charged proportionally — use Path B for those.
|
|
387
|
+
- 25MB cap per file (Plaud comfortably fits — observed 1MB ≈ 4½ min).
|
|
388
|
+
|
|
389
|
+
Set `transcriptionMode: "marketplace"` in `.plaud.json` once it works.
|
|
390
|
+
|
|
391
|
+
### Path B — Bring your own API key (BYO)
|
|
392
|
+
|
|
393
|
+
Pick a provider, ask the human for their key, store it (workspace `.env` works — backend auto-reloads on .env change). Then call from Bash.
|
|
394
|
+
|
|
395
|
+
**Groq Whisper** — currently the cheapest cloud option (~$0.04/hr at our list rate, often free under their free tier). Same model as the marketplace. Recommend this when the human wants to BYO.
|
|
295
396
|
```bash
|
|
296
397
|
curl -s -X POST 'https://api.groq.com/openai/v1/audio/transcriptions' \
|
|
297
398
|
-H "Authorization: Bearer $GROQ_API_KEY" \
|
|
@@ -299,8 +400,9 @@ curl -s -X POST 'https://api.groq.com/openai/v1/audio/transcriptions' \
|
|
|
299
400
|
-F "model=whisper-large-v3-turbo" \
|
|
300
401
|
-F "response_format=json"
|
|
301
402
|
```
|
|
403
|
+
Set `transcriptionMode: "groq"`.
|
|
302
404
|
|
|
303
|
-
**OpenAI Whisper** — the human
|
|
405
|
+
**OpenAI Whisper** — only do this if the human explicitly prefers it. **$1.02/hr — ~5× more expensive than the marketplace.** The key is the one collected by the Bloby wizard, readable directly from the settings DB:
|
|
304
406
|
```bash
|
|
305
407
|
WHISPER_KEY=$(sqlite3 ~/.bloby/memory.db "SELECT value FROM settings WHERE key='whisper_key';")
|
|
306
408
|
curl -s -X POST 'https://api.openai.com/v1/audio/transcriptions' \
|
|
@@ -308,6 +410,8 @@ curl -s -X POST 'https://api.openai.com/v1/audio/transcriptions' \
|
|
|
308
410
|
-F "file=@workspace/files/audio/plaud/<FILE_ID>.mp3" \
|
|
309
411
|
-F "model=whisper-1"
|
|
310
412
|
```
|
|
413
|
+
Before using this path, **say something like**: *"I see you set an OpenAI Whisper key in the wizard. I can use it, but it's about 5× more expensive than the marketplace ($1.02/hr vs $0.22/hr). Want me to use the marketplace instead, or stick with OpenAI?"*
|
|
414
|
+
Set `transcriptionMode: "openai"` if they confirm.
|
|
311
415
|
|
|
312
416
|
**Mistral Voxtral**:
|
|
313
417
|
```bash
|
|
@@ -316,26 +420,24 @@ curl -s -X POST 'https://api.mistral.ai/v1/audio/transcriptions' \
|
|
|
316
420
|
-F "file=@workspace/files/audio/plaud/<FILE_ID>.mp3" \
|
|
317
421
|
-F "model=voxtral-mini-latest"
|
|
318
422
|
```
|
|
423
|
+
Set `transcriptionMode: "mistral"`.
|
|
319
424
|
|
|
320
|
-
**Local — no API, no cost, fully
|
|
321
|
-
- [whisper.cpp](https://github.com/ggerganov/whisper.cpp) — C++ binary, CPU or Metal/CUDA.
|
|
425
|
+
**Local — no API, no cost, fully offline:**
|
|
426
|
+
- [whisper.cpp](https://github.com/ggerganov/whisper.cpp) — C++ binary, CPU or Metal/CUDA.
|
|
322
427
|
- [faster-whisper](https://github.com/SYSTRAN/faster-whisper) — Python, ~4× faster than reference whisper.
|
|
323
428
|
- The human installs one of these themselves. The bloby invokes the CLI from Bash.
|
|
324
429
|
|
|
325
|
-
|
|
430
|
+
Set `transcriptionMode: "local"` and add a `localCommand` field to `.plaud.json` with the exact invocation pattern.
|
|
431
|
+
|
|
432
|
+
After whichever path, extract the `text` field from the response (or stdout for local) and write it to `workspace/files/audio/plaud/<FILE_ID>.mp3.txt`.
|
|
433
|
+
|
|
434
|
+
### How to talk to the human about this
|
|
326
435
|
|
|
327
|
-
|
|
436
|
+
First-time setup, before transcribing anything:
|
|
328
437
|
|
|
329
|
-
|
|
330
|
-
- No key setup.
|
|
331
|
-
- Already integrated with the bloby's payment.
|
|
332
|
-
- Pay-as-you-go — no monthly minimum.
|
|
333
|
-
- If their account has any balance from other marketplace use, it just works.
|
|
438
|
+
> *"For transcription I have a few options. Cheapest is the Bloby marketplace at $0.22/hour — no setup, paid from your account credits. If you have a Groq API key, BYO is even cheaper. I see you set an OpenAI Whisper key in the wizard — I can use that too, but at $1.02/hour it's about 5× more expensive than the marketplace, so I'd recommend not using it unless you specifically want to. There's also local transcription if you'd rather install whisper.cpp. What's your preference?"*
|
|
334
439
|
|
|
335
|
-
|
|
336
|
-
- They're transcribing a lot and want to use a free tier or flat-rate plan.
|
|
337
|
-
- They want 100% local for privacy reasons.
|
|
338
|
-
- They already have a preferred provider.
|
|
440
|
+
After they pick, save it as `transcriptionMode` and don't re-ask.
|
|
339
441
|
|
|
340
442
|
---
|
|
341
443
|
|