driggsby 0.10.1 → 0.10.3
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/assets/replay/att-01.m4a +0 -0
- package/assets/replay/att-02.m4a +0 -0
- package/assets/replay/att-03.m4a +0 -0
- package/assets/replay/att-04.m4a +0 -0
- package/assets/replay/att-05.m4a +0 -0
- package/assets/replay/att-06.m4a +0 -0
- package/assets/replay/att-07.m4a +0 -0
- package/assets/replay/att-08.m4a +0 -0
- package/assets/replay/att-09.m4a +0 -0
- package/assets/replay/att-10.m4a +0 -0
- package/assets/replay/att-11.m4a +0 -0
- package/assets/replay/att-12.m4a +0 -0
- package/assets/replay/att-13.m4a +0 -0
- package/assets/replay/att-14.m4a +0 -0
- package/assets/replay/att-15.m4a +0 -0
- package/assets/replay/att-16.m4a +0 -0
- package/assets/replay/att-17.m4a +0 -0
- package/assets/replay/att-18.m4a +0 -0
- package/assets/replay/att-19.m4a +0 -0
- package/assets/replay/att-20.m4a +0 -0
- package/assets/replay/att-21.m4a +0 -0
- package/assets/replay/att-22.m4a +0 -0
- package/assets/replay/att-23.m4a +0 -0
- package/assets/replay/att-24.m4a +0 -0
- package/assets/replay/att-25.m4a +0 -0
- package/dist/replay/att-story.js +38 -29
- package/dist/replay/feed-frame.js +16 -7
- package/dist/replay/replay-command.js +74 -55
- package/dist/replay/voice.js +130 -0
- package/package.json +3 -3
package/assets/replay/att-01.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-02.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-03.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-04.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-05.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-06.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-07.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-08.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-09.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-10.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-11.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-12.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-13.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-14.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-15.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-16.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-17.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-18.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-19.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-20.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-21.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-22.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-23.m4a
CHANGED
|
Binary file
|
package/assets/replay/att-24.m4a
CHANGED
|
Binary file
|
|
Binary file
|
package/dist/replay/att-story.js
CHANGED
|
@@ -101,6 +101,11 @@ export const STEPS = [
|
|
|
101
101
|
}
|
|
102
102
|
];
|
|
103
103
|
export const LINES = [
|
|
104
|
+
{
|
|
105
|
+
at: 30,
|
|
106
|
+
who: "att",
|
|
107
|
+
text: "Thanks for calling AT&T. In a few words, tell me what you’re calling about."
|
|
108
|
+
},
|
|
104
109
|
{
|
|
105
110
|
at: 40.1,
|
|
106
111
|
who: "agent",
|
|
@@ -209,7 +214,7 @@ export const LINES = [
|
|
|
209
214
|
{
|
|
210
215
|
at: 1506,
|
|
211
216
|
who: "agent",
|
|
212
|
-
text: "Thanks so much
|
|
217
|
+
text: "That sounds perfect. Thanks so much for your help. Bye!"
|
|
213
218
|
}
|
|
214
219
|
];
|
|
215
220
|
export const SILENT = {
|
|
@@ -219,10 +224,10 @@ export const SILENT = {
|
|
|
219
224
|
speak: new Map(),
|
|
220
225
|
};
|
|
221
226
|
export const VOICED = {
|
|
222
|
-
keys: [[0, 0], [40.1,
|
|
223
|
-
endStart:
|
|
224
|
-
total:
|
|
225
|
-
speak: new Map([[40.1, 1.039], [76.4, 3.648], [77.3, 0.361], [161.1, 2.943], [201.6, 0.4], [238.4, 2.19], [753.6, 1.374], [803.1, 9.006], [829.1, 1.496], [1110.5, 2.604], [1128.7, 2.329], [1130.1, 3.862], [1148.5, 3.215], [1205.6, 5.024], [1228.7, 3.584], [1260.8, 4.464], [1262.5, 3.167], [1275, 2.267], [1318.6, 3.522], [1331.3, 0.308], [1365.5, 2.138], [1506,
|
|
227
|
+
keys: [[0, 0], [30, 1.65], [40.1, 6.103], [76.4, 7.592], [77.3, 11.69], [161.1, 13.711], [201.6, 17.104], [238.4, 17.954], [753.6, 24.594], [803.1, 26.418], [829.1, 35.875], [1110.5, 39.029], [1128.7, 42.082], [1130.1, 44.862], [1148.5, 49.174], [1205.6, 52.839], [1228.7, 58.313], [1260.8, 62.346], [1262.5, 67.26], [1275, 70.877], [1318.6, 73.594], [1331.3, 77.566], [1365.5, 79.069], [1506, 81.657], [1509.3, 85.737]],
|
|
228
|
+
endStart: 86.537,
|
|
229
|
+
total: 96.537,
|
|
230
|
+
speak: new Map([[30, 4.003], [40.1, 1.039], [76.4, 3.648], [77.3, 0.361], [161.1, 2.943], [201.6, 0.4], [238.4, 2.19], [753.6, 1.374], [803.1, 9.006], [829.1, 1.496], [1110.5, 2.604], [1128.7, 2.329], [1130.1, 3.862], [1148.5, 3.215], [1205.6, 5.024], [1228.7, 3.584], [1260.8, 4.464], [1262.5, 3.167], [1275, 2.267], [1318.6, 3.522], [1331.3, 0.308], [1365.5, 2.138], [1506, 3.28]]),
|
|
226
231
|
};
|
|
227
232
|
// Each sound and the replay second it starts at, on the voiced timeline.
|
|
228
233
|
export const CUES = [
|
|
@@ -231,99 +236,103 @@ export const CUES = [
|
|
|
231
236
|
file: "att-00.m4a"
|
|
232
237
|
},
|
|
233
238
|
{
|
|
234
|
-
at: 1.
|
|
239
|
+
at: 1.65,
|
|
235
240
|
file: "att-01.m4a"
|
|
236
241
|
},
|
|
237
242
|
{
|
|
238
|
-
at:
|
|
243
|
+
at: 6.103,
|
|
239
244
|
file: "att-02.m4a"
|
|
240
245
|
},
|
|
241
246
|
{
|
|
242
|
-
at: 7.
|
|
247
|
+
at: 7.592,
|
|
243
248
|
file: "att-03.m4a"
|
|
244
249
|
},
|
|
245
250
|
{
|
|
246
|
-
at:
|
|
251
|
+
at: 11.69,
|
|
247
252
|
file: "att-04.m4a"
|
|
248
253
|
},
|
|
249
254
|
{
|
|
250
|
-
at:
|
|
255
|
+
at: 13.711,
|
|
251
256
|
file: "att-05.m4a"
|
|
252
257
|
},
|
|
253
258
|
{
|
|
254
|
-
at:
|
|
259
|
+
at: 17.104,
|
|
255
260
|
file: "att-06.m4a"
|
|
256
261
|
},
|
|
257
262
|
{
|
|
258
|
-
at:
|
|
263
|
+
at: 17.954,
|
|
259
264
|
file: "att-07.m4a"
|
|
260
265
|
},
|
|
261
266
|
{
|
|
262
|
-
at: 20.
|
|
267
|
+
at: 20.344,
|
|
263
268
|
file: "att-08.m4a"
|
|
264
269
|
},
|
|
265
270
|
{
|
|
266
|
-
at:
|
|
271
|
+
at: 24.594,
|
|
267
272
|
file: "att-09.m4a"
|
|
268
273
|
},
|
|
269
274
|
{
|
|
270
|
-
at:
|
|
275
|
+
at: 26.418,
|
|
271
276
|
file: "att-10.m4a"
|
|
272
277
|
},
|
|
273
278
|
{
|
|
274
|
-
at:
|
|
279
|
+
at: 35.875,
|
|
275
280
|
file: "att-11.m4a"
|
|
276
281
|
},
|
|
277
282
|
{
|
|
278
|
-
at:
|
|
283
|
+
at: 39.029,
|
|
279
284
|
file: "att-12.m4a"
|
|
280
285
|
},
|
|
281
286
|
{
|
|
282
|
-
at:
|
|
287
|
+
at: 42.082,
|
|
283
288
|
file: "att-13.m4a"
|
|
284
289
|
},
|
|
285
290
|
{
|
|
286
|
-
at: 44.
|
|
291
|
+
at: 44.862,
|
|
287
292
|
file: "att-14.m4a"
|
|
288
293
|
},
|
|
289
294
|
{
|
|
290
|
-
at:
|
|
295
|
+
at: 49.174,
|
|
291
296
|
file: "att-15.m4a"
|
|
292
297
|
},
|
|
293
298
|
{
|
|
294
|
-
at:
|
|
299
|
+
at: 52.839,
|
|
295
300
|
file: "att-16.m4a"
|
|
296
301
|
},
|
|
297
302
|
{
|
|
298
|
-
at:
|
|
303
|
+
at: 58.313,
|
|
299
304
|
file: "att-17.m4a"
|
|
300
305
|
},
|
|
301
306
|
{
|
|
302
|
-
at: 62.
|
|
307
|
+
at: 62.346,
|
|
303
308
|
file: "att-18.m4a"
|
|
304
309
|
},
|
|
305
310
|
{
|
|
306
|
-
at:
|
|
311
|
+
at: 67.26,
|
|
307
312
|
file: "att-19.m4a"
|
|
308
313
|
},
|
|
309
314
|
{
|
|
310
|
-
at:
|
|
315
|
+
at: 70.877,
|
|
311
316
|
file: "att-20.m4a"
|
|
312
317
|
},
|
|
313
318
|
{
|
|
314
|
-
at: 73.
|
|
319
|
+
at: 73.594,
|
|
315
320
|
file: "att-21.m4a"
|
|
316
321
|
},
|
|
317
322
|
{
|
|
318
|
-
at:
|
|
323
|
+
at: 77.566,
|
|
319
324
|
file: "att-22.m4a"
|
|
320
325
|
},
|
|
321
326
|
{
|
|
322
|
-
at:
|
|
327
|
+
at: 79.069,
|
|
323
328
|
file: "att-23.m4a"
|
|
324
329
|
},
|
|
325
330
|
{
|
|
326
|
-
at:
|
|
331
|
+
at: 81.657,
|
|
327
332
|
file: "att-24.m4a"
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
at: 86.537,
|
|
336
|
+
file: "att-25.m4a"
|
|
328
337
|
}
|
|
329
338
|
];
|
|
@@ -87,7 +87,7 @@ export function renderFrame(input) {
|
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
if (ended)
|
|
90
|
-
feed.push(...result(input.pt - timeline.endStart, w, narrow));
|
|
90
|
+
feed.push(...result(input.pt - timeline.endStart, w, narrow, input.vote ?? "idle"));
|
|
91
91
|
return { feed, doneAt };
|
|
92
92
|
};
|
|
93
93
|
const room = rows - 6;
|
|
@@ -154,7 +154,7 @@ function checkRows(checks, real, inner, narrow, guide) {
|
|
|
154
154
|
out.push(line(guide(), ...checks.slice(r, r + 2).flatMap((c) => check(c, cw))));
|
|
155
155
|
return out;
|
|
156
156
|
}
|
|
157
|
-
function result(t, w, narrow) {
|
|
157
|
+
function result(t, w, narrow, vote) {
|
|
158
158
|
const after = CALL.before + (CALL.after - CALL.before) * ease(t / 0.9);
|
|
159
159
|
const saved = `${money((CALL.before - CALL.after) * CALL.months)} saved`;
|
|
160
160
|
const at = (n) => t >= 0.15 * n;
|
|
@@ -172,7 +172,9 @@ function result(t, w, narrow) {
|
|
|
172
172
|
if (at(2))
|
|
173
173
|
out.push(line(seg("Verizon, same week: ", C.mute), seg("$95.71", C.faint, { strike: true }), seg(" → ", C.mute), seg("$74.01", C.money)));
|
|
174
174
|
if (at(3))
|
|
175
|
-
out.push(line(),
|
|
175
|
+
out.push(line(), vote === "counted"
|
|
176
|
+
? line(seg("✓", C.money, { bold: true }), seg(" Your vote is in. Thanks.", C.text, { bold: true }))
|
|
177
|
+
: line(seg(" j ", C.ink, { bg: C.money, bold: true }), seg(" Vote to roll this out", C.text, { bold: true })));
|
|
176
178
|
return out;
|
|
177
179
|
}
|
|
178
180
|
// Too tall for the window: fold the earliest finished steps into one line
|
|
@@ -201,12 +203,19 @@ function bottomBar(real, pt, ended, w, narrow, timeline) {
|
|
|
201
203
|
return spread(left, right, w);
|
|
202
204
|
}
|
|
203
205
|
function help(input, w, narrow) {
|
|
204
|
-
if (input.join === "open") {
|
|
205
|
-
return spread([seg("Count your vote to roll this out?", C.text, { bold: true })], [seg("enter", C.money, { bold: true }), seg(" yes ", C.mute), seg("esc", C.text), seg(" not now", C.mute)], w);
|
|
206
|
-
}
|
|
207
206
|
const keys = narrow
|
|
208
207
|
? [["space", input.paused ? "play" : "pause"], ["q", "quit"]]
|
|
209
208
|
: [["space", input.paused ? "play" : "pause"], ["f", input.fast ? "1×" : "2×"], ["r", "replay"], ["q", "quit"]];
|
|
210
209
|
const left = keys.flatMap(([key, what], i) => [seg((i > 0 ? (narrow ? " " : " · ") : "") + key, C.mute), seg(` ${what}`, C.faint)]);
|
|
211
|
-
|
|
210
|
+
// j belongs to the ending: nothing about it shows while the call plays.
|
|
211
|
+
if (input.pt < input.timeline.endStart)
|
|
212
|
+
return line(...left);
|
|
213
|
+
// Once pressed, this spot says how the vote went.
|
|
214
|
+
const state = input.vote ?? "idle";
|
|
215
|
+
const vote = state === "counting" ? [seg("counting your vote…", C.mute)]
|
|
216
|
+
: state === "counted" ? [seg("✓ ", C.money, { bold: true }), seg("your vote is in", C.money)]
|
|
217
|
+
: state === "closed" ? [seg("voting has closed", C.faint)]
|
|
218
|
+
: state === "failed" ? [seg("couldn’t count it · ", C.faint), seg(" j ", C.ink, { bg: C.money, bold: true }), seg(" try again", C.money)]
|
|
219
|
+
: [seg(" j ", C.ink, { bg: C.money, bold: true }), seg(" vote to roll this out", C.money)];
|
|
220
|
+
return spread(left, vote, w);
|
|
212
221
|
}
|
|
@@ -2,70 +2,88 @@
|
|
|
2
2
|
// internet bill, in the terminal. On a Mac it plays with sound (recreated
|
|
3
3
|
// voices, the call's real words), paced around them; elsewhere, or with
|
|
4
4
|
// --quiet, it plays silently and faster. Space pauses, f plays at 2x, r
|
|
5
|
-
// starts over, the arrows move two seconds, q quits
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
// starts over, the arrows move two seconds, and q quits; the voice follows
|
|
6
|
+
// each of them in place. Once the call has played to the end, j votes to roll
|
|
7
|
+
// the feature out: signed in, the vote counts in place; signed out, it signs
|
|
8
|
+
// in first, votes, and the command ends.
|
|
10
9
|
import { CliError } from "../cli-error.js";
|
|
11
10
|
import { defaultCredentialEnvironment, readStoredToken } from "../credentials/store.js";
|
|
12
11
|
import { deployFailure, requireDeploySession } from "../deploy/api-session.js";
|
|
13
12
|
import { runLogin } from "../login/login.js";
|
|
14
13
|
import { CUES, SILENT, VOICED } from "./att-story.js";
|
|
15
14
|
import { MIN_COLS, MIN_ROWS, renderFrame } from "./feed-frame.js";
|
|
15
|
+
import { lineAt, startVoice } from "./voice.js";
|
|
16
16
|
import { castVote } from "./vote.js";
|
|
17
|
-
const PLAYER = "/usr/bin/afplay";
|
|
18
17
|
const TICK_MS = 33;
|
|
19
18
|
const RETRY_COMMAND = "npx driggsby@latest replay att";
|
|
20
19
|
export async function runReplay(options, environment = defaultCredentialEnvironment()) {
|
|
21
20
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
22
21
|
throw new CliError(`The replay plays in an interactive terminal. Open one and run:\n ${RETRY_COMMAND}`, 1);
|
|
23
22
|
}
|
|
24
|
-
const
|
|
25
|
-
const ending = await play(
|
|
23
|
+
const voice = options.quiet ? null : await startVoice();
|
|
24
|
+
const ending = await play(voice === null ? SILENT : VOICED, voice, environment);
|
|
26
25
|
if (ending === "quit")
|
|
27
26
|
return 0;
|
|
28
|
-
|
|
27
|
+
// Signing in needs the terminal: the replay has ended, so sign in, vote, done.
|
|
28
|
+
process.stdout.write("To count your vote, sign in to Driggsby first. It takes about a minute.\n\n");
|
|
29
|
+
await runLogin(environment);
|
|
30
|
+
const vote = await voteSignedIn(environment);
|
|
31
|
+
if (vote === "failed")
|
|
32
|
+
return 1;
|
|
33
|
+
process.stdout.write(vote === "counted" ? "\nThanks. Your vote to roll this out is in.\n" : "\nVoting on this has closed. Thanks for watching.\n");
|
|
34
|
+
return 0;
|
|
29
35
|
}
|
|
30
|
-
function
|
|
36
|
+
async function voteSignedIn(environment) {
|
|
37
|
+
const session = await requireDeploySession(environment);
|
|
38
|
+
try {
|
|
39
|
+
return (await castVote(session)) === "voted" ? "counted" : "closed";
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// The reason stays in the terminal's own words (a sign-in to redo, a
|
|
43
|
+
// network to allow); the replay itself only shows that it didn't count.
|
|
44
|
+
process.stdout.write(`${deployFailure(error, session.baseUrl, RETRY_COMMAND).message}\n`);
|
|
45
|
+
return "failed";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function play(timeline, voice, environment) {
|
|
31
49
|
const stdin = process.stdin;
|
|
32
50
|
const stdout = process.stdout;
|
|
33
|
-
const playing = new Set();
|
|
34
|
-
const hush = () => {
|
|
35
|
-
for (const child of playing)
|
|
36
|
-
child.kill();
|
|
37
|
-
playing.clear();
|
|
38
|
-
};
|
|
39
|
-
const playCue = (file, fast) => {
|
|
40
|
-
const path = fileURLToPath(new URL(`../../assets/replay/${file}`, import.meta.url));
|
|
41
|
-
const child = spawn(PLAYER, fast ? ["-r", "2", path] : [path], { stdio: "ignore" });
|
|
42
|
-
child.on("error", () => playing.delete(child));
|
|
43
|
-
child.on("exit", () => playing.delete(child));
|
|
44
|
-
playing.add(child);
|
|
45
|
-
};
|
|
46
51
|
let pt = 0;
|
|
47
52
|
let paused = false;
|
|
48
53
|
let fast = false;
|
|
54
|
+
let vote = "idle";
|
|
49
55
|
let last = Date.now();
|
|
50
56
|
// A full-screen app: the whole terminal, the header at the top and the
|
|
51
57
|
// progress bar and keys anchored to the bottom.
|
|
52
58
|
const draw = () => {
|
|
53
59
|
const cols = Math.max(MIN_COLS, stdout.columns - 1);
|
|
54
60
|
const rows = Math.max(MIN_ROWS, stdout.rows);
|
|
55
|
-
const frame = renderFrame({ pt, nowMs: Date.now(), cols, rows, timeline,
|
|
61
|
+
const frame = renderFrame({ pt, nowMs: Date.now(), cols, rows, timeline, vote, paused, fast });
|
|
56
62
|
stdout.write(`\u001b[H${frame.join("\r\n")}`);
|
|
57
63
|
};
|
|
64
|
+
const rate = () => (fast ? 2 : 1);
|
|
65
|
+
// A jump picks up whatever line is being said at the new moment, mid-word.
|
|
58
66
|
const seek = (to) => {
|
|
59
|
-
hush();
|
|
60
67
|
pt = Math.max(0, Math.min(timeline.total, to));
|
|
68
|
+
if (voice === null)
|
|
69
|
+
return;
|
|
70
|
+
const line = lineAt(CUES, pt);
|
|
71
|
+
if (line === null)
|
|
72
|
+
voice.stop();
|
|
73
|
+
else
|
|
74
|
+
voice.play(line.file, line.offset, rate(), paused);
|
|
61
75
|
};
|
|
62
76
|
return new Promise((resolve) => {
|
|
63
77
|
stdout.write("\u001b[?1049h\u001b[?25l\u001b[2J");
|
|
64
78
|
stdin.setRawMode(true);
|
|
65
79
|
stdin.resume();
|
|
80
|
+
let done = false;
|
|
66
81
|
const finish = (ending) => {
|
|
82
|
+
if (done)
|
|
83
|
+
return;
|
|
84
|
+
done = true;
|
|
67
85
|
clearInterval(timer);
|
|
68
|
-
|
|
86
|
+
voice?.close();
|
|
69
87
|
stdin.off("data", onKey);
|
|
70
88
|
stdin.setRawMode(false);
|
|
71
89
|
stdin.pause();
|
|
@@ -73,6 +91,23 @@ function play(timeline, sound) {
|
|
|
73
91
|
stdout.write("\u001b[?25h\u001b[?1049l");
|
|
74
92
|
resolve(ending);
|
|
75
93
|
};
|
|
94
|
+
// j, signed in: the vote counts in place and the bar shows how it went.
|
|
95
|
+
const castInPlace = async () => {
|
|
96
|
+
if ((await readStoredToken(environment)) === null) {
|
|
97
|
+
finish("sign-in");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
vote = "counting";
|
|
101
|
+
try {
|
|
102
|
+
const session = await requireDeploySession(environment);
|
|
103
|
+
vote = (await castVote(session)) === "voted" ? "counted" : "closed";
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
vote = "failed";
|
|
107
|
+
}
|
|
108
|
+
if (!done)
|
|
109
|
+
draw();
|
|
110
|
+
};
|
|
76
111
|
const onResize = () => {
|
|
77
112
|
stdout.write("\u001b[2J");
|
|
78
113
|
draw();
|
|
@@ -83,22 +118,25 @@ function play(timeline, sound) {
|
|
|
83
118
|
finish("quit");
|
|
84
119
|
return;
|
|
85
120
|
}
|
|
86
|
-
// j votes right away: no prompt to confirm.
|
|
87
121
|
if (key === "j") {
|
|
88
|
-
|
|
89
|
-
|
|
122
|
+
// Only once the call has played to the end.
|
|
123
|
+
if (pt >= timeline.endStart && (vote === "idle" || vote === "failed"))
|
|
124
|
+
void castInPlace();
|
|
90
125
|
}
|
|
91
|
-
if (key === " ") {
|
|
92
|
-
hush();
|
|
126
|
+
else if (key === " ") {
|
|
93
127
|
paused = !paused;
|
|
128
|
+
if (paused)
|
|
129
|
+
voice?.pause();
|
|
130
|
+
else
|
|
131
|
+
voice?.resume();
|
|
94
132
|
}
|
|
95
133
|
else if (key === "f") {
|
|
96
|
-
hush();
|
|
97
134
|
fast = !fast;
|
|
135
|
+
voice?.rate(rate());
|
|
98
136
|
}
|
|
99
137
|
else if (key === "r") {
|
|
100
|
-
seek(0);
|
|
101
138
|
paused = false;
|
|
139
|
+
seek(0);
|
|
102
140
|
}
|
|
103
141
|
else if (key === "\u001b[C")
|
|
104
142
|
seek(pt + 2);
|
|
@@ -111,11 +149,11 @@ function play(timeline, sound) {
|
|
|
111
149
|
const dt = (now - last) / 1000;
|
|
112
150
|
last = now;
|
|
113
151
|
if (!paused && pt < timeline.total) {
|
|
114
|
-
const next = Math.min(timeline.total, pt + dt * (
|
|
115
|
-
if (
|
|
152
|
+
const next = Math.min(timeline.total, pt + dt * rate());
|
|
153
|
+
if (voice !== null)
|
|
116
154
|
for (const cue of CUES)
|
|
117
155
|
if (cue.at > pt && cue.at <= next)
|
|
118
|
-
|
|
156
|
+
voice.play(cue.file, 0, rate(), false);
|
|
119
157
|
pt = next;
|
|
120
158
|
}
|
|
121
159
|
draw();
|
|
@@ -125,22 +163,3 @@ function play(timeline, sound) {
|
|
|
125
163
|
draw();
|
|
126
164
|
});
|
|
127
165
|
}
|
|
128
|
-
async function vote(environment) {
|
|
129
|
-
const out = (text) => {
|
|
130
|
-
process.stdout.write(text);
|
|
131
|
-
};
|
|
132
|
-
if ((await readStoredToken(environment)) === null) {
|
|
133
|
-
out("To count your vote, sign in to Driggsby first. It takes about a minute.\n\n");
|
|
134
|
-
await runLogin(environment);
|
|
135
|
-
out("\n");
|
|
136
|
-
}
|
|
137
|
-
const session = await requireDeploySession(environment);
|
|
138
|
-
try {
|
|
139
|
-
const outcome = await castVote(session);
|
|
140
|
-
out(outcome === "voted" ? "Thanks. Your vote to roll this out is in.\n" : "Voting on this has closed. Thanks for watching.\n");
|
|
141
|
-
return 0;
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
throw deployFailure(error, session.baseUrl, RETRY_COMMAND);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// The replay's voice on a Mac: one player, macOS's own, driven over a pipe
|
|
2
|
+
// through the built-in osascript. It pauses, resumes, changes speed and
|
|
3
|
+
// stops in place and at once, and only one line ever plays. When the replay
|
|
4
|
+
// ends, or its process dies, the pipe closes and the player stops and exits.
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
const OSASCRIPT = "/usr/bin/osascript";
|
|
9
|
+
const READY_MS = 3_000;
|
|
10
|
+
// Commands arrive one per line, tab-separated: play <path> <offset> <rate>
|
|
11
|
+
// <paused 0|1>, pause, resume, rate <rate>, stop. A play past the end of its
|
|
12
|
+
// file stays silent, and a resume only continues a line a pause (or a paused
|
|
13
|
+
// jump) held, never one that had already finished. End of input stops the
|
|
14
|
+
// player and exits.
|
|
15
|
+
const PLAYER_SCRIPT = `
|
|
16
|
+
ObjC.import("AVFoundation");
|
|
17
|
+
ObjC.import("Foundation");
|
|
18
|
+
function run() {
|
|
19
|
+
const input = $.NSFileHandle.fileHandleWithStandardInput;
|
|
20
|
+
const output = $.NSFileHandle.fileHandleWithStandardOutput;
|
|
21
|
+
output.writeData($("ready\\n").dataUsingEncoding($.NSUTF8StringEncoding));
|
|
22
|
+
let player = null;
|
|
23
|
+
let held = false;
|
|
24
|
+
let buffer = "";
|
|
25
|
+
for (;;) {
|
|
26
|
+
const data = input.availableData;
|
|
27
|
+
if (Number(data.length) === 0) break;
|
|
28
|
+
buffer += $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding).js;
|
|
29
|
+
let end;
|
|
30
|
+
while ((end = buffer.indexOf("\\n")) >= 0) {
|
|
31
|
+
const parts = buffer.slice(0, end).split("\\t");
|
|
32
|
+
buffer = buffer.slice(end + 1);
|
|
33
|
+
const command = parts[0];
|
|
34
|
+
if (command === "play") {
|
|
35
|
+
if (player) player.stop;
|
|
36
|
+
player = null;
|
|
37
|
+
held = false;
|
|
38
|
+
const next = $.AVAudioPlayer.alloc.initWithContentsOfURLError($.NSURL.fileURLWithPath(parts[1]), null);
|
|
39
|
+
const offset = Number(parts[2]);
|
|
40
|
+
if (!next.isNil() && offset >= 0 && offset < next.duration) {
|
|
41
|
+
next.enableRate = true;
|
|
42
|
+
next.prepareToPlay;
|
|
43
|
+
next.rate = Number(parts[3]);
|
|
44
|
+
next.currentTime = offset;
|
|
45
|
+
if (parts[4] === "1") held = true;
|
|
46
|
+
else next.play;
|
|
47
|
+
player = next;
|
|
48
|
+
}
|
|
49
|
+
} else if (command === "pause") {
|
|
50
|
+
held = Boolean(player && player.playing);
|
|
51
|
+
if (held) player.pause;
|
|
52
|
+
} else if (command === "resume") {
|
|
53
|
+
if (held) player.play;
|
|
54
|
+
held = false;
|
|
55
|
+
} else if (command === "rate") {
|
|
56
|
+
if (player) player.rate = Number(parts[1]);
|
|
57
|
+
} else if (command === "stop") {
|
|
58
|
+
if (player) player.stop;
|
|
59
|
+
player = null;
|
|
60
|
+
held = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (player) player.stop;
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
// The line under a moment of the replay: the last sound started at or before
|
|
68
|
+
// it, and how far into it the moment is (past its end, the player stays silent).
|
|
69
|
+
export function lineAt(cues, pt) {
|
|
70
|
+
let found = null;
|
|
71
|
+
for (const cue of cues) {
|
|
72
|
+
if (cue.at > pt)
|
|
73
|
+
break;
|
|
74
|
+
found = cue;
|
|
75
|
+
}
|
|
76
|
+
return found === null ? null : { file: found.file, offset: pt - found.at };
|
|
77
|
+
}
|
|
78
|
+
// One command line for the player; null for a path it couldn't carry.
|
|
79
|
+
export function playerCommand(parts) {
|
|
80
|
+
const fields = parts.map(String);
|
|
81
|
+
return fields.some((field) => /[\t\n\r]/.test(field)) ? null : `${fields.join("\t")}\n`;
|
|
82
|
+
}
|
|
83
|
+
// The player, ready to speak, or null where there is none (not a Mac, or it
|
|
84
|
+
// didn't start): the replay then plays silently.
|
|
85
|
+
export function startVoice() {
|
|
86
|
+
if (process.platform !== "darwin" || !existsSync(OSASCRIPT))
|
|
87
|
+
return Promise.resolve(null);
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const child = spawn(OSASCRIPT, ["-l", "JavaScript", "-e", PLAYER_SCRIPT], { stdio: ["pipe", "pipe", "ignore"] });
|
|
90
|
+
let settled = false;
|
|
91
|
+
const settle = (voice) => {
|
|
92
|
+
if (settled)
|
|
93
|
+
return;
|
|
94
|
+
settled = true;
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
if (voice === null)
|
|
97
|
+
child.kill();
|
|
98
|
+
resolve(voice);
|
|
99
|
+
};
|
|
100
|
+
const timer = setTimeout(() => { settle(null); }, READY_MS);
|
|
101
|
+
child.on("error", () => { settle(null); });
|
|
102
|
+
child.on("exit", () => { settle(null); });
|
|
103
|
+
// The player may be gone before a write lands; a lost line is silence.
|
|
104
|
+
child.stdin.on("error", () => undefined);
|
|
105
|
+
child.stdout.on("data", (chunk) => {
|
|
106
|
+
if (chunk.toString("utf8").includes("ready"))
|
|
107
|
+
settle(voiceOver(child));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function voiceOver(child) {
|
|
112
|
+
const send = (parts) => {
|
|
113
|
+
const line = playerCommand(parts);
|
|
114
|
+
if (line !== null && child.stdin.writable)
|
|
115
|
+
child.stdin.write(line);
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
play: (file, offset, rate, paused) => {
|
|
119
|
+
send(["play", fileURLToPath(new URL(`../../assets/replay/${file}`, import.meta.url)), offset.toFixed(3), rate, paused ? 1 : 0]);
|
|
120
|
+
},
|
|
121
|
+
pause: () => { send(["pause"]); },
|
|
122
|
+
resume: () => { send(["resume"]); },
|
|
123
|
+
rate: (rate) => { send(["rate", rate]); },
|
|
124
|
+
stop: () => { send(["stop"]); },
|
|
125
|
+
close: () => {
|
|
126
|
+
send(["stop"]);
|
|
127
|
+
child.stdin.end();
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driggsby",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Set up and build on Driggsby, the MCP server for personal finance.",
|
|
5
5
|
"license": "Apache-2.0 AND OFL-1.1",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"node": ">=18"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@driggsby/deploy": "0.10.
|
|
35
|
-
"@driggsby/sdk": "0.10.
|
|
34
|
+
"@driggsby/deploy": "0.10.3",
|
|
35
|
+
"@driggsby/sdk": "0.10.3"
|
|
36
36
|
}
|
|
37
37
|
}
|