简体中文 | English
No unreleased changes.
- Added an optional
artifactsmanifest to completed results. The manifest lists artifact category, role, filename, media type, andspeaker_labelwhere relevant; it does not expose local paths, job runtime paths, hosts, tokens, or debug data. Default clients can continue to rely only on theresult.jsonprimary view, and must treat unknown or missingartifactsfields as compatible. - Added the optional Rust kernel bridge foundation and
RUST_KERNEL_MODE. The defaultoffkeeps current Python implementations;requiredmakes selected Rust-backed paths import and execute successfully or fail closed. - Added an optional Rust-backed voiceprint scoring kernel for explicit
RUST_KERNEL_MODE=requiredruns. 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=requiredruns. The default remains Python post-processing; result segments keep stablespeaker_labelvalues, duplicate display names are disambiguated instead of merged, andsegments[].wordsremains 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.jsoncompatibility. - Added optional Rust-backed artifact manifest helper validation for explicit
RUST_KERNEL_MODE=requiredruns. The default remains Python-owned contract assembly; selected Rust helper validation must import and run successfully or fail closed.
- Raised the
python-multipartfloor to0.0.27to avoidCVE-2026-42561while keeping the dependency range minimal.
- Added a Rust foundation heavy gate. PR first rounds, pushes to
main, and manual runs build the internalvoscript_corewheel, 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.
- 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.xpackage series towhisperx==3.3.1, with compatiblepyannote.audio==3.3.2and cuDNN9-compatiblefaster-whisper>=1.2.1,<2.0.0/ctranslate2>=4.7.1,<5.0, plus boundedpyannote.*andpandastransitive dependencies. Docker installs WhisperX with--no-depsso its old ASR transitive dependencies cannot replace the currentnumpy<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.
- 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
/healthztime 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 setpipeline,asr,cuda, orcuda:0after 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_timinglogs.
- 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.
- 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.
- 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=0andcount: 1. It also no longer injects an emptyCUDA_VISIBLE_DEVICESby 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:0remain 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 receivedevice="cuda"with the matchingdevice_index, avoidingunsupported 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.
- Added optional
MODEL_IDLE_TIMEOUT_SECsupport. The default180seconds (3 minutes) unloads loaded GPU models only after the serialized GPU runtime has stayed idle for the configured timeout; explicitly set0to 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.
- 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.
- Added
configuration.en.md/configuration.zh.mdwith 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,
noisereduceis not SNR-gated, cohort size below 10 uses raw-cosine fallback, and full AS-norm validation requires cohort size >=10. - Documented
MODEL_IDLE_TIMEOUT_SECin the full configuration reference, quickstart,.env.example, and compose defaults. - Updated README, quickstart, configuration,
.env.example, and compose comments to explain thatCUDA_VISIBLE_DEVICESis unset by default, the container can see every Docker-exposed GPU, visibility limits require an explicit override/env injection, and in-containercuda:Nindexes are remapped from that visible set.
- 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.mdso automated code review focuses on VoScript-specific reliability, privacy, model lifecycle, API, and documentation risks.
- Internal live validation covered the 0.7.5 idle unload, multi-GPU lazy-load
placement, fixed
DEVICE=cuda:Npinning, faster-whisperdevice_index, pyannote local snapshot/config loading, and defaultMODEL_IDLE_TIMEOUT_SEC=180behavior.
- Speaker identity preservation follow-up (#8): result artifacts preserve the raw diarization
speaker_labeleven when multiple clusters match the same enrolled voiceprint. Display names are still disambiguated for readability, but downstream clients can safely usespeaker_labelas 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.5operating point, so weak scores such as0.5713no 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.npynow 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_modelfromPOST /api/transcribenow uses the server-sideDENOISE_MODEL; explicitly sendingdenoise_model=nonedisables denoising for that request. Explicitsnr_thresholdcontinues to override the DeepFilterNetDENOISE_SNR_THRESHOLDgate;noisereduceis not controlled by that gate.
- 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, andMAX_EMBED_DURATION=10.0. - Centralized the new defaults in
app/config.pyso pyannote/off-turn and embedding-window tuning no longer require providers to read these environment variables directly.
- Added
voiceprint-tuning.en.mdandvoiceprint-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.
- Added local contract coverage for
status=completed, rawsegments[].speaker_label, optionalalignmentmetadata, 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.
- 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=1before 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-runtimeso recent transformers safety checks can load WhisperX's default Chinese PyTorch.binalignment weights. - Chinese alignment remains enabled by default:
zhis no longer placed inWHISPERX_ALIGN_DISABLED_LANGUAGESby 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, andWHISPERX_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) duringfrom_pretrained. It does not use a process-global allowlist or disable the weights-only safety check. - Sanitized failure metadata: completed results may include an
alignmentobject recordingsucceeded,skipped, orfailedstatus. Torch safety blocks are classified asreason=torch_version_blocked, and logs no longer include raw alignment exception text that might contain local paths or credentials.
- Rebuild the container image to pick up the torch 2.6 base image. Existing model cache volumes remain compatible.
- Existing transcription results remain compatible.
alignmentis additive, andwords[]was already optional.
- Pipeline/provider/infra layering: the former flat pipeline, job runtime, audio handling, and voiceprint database modules are split into
pipeline/,providers/,infra/,application/, andvoiceprints/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 oncurl; it uses Python standard library probing against/healthz, matching the runtime image.
- Internal live validation: the
feat/v0.7.2candidate 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.
- 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.
- Existing HTTP endpoints and persisted
status.json/result.jsonshapes remain compatible. - Internal planning documents are not part of the public release documentation.
- 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 andPOST /api/voiceprints/rebuild-cohortfrom running simultaneous rebuilds. - ABA-safe dirty tracking: A
_cohort_generationversion counter replaces a boolean dirty flag, ensuring enrollments that arrive during a rebuild are not silently lost.
- Graceful daemon shutdown: Lifespan teardown sets
_stop_eventand joins the thread with a 5-second timeout, preventing zombie threads on process exit. - Atomic write cleanup:
_atomic_write_jsonnow wraps the temp file intry/finally, deleting orphaned.tmpfiles whenjson.dumporfsyncraises an exception. speaker_idinput validation:POST /api/voiceprints/enrollnow validates thespeaker_idForm field against^spk_[A-Za-z0-9_-]{1,64}$, matching the path-parameter endpoints; invalid values return 422.- pip-audit hard gate: The
|| echofallback 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.pyhas been removed; the daemon thread is the sole auto-rebuild path. - In-flight concurrent dedup:
_in_flight_hashesdict 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_flightis now called after_write_statussucceeds, guaranteeing concurrent duplicates always find a livestatus.jsonfor the redirected job ID. If the initialstatus.jsonwrite fails, the endpoint aborts with503before any in-flight entry is registered._write_statusnow returnsboolto make the failure detectable. - Segment speaker reassignment semantics hardened:
PUT /api/transcriptions/{tr_id}/segments/{seg_id}/speakernow validatesspeaker_idagainst^spk_[A-Za-z0-9_-]{1,64}$(422 on format error) and verifies the voiceprint exists in the DB (404 if not found). Whenspeaker_idis omitted, any previously assignedspeaker_idis explicitly cleared tonull.speaker_mapis never modified by this endpoint (it records diarization-model results);unique_speakersis recalculated from all segments after each edit. - CI unit test gate:
pytest tests/unit/ tests/test_security.pyreplaces the old explicit file list —test_job_service.py,test_main_lifespan.py, andtest_voiceprint_db.pyare now gated by CI.
- All existing HTTP endpoints are unchanged.
- Cohort auto-refresh is a visible behavior change: long-running services no longer need manual
rebuild-cohortcalls, though manual calls remain supported and bypass the debounce.
- 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 exampleMaple,Maple (2)).
no_repeat_ngram_sizeparameter:POST /api/transcribegains an optional integer fieldno_repeat_ngram_size(default0, 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_sizewith a non-integer value (e.g."banana") returns HTTP 422. paramsfield: completed job results now includeno_repeat_ngram_sizein theparamsobject, recording the actual integer used (0when disabled).
- 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).
docker-compose.ymladds./app:/appvolume mount so code changes deploy via rsync without an image rebuild.
- All existing HTTP contracts unchanged.
no_repeat_ngram_sizeis a new optional field; old clients ignore it.params.no_repeat_ngram_sizeis a new field; historical transcriptions without it should be treated as0.
- Path traversal protection:
_safe_tr_dir()+ FastAPIPath(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.npyfiles (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-Policymeta tag +sessionStoragereplacinglocalStoragefor API key storage (SEC-H2/H3) - Security response headers:
X-Content-Type-Options,X-Frame-Options,Referrer-Policy,X-XSS-Protectionmiddleware
- 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 tostatus.json;recover_orphan_jobs()repairs orphan jobs at startup; completed transcriptions accessible viaGET /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
- Async upload:
aiofilesstreaming 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_scanusesembs_normed @ q_normedfor batch cosine computation (PERF-H8)
- 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)
- All existing HTTP API contracts unchanged
- No data migration required on upgrade
- 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_*.npyfiles) and saves it asdata/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.
- All existing endpoint behaviors unchanged.
- In fresh deployments with no transcriptions yet, voiceprint identification automatically falls back to the 0.4.0 cosine logic.
VOICEPRINT_THRESHOLDis now a configurable env var (default0.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(default0.75).
- New env var
DENOISE_MODEL:none(default) |deepfilternet|noisereduce. - New env var
DENOISE_SNR_THRESHOLD(default10.0dB): DeepFilterNet only; when the recording's SNR is at or above this value, DeepFilterNet is skipped to avoid degrading already-clean audio.noisereduceis not SNR-gated. - New
denoisingpipeline status (inserted afterconverting, beforetranscribing; only appears when denoising is enabled). POST /api/transcribegains two optional fields:denoise_model(string) andsnr_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.
POST /api/transcribegains anosdfield (bool, defaultfalse).- When enabled, each segment includes a
has_overlap: boolfield 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.
- A top-level
paramsobject 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/configglobal endpoint has been removed — configuration is returned alongside job results, making each result self-contained.
- The
languagefield inPOST /api/transcribenow defaults to empty (auto-detect) instead of"zh". - When
languageis omitted, Whisper detects the language automatically; the service injects aninitial_promptthat nudges the decoder toward Simplified Chinese output, keeping Mandarin audio correct without forcing the language. - Passing
language=zhorlanguage=enexplicitly is unchanged. params.languagein the job result now shows"auto"when auto-detection was used, rather than a specific language code.
- Every uploaded file is SHA256-hashed on arrival. If an identical file already has a completed job,
POST /api/transcribereturns 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
idworks exactly like any other — poll or export as normal.
- HTTP contract is fully backwards-compatible:
has_overlapandparamsare additive fields; old clients ignore them. deduplicatedis a new optional field onPOST /api/transcriberesponses; 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).
Three independent core upgrades released together.
app/pipeline.py'stranscribe+align_segmentsnow usewhisperx'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 (nowords[]) instead of failing the whole job. - Version pin:
whisperx==3.1.6. It matches ourpyannote==3.1.1pin 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.
app/voiceprint_db.pyno longer usesindex.json + *.npyfiles.- Single
voiceprints.db(sqlite 3) + sqlite-vec'svec0virtual table for vector similarity. Top-k nearest-neighbour search runs inO(log N)through the extension. - All writes run inside sqlite transactions with WAL enabled; the old
hand-rolled
tempfile + os.replaceatomic writer is gone. - Concurrency: Python-level
threading.RLock+ sqlite WAL. - Automatic migration on first boot: if
index.json+.npyfiles exist, they're imported into sqlite in a single transaction andindex.jsonis renamed toindex.json.migrated.bak. The.npyfiles 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.
pyannote/wespeaker-voxceleb-resnet34-LMreplaces speechbrain's ECAPA.- Dependency diet:
speechbrainis no longer required. WeSpeaker's wrapper class ships withpyannote.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.
- 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.dborDELETE /api/voiceprints/{id}one by one, then re-enroll. - One more HuggingFace gated model to agree to (WeSpeaker).
- HTTP contract unchanged (
segments[i].wordsis 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.
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:latestautomatically. - 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.
Full hardening pass following real-audio end-to-end testing and an independent penetration test.
- Container no longer runs as root. A non-root
appuser (uid/gid 1000 by default, overridable viaAPP_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/transcribenow reads the upload in 1 MiB chunks and aborts withHTTP 413when the total exceedsMAX_UPLOAD_BYTES(default 2 GiB, configurable). The partial file is removed so disk doesn't leak. - Upload filename sanitization.
PurePosixPath(filename).namestrips any client-supplied directory components — an attacker-setfilename=../../etc/passwd.wavbecomespasswd.wavbefore the path is built. ffmpegargv 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_digestreplaces plain!=for the bearer/key check. Eliminates any theoretical timing side channel. - Tighter public-path matching.
/docs,/redoc,/openapi.jsonmoved fromstartswith()prefixes to exact-match only./docsXYZused to slip past auth; now it correctly returns 401. - Concurrency-safe, atomic
VoiceprintDB. Every mutation holdsthreading.Lock;index.jsonand every.npywrite goes throughtempfile+os.replaceso a crash mid-write can't corrupt the index. np.load(..., allow_pickle=False)everywhere. Closes the pickle-deserialization RCE path.
- New env var
MAX_UPLOAD_BYTES(default2147483648, 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/huggingfaceto/cache.HF_HOME,HUGGINGFACE_HUB_CACHE,TORCH_HOME, andXDG_CACHE_HOMEall point at/cacheso the non-root user can write it. docker-compose.ymlmounts${MODEL_CACHE_DIR}both at/cache(read-write, HF hub cache) and/models:ro(read-only, forpipeline.py's local-first checkpoint resolver), so cold starts don't round-trip through HuggingFace.
_convert_to_wavnow shells out toffmpegdirectly instead of going throughpydub.AudioSegment. FixesKeyError('codec_type')that pydub hits on newer ffmpeg's Opus/container output (jiaaro/pydub#638).GET /now reaches browsers whenAPI_KEYis 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,
VoiceTranscribeProvidernow keeps the rawspeaker_label(SPEAKER_XX) inspeakerSegments[].speaker, so re-enrollment still works after an automatic voiceprint match.
- The container's HF cache path moved from
/root/.cache/huggingfaceto/cache. If you had a host directory bind-mounted at the old path, update your compose (or just use this repo's newdocker-compose.yml, which handles both mounts for you). - Requests beyond
MAX_UPLOAD_BYTESnow fail with413instead of silently succeeding. Default 2 GiB fits every real-world audio we tested.
- First public release of the private transcription backend used by BetterAINote.
- Async job pipeline:
queued → converting → transcribing → identifying → completed. - faster-whisper
large-v3+ pyannote3.1+ ECAPA-TDNN speaker embeddings. - Persistent voiceprint DB with cosine-similarity auto-match.
- Optional
API_KEYbearer auth on all/api/*routes. - Portable
docker-compose.yml(data/model paths configurable via env). - Dependency pins to keep
pyannote.audio==3.1.1usable:numpy<2(pyannote 3.1.1 usesnp.NaN, removed in numpy 2.x).huggingface_hub<0.24(keeps theuse_auth_tokenkwarg pyannote calls).