voxhelm 0.1.0__py3-none-any.whl

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 (52) hide show
  1. config/__init__.py +1 -0
  2. config/asgi.py +7 -0
  3. config/settings.py +425 -0
  4. config/urls.py +19 -0
  5. config/wsgi.py +7 -0
  6. jobs/__init__.py +1 -0
  7. jobs/apps.py +6 -0
  8. jobs/artifacts.py +233 -0
  9. jobs/media.py +190 -0
  10. jobs/migrations/0001_initial.py +67 -0
  11. jobs/migrations/0002_alter_job_job_type_alter_jobartifact_kind.py +23 -0
  12. jobs/migrations/0003_job_dispatch_mode_job_operator_and_more.py +35 -0
  13. jobs/migrations/0004_stagedmedia.py +48 -0
  14. jobs/migrations/0005_rename_jobs_staged_producer_76b546_idx_jobs_staged_produce_cbe898_idx_and_more.py +23 -0
  15. jobs/migrations/0006_alter_jobartifact_kind.py +18 -0
  16. jobs/migrations/0007_remote_workers.py +102 -0
  17. jobs/migrations/__init__.py +0 -0
  18. jobs/models.py +176 -0
  19. jobs/remote_worker_cli.py +985 -0
  20. jobs/remote_workers.py +1293 -0
  21. jobs/services.py +1510 -0
  22. jobs/staging.py +162 -0
  23. jobs/tasks.py +15 -0
  24. jobs/urls.py +25 -0
  25. jobs/views.py +184 -0
  26. lane_scheduler.py +294 -0
  27. operators/__init__.py +1 -0
  28. operators/apps.py +6 -0
  29. operators/forms.py +65 -0
  30. operators/management/__init__.py +1 -0
  31. operators/management/commands/__init__.py +1 -0
  32. operators/management/commands/bootstrap_operator.py +56 -0
  33. operators/templates/operators/root.html +302 -0
  34. operators/views.py +224 -0
  35. synthesis/__init__.py +1 -0
  36. synthesis/service.py +324 -0
  37. synthesis/views.py +139 -0
  38. transcriptions/__init__.py +1 -0
  39. transcriptions/apps.py +6 -0
  40. transcriptions/diarization.py +366 -0
  41. transcriptions/errors.py +15 -0
  42. transcriptions/formats.py +153 -0
  43. transcriptions/input_media.py +112 -0
  44. transcriptions/known_speaker.py +666 -0
  45. transcriptions/observability.py +101 -0
  46. transcriptions/service.py +663 -0
  47. transcriptions/views.py +214 -0
  48. transcriptions/wyoming.py +424 -0
  49. voxhelm-0.1.0.dist-info/METADATA +425 -0
  50. voxhelm-0.1.0.dist-info/RECORD +52 -0
  51. voxhelm-0.1.0.dist-info/WHEEL +4 -0
  52. voxhelm-0.1.0.dist-info/entry_points.txt +3 -0
config/__init__.py ADDED
@@ -0,0 +1 @@
1
+
config/asgi.py ADDED
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ from django.core.asgi import get_asgi_application
4
+
5
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
6
+
7
+ application = get_asgi_application()
config/settings.py ADDED
@@ -0,0 +1,425 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ BASE_DIR = Path(__file__).resolve().parent.parent
8
+ VOXHELM_OPERATOR_PRODUCER_LABEL = "__operator_ui__"
9
+ VOXHELM_RESERVED_BEARER_TOKEN_LABELS = {VOXHELM_OPERATOR_PRODUCER_LABEL}
10
+
11
+
12
+ def env_list(name: str, *, default: str = "") -> list[str]:
13
+ raw = os.getenv(name, default)
14
+ return [item.strip() for item in raw.replace("\n", ",").split(",") if item.strip()]
15
+
16
+
17
+ def env_bool(name: str, *, default: bool = False) -> bool:
18
+ raw = os.getenv(name)
19
+ if raw is None:
20
+ return default
21
+ return raw.lower() in {"1", "true", "yes", "on"}
22
+
23
+
24
+ def env_map(name: str) -> dict[str, str]:
25
+ raw = os.getenv(name, "").strip()
26
+ if not raw:
27
+ return {}
28
+ entries = (
29
+ entry.split("=", 1) for entry in raw.replace("\n", ",").split(",") if entry.strip()
30
+ )
31
+ return {
32
+ key.strip(): value.strip()
33
+ for key, value in entries
34
+ }
35
+
36
+
37
+ def env_tokens(name: str) -> dict[str, str]:
38
+ raw = os.getenv(name, "").strip()
39
+ if not raw:
40
+ return {}
41
+ if raw.startswith("{"):
42
+ parsed = json.loads(raw)
43
+ if not isinstance(parsed, dict):
44
+ raise ValueError(f"{name} must be a JSON object when JSON syntax is used.")
45
+ return validate_bearer_token_labels(
46
+ name,
47
+ {str(key).strip(): str(value).strip() for key, value in parsed.items()},
48
+ )
49
+
50
+ tokens: dict[str, str] = {}
51
+ for entry in raw.replace("\n", ",").split(","):
52
+ normalized = entry.strip()
53
+ if not normalized:
54
+ continue
55
+ if "=" not in normalized:
56
+ raise ValueError(f"Invalid {name} entry '{normalized}'. Use label=token pairs.")
57
+ label, token = normalized.split("=", 1)
58
+ tokens[label.strip()] = token.strip()
59
+ return validate_bearer_token_labels(name, tokens)
60
+
61
+
62
+ def validate_unique_token_values(name: str, tokens: dict[str, str]) -> dict[str, str]:
63
+ seen: dict[str, str] = {}
64
+ duplicates: list[str] = []
65
+ for label, token in tokens.items():
66
+ if token in seen:
67
+ duplicates.append(f"{seen[token]} and {label}")
68
+ seen[token] = label
69
+ if duplicates:
70
+ joined = ", ".join(duplicates)
71
+ raise ValueError(f"{name} maps one bearer token to multiple labels: {joined}.")
72
+ return tokens
73
+
74
+
75
+ def validate_disjoint_token_values(
76
+ left_name: str,
77
+ left_tokens: dict[str, str],
78
+ right_name: str,
79
+ right_tokens: dict[str, str],
80
+ ) -> dict[str, str]:
81
+ left_labels_by_token = {token: label for label, token in left_tokens.items()}
82
+ overlaps = [
83
+ f"{left_labels_by_token[token]} and {label}"
84
+ for label, token in right_tokens.items()
85
+ if token in left_labels_by_token
86
+ ]
87
+ if overlaps:
88
+ joined = ", ".join(overlaps)
89
+ raise ValueError(
90
+ f"{left_name} and {right_name} must not share bearer token values: {joined}."
91
+ )
92
+ return right_tokens
93
+
94
+
95
+ def validate_bearer_token_labels(name: str, tokens: dict[str, str]) -> dict[str, str]:
96
+ empty_labels = [label for label in tokens if not label]
97
+ if empty_labels:
98
+ raise ValueError(f"{name} contains an empty label.")
99
+ empty_token_labels = sorted(label for label, token in tokens.items() if not token)
100
+ if empty_token_labels:
101
+ labels = ", ".join(empty_token_labels)
102
+ raise ValueError(f"{name} contains empty token value(s) for label(s): {labels}.")
103
+ reserved = sorted(VOXHELM_RESERVED_BEARER_TOKEN_LABELS.intersection(tokens))
104
+ if reserved:
105
+ labels = ", ".join(reserved)
106
+ raise ValueError(f"{name} contains reserved label(s): {labels}.")
107
+ return tokens
108
+
109
+
110
+ def validate_positive_int(name: str, value: int) -> int:
111
+ if value <= 0:
112
+ raise ValueError(f"{name} must be a positive integer.")
113
+ return value
114
+
115
+
116
+ REMOTE_PULL_SHARED_ARTIFACT_BACKENDS = {"s3"}
117
+ TRANSCRIPTION_EXECUTION_MODES = {"django_tasks", "remote_pull"}
118
+
119
+
120
+ def validate_transcription_execution_mode(mode: str) -> str:
121
+ if mode not in TRANSCRIPTION_EXECUTION_MODES:
122
+ accepted = ", ".join(sorted(TRANSCRIPTION_EXECUTION_MODES))
123
+ raise ValueError(f"VOXHELM_TRANSCRIPTION_EXECUTION_MODE must be one of: {accepted}.")
124
+ return mode
125
+
126
+
127
+ def validate_remote_pull_worker_tokens(
128
+ execution_mode: str,
129
+ worker_tokens: dict[str, str],
130
+ ) -> None:
131
+ if execution_mode == "remote_pull" and not worker_tokens:
132
+ raise ValueError(
133
+ "VOXHELM_TRANSCRIPTION_EXECUTION_MODE=remote_pull requires "
134
+ "VOXHELM_WORKER_TOKENS to configure at least one worker token."
135
+ )
136
+
137
+
138
+ def validate_remote_pull_artifact_backend(execution_mode: str, artifact_backend: str) -> None:
139
+ if execution_mode != "remote_pull":
140
+ return
141
+ if artifact_backend in REMOTE_PULL_SHARED_ARTIFACT_BACKENDS:
142
+ return
143
+ accepted = ", ".join(sorted(REMOTE_PULL_SHARED_ARTIFACT_BACKENDS))
144
+ raise ValueError(
145
+ "VOXHELM_TRANSCRIPTION_EXECUTION_MODE=remote_pull requires "
146
+ f"VOXHELM_ARTIFACT_BACKEND to be one of: {accepted}."
147
+ )
148
+
149
+
150
+ def validate_remote_pull_s3_configuration(
151
+ execution_mode: str,
152
+ artifact_backend: str,
153
+ values: dict[str, str],
154
+ ) -> None:
155
+ if execution_mode != "remote_pull" or artifact_backend != "s3":
156
+ return
157
+ missing = sorted(name for name, value in values.items() if not value)
158
+ if missing:
159
+ joined = ", ".join(missing)
160
+ raise ValueError(
161
+ "VOXHELM_TRANSCRIPTION_EXECUTION_MODE=remote_pull requires complete "
162
+ f"S3 artifact configuration: {joined}."
163
+ )
164
+
165
+
166
+ def get_accepted_stt_models() -> set[str]:
167
+ from django.conf import settings as django_settings
168
+
169
+ models = {
170
+ "gpt-4o-mini-transcribe",
171
+ "whisper-1",
172
+ django_settings.VOXHELM_MLX_MODEL,
173
+ django_settings.VOXHELM_WHISPERCPP_MODEL,
174
+ }
175
+ if django_settings.VOXHELM_WHISPERKIT_ENABLED:
176
+ models.update({"whisperkit", django_settings.VOXHELM_WHISPERKIT_MODEL})
177
+ return models
178
+
179
+
180
+ def get_batch_accepted_stt_models() -> set[str]:
181
+ return {"auto", *get_accepted_stt_models()}
182
+
183
+
184
+ SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "dev-only-secret-key")
185
+ DEBUG = os.getenv("DJANGO_DEBUG", "").lower() in {"1", "true", "yes", "on"}
186
+ ALLOWED_HOSTS = env_list("VOXHELM_ALLOWED_HOSTS", default="localhost,127.0.0.1")
187
+ CSRF_TRUSTED_ORIGINS = env_list("VOXHELM_CSRF_TRUSTED_ORIGINS")
188
+ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
189
+ ROOT_URLCONF = "config.urls"
190
+ WSGI_APPLICATION = "config.wsgi.application"
191
+ ASGI_APPLICATION = "config.asgi.application"
192
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
193
+
194
+ INSTALLED_APPS = [
195
+ "django.contrib.auth",
196
+ "django.contrib.contenttypes",
197
+ "django.contrib.sessions",
198
+ "django_tasks",
199
+ "django_tasks_db",
200
+ "operators",
201
+ "transcriptions",
202
+ "jobs",
203
+ ]
204
+
205
+ MIDDLEWARE = [
206
+ "django.middleware.security.SecurityMiddleware",
207
+ "django.contrib.sessions.middleware.SessionMiddleware",
208
+ "django.middleware.common.CommonMiddleware",
209
+ "django.middleware.csrf.CsrfViewMiddleware",
210
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
211
+ ]
212
+
213
+ TEMPLATES = [
214
+ {
215
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
216
+ "DIRS": [],
217
+ "APP_DIRS": True,
218
+ "OPTIONS": {
219
+ "context_processors": [
220
+ "django.template.context_processors.request",
221
+ "django.contrib.auth.context_processors.auth",
222
+ ]
223
+ },
224
+ }
225
+ ]
226
+
227
+ DATABASES = {
228
+ "default": {
229
+ "ENGINE": "django.db.backends.sqlite3",
230
+ "NAME": BASE_DIR / "db.sqlite3",
231
+ }
232
+ }
233
+
234
+ LANGUAGE_CODE = "en-us"
235
+ TIME_ZONE = "UTC"
236
+ USE_I18N = True
237
+ USE_TZ = True
238
+
239
+ LOGIN_URL = "/"
240
+ LOGIN_REDIRECT_URL = "/"
241
+ LOGOUT_REDIRECT_URL = "/"
242
+
243
+ VOXHELM_BEARER_TOKENS = env_tokens("VOXHELM_BEARER_TOKENS")
244
+ VOXHELM_WORKER_TOKENS = validate_disjoint_token_values(
245
+ "VOXHELM_BEARER_TOKENS",
246
+ VOXHELM_BEARER_TOKENS,
247
+ "VOXHELM_WORKER_TOKENS",
248
+ validate_unique_token_values(
249
+ "VOXHELM_WORKER_TOKENS",
250
+ env_tokens("VOXHELM_WORKER_TOKENS"),
251
+ ),
252
+ )
253
+ VOXHELM_STT_BACKEND = os.getenv("VOXHELM_STT_BACKEND", "whispercpp").strip()
254
+ VOXHELM_STT_FALLBACK_BACKEND = os.getenv("VOXHELM_STT_FALLBACK_BACKEND", "mlx").strip()
255
+ VOXHELM_MLX_MODEL = os.getenv("VOXHELM_MLX_MODEL", "mlx-community/whisper-large-v3-mlx")
256
+ # Conditioning the decoder on previously generated text is Whisper's main cause of
257
+ # runaway repetition loops on long audio (a hallucinated phrase feeds itself across
258
+ # 30s windows). Default off; flip to true to restore upstream Whisper behaviour.
259
+ VOXHELM_MLX_CONDITION_ON_PREVIOUS_TEXT = env_bool(
260
+ "VOXHELM_MLX_CONDITION_ON_PREVIOUS_TEXT", default=False
261
+ )
262
+ VOXHELM_WHISPERCPP_MODEL = os.getenv("VOXHELM_WHISPERCPP_MODEL", "ggml-large-v3.bin").strip()
263
+ VOXHELM_WHISPERCPP_BIN = os.getenv("VOXHELM_WHISPERCPP_BIN", "/opt/homebrew/bin/whisper-cli")
264
+ VOXHELM_WHISPERCPP_PROCESSORS = int(os.getenv("VOXHELM_WHISPERCPP_PROCESSORS", "4"))
265
+ # whisper.cpp anti-hallucination controls. max-context 0 disables conditioning on
266
+ # previous text (the whisper.cpp equivalent of condition_on_previous_text=False);
267
+ # -1 restores the upstream unbounded default. suppress-nst drops non-speech tokens,
268
+ # which curbs hallucinated text over music/silence (e.g. outro-music loops).
269
+ VOXHELM_WHISPERCPP_MAX_CONTEXT = int(os.getenv("VOXHELM_WHISPERCPP_MAX_CONTEXT", "0"))
270
+ VOXHELM_WHISPERCPP_SUPPRESS_NST = env_bool("VOXHELM_WHISPERCPP_SUPPRESS_NST", default=True)
271
+ VOXHELM_WHISPERKIT_ENABLED = env_bool("VOXHELM_WHISPERKIT_ENABLED", default=False)
272
+ VOXHELM_WHISPERKIT_HOST = os.getenv("VOXHELM_WHISPERKIT_HOST", "127.0.0.1").strip()
273
+ VOXHELM_WHISPERKIT_PORT = int(os.getenv("VOXHELM_WHISPERKIT_PORT", "50060"))
274
+ VOXHELM_WHISPERKIT_BASE_URL = os.getenv(
275
+ "VOXHELM_WHISPERKIT_BASE_URL",
276
+ f"http://127.0.0.1:{VOXHELM_WHISPERKIT_PORT}/v1",
277
+ ).strip()
278
+ VOXHELM_WHISPERKIT_MODEL = os.getenv("VOXHELM_WHISPERKIT_MODEL", "large-v3-v20240930").strip()
279
+ VOXHELM_WHISPERKIT_AUDIO_ENCODER_COMPUTE_UNITS = os.getenv(
280
+ "VOXHELM_WHISPERKIT_AUDIO_ENCODER_COMPUTE_UNITS",
281
+ "cpuAndGPU",
282
+ ).strip()
283
+ VOXHELM_WHISPERKIT_TEXT_DECODER_COMPUTE_UNITS = os.getenv(
284
+ "VOXHELM_WHISPERKIT_TEXT_DECODER_COMPUTE_UNITS",
285
+ "cpuAndGPU",
286
+ ).strip()
287
+ VOXHELM_WHISPERKIT_CONCURRENT_WORKER_COUNT = int(
288
+ os.getenv("VOXHELM_WHISPERKIT_CONCURRENT_WORKER_COUNT", "8")
289
+ )
290
+ VOXHELM_WHISPERKIT_CHUNKING_STRATEGY = os.getenv(
291
+ "VOXHELM_WHISPERKIT_CHUNKING_STRATEGY",
292
+ "vad",
293
+ ).strip()
294
+ VOXHELM_WHISPERKIT_TIMEOUT_SECONDS = int(
295
+ os.getenv("VOXHELM_WHISPERKIT_TIMEOUT_SECONDS", "900")
296
+ )
297
+ VOXHELM_STT_DEBUG_LOGGING = env_bool("VOXHELM_STT_DEBUG_LOGGING", default=False)
298
+ VOXHELM_DIARIZATION_BACKEND = os.getenv("VOXHELM_DIARIZATION_BACKEND", "none").strip()
299
+ VOXHELM_PYANNOTE_MODEL = os.getenv(
300
+ "VOXHELM_PYANNOTE_MODEL",
301
+ "pyannote/speaker-diarization-3.1",
302
+ ).strip()
303
+ VOXHELM_PYANNOTE_DEVICE = os.getenv("VOXHELM_PYANNOTE_DEVICE", "auto").strip()
304
+ VOXHELM_HUGGINGFACE_TOKEN = os.getenv(
305
+ "VOXHELM_HUGGINGFACE_TOKEN",
306
+ os.getenv("HF_TOKEN", ""),
307
+ ).strip()
308
+ VOXHELM_MODEL_CACHE_DIR = Path(
309
+ os.getenv("VOXHELM_MODEL_CACHE_DIR", str(BASE_DIR / "var" / "models"))
310
+ )
311
+ VOXHELM_WYOMING_STT_HOST = os.getenv("VOXHELM_WYOMING_STT_HOST", "0.0.0.0").strip()
312
+ VOXHELM_WYOMING_STT_PORT = int(os.getenv("VOXHELM_WYOMING_STT_PORT", "10300"))
313
+ VOXHELM_WYOMING_STT_BACKEND = os.getenv("VOXHELM_WYOMING_STT_BACKEND", "").strip()
314
+ VOXHELM_WYOMING_STT_MODEL = os.getenv("VOXHELM_WYOMING_STT_MODEL", "").strip()
315
+ VOXHELM_WYOMING_STT_LANGUAGE = os.getenv("VOXHELM_WYOMING_STT_LANGUAGE", "").strip()
316
+ VOXHELM_WYOMING_STT_LANGUAGES = env_list("VOXHELM_WYOMING_STT_LANGUAGES")
317
+ VOXHELM_WYOMING_STT_PROMPT = os.getenv("VOXHELM_WYOMING_STT_PROMPT", "").strip()
318
+ VOXHELM_WYOMING_STT_NORMALIZE_TRANSCRIPT = env_bool(
319
+ "VOXHELM_WYOMING_STT_NORMALIZE_TRANSCRIPT",
320
+ default=True,
321
+ )
322
+ VOXHELM_WYOMING_SAMPLES_PER_CHUNK = int(
323
+ os.getenv("VOXHELM_WYOMING_SAMPLES_PER_CHUNK", "1024")
324
+ )
325
+ VOXHELM_LANE_SCHEDULER_ENABLED = env_bool("VOXHELM_LANE_SCHEDULER_ENABLED", default=False)
326
+ VOXHELM_LANE_SCHEDULER_DIR = Path(
327
+ os.getenv("VOXHELM_LANE_SCHEDULER_DIR", str(BASE_DIR / "var" / "lane-scheduler"))
328
+ )
329
+ VOXHELM_LANE_SCHEDULER_STALE_SECONDS = int(
330
+ os.getenv("VOXHELM_LANE_SCHEDULER_STALE_SECONDS", "1800")
331
+ )
332
+ VOXHELM_MAX_UPLOAD_BYTES = int(os.getenv("VOXHELM_MAX_UPLOAD_BYTES", str(25 * 1024 * 1024)))
333
+ VOXHELM_MAX_UPLOAD_MIB = VOXHELM_MAX_UPLOAD_BYTES // (1024 * 1024)
334
+ VOXHELM_MAX_URL_DOWNLOAD_BYTES = int(
335
+ os.getenv("VOXHELM_MAX_URL_DOWNLOAD_BYTES", str(128 * 1024 * 1024))
336
+ )
337
+ VOXHELM_URL_FETCH_TIMEOUT_SECONDS = int(os.getenv("VOXHELM_URL_FETCH_TIMEOUT_SECONDS", "60"))
338
+ VOXHELM_BATCH_MAX_DOWNLOAD_BYTES = int(
339
+ os.getenv("VOXHELM_BATCH_MAX_DOWNLOAD_BYTES", str(512 * 1024 * 1024))
340
+ )
341
+ VOXHELM_BATCH_MAX_STAGED_UPLOAD_BYTES = int(
342
+ os.getenv("VOXHELM_BATCH_MAX_STAGED_UPLOAD_BYTES", str(512 * 1024 * 1024))
343
+ )
344
+ VOXHELM_BATCH_MAX_STAGED_UPLOAD_MIB = VOXHELM_BATCH_MAX_STAGED_UPLOAD_BYTES // (1024 * 1024)
345
+ VOXHELM_STAGED_INPUT_RETENTION_SECONDS = int(
346
+ os.getenv("VOXHELM_STAGED_INPUT_RETENTION_SECONDS", str(24 * 60 * 60))
347
+ )
348
+ VOXHELM_ALLOWED_URL_HOSTS = set(env_list("VOXHELM_ALLOWED_URL_HOSTS"))
349
+ VOXHELM_TRUSTED_HTTP_HOSTS = set(env_list("VOXHELM_TRUSTED_HTTP_HOSTS"))
350
+ VOXHELM_ACCEPTED_MODELS = get_accepted_stt_models()
351
+ VOXHELM_BATCH_ACCEPTED_MODELS = get_batch_accepted_stt_models()
352
+ VOXHELM_TTS_BACKEND = os.getenv("VOXHELM_TTS_BACKEND", "piper").strip()
353
+ VOXHELM_PIPER_VOICE_DIR = Path(
354
+ os.getenv("VOXHELM_PIPER_VOICE_DIR", str(BASE_DIR / "var" / "piper"))
355
+ )
356
+ VOXHELM_PIPER_VOICES = env_list("VOXHELM_PIPER_VOICES")
357
+ VOXHELM_PIPER_DEFAULT_VOICE = os.getenv("VOXHELM_PIPER_DEFAULT_VOICE", "").strip()
358
+ VOXHELM_PIPER_LANGUAGE_VOICES = env_map("VOXHELM_PIPER_LANGUAGE_VOICES")
359
+ VOXHELM_TTS_MAX_INPUT_CHARS = int(os.getenv("VOXHELM_TTS_MAX_INPUT_CHARS", "5000"))
360
+ VOXHELM_ACCEPTED_SPEECH_MODELS = {
361
+ "auto",
362
+ "piper",
363
+ "tts-1",
364
+ "tts-1-hd",
365
+ }
366
+ VOXHELM_TASK_QUEUE = os.getenv("VOXHELM_TASK_QUEUE", "default")
367
+ VOXHELM_FFMPEG_BIN = os.getenv("VOXHELM_FFMPEG_BIN", "ffmpeg")
368
+ VOXHELM_ARTIFACT_BACKEND = os.getenv("VOXHELM_ARTIFACT_BACKEND", "filesystem").strip()
369
+ VOXHELM_ARTIFACT_ROOT = Path(
370
+ os.getenv("VOXHELM_ARTIFACT_ROOT", str(BASE_DIR / "var" / "artifacts"))
371
+ )
372
+ VOXHELM_ARTIFACT_BUCKET = os.getenv("VOXHELM_ARTIFACT_BUCKET", "voxhelm")
373
+ VOXHELM_ARTIFACT_PREFIX = os.getenv("VOXHELM_ARTIFACT_PREFIX", "voxhelm")
374
+ VOXHELM_ARTIFACT_S3_ENDPOINT_URL = os.getenv("VOXHELM_ARTIFACT_S3_ENDPOINT_URL", "").strip()
375
+ VOXHELM_ARTIFACT_S3_REGION = os.getenv("VOXHELM_ARTIFACT_S3_REGION", "us-east-1")
376
+ VOXHELM_ARTIFACT_S3_ACCESS_KEY_ID = os.getenv("VOXHELM_ARTIFACT_S3_ACCESS_KEY_ID", "").strip()
377
+ VOXHELM_ARTIFACT_S3_SECRET_ACCESS_KEY = os.getenv(
378
+ "VOXHELM_ARTIFACT_S3_SECRET_ACCESS_KEY", ""
379
+ ).strip()
380
+ VOXHELM_ARTIFACT_S3_FORCE_PATH_STYLE = env_bool(
381
+ "VOXHELM_ARTIFACT_S3_FORCE_PATH_STYLE",
382
+ default=True,
383
+ )
384
+ VOXHELM_TRANSCRIPTION_EXECUTION_MODE = validate_transcription_execution_mode(
385
+ os.getenv(
386
+ "VOXHELM_TRANSCRIPTION_EXECUTION_MODE",
387
+ "django_tasks",
388
+ ).strip()
389
+ )
390
+ VOXHELM_REMOTE_WORKER_LEASE_SECONDS = validate_positive_int(
391
+ "VOXHELM_REMOTE_WORKER_LEASE_SECONDS",
392
+ int(os.getenv("VOXHELM_REMOTE_WORKER_LEASE_SECONDS", str(30 * 60))),
393
+ )
394
+ VOXHELM_REMOTE_WORKER_POLL_SECONDS = validate_positive_int(
395
+ "VOXHELM_REMOTE_WORKER_POLL_SECONDS",
396
+ int(os.getenv("VOXHELM_REMOTE_WORKER_POLL_SECONDS", "5")),
397
+ )
398
+ VOXHELM_REMOTE_WORKER_MAX_ATTEMPTS = validate_positive_int(
399
+ "VOXHELM_REMOTE_WORKER_MAX_ATTEMPTS",
400
+ int(os.getenv("VOXHELM_REMOTE_WORKER_MAX_ATTEMPTS", "3")),
401
+ )
402
+ validate_remote_pull_worker_tokens(
403
+ VOXHELM_TRANSCRIPTION_EXECUTION_MODE,
404
+ VOXHELM_WORKER_TOKENS,
405
+ )
406
+ validate_remote_pull_artifact_backend(
407
+ VOXHELM_TRANSCRIPTION_EXECUTION_MODE,
408
+ VOXHELM_ARTIFACT_BACKEND,
409
+ )
410
+ validate_remote_pull_s3_configuration(
411
+ VOXHELM_TRANSCRIPTION_EXECUTION_MODE,
412
+ VOXHELM_ARTIFACT_BACKEND,
413
+ {
414
+ "VOXHELM_ARTIFACT_S3_ENDPOINT_URL": VOXHELM_ARTIFACT_S3_ENDPOINT_URL,
415
+ "VOXHELM_ARTIFACT_S3_ACCESS_KEY_ID": VOXHELM_ARTIFACT_S3_ACCESS_KEY_ID,
416
+ "VOXHELM_ARTIFACT_S3_SECRET_ACCESS_KEY": VOXHELM_ARTIFACT_S3_SECRET_ACCESS_KEY,
417
+ "VOXHELM_ARTIFACT_BUCKET": VOXHELM_ARTIFACT_BUCKET,
418
+ },
419
+ )
420
+
421
+ TASKS = {
422
+ "default": {
423
+ "BACKEND": os.getenv("VOXHELM_TASKS_BACKEND", "django_tasks_db.backend.DatabaseBackend")
424
+ }
425
+ }
config/urls.py ADDED
@@ -0,0 +1,19 @@
1
+ from django.urls import include, path
2
+
3
+ from operators.views import logout_view, operator_artifact, root
4
+ from synthesis.views import audio_speech
5
+ from transcriptions.views import audio_transcriptions, health
6
+
7
+ urlpatterns = [
8
+ path("", root, name="root"),
9
+ path("logout", logout_view, name="logout"),
10
+ path(
11
+ "transcripts/<uuid:job_id>/artifacts/<str:format_name>",
12
+ operator_artifact,
13
+ name="operator-artifact",
14
+ ),
15
+ path("v1/health", health, name="health"),
16
+ path("v1/audio/speech", audio_speech, name="audio-speech"),
17
+ path("v1/audio/transcriptions", audio_transcriptions, name="audio-transcriptions"),
18
+ path("v1/", include("jobs.urls")),
19
+ ]
config/wsgi.py ADDED
@@ -0,0 +1,7 @@
1
+ import os
2
+
3
+ from django.core.wsgi import get_wsgi_application
4
+
5
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
6
+
7
+ application = get_wsgi_application()
jobs/__init__.py ADDED
@@ -0,0 +1 @@
1
+
jobs/apps.py ADDED
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class JobsConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "jobs"