dictawhisper 0.0.13 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/bin/dictawhisper.mjs +14 -0
- package/config.example.json +41 -0
- package/dist/ui/_app/immutable/assets/0.C6M3gHo0.css +1 -0
- package/dist/ui/_app/immutable/assets/2.C96vgj_4.css +1 -0
- package/dist/ui/_app/immutable/chunks/BSs9MmST.js +3 -0
- package/dist/ui/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/dist/ui/_app/immutable/chunks/BslYtab-.js +1 -0
- package/dist/ui/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/dist/ui/_app/immutable/entry/app.BJlRju4c.js +2 -0
- package/dist/ui/_app/immutable/entry/start.C5XBbXTT.js +1 -0
- package/dist/ui/_app/immutable/nodes/0.D23pkrHr.js +1 -0
- package/dist/ui/_app/immutable/nodes/1.C5T-U4uV.js +1 -0
- package/dist/ui/_app/immutable/nodes/2.59uAq_ko.js +5 -0
- package/dist/ui/_app/version.json +1 -0
- package/dist/ui/favicon.png +0 -0
- package/dist/ui/favicon.svg +3 -0
- package/dist/ui/index.html +38 -0
- package/dist/ui/logo.png +0 -0
- package/package.json +12 -3
- package/scripts/transcribe_faster_whisper.py +428 -0
- package/src/apiRoutes.ts +464 -0
- package/src/classes/Queue.ts +77 -0
- package/src/classes/Watcher.ts +117 -0
- package/src/cli.ts +132 -0
- package/src/config.ts +238 -0
- package/src/dev.ts +63 -0
- package/src/doctor.ts +12 -0
- package/src/lib/alignLib.ts +154 -0
- package/src/lib/audioLib.ts +202 -0
- package/src/lib/audioProbeLib.ts +112 -0
- package/src/lib/cleanupProvenanceLib.ts +80 -0
- package/src/lib/downloadLib.ts +8 -0
- package/src/lib/fileSettleLib.ts +213 -0
- package/src/lib/fsLib.ts +37 -0
- package/src/lib/healthLib.ts +377 -0
- package/src/lib/inboxUrl.ts +123 -0
- package/src/lib/journalEmbedLib.ts +127 -0
- package/src/lib/journalIndexLib.ts +923 -0
- package/src/lib/journalQueryLib.ts +269 -0
- package/src/lib/journalService.ts +287 -0
- package/src/lib/jsonLib.ts +71 -0
- package/src/lib/localberthGet.ts +14 -0
- package/src/lib/ollanetLib.ts +117 -0
- package/src/lib/ollanetReadyLib.ts +7 -0
- package/src/lib/organizationLib.ts +182 -0
- package/src/lib/pathAllowLib.ts +60 -0
- package/src/lib/queueLib.ts +30 -0
- package/src/lib/speechCleanupLib.ts +14 -0
- package/src/lib/tagConsolidateLib.ts +431 -0
- package/src/lib/tailscaleLib.ts +78 -0
- package/src/lib/transcriptionLib.ts +484 -0
- package/src/lib/whisperLib.ts +409 -0
- package/src/mcp.ts +151 -0
- package/src/prompts/cleanTranscription.ts +62 -0
- package/src/prompts/consolidateTags.ts +38 -0
- package/src/prompts/whisperInitial.ts +11 -0
- package/src/server.ts +220 -0
- package/src/socketEvents.ts +72 -0
- package/src/types/transcription.ts +56 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Transcribe audio with faster-whisper; write OpenAI-Whisper-compatible JSON.
|
|
3
|
+
|
|
4
|
+
One-shot:
|
|
5
|
+
python transcribe_faster_whisper.py --audio FILE --output FILE.json ...
|
|
6
|
+
|
|
7
|
+
Persistent worker (model loaded once; stdin/stdout JSON lines):
|
|
8
|
+
python transcribe_faster_whisper.py --worker --model large-v3 --device cuda ...
|
|
9
|
+
stdin: {"id":"1","cmd":"transcribe","audio":"...","output":"...","initial_prompt":"..."}
|
|
10
|
+
{"id":"2","cmd":"shutdown"}
|
|
11
|
+
stdout: {"type":"ready","model":"...","device":"..."}
|
|
12
|
+
{"id":"1","type":"progress","pct":12.3,"start":0,"end":4.2,"text":"..."}
|
|
13
|
+
{"id":"1","type":"done","ok":true,"elapsed":18.4,"chars":123,"segments":10}
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Callable
|
|
25
|
+
|
|
26
|
+
from faster_whisper import WhisperModel
|
|
27
|
+
|
|
28
|
+
MODEL_ALIASES = {
|
|
29
|
+
"large": "large-v3",
|
|
30
|
+
"large-v3": "large-v3",
|
|
31
|
+
"turbo": "large-v3-turbo",
|
|
32
|
+
"large-v3-turbo": "large-v3-turbo",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Light journal context only — do not ask the model to rewrite/polish speech.
|
|
36
|
+
# Downstream LLM cleanup already removes fillers while preserving content.
|
|
37
|
+
DEFAULT_INITIAL_PROMPT = (
|
|
38
|
+
"This is a personal audio journal spoken in English. "
|
|
39
|
+
"Transcribe the speech accurately with natural punctuation and capitalization. "
|
|
40
|
+
"Keep filler words, false starts, and repetitions as spoken. "
|
|
41
|
+
"Do not paraphrase. Prefer unusual names and terms over common words that sound similar."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
KEEP_KEYS = (
|
|
45
|
+
"cleanedTranscription",
|
|
46
|
+
"tags",
|
|
47
|
+
"thinking",
|
|
48
|
+
"meta",
|
|
49
|
+
"elapsed",
|
|
50
|
+
"cleanupError",
|
|
51
|
+
"cleanupAttempts",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _reconfigure_stdio() -> None:
|
|
56
|
+
"""Windows cp1252 cannot print Whisper tokens like ⁄ or ş; do not fail the job for a log line."""
|
|
57
|
+
for stream in (sys.stdout, sys.stderr):
|
|
58
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
59
|
+
if not reconfigure:
|
|
60
|
+
continue
|
|
61
|
+
try:
|
|
62
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
63
|
+
except Exception:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
_reconfigure_stdio()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def resolve_model(name: str) -> str:
|
|
71
|
+
key = name.strip().lower()
|
|
72
|
+
if key not in MODEL_ALIASES:
|
|
73
|
+
supported = ", ".join(sorted(MODEL_ALIASES))
|
|
74
|
+
raise SystemExit(f"Unsupported model '{name}'. Use one of: {supported}")
|
|
75
|
+
return MODEL_ALIASES[key]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _write_line(msg: str, stream) -> None:
|
|
79
|
+
try:
|
|
80
|
+
print(msg, file=stream, flush=True)
|
|
81
|
+
except UnicodeEncodeError:
|
|
82
|
+
encoding = getattr(stream, "encoding", None) or "utf-8"
|
|
83
|
+
payload = (msg + "\n").encode(encoding, errors="replace")
|
|
84
|
+
buf = getattr(stream, "buffer", None)
|
|
85
|
+
if buf is not None:
|
|
86
|
+
buf.write(payload)
|
|
87
|
+
buf.flush()
|
|
88
|
+
return
|
|
89
|
+
print(payload.decode(encoding, errors="replace"), file=stream, flush=True)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def log(msg: str) -> None:
|
|
93
|
+
"""Print immediately — piped stdout is block-buffered unless flushed."""
|
|
94
|
+
_write_line(msg, sys.stdout)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def log_err(msg: str) -> None:
|
|
98
|
+
_write_line(msg, sys.stderr)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def emit(payload: dict[str, Any]) -> None:
|
|
102
|
+
_write_line(json.dumps(payload, ensure_ascii=False), sys.stdout)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def load_existing(output_path: Path) -> dict[str, Any]:
|
|
106
|
+
if not output_path.is_file():
|
|
107
|
+
return {}
|
|
108
|
+
try:
|
|
109
|
+
loaded = json.loads(output_path.read_text(encoding="utf-8"))
|
|
110
|
+
return loaded if isinstance(loaded, dict) else {}
|
|
111
|
+
except Exception:
|
|
112
|
+
return {}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def write_sidecar(
|
|
116
|
+
output_path: Path,
|
|
117
|
+
*,
|
|
118
|
+
texts: list[str],
|
|
119
|
+
segments: list[dict[str, Any]],
|
|
120
|
+
language: str,
|
|
121
|
+
model_name: str,
|
|
122
|
+
device: str,
|
|
123
|
+
compute_type: str,
|
|
124
|
+
duration: float | None,
|
|
125
|
+
vad_filter: bool,
|
|
126
|
+
) -> dict[str, Any]:
|
|
127
|
+
existing = load_existing(output_path)
|
|
128
|
+
payload = {key: existing[key] for key in KEEP_KEYS if key in existing}
|
|
129
|
+
payload.update(
|
|
130
|
+
{
|
|
131
|
+
"text": "".join(texts).strip(),
|
|
132
|
+
"segments": segments,
|
|
133
|
+
"language": language,
|
|
134
|
+
"whisper": {
|
|
135
|
+
"engine": "faster-whisper",
|
|
136
|
+
"model": model_name,
|
|
137
|
+
"device": device,
|
|
138
|
+
"compute_type": compute_type,
|
|
139
|
+
"duration": duration,
|
|
140
|
+
"vad_filter": vad_filter,
|
|
141
|
+
"word_timestamps": True,
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
output_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
147
|
+
return payload
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def transcribe_to_sidecar(
|
|
151
|
+
model: WhisperModel,
|
|
152
|
+
audio_path: Path,
|
|
153
|
+
output_path: Path,
|
|
154
|
+
*,
|
|
155
|
+
language: str,
|
|
156
|
+
initial_prompt: str | None,
|
|
157
|
+
vad_filter: bool,
|
|
158
|
+
model_name: str,
|
|
159
|
+
device: str,
|
|
160
|
+
compute_type: str,
|
|
161
|
+
log_fn: Callable[[str], None],
|
|
162
|
+
progress_fn: Callable[[dict[str, Any]], None] | None = None,
|
|
163
|
+
) -> dict[str, Any]:
|
|
164
|
+
if not audio_path.is_file():
|
|
165
|
+
raise FileNotFoundError(f"audio not found: {audio_path}")
|
|
166
|
+
|
|
167
|
+
log_fn(f"[transcribe] audio={audio_path}")
|
|
168
|
+
log_fn("[transcribe] starting transcription...")
|
|
169
|
+
|
|
170
|
+
segments_iter, info = model.transcribe(
|
|
171
|
+
str(audio_path),
|
|
172
|
+
language=language,
|
|
173
|
+
task="transcribe",
|
|
174
|
+
beam_size=5,
|
|
175
|
+
vad_filter=vad_filter,
|
|
176
|
+
initial_prompt=initial_prompt or None,
|
|
177
|
+
condition_on_previous_text=False,
|
|
178
|
+
compression_ratio_threshold=2.2,
|
|
179
|
+
word_timestamps=True,
|
|
180
|
+
log_progress=False,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
duration = float(info.duration or 0.0)
|
|
184
|
+
if duration > 0:
|
|
185
|
+
log_fn(f"[transcribe] audio duration={duration:.1f}s language={info.language}")
|
|
186
|
+
else:
|
|
187
|
+
log_fn(f"[transcribe] language={info.language}")
|
|
188
|
+
|
|
189
|
+
segments: list[dict[str, Any]] = []
|
|
190
|
+
texts: list[str] = []
|
|
191
|
+
for i, segment in enumerate(segments_iter):
|
|
192
|
+
text = segment.text
|
|
193
|
+
texts.append(text)
|
|
194
|
+
words = []
|
|
195
|
+
for word in segment.words or []:
|
|
196
|
+
if word.start is None or word.end is None:
|
|
197
|
+
continue
|
|
198
|
+
words.append(
|
|
199
|
+
{
|
|
200
|
+
"word": word.word,
|
|
201
|
+
"start": float(word.start),
|
|
202
|
+
"end": float(word.end),
|
|
203
|
+
"probability": float(word.probability) if word.probability is not None else None,
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
segments.append(
|
|
207
|
+
{
|
|
208
|
+
"id": i,
|
|
209
|
+
"seek": 0,
|
|
210
|
+
"start": segment.start,
|
|
211
|
+
"end": segment.end,
|
|
212
|
+
"text": text,
|
|
213
|
+
"words": words,
|
|
214
|
+
"tokens": [],
|
|
215
|
+
"temperature": segment.temperature,
|
|
216
|
+
"avg_logprob": segment.avg_logprob,
|
|
217
|
+
"compression_ratio": segment.compression_ratio,
|
|
218
|
+
"no_speech_prob": segment.no_speech_prob,
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
if duration > 0:
|
|
222
|
+
pct = min(100.0, (segment.end / duration) * 100.0)
|
|
223
|
+
log_fn(
|
|
224
|
+
f"[transcribe] {pct:5.1f}% "
|
|
225
|
+
f"({segment.start:7.1f}-{segment.end:7.1f}s / {duration:.1f}s) "
|
|
226
|
+
f"{text.strip()}"
|
|
227
|
+
)
|
|
228
|
+
if progress_fn:
|
|
229
|
+
progress_fn(
|
|
230
|
+
{
|
|
231
|
+
"pct": round(pct, 1),
|
|
232
|
+
"start": float(segment.start),
|
|
233
|
+
"end": float(segment.end),
|
|
234
|
+
"text": text.strip(),
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
log_fn(f"[{segment.start:7.2f} -> {segment.end:7.2f}] {text.strip()}")
|
|
239
|
+
if progress_fn:
|
|
240
|
+
progress_fn(
|
|
241
|
+
{
|
|
242
|
+
"pct": None,
|
|
243
|
+
"start": float(segment.start),
|
|
244
|
+
"end": float(segment.end),
|
|
245
|
+
"text": text.strip(),
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if duration > 0:
|
|
250
|
+
log_fn(f"[transcribe] 100.0% done ({duration:.1f}s)")
|
|
251
|
+
|
|
252
|
+
payload = write_sidecar(
|
|
253
|
+
output_path,
|
|
254
|
+
texts=texts,
|
|
255
|
+
segments=segments,
|
|
256
|
+
language=info.language,
|
|
257
|
+
model_name=model_name,
|
|
258
|
+
device=device,
|
|
259
|
+
compute_type=compute_type,
|
|
260
|
+
duration=info.duration,
|
|
261
|
+
vad_filter=vad_filter,
|
|
262
|
+
)
|
|
263
|
+
log_fn(f"[transcribe] wrote {output_path} ({len(payload['text'])} chars, {len(segments)} segments)")
|
|
264
|
+
return {"chars": len(payload["text"]), "segments": len(segments), "duration": duration}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
268
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
269
|
+
parser.add_argument("--worker", action="store_true", help="Long-lived stdin/stdout JSON worker")
|
|
270
|
+
parser.add_argument("--audio", help="Path to audio file (one-shot)")
|
|
271
|
+
parser.add_argument("--output", help="Path to output .json file (one-shot)")
|
|
272
|
+
parser.add_argument(
|
|
273
|
+
"--model",
|
|
274
|
+
default="large-v3",
|
|
275
|
+
help="Model alias: large-v3|large|turbo|large-v3-turbo (default: large-v3)",
|
|
276
|
+
)
|
|
277
|
+
parser.add_argument("--device", default="cuda", choices=("cuda", "cpu"))
|
|
278
|
+
parser.add_argument(
|
|
279
|
+
"--compute-type",
|
|
280
|
+
default="float16",
|
|
281
|
+
help="CTranslate2 compute type (default: float16). Use int8_float16 if VRAM is tight.",
|
|
282
|
+
)
|
|
283
|
+
parser.add_argument("--language", default="en")
|
|
284
|
+
parser.add_argument(
|
|
285
|
+
"--initial-prompt",
|
|
286
|
+
default=DEFAULT_INITIAL_PROMPT,
|
|
287
|
+
help="Optional first-window prompt. Pass empty string to disable.",
|
|
288
|
+
)
|
|
289
|
+
parser.add_argument(
|
|
290
|
+
"--vad-filter",
|
|
291
|
+
action=argparse.BooleanOptionalAction,
|
|
292
|
+
default=True,
|
|
293
|
+
help="Silero VAD to skip silence (default: on)",
|
|
294
|
+
)
|
|
295
|
+
return parser
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def run_oneshot(args: argparse.Namespace) -> int:
|
|
299
|
+
if not args.audio or not args.output:
|
|
300
|
+
print("[transcribe] --audio and --output are required in one-shot mode", file=sys.stderr, flush=True)
|
|
301
|
+
return 1
|
|
302
|
+
|
|
303
|
+
audio_path = Path(args.audio)
|
|
304
|
+
output_path = Path(args.output)
|
|
305
|
+
model_name = resolve_model(args.model)
|
|
306
|
+
log(f"[transcribe] model={model_name} device={args.device} compute_type={args.compute_type}")
|
|
307
|
+
log("[transcribe] loading model...")
|
|
308
|
+
model = WhisperModel(model_name, device=args.device, compute_type=args.compute_type)
|
|
309
|
+
log("[transcribe] model loaded; starting transcription...")
|
|
310
|
+
transcribe_to_sidecar(
|
|
311
|
+
model,
|
|
312
|
+
audio_path,
|
|
313
|
+
output_path,
|
|
314
|
+
language=args.language,
|
|
315
|
+
initial_prompt=args.initial_prompt,
|
|
316
|
+
vad_filter=args.vad_filter,
|
|
317
|
+
model_name=model_name,
|
|
318
|
+
device=args.device,
|
|
319
|
+
compute_type=args.compute_type,
|
|
320
|
+
log_fn=log,
|
|
321
|
+
)
|
|
322
|
+
# CTranslate2/CUDA often abort() during teardown on Windows (exit 0xC0000409)
|
|
323
|
+
# after a successful run. Skip destructors so Node sees a clean 0.
|
|
324
|
+
os._exit(0)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def run_worker(args: argparse.Namespace) -> None:
|
|
328
|
+
model_name = resolve_model(args.model)
|
|
329
|
+
log_err(f"[transcribe] worker model={model_name} device={args.device} compute_type={args.compute_type}")
|
|
330
|
+
log_err("[transcribe] loading model...")
|
|
331
|
+
model = WhisperModel(model_name, device=args.device, compute_type=args.compute_type)
|
|
332
|
+
log_err("[transcribe] model loaded")
|
|
333
|
+
emit({"type": "ready", "model": model_name, "device": args.device, "compute_type": args.compute_type})
|
|
334
|
+
|
|
335
|
+
for raw in sys.stdin:
|
|
336
|
+
line = raw.strip()
|
|
337
|
+
if not line:
|
|
338
|
+
continue
|
|
339
|
+
try:
|
|
340
|
+
job = json.loads(line)
|
|
341
|
+
except json.JSONDecodeError as error:
|
|
342
|
+
emit({"type": "error", "error": f"invalid json: {error}"})
|
|
343
|
+
continue
|
|
344
|
+
|
|
345
|
+
job_id = str(job.get("id") or "")
|
|
346
|
+
cmd = str(job.get("cmd") or "transcribe")
|
|
347
|
+
if cmd == "shutdown":
|
|
348
|
+
log_err("[transcribe] worker shutdown")
|
|
349
|
+
# Same Windows CUDA teardown: skip destructors on the way out.
|
|
350
|
+
os._exit(0)
|
|
351
|
+
|
|
352
|
+
if cmd != "transcribe":
|
|
353
|
+
emit({"id": job_id, "type": "done", "ok": False, "error": f"unknown cmd {cmd}"})
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
job_model = resolve_model(str(job.get("model") or args.model))
|
|
357
|
+
job_device = str(job.get("device") or args.device)
|
|
358
|
+
job_compute = str(job.get("compute_type") or args.compute_type)
|
|
359
|
+
if job_model != model_name or job_device != args.device or job_compute != args.compute_type:
|
|
360
|
+
emit(
|
|
361
|
+
{
|
|
362
|
+
"id": job_id,
|
|
363
|
+
"type": "done",
|
|
364
|
+
"ok": False,
|
|
365
|
+
"error": (
|
|
366
|
+
f"worker mismatch (loaded {model_name}/{args.device}/{args.compute_type}, "
|
|
367
|
+
f"job {job_model}/{job_device}/{job_compute})"
|
|
368
|
+
),
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
audio = job.get("audio")
|
|
374
|
+
output = job.get("output")
|
|
375
|
+
if not audio or not output:
|
|
376
|
+
emit({"id": job_id, "type": "done", "ok": False, "error": "audio and output are required"})
|
|
377
|
+
continue
|
|
378
|
+
|
|
379
|
+
started = time.time()
|
|
380
|
+
try:
|
|
381
|
+
result = transcribe_to_sidecar(
|
|
382
|
+
model,
|
|
383
|
+
Path(str(audio)),
|
|
384
|
+
Path(str(output)),
|
|
385
|
+
language=str(job.get("language") or args.language),
|
|
386
|
+
initial_prompt=job.get("initial_prompt", args.initial_prompt),
|
|
387
|
+
vad_filter=bool(job.get("vad_filter", args.vad_filter)),
|
|
388
|
+
model_name=model_name,
|
|
389
|
+
device=args.device,
|
|
390
|
+
compute_type=args.compute_type,
|
|
391
|
+
log_fn=log_err,
|
|
392
|
+
progress_fn=lambda payload, _id=job_id: emit({"id": _id, "type": "progress", **payload}),
|
|
393
|
+
)
|
|
394
|
+
emit(
|
|
395
|
+
{
|
|
396
|
+
"id": job_id,
|
|
397
|
+
"type": "done",
|
|
398
|
+
"ok": True,
|
|
399
|
+
"elapsed": round(time.time() - started, 2),
|
|
400
|
+
"chars": result["chars"],
|
|
401
|
+
"segments": result["segments"],
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
except Exception as error:
|
|
405
|
+
emit(
|
|
406
|
+
{
|
|
407
|
+
"id": job_id,
|
|
408
|
+
"type": "done",
|
|
409
|
+
"ok": False,
|
|
410
|
+
"elapsed": round(time.time() - started, 2),
|
|
411
|
+
"error": str(error),
|
|
412
|
+
}
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
log_err("[transcribe] worker stdin closed")
|
|
416
|
+
os._exit(0)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def main() -> int:
|
|
420
|
+
args = build_parser().parse_args()
|
|
421
|
+
if args.worker:
|
|
422
|
+
run_worker(args)
|
|
423
|
+
return 0
|
|
424
|
+
return run_oneshot(args)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
if __name__ == "__main__":
|
|
428
|
+
raise SystemExit(main())
|