@drakulavich/kesha-voice-kit 1.19.0-beta.2 → 1.19.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/SKILL.md +27 -3
- package/man/kesha.1 +1 -1
- package/package.json +2 -2
- package/src/cli/dispatch.ts +21 -67
- package/src/diagnostic-log.ts +5 -15
- package/src/engine.ts +11 -10
- package/src/install-hint.ts +12 -0
- package/src/status.ts +2 -1
- package/src/synth.ts +2 -1
- package/src/transcribe.ts +5 -7
package/SKILL.md
CHANGED
|
@@ -25,6 +25,7 @@ Local voice toolkit: transcribe voice messages to text, synthesize speech, detec
|
|
|
25
25
|
- **Need to send a voice note (Telegram, WhatsApp, Signal, Discord)**: synthesize directly into messenger-native OGG/Opus with `kesha say --format ogg-opus --out reply.ogg "<text>"`. Default is mono 24 kHz @ 32 kbps - what Telegram `sendVoice` expects. No WAV redirect and no `ffmpeg` round-trip.
|
|
26
26
|
- **Need local file playback/debug output**: WAV is still available with `kesha say --out reply.wav "<text>"`, but do not use WAV for Telegram voice replies. Auto-routes by detected language (Kokoro-82M for English, Vosk-TTS for Russian). On darwin-arm64, English Kokoro uses FluidAudio CoreML instead of ONNX. For other languages and ~180 more voices use `--voice macos-*` on macOS (zero model download).
|
|
27
27
|
- **Need to detect what language a file is in** before choosing a pipeline: `kesha --json audio.ogg` returns both audio-based and text-based language detection with confidence scores.
|
|
28
|
+
- **Need to capture your own voice** for transcription or as a voice-note source: `kesha record --out clip.wav` records up to 120s (override with `--max-seconds`) of mono 16 kHz WAV from the default microphone. Pipe straight into `kesha --json clip.wav` to close the loop.
|
|
28
29
|
|
|
29
30
|
## OpenClaw plugin setup
|
|
30
31
|
|
|
@@ -150,9 +151,12 @@ Each `segment.speaker` is a number (cluster id, stable within one file). On Linu
|
|
|
150
151
|
- `kesha audio.ogg` — plain transcript on stdout
|
|
151
152
|
- `kesha --format transcript audio.ogg` — transcript + `[lang: ru, confidence: 0.99]` footer
|
|
152
153
|
- `kesha --json --timestamps audio.ogg` — JSON with timestamped `segments`
|
|
154
|
+
- `kesha --toon audio.ogg` — TOON (compact, LLM-friendly JSON encoding); preferred when piping multi-file results to an LLM/agent
|
|
153
155
|
- `kesha --verbose audio.ogg` — human-readable with language info
|
|
154
156
|
- `kesha --lang en audio.ogg` — warn if detected language differs (useful sanity check)
|
|
155
157
|
|
|
158
|
+
**Long audio:** files ≥ 120 s auto-engage Silero VAD chunking; force on with `--vad` or off with `--no-vad`. Short files use full-file ASR by default.
|
|
159
|
+
|
|
156
160
|
## TTS: synthesize speech
|
|
157
161
|
|
|
158
162
|
```bash
|
|
@@ -188,12 +192,25 @@ Format is also inferred from `--out` extension (`.ogg` / `.opus` / `.oga` → OG
|
|
|
188
192
|
|
|
189
193
|
## Install
|
|
190
194
|
|
|
195
|
+
**Humans use `kesha init` (guided). Agents and scripts use `kesha install` (deterministic).**
|
|
196
|
+
|
|
191
197
|
```bash
|
|
192
|
-
bun add -g @drakulavich/kesha-voice-kit # global CLI install
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
bun add -g @drakulavich/kesha-voice-kit # global CLI install (always first)
|
|
199
|
+
|
|
200
|
+
# For humans: interactive setup that prompts for backend / TTS / VAD / diarize
|
|
201
|
+
kesha init
|
|
202
|
+
|
|
203
|
+
# For agents and CI: explicit, scriptable install commands
|
|
204
|
+
kesha install # engine only (~350 MB)
|
|
205
|
+
kesha install --tts # + Kokoro + Vosk-TTS RU (~990 MB more)
|
|
206
|
+
kesha install --tts --vad # + Silero VAD (long-audio chunking)
|
|
207
|
+
kesha install --tts --vad --diarize # + speaker diarization (darwin-arm64 only)
|
|
195
208
|
```
|
|
196
209
|
|
|
210
|
+
Kesha's runtime error/warning messages adapt to the same split: when `kesha` is invoked from a TTY, hints suggest `kesha init`; when stderr is piped (CI logs, OpenClaw, agent subprocess), hints suggest the equivalent `kesha install [...flags]`. Both run the same install code under the hood — pick the one your caller is.
|
|
211
|
+
|
|
212
|
+
For pre-release builds: `bun add -g @drakulavich/kesha-voice-kit@beta` (current `beta` channel; `@latest` stays on the last stable release).
|
|
213
|
+
|
|
197
214
|
No system deps — English G2P is embedded (`misaki-rs`); Russian G2P is bundled inside Vosk-TTS. `macos-*` voices need no install either — they use voices already on the Mac.
|
|
198
215
|
|
|
199
216
|
## Supported languages
|
|
@@ -207,6 +224,13 @@ No system deps — English G2P is embedded (`misaki-rs`); Russian G2P is bundled
|
|
|
207
224
|
- ASR: ~19× faster than OpenAI Whisper on Apple Silicon (CoreML via FluidAudio), ~2.5× on CPU (ONNX via `ort`).
|
|
208
225
|
- TTS: sub-second latency for short utterances on Apple Silicon.
|
|
209
226
|
|
|
227
|
+
## Troubleshooting
|
|
228
|
+
|
|
229
|
+
- `kesha doctor` — collect support diagnostics without changing local state. Add `--json` for machine-readable output, `--redact` to scrub secrets and home paths before sharing.
|
|
230
|
+
- `kesha logs` — manage privacy-safe local diagnostic logs. Default mode is `retain-on-failure` (events buffered in memory, flushed to disk only when a command fails). `kesha logs mode on` captures every run, `kesha logs path` prints the NDJSON file, `kesha logs disable` turns it off entirely.
|
|
231
|
+
- `kesha support-bundle --output bundle.tar.gz` — produce a redacted `.tar.gz` for filing an issue. Add `--include-logs` to bundle a bounded tail of diagnostic logs.
|
|
232
|
+
- `kesha stats` — manage local anonymous performance stats (per-command latency percentiles). Actions: `enable | disable | status | week | errors | export | reset | vacuum | retention`. Stays on the machine.
|
|
233
|
+
|
|
210
234
|
## Why local
|
|
211
235
|
|
|
212
236
|
No API keys to manage. No per-minute billing. Voice data never leaves the machine — important for regulated industries, personal messaging, and anything that shouldn't be in a third-party log.
|
package/man/kesha.1
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drakulavich/kesha-voice-kit",
|
|
3
|
-
"version": "1.19.0
|
|
3
|
+
"version": "1.19.0",
|
|
4
4
|
"description": "Give your local tools a voice: fast STT in 25 languages (~19x faster than Whisper on Apple Silicon, ONNX CPU fallback), TTS via Kokoro/Vosk/macOS voices, VAD + 107-language detection. Rust engine, Bun CLI, OpenClaw skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
]
|
|
33
33
|
},
|
|
34
34
|
"keshaEngine": {
|
|
35
|
-
"version": "1.19.0
|
|
35
|
+
"version": "1.19.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"test": "bun test",
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runMain } from "citty";
|
|
1
|
+
import { runMain, type CommandDef } from "citty";
|
|
2
2
|
import { existsSync } from "fs";
|
|
3
3
|
import { log } from "../log";
|
|
4
4
|
import { suggestCommand } from "../suggest-command";
|
|
@@ -15,19 +15,23 @@ import { statusCommand } from "./status";
|
|
|
15
15
|
import { supportBundleCommand } from "./support-bundle";
|
|
16
16
|
import { mainCommand } from "./main";
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
// Single source of truth: keyed lookup also feeds the `did you mean` suggester.
|
|
19
|
+
// `CommandDef<any>` is intentional — citty's generic is invariant in the args
|
|
20
|
+
// shape, and each subcommand has its own arg schema; the value here is only
|
|
21
|
+
// passed back to `runMain`, which re-reads the schema from the def itself.
|
|
22
|
+
const SUBCOMMANDS: Record<string, CommandDef<any>> = {
|
|
23
|
+
doctor: doctorCommand,
|
|
24
|
+
init: initCommand,
|
|
25
|
+
install: installCommand,
|
|
26
|
+
logs: logsCommand,
|
|
27
|
+
status: statusCommand,
|
|
28
|
+
record: recordCommand,
|
|
29
|
+
say: sayCommand,
|
|
30
|
+
stats: statsCommand,
|
|
31
|
+
"support-bundle": supportBundleCommand,
|
|
32
|
+
completions: completionsCommand,
|
|
33
|
+
manpage: manpageCommand,
|
|
34
|
+
};
|
|
31
35
|
|
|
32
36
|
function isPathLike(arg: string): boolean {
|
|
33
37
|
return arg.includes(".") || arg.includes("/") || existsSync(arg);
|
|
@@ -36,58 +40,8 @@ function isPathLike(arg: string): boolean {
|
|
|
36
40
|
export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
|
|
37
41
|
const [firstArg, ...restArgs] = rawArgs;
|
|
38
42
|
|
|
39
|
-
if (firstArg
|
|
40
|
-
await runMain(
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (firstArg === "init") {
|
|
45
|
-
await runMain(initCommand, { rawArgs: restArgs });
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (firstArg === "completions") {
|
|
50
|
-
await runMain(completionsCommand, { rawArgs: restArgs });
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (firstArg === "install") {
|
|
55
|
-
await runMain(installCommand, { rawArgs: restArgs });
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (firstArg === "logs") {
|
|
60
|
-
await runMain(logsCommand, { rawArgs: restArgs });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (firstArg === "manpage") {
|
|
65
|
-
await runMain(manpageCommand, { rawArgs: restArgs });
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (firstArg === "status") {
|
|
70
|
-
await runMain(statusCommand, { rawArgs: restArgs });
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (firstArg === "record") {
|
|
75
|
-
await runMain(recordCommand, { rawArgs: restArgs });
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (firstArg === "say") {
|
|
80
|
-
await runMain(sayCommand, { rawArgs: restArgs });
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (firstArg === "stats") {
|
|
85
|
-
await runMain(statsCommand, { rawArgs: restArgs });
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (firstArg === "support-bundle") {
|
|
90
|
-
await runMain(supportBundleCommand, { rawArgs: restArgs });
|
|
43
|
+
if (firstArg && Object.hasOwn(SUBCOMMANDS, firstArg)) {
|
|
44
|
+
await runMain(SUBCOMMANDS[firstArg], { rawArgs: restArgs });
|
|
91
45
|
return;
|
|
92
46
|
}
|
|
93
47
|
|
|
@@ -95,7 +49,7 @@ export async function runCli(rawArgs = process.argv.slice(2)): Promise<void> {
|
|
|
95
49
|
// Extensionless existing files remain valid transcription inputs; missing
|
|
96
50
|
// bare tokens are more likely command typos and should not start the engine.
|
|
97
51
|
if (firstArg && !firstArg.startsWith("-") && !isPathLike(firstArg)) {
|
|
98
|
-
const suggestion = suggestCommand(firstArg, SUBCOMMANDS);
|
|
52
|
+
const suggestion = suggestCommand(firstArg, Object.keys(SUBCOMMANDS));
|
|
99
53
|
log.error(`unknown command '${firstArg}'`);
|
|
100
54
|
if (suggestion && suggestion !== firstArg) {
|
|
101
55
|
log.warn(`(Did you mean ${suggestion}?)`);
|
package/src/diagnostic-log.ts
CHANGED
|
@@ -100,9 +100,8 @@ function readConfig(): DiagnosticLogConfig {
|
|
|
100
100
|
if (!existsSync(statePath)) return defaultConfig();
|
|
101
101
|
try {
|
|
102
102
|
const parsed = JSON.parse(readFileSync(statePath, "utf8")) as Partial<DiagnosticLogConfig>;
|
|
103
|
-
const parsedMode = parseMode(parsed.mode);
|
|
104
103
|
return {
|
|
105
|
-
mode:
|
|
104
|
+
mode: parseDiagnosticLogMode(parsed.mode) ?? defaultConfig().mode,
|
|
106
105
|
maxBytes: positiveInt(parsed.maxBytes, DEFAULT_MAX_BYTES),
|
|
107
106
|
retain: positiveInt(parsed.retain, DEFAULT_RETAIN),
|
|
108
107
|
};
|
|
@@ -111,11 +110,7 @@ function readConfig(): DiagnosticLogConfig {
|
|
|
111
110
|
}
|
|
112
111
|
}
|
|
113
112
|
|
|
114
|
-
export function parseDiagnosticLogMode(value:
|
|
115
|
-
return parseMode(value);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function parseMode(value: unknown): DiagnosticLogMode | null {
|
|
113
|
+
export function parseDiagnosticLogMode(value: unknown): DiagnosticLogMode | null {
|
|
119
114
|
return value === "off" || value === "on" || value === "retain-on-failure" ? value : null;
|
|
120
115
|
}
|
|
121
116
|
|
|
@@ -219,9 +214,11 @@ export function resetDiagnosticLogs(): { deleted: number; bytes: number; dir: st
|
|
|
219
214
|
|
|
220
215
|
export function createDiagnosticLogSession(): DiagnosticLogSession {
|
|
221
216
|
const status = getDiagnosticLogStatus();
|
|
217
|
+
if (status.mode === "off") {
|
|
218
|
+
return { event: () => false, finish: () => false };
|
|
219
|
+
}
|
|
222
220
|
const buffered: Uint8Array[] = [];
|
|
223
221
|
let finished = false;
|
|
224
|
-
if (status.mode === "off") return noopSession();
|
|
225
222
|
|
|
226
223
|
return {
|
|
227
224
|
event(event: string, fields: DiagnosticLogFields = {}): boolean {
|
|
@@ -261,13 +258,6 @@ export function createDiagnosticLogSession(): DiagnosticLogSession {
|
|
|
261
258
|
};
|
|
262
259
|
}
|
|
263
260
|
|
|
264
|
-
function noopSession(): DiagnosticLogSession {
|
|
265
|
-
return {
|
|
266
|
-
event: () => false,
|
|
267
|
-
finish: () => false,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
261
|
function appendDiagnosticLogLine(line: Uint8Array, status: DiagnosticLogStatus): void {
|
|
272
262
|
mkdirSync(status.dir, { recursive: true });
|
|
273
263
|
rotateIfNeeded(status.activePath, line.byteLength, status.maxBytes, status.retain);
|
package/src/engine.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, statSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
+
import { installHint } from "./install-hint";
|
|
3
4
|
import { log } from "./log";
|
|
4
5
|
import { defaultEngineBinPath, keshaCacheDir } from "./paths";
|
|
5
6
|
import { engineAbortError, registerProcessTree } from "./process-tree";
|
|
@@ -219,17 +220,21 @@ export async function preflightTranscribeEngineWithSegments(
|
|
|
219
220
|
if (hasDiarizeModelLayout(modelPath)) return;
|
|
220
221
|
throw new Error(
|
|
221
222
|
`speaker diarization requires a model path\n\nCaused by:\n diarization model not found at ${modelPath}. ` +
|
|
222
|
-
|
|
223
|
+
`Run \`${installHint("--diarize")}\` (or set KESHA_DIARIZE_MODEL_PATH).`,
|
|
223
224
|
);
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
function vadArg(vad: VadMode | undefined): string[] {
|
|
228
|
+
if (vad === "on") return ["--vad"];
|
|
229
|
+
if (vad === "off") return ["--no-vad"];
|
|
230
|
+
return [];
|
|
231
|
+
}
|
|
232
|
+
|
|
226
233
|
export async function transcribeEngine(
|
|
227
234
|
audioPath: string,
|
|
228
235
|
opts: TranscribeEngineOptions = {},
|
|
229
236
|
): Promise<string> {
|
|
230
|
-
const args = ["transcribe", audioPath];
|
|
231
|
-
if (opts.vad === "on") args.push("--vad");
|
|
232
|
-
else if (opts.vad === "off") args.push("--no-vad");
|
|
237
|
+
const args = ["transcribe", audioPath, ...vadArg(opts.vad)];
|
|
233
238
|
const { stdout, stderr, exitCode } = await runEngine(args, { signal: opts.signal });
|
|
234
239
|
if (exitCode !== 0) {
|
|
235
240
|
throw new Error(stderr || `kesha-engine exited with code ${exitCode}`);
|
|
@@ -266,12 +271,8 @@ export async function transcribeEngineWithSegments(
|
|
|
266
271
|
): Promise<TranscriptionOutput> {
|
|
267
272
|
await preflightTranscribeEngineWithSegments(opts);
|
|
268
273
|
|
|
269
|
-
const args = ["transcribe", audioPath, "--json"];
|
|
270
|
-
if (opts.
|
|
271
|
-
else if (opts.vad === "off") args.push("--no-vad");
|
|
272
|
-
if (opts.speakers) {
|
|
273
|
-
args.push("--speakers");
|
|
274
|
-
}
|
|
274
|
+
const args = ["transcribe", audioPath, "--json", ...vadArg(opts.vad)];
|
|
275
|
+
if (opts.speakers) args.push("--speakers");
|
|
275
276
|
const { stdout, stderr, exitCode } = await runEngine(args, { signal: opts.signal });
|
|
276
277
|
if (exitCode !== 0) {
|
|
277
278
|
throw new Error(stderr || `kesha-engine exited with code ${exitCode}`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Pick the right setup-command hint for error/warning messages. Humans on an
|
|
2
|
+
// interactive TTY get `kesha init` (guided + idempotent), agents and scripts
|
|
3
|
+
// piping stderr get `kesha install [...flags]` (deterministic + scriptable).
|
|
4
|
+
//
|
|
5
|
+
// Flags forward to BOTH paths. `kesha init` accepts the same module-selection
|
|
6
|
+
// flags as `kesha install` (`--tts`, `--vad`, `--diarize`, `--coreml`, `--onnx`,
|
|
7
|
+
// `--no-cache`) — they preselect modules in the interactive wizard rather than
|
|
8
|
+
// being install-only flags. Source of truth: `src/cli/init.ts::initCommand.args`.
|
|
9
|
+
export function installHint(...flags: string[]): string {
|
|
10
|
+
const verb = process.stderr.isTTY ? "kesha init" : "kesha install";
|
|
11
|
+
return [verb, ...flags].join(" ");
|
|
12
|
+
}
|
package/src/status.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync, statSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { isEngineInstalled, getEngineBinPath, getEngineCapabilities } from "./engine";
|
|
4
|
+
import { installHint } from "./install-hint";
|
|
4
5
|
import { log } from "./log";
|
|
5
6
|
import { keshaCacheDir } from "./paths";
|
|
6
7
|
import { fluidKokoroCacheInfo } from "./fluid-kokoro-cache";
|
|
@@ -82,7 +83,7 @@ export async function showStatus(options: ShowStatusOptions = {}): Promise<void>
|
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
if (!installed) {
|
|
85
|
-
log.warn(
|
|
86
|
+
log.warn(`Run \`${installHint()}\` to download the engine and models.`);
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
88
89
|
}
|
package/src/synth.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
spawnStdioWithDebugFd,
|
|
6
6
|
type EngineCapabilities,
|
|
7
7
|
} from "./engine";
|
|
8
|
+
import { installHint } from "./install-hint";
|
|
8
9
|
import { log } from "./log";
|
|
9
10
|
import { registerProcessTree } from "./process-tree";
|
|
10
11
|
|
|
@@ -116,7 +117,7 @@ export async function say(opts: SayOptions): Promise<Uint8Array> {
|
|
|
116
117
|
|
|
117
118
|
if (!isEngineInstalled()) {
|
|
118
119
|
throw new SayError(
|
|
119
|
-
|
|
120
|
+
`kesha-engine not installed. run: ${installHint("--tts")}`,
|
|
120
121
|
1,
|
|
121
122
|
"",
|
|
122
123
|
);
|
package/src/transcribe.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type TranscriptionOutput,
|
|
7
7
|
type VadMode,
|
|
8
8
|
} from "./engine";
|
|
9
|
+
import { installHint } from "./install-hint";
|
|
9
10
|
|
|
10
11
|
export type { VadMode };
|
|
11
12
|
export type { TranscriptionOutput };
|
|
@@ -31,13 +32,10 @@ export async function transcribe(audioPath: string, opts: TranscribeOptions = {}
|
|
|
31
32
|
export async function preflightTranscribeWithSegments(opts: TranscribeOptions = {}): Promise<void> {
|
|
32
33
|
if (!isEngineInstalled()) {
|
|
33
34
|
throw new Error(
|
|
34
|
-
"Error: No transcription backend is installed
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
"║ bun add -g @drakulavich/kesha-voice-kit ║\n" +
|
|
39
|
-
"║ kesha install ║\n" +
|
|
40
|
-
"╚══════════════════════════════════════════════════════════╝",
|
|
35
|
+
"Error: No transcription backend is installed.\n\n" +
|
|
36
|
+
"Run the following to get started:\n\n" +
|
|
37
|
+
" bun add -g @drakulavich/kesha-voice-kit\n" +
|
|
38
|
+
` ${installHint()}`,
|
|
41
39
|
);
|
|
42
40
|
}
|
|
43
41
|
|