Skip to content

Latest commit

 

History

History
580 lines (456 loc) · 37.8 KB

changelog.en.md

File metadata and controls

580 lines (456 loc) · 37.8 KB

Changelog

简体中文 | English

Unreleased

No unreleased changes.

0.8.4 — Rust kernel foundation and release gates (2026-06-10)

Features

  • Added an optional artifacts manifest to completed results. The manifest lists artifact category, role, filename, media type, and speaker_label where relevant; it does not expose local paths, job runtime paths, hosts, tokens, or debug data. Default clients can continue to rely only on the result.json primary view, and must treat unknown or missing artifacts fields as compatible.
  • Added the optional Rust kernel bridge foundation and RUST_KERNEL_MODE. The default off keeps current Python implementations; required makes selected Rust-backed paths import and execute successfully or fail closed.
  • Added an optional Rust-backed voiceprint scoring kernel for explicit RUST_KERNEL_MODE=required runs. The default remains Python scoring, and the public speaker/voiceprint result contract is unchanged.
  • Added optional Rust-backed result post-processing for explicit RUST_KERNEL_MODE=required runs. The default remains Python post-processing; result segments keep stable speaker_label values, duplicate display names are disambiguated instead of merged, and segments[].words remains optional.
  • Added artifact/status/schema helper contracts. Artifact manifests are normalized through public-safe stable / optional / experimental categories, persisted status payloads share one contract helper, and schema versions stay optional-first for legacy result.json / status.json compatibility.
  • Added optional Rust-backed artifact manifest helper validation for explicit RUST_KERNEL_MODE=required runs. The default remains Python-owned contract assembly; selected Rust helper validation must import and run successfully or fail closed.

Security

  • Raised the python-multipart floor to 0.0.27 to avoid CVE-2026-42561 while keeping the dependency range minimal.

CI

  • Added a Rust foundation heavy gate. PR first rounds, pushes to main, and manual runs build the internal voscript_core wheel, smoke-test the extension, and run Docker packaging smoke with that wheel. Later PR updates do not rerun the heavy gate automatically; trigger it manually before merge when needed.
  • Extended Rust kernel tests with voiceprint scoring golden cases for raw cosine, AS-norm activation, small-cohort raw fallback, ambiguous top-2 margins, and non-finite embedding rejection.
  • Extended Rust kernel and Docker smoke coverage to include result post-processing segment assembly, display-name disambiguation, and word normalization.
  • Extended contract tests for legacy / unknown artifact manifests, persisted status payloads, optional schema versions, and Rust artifact helper bridge validation.
  • Added a 0.8.4 Rust-kernel release-gate matrix covering selected voiceprint, post-process, artifact, and status helper paths with explicit hard-fail, rollback, CI, and internal performance-baseline evidence.
  • Added a repository-owned public release scan and CI gate so PR/release checks fail before private paths, raw validation material, real IDs, or secret-like values reach public release text.

0.7.6 — Health, alignment, and embedding runtime fixes (2026-05-07)

Security

  • Updated the dependency security baseline and FOSSA policy test coverage to tighten the public dependency scanning flow.
  • Moved WhisperX alignment from the yanked 3.1.x package series to whisperx==3.3.1, with compatible pyannote.audio==3.3.2 and cuDNN9-compatible faster-whisper>=1.2.1,<2.0.0 / ctranslate2>=4.7.1,<5.0, plus bounded pyannote.* and pandas transitive dependencies. Docker installs WhisperX with --no-deps so its old ASR transitive dependencies cannot replace the current numpy<2 / SciPy 1.11.x / cuDNN9 runtime baseline or trigger cuDNN8 library lookups; the small Punkt sentence-span API used by alignment is provided by an internal compatibility shim instead of pulling the full NLTK distribution.

Reliability

  • Transcription jobs no longer run full Python GC before/after every GPU job; active job boundaries only clear the CUDA cache, while full GC remains on the idle-unload path. This avoids long GIL holds that can make /healthz time out after large alignment results complete.
  • WhisperX forced-alignment models are now cached by language / model / device and default to WHISPERX_ALIGN_DEVICE=cpu, isolating alignment from GPU ASR, diarization, and embedding runtimes. Operators can explicitly set pipeline, asr, cuda, or cuda:0 after validating CUDA alignment stability.
  • The ASR hallucination guard now filters short single-segment stock outros dominated by markers such as "like / subscribe / repost / tip", including raw ASR segments that are slightly over 30 seconds, while keeping normal contextual words in longer meeting transcripts.
  • The embedding stage now reads the normalized WAV once and slices it by diarization turns, avoiding repeated torchaudio native decoding for every turn. It falls back to the previous segmented loader on read failure and adds aggregate embedding_audio_load_timing logs.

Observability

  • Added safe model-load and transcription-stage timing logs for ASR, diarization, embedding, voiceprint match, enhancement, and pipeline stage timing. Logs record only stage, model, elapsed time, and aggregate metrics; they do not include filenames, paths, job IDs, speaker IDs, hosts, or tokens.

Validation

  • Internal live validation covered 0.7.6 health stability during GPU cleanup, the WhisperX alignment runtime, the stock outro hallucination guard, and the embedding audio slicing / single soundfile load path.

0.7.5 — Idle GPU model unload and CI quality gates (2026-04-29)

Bug Fixes

  • Fixed multi-GPU / per-model lazy-load placement (#20): Docker Compose now requests all available NVIDIA GPUs by default instead of limiting the container with CUDA_VISIBLE_DEVICES=0 and count: 1. It also no longer injects an empty CUDA_VISIBLE_DEVICES by default; operators must use a compose override or explicit env injection to restrict the visible set.
  • ASR/faster-whisper, diarization/pyannote, and embedding/WeSpeaker no longer share one pipeline-level device. Each model chooses the visible GPU with the most free memory during its own lazy load; fixed settings such as DEVICE=cuda:0 remain pinned and do not auto-move to another GPU.
  • Fixed faster-whisper CUDA device arguments: internal torch-facing device state can still use cuda:0 / cuda:1, but faster-whisper loads now receive device="cuda" with the matching device_index, avoiding unsupported device cuda:0.
  • Fixed pyannote local-cache loading: when complete Hugging Face snapshots are already present, diarization now receives a runtime-localized config whose nested segmentation / embedding models also point at local weight files. Missing caches still fall back to Hub repo ids, and incomplete local artifacts fail explicitly before model loading.

Features

  • Added optional MODEL_IDLE_TIMEOUT_SEC support. The default 180 seconds (3 minutes) unloads loaded GPU models only after the serialized GPU runtime has stayed idle for the configured timeout; explicitly set 0 to disable idle unload and keep models resident.
  • On the next lazy model load after an unload, CUDA reloads now choose the visible device with the most free memory. Probe failures safely fall back to the configured DEVICE.

Reliability

  • The idle-unload daemon shares the GPU semaphore with transcription work and rechecks the idle timestamp after acquiring it, avoiding stale pre-wait unload decisions when a new job completes while the daemon is queued.

Documentation

  • Added configuration.en.md / configuration.zh.md with the full v0.7.5 configuration and tuning reference, covering service env, supported and not-yet-exposed ASR settings, denoise override semantics, diarization / alignment, embedding, voiceprint / AS-norm, result contracts, and public-safe E2E validation wording.
  • Updated README, quickstart, API, voiceprint tuning, .env.example, and compose links so users can find the full configuration reference from public docs.
  • Tightened denoise and AS-norm validation wording: the SNR gate applies only to DeepFilterNet, noisereduce is not SNR-gated, cohort size below 10 uses raw-cosine fallback, and full AS-norm validation requires cohort size >=10.
  • Documented MODEL_IDLE_TIMEOUT_SEC in the full configuration reference, quickstart, .env.example, and compose defaults.
  • Updated README, quickstart, configuration, .env.example, and compose comments to explain that CUDA_VISIBLE_DEVICES is unset by default, the container can see every Docker-exposed GPU, visibility limits require an explicit override/env injection, and in-container cuda:N indexes are remapped from that visible set.

CI

  • Added Codecov coverage and test-result uploads for the existing CI test job, keeping the required check name unchanged.
  • Added non-required FOSSA and Claude Code review workflows that skip cleanly when their repository secrets are not configured.
  • Added REVIEW.md so automated code review focuses on VoScript-specific reliability, privacy, model lifecycle, API, and documentation risks.

Validation

  • Internal live validation covered the 0.7.5 idle unload, multi-GPU lazy-load placement, fixed DEVICE=cuda:N pinning, faster-whisper device_index, pyannote local snapshot/config loading, and default MODEL_IDLE_TIMEOUT_SEC=180 behavior.

0.7.4 — Environment defaults and contract prep (2026-04-26)

Bug Fixes

  • Speaker identity preservation follow-up (#8): result artifacts preserve the raw diarization speaker_label even when multiple clusters match the same enrolled voiceprint. Display names are still disambiguated for readability, but downstream clients can safely use speaker_label as the stable cluster key.
  • AS-norm sample-count-aware calibration: active AS-norm matching now applies a z-score-specific dynamic threshold based on each candidate's enrolled sample count and sample spread. Single-sample candidates are stricter than the 0.5 operating point, so weak scores such as 0.5713 no longer auto-name a speaker.
  • AS-norm ambiguity guard: active AS-norm matching now reranks candidates by normalized score before applying threshold and top-1/top-2 margin checks. Ambiguous candidates remain unnamed for review.
  • AS-norm cohort preservation: direct-loading an existing asnorm_cohort.npy now marks the auto-rebuild state clean, and background rebuilds no longer replace a larger persisted/in-memory cohort with fewer transcription embeddings. Clearing transcription results therefore cannot indirectly shrink the AS-norm cohort.
  • Denoise env/API precedence: omitting denoise_model from POST /api/transcribe now uses the server-side DENOISE_MODEL; explicitly sending denoise_model=none disables denoising for that request. Explicit snr_threshold continues to override the DeepFilterNet DENOISE_SNR_THRESHOLD gate; noisereduce is not controlled by that gate.

Configuration

  • Published and aligned the v0.7.4 runtime defaults across .env.example, compose, and docs: DENOISE_MODEL=none, DENOISE_SNR_THRESHOLD=10.0, VOICEPRINT_THRESHOLD=0.75, PYANNOTE_MIN_DURATION_OFF=0.5, MIN_EMBED_DURATION=1.5, and MAX_EMBED_DURATION=10.0.
  • Centralized the new defaults in app/config.py so pyannote/off-turn and embedding-window tuning no longer require providers to read these environment variables directly.

Documentation

  • Added voiceprint-tuning.en.md and voiceprint-tuning.zh.md, documenting voiceprint-related environment variables, API parameters, current hardcoded AS-norm/raw threshold defaults, cohort/top_n/margin behavior, and tuning guidance.
  • Updated README, quickstart, API reference, and voiceprint tuning docs with the v0.7.4 public env defaults, result contract anchors, and denoise override semantics.
  • Fixed the compose comment that pointed at the nonexistent docs/configuration.md; it now points at the checked-in voiceprint tuning reference.

Tests

  • Added local contract coverage for status=completed, raw segments[].speaker_label, optional alignment metadata, and denoise env/API precedence.
  • Added AS-norm cohort regressions covering clean direct-load auto ticks, empty/small transcription sources preserving a larger cohort, and explicit rebuild retaining its manual semantics.

0.7.3 — Runtime stability hotfixes (2026-04-25)

Bug Fixes

  • Diarization cold-start resilience: pyannote diarization and WeSpeaker embedding loading now check the existing Hugging Face snapshot cache first and load from it when complete, avoiding unnecessary network downloads on warm deployments.
  • Xet/CAS download bypass by default: the runtime now sets HF_HUB_DISABLE_XET=1 before importing Hugging Face Hub clients unless operators explicitly override it. This avoids the hf-xet/CAS bridge path that can fail with TLS EOF errors in some remote environments.
  • Faster cache fallback: Docker and compose defaults include HF_HUB_ETAG_TIMEOUT=3, so Hub metadata checks fall back to local cache more quickly when the network is slow or unreliable.
  • ASR repetition guard: prompt-contaminated repetition runs, such as the Chinese output instruction being repeated as transcript text, are filtered before downstream diarization, punctuation, and artifacts.
  • Chinese alignment prerequisite fix: the Docker base image is upgraded to pytorch/pytorch:2.6.0-cuda12.4-cudnn9-runtime so recent transformers safety checks can load WhisperX's default Chinese PyTorch .bin alignment weights.
  • Chinese alignment remains enabled by default: zh is no longer placed in WHISPERX_ALIGN_DISABLED_LANGUAGES by default. Use that variable only as an explicit temporary operational fallback.
  • Configurable alignment policy: added WHISPERX_ALIGN_DISABLED_LANGUAGES, WHISPERX_ALIGN_MODEL_MAP, WHISPERX_ALIGN_MODEL_DIR, and WHISPERX_ALIGN_CACHE_ONLY.
  • Safe pyannote checkpoint loading: for PyTorch 2.6 weights-only checkpoint loading, pyannote diarization model loading now temporarily trusts only the required checkpoint metadata types (TorchVersion, Problem, Specifications, Resolution) during from_pretrained. It does not use a process-global allowlist or disable the weights-only safety check.
  • Sanitized failure metadata: completed results may include an alignment object recording succeeded, skipped, or failed status. Torch safety blocks are classified as reason=torch_version_blocked, and logs no longer include raw alignment exception text that might contain local paths or credentials.

Deployment

  • Rebuild the container image to pick up the torch 2.6 base image. Existing model cache volumes remain compatible.

Compatibility

  • Existing transcription results remain compatible. alignment is additive, and words[] was already optional.

0.7.2 — Architecture foundation + stability hardening (2026-04-24)

Architecture

  • Pipeline/provider/infra layering: the former flat pipeline, job runtime, audio handling, and voiceprint database modules are split into pipeline/, providers/, infra/, application/, and voiceprints/ boundaries. The public HTTP API remains compatible, while the internal layout now matches the planned stage/provider architecture.
  • Canonical pipeline slots: the codebase now has stable stage/provider boundaries for normalization, enhancement, ASR, diarization, speaker embedding, voiceprint matching, post-processing, and artifact persistence. These internal extension points are not part of the public API contract in 0.7.2.
  • Release hygiene: FastAPI metadata now reports 0.7.2, and Docker healthcheck no longer depends on curl; it uses Python standard library probing against /healthz, matching the runtime image.

Stability and validation

  • Internal live validation: the feat/v0.7.2 candidate was validated with the live API suite, overlap bench, and internal validation runs.
  • AS-norm enrollment probe: an internal validation sample was enrolled, the cohort was rebuilt, and a separate probe clip matched the newly enrolled speaker. This validates the voiceprint API, rebuild entrypoint, and raw-cosine fallback; without trustworthy >=10 cohort evidence, it must not be described as a full AS-norm scoring-path validation.
  • Security and failure-path hardening: additional tests cover corrupt results, partial upload cleanup, export name injection, failed status persistence, runner failure paths, and in-flight dedup cleanup.

Known trade-offs

  • GPU serialization scope: the v0.7.2 architecture runs the current full transcription pipeline under the existing serialized GPU work guard. This favors release stability after the large refactor but can reduce throughput because some CPU/IO work is also serialized.
  • Job restart semantics: queued or in-progress jobs are still marked failed after process restart rather than resumed. This is explicit behavior, not silent recovery.

Compatibility

  • Existing HTTP endpoints and persisted status.json / result.json shapes remain compatible.
  • Internal planning documents are not part of the public release documentation.

0.7.1 — Auto cohort rebuild + thread-safety fixes (2026-04-22)

New Features

  • Automatic AS-norm cohort rebuild: A background daemon thread (cohort-rebuild) checks every 60 s for un-reflected enrollments. If there are pending enrollments and at least 30 s have elapsed since the last one (debounce), it triggers a rebuild automatically. Newly enrolled speakers enter the matching path within approximately 90 seconds with no manual operation; full AS-norm scoring requires cohort >=10, otherwise raw-cosine fallback remains active.
  • Concurrent rebuild protection: _cohort_rebuild_lock (non-blocking acquire) prevents the background thread and POST /api/voiceprints/rebuild-cohort from running simultaneous rebuilds.
  • ABA-safe dirty tracking: A _cohort_generation version counter replaces a boolean dirty flag, ensuring enrollments that arrive during a rebuild are not silently lost.

Bug Fixes

  • Graceful daemon shutdown: Lifespan teardown sets _stop_event and joins the thread with a 5-second timeout, preventing zombie threads on process exit.
  • Atomic write cleanup: _atomic_write_json now wraps the temp file in try/finally, deleting orphaned .tmp files when json.dump or fsync raises an exception.
  • speaker_id input validation: POST /api/voiceprints/enroll now validates the speaker_id Form field against ^spk_[A-Za-z0-9_-]{1,64}$, matching the path-parameter endpoints; invalid values return 422.
  • pip-audit hard gate: The || echo fallback is removed from CI; audit failures now block the build.
  • Removed legacy CQ-C1 counter: The per-10-transcription cohort rebuild trigger in job_service.py has been removed; the daemon thread is the sole auto-rebuild path.
  • In-flight concurrent dedup: _in_flight_hashes dict prevents two concurrent requests with identical audio from both burning GPU time; the second caller is redirected to the first job's ID immediately without starting a new transcription.
  • Durability window fully closed: register_in_flight is now called after _write_status succeeds, guaranteeing concurrent duplicates always find a live status.json for the redirected job ID. If the initial status.json write fails, the endpoint aborts with 503 before any in-flight entry is registered. _write_status now returns bool to make the failure detectable.
  • Segment speaker reassignment semantics hardened: PUT /api/transcriptions/{tr_id}/segments/{seg_id}/speaker now validates speaker_id against ^spk_[A-Za-z0-9_-]{1,64}$ (422 on format error) and verifies the voiceprint exists in the DB (404 if not found). When speaker_id is omitted, any previously assigned speaker_id is explicitly cleared to null. speaker_map is never modified by this endpoint (it records diarization-model results); unique_speakers is recalculated from all segments after each edit.
  • CI unit test gate: pytest tests/unit/ tests/test_security.py replaces the old explicit file list — test_job_service.py, test_main_lifespan.py, and test_voiceprint_db.py are now gated by CI.

Compatibility

  • All existing HTTP endpoints are unchanged.
  • Cohort auto-refresh is a visible behavior change: long-running services no longer need manual rebuild-cohort calls, though manual calls remain supported and bypass the debounce.

0.7.0 — Speaker identity preservation + ngram dedup parameter (2026-04-21)

Bug fix

  • Speaker identity preservation: when diarization produces multiple clusters (e.g. SPEAKER_00, SPEAKER_02) that match the same enrolled speaker, each original diarization label remains a distinct output speaker identity. Display names are disambiguated for readability (for example Maple, Maple (2)).

Features

  • no_repeat_ngram_size parameter: POST /api/transcribe gains an optional integer field no_repeat_ngram_size (default 0, disabled). When set to ≥ 3, the value is forwarded to faster-whisper to suppress n-gram repetitions in the transcript (e.g. "for example for example for example" → "for example"). Values < 3 or omitted are treated as disabled.
  • Input validation: no_repeat_ngram_size with a non-integer value (e.g. "banana") returns HTTP 422.
  • params field: completed job results now include no_repeat_ngram_size in the params object, recording the actual integer used (0 when disabled).

Tests

  • Added 43 E2E tests across 8 test classes: TestSpeakerIdentityPreservation, TestSecurity, TestSegmentReassignment, TestSpeakerManagement, TestExportFormats, TestOutputSchema, TestNoRepeatNgramSize, TestEdgeCases, TestLongChains.
  • Test suite: 84 collected (78 pass, 6 expected skips).

Deployment

  • docker-compose.yml adds ./app:/app volume mount so code changes deploy via rsync without an image rebuild.

Compatibility

  • All existing HTTP contracts unchanged.
  • no_repeat_ngram_size is a new optional field; old clients ignore it.
  • params.no_repeat_ngram_size is a new field; historical transcriptions without it should be treated as 0.

0.6.0 — Security Hardening + Architecture Restructure (2026-04-21)

Security

  • Path traversal protection: _safe_tr_dir() + FastAPI Path(pattern=r"^tr_[A-Za-z0-9_-]{1,64}$") parameter validation prevent directory traversal attacks (SEC-C1)
  • Pickle RCE fix: np.load(..., allow_pickle=False) prevents arbitrary code execution from malicious .npy files (SEC-C2)
  • Zero-vector defense: identify() returns early on all-zero embeddings to prevent AS-norm semantic mismatch
  • Voiceprint deduplication: add_speaker() deduplicates by name, preventing enrollment pollution (CQ-C3)
  • Tightened frontend CSP: Content-Security-Policy meta tag + sessionStorage replacing localStorage for API key storage (SEC-H2/H3)
  • Security response headers: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, X-XSS-Protection middleware

Architecture Restructure

  • main.py refactor: from ~980 lines to ~160-line orchestration entry point; new modules: app/config.py (all env vars), app/api/routers/ (transcriptions / voiceprints / health), app/api/deps.py (FastAPI dependency injection), app/services/audio_service.py, app/services/job_service.py
  • Job state persistence: _write_status() writes job status to status.json; recover_orphan_jobs() repairs orphan jobs at startup; completed transcriptions accessible via GET /api/transcriptions/{id} after restart (AR-C2)
  • LRU job cache: in-memory job dict replaced with _LRUJobsDict (max 200 entries) to prevent long-running memory leaks

Performance

  • Async upload: aiofiles streaming write + streaming SHA256 hash; uploads no longer block the event loop (PERF-C2)
  • Segment-level audio loading: torchaudio.load(frame_offset, num_frames) reads only speaker segments; GPU memory use drops >1000× for long recordings (PERF-H1)
  • NumPy BLAS cosine scan: _python_cosine_scan uses embs_normed @ q_normed for batch cosine computation (PERF-H8)

CI/CD

  • Test gate: CI no longer uses || true; failing tests block merge (CD-H1)
  • pip-audit security scan: dependency vulnerability scanning on every CI run (CD-C2)
  • Test suite: added tests/test_security.py, tests/test_voiceprint_db.py, tests/test_job_service.py (15 tests total)

Compatibility

  • All existing HTTP API contracts unchanged
  • No data migration required on upgrade

0.5.0 — AS-norm voiceprint scoring (2026-04-20)

AS-norm voiceprint scoring

  • Introduces ASNormScorer (voiceprint_db.py): wraps raw cosine scores with Adaptive Score Normalization against an impostor cohort, eliminating speaker-dependent baseline bias and improving relative EER by 15–30%.
  • On startup, the service automatically builds a cohort from existing transcription embeddings (emb_*.npy files) and saves it as data/transcriptions/asnorm_cohort.npy. Silently falls back to raw cosine if cohort build fails.
  • When AS-norm is active, the effective threshold is fixed at 0.5 (normalized operating point); otherwise the 0.4.0 adaptive cosine threshold is used.
  • New endpoint: POST /api/voiceprints/rebuild-cohort — manually rebuild the impostor cohort.

Compatibility

  • All existing endpoint behaviors unchanged.
  • In fresh deployments with no transcriptions yet, voiceprint identification automatically falls back to the 0.4.0 cosine logic.

0.4.0 — Adaptive voiceprint threshold + noise reduction SNR gate + OSD (2026-04-19)

Adaptive voiceprint threshold

  • VOICEPRINT_THRESHOLD is now a configurable env var (default 0.75) used as the base threshold.
  • Each speaker's effective threshold is automatically relaxed based on the cosine variance of their enrolled samples: 1 sample gives a fixed −0.05 relaxation; 2+ samples apply min(3×std, 0.10); absolute floor is 0.60.
  • A/B test on 10 PLAUD Pin recordings: recall improved from 50% to 70% with zero false identifications.
  • New env var: VOICEPRINT_THRESHOLD (default 0.75).

Noise reduction + SNR gate

  • New env var DENOISE_MODEL: none (default) | deepfilternet | noisereduce.
  • New env var DENOISE_SNR_THRESHOLD (default 10.0 dB): DeepFilterNet only; when the recording's SNR is at or above this value, DeepFilterNet is skipped to avoid degrading already-clean audio. noisereduce is not SNR-gated.
  • New denoising pipeline status (inserted after converting, before transcribing; only appears when denoising is enabled).
  • POST /api/transcribe gains two optional fields: denoise_model (string) and snr_threshold (float), allowing per-request overrides.
  • DeepFilterNet harms high-SNR recordings (>10 dB): segment count increases 100–145%, proxy CER degrades 20–91%. The DeepFilterNet SNR gate protects clean audio automatically.
  • CUDA OOM fix: after DeepFilterNet processes long audio (~15 GB PyTorch CUDA reserved), torch.cuda.empty_cache() + gc.collect() are called before invoking Whisper, resolving ctranslate2 OOM.

Overlapped speech detection (OSD)

  • POST /api/transcribe gains an osd field (bool, default false).
  • When enabled, each segment includes a has_overlap: bool field indicating whether two or more speakers were detected talking simultaneously at that segment's midpoint.
  • Uses pyannote/segmentation-3.0 (shared with the diarization pipeline — no additional model download needed).
  • Average of 9.7% overlapping segments across 10 real meeting recordings, within the expected range for normal conversation.

Result structure changes

  • A top-level params object is now included in every completed job result, recording the actual configuration used for that transcription run (language, denoise model, SNR threshold, voiceprint threshold, OSD flag, speaker count constraints).
  • The GET /api/config global endpoint has been removed — configuration is returned alongside job results, making each result self-contained.

Language auto-detection (no longer defaults to zh)

  • The language field in POST /api/transcribe now defaults to empty (auto-detect) instead of "zh".
  • When language is omitted, Whisper detects the language automatically; the service injects an initial_prompt that nudges the decoder toward Simplified Chinese output, keeping Mandarin audio correct without forcing the language.
  • Passing language=zh or language=en explicitly is unchanged.
  • params.language in the job result now shows "auto" when auto-detection was used, rather than a specific language code.

File hash deduplication

  • Every uploaded file is SHA256-hashed on arrival. If an identical file already has a completed job, POST /api/transcribe returns that existing result immediately without re-running Whisper.
  • The response when a dedup hit occurs includes deduplicated: true: { "id": "tr_existing_id", "status": "completed", "deduplicated": true }.
  • The returned id works exactly like any other — poll or export as normal.

Compatibility

  • HTTP contract is fully backwards-compatible: has_overlap and params are additive fields; old clients ignore them.
  • deduplicated is a new optional field on POST /api/transcribe responses; old clients ignore it.
  • Recordings from high-quality devices such as PLAUD Pin are handled identically unless denoising is explicitly enabled.
  • Recommended config: DENOISE_MODEL=none (PLAUD Pin / high-quality mic); DENOISE_MODEL=deepfilternet + DENOISE_SNR_THRESHOLD=10.0 (noisy environments).

0.3.0 — WhisperX alignment + sqlite voiceprint DB + WeSpeaker (2026-04-18)

Three independent core upgrades released together.

WhisperX forced alignment

  • app/pipeline.py's transcribe + align_segments now use whisperx's forced-alignment pipeline.
  • Each segment in the result carries an optional words: [{word, start, end, score}, …] field — word-level timestamps.
  • Still CTranslate2-based internally, so the existing local /models/faster-whisper-<size> cache continues to satisfy cold starts without round-tripping HuggingFace.
  • First boot downloads a wav2vec2 alignment model (cached under /cache). Alignment for Chinese audio can fail on some utterances — when that happens we gracefully fall back to segment-level timestamps (no words[]) instead of failing the whole job.
  • Version pin: whisperx==3.1.6. It matches our pyannote==3.1.1 pin while the Docker base supplies torch. 3.1.x is marked "yanked" on PyPI but pip installs yanked packages when the version is pinned explicitly in requirements.

Voiceprint store moved to sqlite + sqlite-vec

  • app/voiceprint_db.py no longer uses index.json + *.npy files.
  • Single voiceprints.db (sqlite 3) + sqlite-vec's vec0 virtual table for vector similarity. Top-k nearest-neighbour search runs in O(log N) through the extension.
  • All writes run inside sqlite transactions with WAL enabled; the old hand-rolled tempfile + os.replace atomic writer is gone.
  • Concurrency: Python-level threading.RLock + sqlite WAL.
  • Automatic migration on first boot: if index.json + .npy files exist, they're imported into sqlite in a single transaction and index.json is renamed to index.json.migrated.bak. The .npy files are left in place for rollback.
  • If sqlite-vec fails to load (build without the extension), falls back to a Python-side cosine full scan.

WeSpeaker ResNet34 replaces ECAPA-TDNN

  • pyannote/wespeaker-voxceleb-resnet34-LM replaces speechbrain's ECAPA.
  • Dependency diet: speechbrain is no longer required. WeSpeaker's wrapper class ships with pyannote.audio.
  • Embedding dimension goes from 192 (ECAPA) to ~256 (WeSpeaker).
  • Breaking: the new embedding space is not cosine-comparable with ECAPA's. Every voiceprint enrolled under 0.2.x has to be re-enrolled — comparing old ECAPA vectors against new WeSpeaker queries yields essentially random cosine similarities.
  • HF gated model: accept the agreement at https://huggingface.co/pyannote/wespeaker-voxceleb-resnet34-LM before first use — same process as for speaker-diarization-3.1 and segmentation-3.0.
  • The per-speaker chunk selection (≥1 s, top-10 longest, mean pooled) is unchanged.

Upgrade-breaking changes

  • Bigger container image: WhisperX pulls wav2vec2 deps + the alignment model weights (~1 GB extra cache on first run).
  • Old voiceprints must be re-enrolled. Either delete data/voiceprints/voiceprints.db or DELETE /api/voiceprints/{id} one by one, then re-enroll.
  • One more HuggingFace gated model to agree to (WeSpeaker).

Stays compatible

  • HTTP contract unchanged (segments[i].words is a new optional field — old clients ignore it).
  • No new env vars.
  • Container name voscript, port, data-dir layout, non-root user, and API_KEY enforcement all unchanged.

0.2.1 — Renamed to voscript (2026-04-18)

Decoupled from BetterAINote — the service stands on its own, so it now has its own name:

  • Repo: MapleEve/openplaud-voice-transcribe → MapleEve/voscript (GitHub keeps a permanent 301 from the old slug, existing clones keep working).
  • Docker service / container name: voice-transcribe → voscript (docker logs voscript, docker exec voscript …).
  • Image name: compose produces voscript-voscript:latest automatically.
  • README / docs: repositioned as an independent transcription service, with BetterAINote called out as one known consumer rather than the service's identity.
  • HTTP contract, file layout, env vars, on-disk data layout: unchanged. Existing clients keep working with zero code changes.

0.2.0 — Post red-team hardening (2026-04-18)

Full hardening pass following real-audio end-to-end testing and an independent penetration test.

Security hardening

  • Container no longer runs as root. A non-root app user (uid/gid 1000 by default, overridable via APP_UID/APP_GID). An RCE inside the container only owns the service's own uid — it can't read other root-owned files on the host.
  • Upload size limit. /api/transcribe now reads the upload in 1 MiB chunks and aborts with HTTP 413 when the total exceeds MAX_UPLOAD_BYTES (default 2 GiB, configurable). The partial file is removed so disk doesn't leak.
  • Upload filename sanitization. PurePosixPath(filename).name strips any client-supplied directory components — an attacker-set filename=../../etc/passwd.wav becomes passwd.wav before the path is built.
  • ffmpeg argv hardening. -- is inserted before the input path so a filename starting with - can't be interpreted as a flag.
  • Constant-time key comparison. hmac.compare_digest replaces plain != for the bearer/key check. Eliminates any theoretical timing side channel.
  • Tighter public-path matching. /docs, /redoc, /openapi.json moved from startswith() prefixes to exact-match only. /docsXYZ used to slip past auth; now it correctly returns 401.
  • Concurrency-safe, atomic VoiceprintDB. Every mutation holds threading.Lock; index.json and every .npy write goes through tempfile + os.replace so a crash mid-write can't corrupt the index.
  • np.load(..., allow_pickle=False) everywhere. Closes the pickle-deserialization RCE path.

Features & config

  • New env var MAX_UPLOAD_BYTES (default 2147483648, i.e. 2 GiB) configurable in .env / compose.
  • New env vars APP_UID / APP_GID (default 1000) for hosts where the bind-mount owner isn't uid 1000.
  • HuggingFace cache migrated from the container's /root/.cache/huggingface to /cache. HF_HOME, HUGGINGFACE_HUB_CACHE, TORCH_HOME, and XDG_CACHE_HOME all point at /cache so the non-root user can write it.
  • docker-compose.yml mounts ${MODEL_CACHE_DIR} both at /cache (read-write, HF hub cache) and /models:ro (read-only, for pipeline.py's local-first checkpoint resolver), so cold starts don't round-trip through HuggingFace.

Bug fixes

  • _convert_to_wav now shells out to ffmpeg directly instead of going through pydub.AudioSegment. Fixes KeyError('codec_type') that pydub hits on newer ffmpeg's Opus/container output (jiaaro/pydub#638).
  • GET / now reaches browsers when API_KEY is set. Previously the bundled Web UI was effectively unreachable because browsers can't attach a Bearer header to a direct navigation; the UI's own fetch-to-/api/* calls still carry the key.
  • On the BetterAINote side, VoiceTranscribeProvider now keeps the raw speaker_label (SPEAKER_XX) in speakerSegments[].speaker, so re-enrollment still works after an automatic voiceprint match.

Breaking

  • The container's HF cache path moved from /root/.cache/huggingface to /cache. If you had a host directory bind-mounted at the old path, update your compose (or just use this repo's new docker-compose.yml, which handles both mounts for you).
  • Requests beyond MAX_UPLOAD_BYTES now fail with 413 instead of silently succeeding. Default 2 GiB fits every real-world audio we tested.

0.1.0 — Initial public release

  • First public release of the private transcription backend used by BetterAINote.
  • Async job pipeline: queued → converting → transcribing → identifying → completed.
  • faster-whisper large-v3 + pyannote 3.1 + ECAPA-TDNN speaker embeddings.
  • Persistent voiceprint DB with cosine-similarity auto-match.
  • Optional API_KEY bearer auth on all /api/* routes.
  • Portable docker-compose.yml (data/model paths configurable via env).
  • Dependency pins to keep pyannote.audio==3.1.1 usable:
    • numpy<2 (pyannote 3.1.1 uses np.NaN, removed in numpy 2.x).
    • huggingface_hub<0.24 (keeps the use_auth_token kwarg pyannote calls).