Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
ac4db1c
feat: add overlapped speech detection (OSD) + MossFormer2 separation
MapleEve Apr 21, 2026
facdc25
Revert "feat: add overlapped speech detection (OSD) + MossFormer2 sep…
MapleEve Apr 21, 2026
dddac53
fix(pipeline): MIN_EMBED_DURATION 改为1.5s,支持env配置
MapleEve Apr 21, 2026
8b15d51
fix(security): 修复 XSS - escapeJs 完整转义 + data-* 事件委托
MapleEve Apr 21, 2026
80cf778
fix(dockerfile): 删除冗余chown,添加digest pin TODO
MapleEve Apr 21, 2026
e38553b
fix(security): ffmpeg 添加超时保护,删除 /tmp/_fake_git 死代码
MapleEve Apr 21, 2026
a47ce11
fix(voiceprint): identify()零向量防御,use_auth_token改token
MapleEve Apr 21, 2026
9bb8190
fix(security): 添加安全响应头中间件,修复日志注入
MapleEve Apr 21, 2026
9bb3fd6
fix(main): reassign_speaker同步speaker_map
MapleEve Apr 21, 2026
e1f5eb6
fix(frontend): 轮询改指数退避,fetch 错误提示
MapleEve Apr 21, 2026
9df7b21
fix(docs): 修正AS-norm冷启动、阈值、cohort生命周期、OSD删除、enroll幂等性、jobs/transcript…
MapleEve Apr 21, 2026
5bfe381
feat(ci): 添加基础CI workflow + docker-compose healthcheck
MapleEve Apr 21, 2026
7898395
fix(style): CORS headers 收敛,__import__ 改标准 import
MapleEve Apr 21, 2026
f204356
fix(quality): 静态SQL替换f-string,异常日志改善
MapleEve Apr 21, 2026
15651c1
fix(perf+docs): PERF-H8 batch cosine scan, DOC-C1 similarity semantic…
MapleEve Apr 21, 2026
d3ea644
fix(security): SEC-C1 pickle RCE, SEC-C2 path traversal, SEC-C3 API_K…
MapleEve Apr 21, 2026
6d7e30b
fix(logic): CQ-C1 cohort auto-rebuild, CQ-C2 AS-norm threshold, CQ-C3…
MapleEve Apr 21, 2026
a6aeb8b
merge: fix/security-critical + fix/logic-critical → review-fixes
MapleEve Apr 21, 2026
059a938
perf(pipeline): PERF-H1 segment-level torchaudio.load, avoid whole-fi…
MapleEve Apr 21, 2026
1b8f158
refactor(arch): extract config.py
MapleEve Apr 21, 2026
ff6d954
refactor(arch): create api/deps + routers
MapleEve Apr 21, 2026
46b355e
refactor(arch): extract services/
MapleEve Apr 21, 2026
7531aa7
refactor(arch): slim main.py to ~60 lines
MapleEve Apr 21, 2026
6deed6b
perf(upload): PERF-C2 async file write + streaming SHA256, unblock ev…
MapleEve Apr 21, 2026
dc4b735
fix(jobs): AR-C2 write-through status.json, fallback read + orphan re…
MapleEve Apr 21, 2026
59f14bc
test(core): TEST-C1~C4 security paths, VoiceprintDB logic, job persis…
MapleEve Apr 21, 2026
759fcc1
fix(pre-release): C1 job_id路径验证, C2 CI测试门, I1-I6 import/status/Conten…
MapleEve Apr 21, 2026
e735559
docs: sync all docs to v0.6.0, add CONTRIBUTING.md, rewrite READMEs
MapleEve Apr 21, 2026
d8484cb
fix: correct pipeline init arg order and pyannote token kwarg
MapleEve Apr 21, 2026
4f8078a
test(e2e): add core API E2E test suite
MapleEve Apr 21, 2026
b4ed401
feat: cleanup converted audio artifacts + add audio download endpoint
MapleEve Apr 21, 2026
586439b
feat: v0.7.0 — speaker consolidation + no_repeat_ngram_size
MapleEve Apr 21, 2026
00f053c
docs: rewrite READMEs + fix Voice Transcribe → VoScript
MapleEve Apr 21, 2026
14a904e
docs: rename OpenPlaud(Maple) → BetterAINote across all docs
MapleEve Apr 21, 2026
1d5a87e
ci: publish Docker image to Docker Hub on version tag
MapleEve Apr 21, 2026
605e73e
fix: remove unused imports (ruff F401)
MapleEve Apr 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- run: pip install ruff
- run: ruff check app/ --ignore E501
- run: ruff format --check app/

test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip
- name: Install lightweight test dependencies (no GPU packages)
run: pip install pytest pytest-cov fastapi httpx numpy aiofiles starlette python-multipart
- name: Run tests
run: |
pytest tests/test_security.py tests/test_voiceprint_db.py tests/test_job_service.py \
-v --tb=short --no-header

security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install pip-audit
run: pip install pip-audit
- name: Run pip-audit
run: pip-audit -r requirements.txt --ignore-vuln PYSEC-2022-42969 || true
# || true: 不阻断构建,但在 Summary 中显示漏洞报告
Comment on lines +47 to +48

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pip-audit ... || true forces this step to succeed even when vulnerabilities are found, which makes the security-scan job non-enforcing and can hide regressions. If the intent is non-blocking reporting, consider using GitHub Actions’ continue-on-error: true (keeps the failure visible) or uploading the audit output as an artifact; otherwise drop || true to gate on new vulnerabilities.

Suggested change
run: pip-audit -r requirements.txt --ignore-vuln PYSEC-2022-42969 || true
# || true: 不阻断构建,但在 Summary 中显示漏洞报告
continue-on-error: true
run: pip-audit -r requirements.txt --ignore-vuln PYSEC-2022-42969
# 非阻断执行,但保留步骤失败状态以便在 Actions 中可见

Copilot uses AI. Check for mistakes.
23 changes: 18 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: Build and publish Docker image
on:
release:
types: [published]
push:
tags:
- 'v*'
workflow_dispatch:

jobs:
Expand All @@ -26,14 +29,24 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Compute image tags
id: tags
run: |
IMAGE="ghcr.io/${{ github.repository }}"
TAGS="$IMAGE:latest"
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${GITHUB_REF#refs/tags/}"
TAGS="$TAGS,$IMAGE:$VERSION"
GHCR_IMAGE="ghcr.io/${{ github.repository }}"
DOCKERHUB_IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/voscript"
TAGS="$GHCR_IMAGE:latest,$DOCKERHUB_IMAGE:latest"
# Tag-triggered builds (release or push-tag) get the version tag too.
# Strip leading "v" for Docker Hub convention (0.7.0), keep raw ref for GHCR.
if [ "${{ github.event_name }}" = "release" ] || [ "${{ github.event_name }}" = "push" ]; then
RAW_VERSION="${GITHUB_REF#refs/tags/}"
STRIPPED_VERSION="${RAW_VERSION#v}"
TAGS="$TAGS,$GHCR_IMAGE:$RAW_VERSION,$DOCKERHUB_IMAGE:$STRIPPED_VERSION"
fi
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,7 @@ logs/
node_modules/
tmp/
.claude/

# Code review intermediate artifacts
.full-review/
.full-review-archive/
89 changes: 89 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 贡献指南

感谢你考虑向 voscript 贡献!

## 开始之前

- 提 issue 之前先搜一下是否已有相同问题
- 大的改动建议先开 issue 讨论设计方向

## 开发环境

```bash
# 安装依赖(需要 Python 3.11+)
pip install fastapi uvicorn pytest pytest-asyncio aiofiles httpx starlette python-multipart

# 运行测试
pytest tests/ -v

Comment on lines +16 to +18

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contributor docs recommend running pytest tests/ -v, but tests/e2e/ will be collected by default and currently attempts to reach a live server (with external defaults). Either adjust the recommended command to exclude E2E (e.g. pytest tests/test_*.py) or document how to opt into E2E explicitly (markers/env var).

Copilot uses AI. Check for mistakes.
# 启动本地服务(需要 GPU + 模型文件)
cd app && uvicorn main:app --reload --port 8780
```

## 提 PR 须知

1. **Fork → 新建分支 → PR**,分支命名建议:`feat/xxx`、`fix/xxx`、`docs/xxx`
2. **测试**:新功能必须附带对应测试(`tests/` 目录),所有测试必须通过
3. **文档**:影响 API 或行为的改动需同步更新 `doc/` 中的相关文档
4. **提交信息**:用英文小写动词开头,例如 `fix: handle zero-length audio` / `feat: add speaker rename API`

## 代码风格

- Python:PEP 8,函数命名 `snake_case`,类型注解尽量完整
- 不要引入 print 调试语句,用 `logger.debug()`
- 安全敏感代码(鉴权、文件路径)改动务必在 PR 描述中说明

## 报告安全问题

**不要开公开 issue**。请直接发 email 或通过 GitHub Security Advisory 私下报告。
详见 [SECURITY.md](./SECURITY.md)。

## 行为准则

友善、建设性地交流。维护者有权关闭不遵守基本礼仪的讨论。

---

# Contributing (English)

Thank you for contributing to voscript!

## Before You Start

- Search existing issues before filing a new one
- For large changes, open an issue first to discuss design direction

## Development Setup

```bash
# Requires Python 3.11+
pip install fastapi uvicorn pytest pytest-asyncio aiofiles httpx starlette python-multipart

# Run tests
pytest tests/ -v

# Start local server (requires GPU + model files)
cd app && uvicorn main:app --reload --port 8780
```

## Pull Request Guidelines

1. **Fork → branch → PR** — suggested branch names: `feat/xxx`, `fix/xxx`, `docs/xxx`
2. **Tests**: new features must include tests; all tests must pass
3. **Docs**: changes to API behavior must update the relevant files in `doc/`
4. **Commit messages**: lowercase verb prefix, e.g. `fix: handle zero-length audio`

## Code Style

- Python: PEP 8, `snake_case` for functions, type annotations where practical
- Use `logger.debug()` instead of `print`
- Security-sensitive changes (auth, file paths) must be explained in the PR description

## Reporting Security Issues

**Do not open a public issue.** Use GitHub Security Advisory or email privately.
See [SECURITY.md](./SECURITY.md).

## Code of Conduct

Be kind and constructive. Maintainers may close discussions that violate basic courtesy norms.
133 changes: 66 additions & 67 deletions README.en.md
Original file line number Diff line number Diff line change
@@ -1,93 +1,92 @@
# VoScript
<div align="center">

# 🎙️ VoScript

[简体中文](./README.md) | **English**

Self-hosted GPU transcription service — **meeting transcripts that remember
their speakers.** A small HTTP API that turns audio into timestamped text
labelled with speaker names, and that auto-recognizes returning voices
across recordings.
<a href="https://github.com/MapleEve/voscript/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/MapleEve/voscript/ci.yml?branch=main&style=for-the-badge" alt="CI" />
</a>
<img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=for-the-badge" alt="License: Apache 2.0" />
<img src="https://img.shields.io/badge/Docker-ready-blue?style=for-the-badge&logo=docker" alt="Docker ready" />

```
Audio ──► faster-whisper large-v3 (transcription)
──► pyannote 3.1 (speaker diarization)
──► DeepFilterNet / noisereduce (optional denoising)
──► WeSpeaker ResNet34 (speaker embeddings)
──► VoiceprintDB (cosine match vs. enrolled speakers)
──► timestamped text with identified speaker names
```
**Meeting recordings → transcripts with real speaker names. Self-hosted, GPU-powered, remembers every voice.**

What sets it apart from a plain whisper wrapper: the **persistent
voiceprint library**. Enroll a speaker once, and from then on every
recording they appear in gets their real name automatically.
[Quickstart](./doc/quickstart.en.md) · [API Reference](./doc/api.en.md) · [Security](./doc/security.en.md) · [Benchmarks](./doc/benchmarks.en.md) · [Changelog](./doc/changelog.en.md)

> Example consumer: [OpenPlaud(Maple)](https://github.com/MapleEve/openplaud)
> uses voscript as the backend for meeting recordings. voscript itself is
> just an HTTP service — any client that can POST multipart audio works.
</div>

## Documentation
---

All detailed docs live in [`doc/`](./doc/). Chinese is the default, every
page has an English counterpart:
You have a meeting recording with six people. You want to know who said what. Whisper gives you a wall of text. pyannote can split it into "Speaker A / Speaker B / Speaker C" — but it doesn't know who anyone is. You still have to label every recording by hand.

| Topic | 中文 | English |
| --- | --- | --- |
| Quickstart | [quickstart.zh.md](./doc/quickstart.zh.md) | [quickstart.en.md](./doc/quickstart.en.md) |
| API reference | [api.zh.md](./doc/api.zh.md) | [api.en.md](./doc/api.en.md) |
| **Install guide for AI agents** | [ai-install.zh.md](./doc/ai-install.zh.md) | [ai-install.en.md](./doc/ai-install.en.md) |
| **Usage guide for AI agents** | [ai-usage.zh.md](./doc/ai-usage.zh.md) | [ai-usage.en.md](./doc/ai-usage.en.md) |
| Security policy | [security.zh.md](./doc/security.zh.md) | [security.en.md](./doc/security.en.md) |
| Benchmarks (real-audio wall clock + resource usage) | [benchmarks.zh.md](./doc/benchmarks.zh.md) | [benchmarks.en.md](./doc/benchmarks.en.md) |
| Changelog | [changelog.zh.md](./doc/changelog.zh.md) | [changelog.en.md](./doc/changelog.en.md) |

First-time deployers: start with the [Quickstart](./doc/quickstart.en.md).
AI agents integrating the API: read the [AI usage guide](./doc/ai-usage.en.md).
AI agents deploying the service for a user: read the
[AI install guide](./doc/ai-install.en.md).

## Features
VoScript fixes that: **enroll a voice once, and it gets automatically identified in every future recording**. Not "Speaker 2" — "Maple".

- **Async job pipeline**: `queued → converting → denoising (optional) → transcribing → identifying → completed`
- **Chinese + multilingual transcription** (WhisperX + faster-whisper large-v3, **word-level timestamps** via forced alignment; omit `language` to auto-detect — Mandarin audio outputs Simplified Chinese)
- **Speaker diarization** (pyannote 3.1) + **WeSpeaker ResNet34** embeddings
- **Adaptive voiceprint threshold**: `VOICEPRINT_THRESHOLD` (default 0.75) is the base; the actual threshold relaxes per-speaker based on intra-cluster std of enrolled embeddings — fixed −0.05 for 1 sample, `min(3×std, 0.10)` for 2+, floor at 0.60. Lifted recall from 50% to 70% on 10 real recordings with zero false positives
- **Optional denoising with SNR gate**: `DENOISE_MODEL` (`none` | `deepfilternet` | `noisereduce`); `DENOISE_SNR_THRESHOLD` (default 10.0 dB) — audio above this SNR is considered clean and skipped automatically, preventing DeepFilterNet from degrading already-clean recordings
- **AS-norm voiceprint scoring**: at startup, automatically builds an impostor cohort from existing transcription embeddings and applies Adaptive Score Normalization — eliminates speaker-dependent baseline bias, ~15–30% relative EER improvement
- **Persistent voiceprints**: enroll once, auto-match across future recordings. sqlite + sqlite-vec under the hood — top-k nearest-neighbour search scales to thousands of speakers
- **File hash deduplication**: submitting the same file twice returns the existing result immediately, skipping Whisper GPU inference
- **Stable HTTP contract**: `/api/transcribe`, `/api/jobs/{id}`, `/api/voiceprints*`, etc. — any HTTP client works
- **Container runs as non-root**; all `/api/*` routes accept optional Bearer / `X-API-Key` auth (constant-time compare); uploads capped by `MAX_UPLOAD_BYTES`; voiceprint DB is concurrency-safe with atomic writes — full hardening list in [`doc/security.en.md`](./doc/security.en.md)
- Minimal built-in web UI at `/` for manual testing
```
Audio ──► faster-whisper large-v3 transcription + word-level timestamps
──► pyannote 3.1 speaker diarization
──► WeSpeaker ResNet34 speaker embeddings
──► VoiceprintDB (AS-norm) match against enrolled voices
──► timestamped transcript with real speaker names
```

## 30-second start

```bash
git clone https://github.com/MapleEve/voscript.git
cd voscript

cp .env.example .env
# edit .env — at minimum set HF_TOKEN and API_KEY
> **Security**: set a strong `API_KEY` in `.env` before exposing this on any network. Without it, anyone can delete your voiceprint library or trigger GPU jobs.

```bash
git clone https://github.com/MapleEve/voscript.git && cd voscript
cp .env.example .env # at minimum: HF_TOKEN and API_KEY
docker compose up -d --build
curl -sf http://localhost:8780/healthz
```

Full steps + troubleshooting in [`doc/quickstart.en.md`](./doc/quickstart.en.md).
Full setup + troubleshooting → [`doc/quickstart.en.md`](./doc/quickstart.en.md)

## Features

- **Persistent voiceprint library** — enroll once, auto-match across all future recordings. sqlite + sqlite-vec under the hood, top-k nearest-neighbour search, scales to thousands of speakers
- **AS-norm scoring** — builds an impostor cohort from existing transcription embeddings at startup; eliminates speaker-dependent baseline bias, ~15–30% relative EER improvement
- **Adaptive threshold** — each speaker's match threshold relaxes dynamically based on enrollment variance; lifted recall from 50% to 70% on 10 real recordings with zero false positives
- **Speaker cluster consolidation** — when diarization splits one person into multiple clusters, they're automatically merged to a single label
- **Word-level timestamps** — WhisperX forced alignment, every word precisely timed
- **Optional denoising with SNR gate** — DeepFilterNet / noisereduce; audio above the SNR threshold is treated as clean and skipped automatically (prevents degrading already-clean recordings)
- **File hash deduplication** — submitting the same file twice returns the existing result immediately, no GPU re-run
- **Job persistence** — completed transcriptions remain accessible after restart
- **Ngram dedup** — `no_repeat_ngram_size` parameter suppresses repetitive filler words in the transcript
- **Plain HTTP contract** — any client that can send multipart/form-data works, no framework lock-in

Security: path traversal protection, non-root container, upload size cap, constant-time auth, atomic writes — full list in [`doc/security.en.md`](./doc/security.en.md)

## Integration

It's a plain HTTP service. Two config values and you're done:

- **Transcription base URL**: `http://<host>:8780`
- **API key**: the `API_KEY` you set in `.env`

[BetterAINote](https://github.com/MapleEve/openplaud) connects this way. Any other client works the same. Full API contract → [`doc/api.en.md`](./doc/api.en.md)

## How to integrate
## Documentation

| Topic | 中文 | English |
| --- | --- | --- |
| Quickstart | [quickstart.zh.md](./doc/quickstart.zh.md) | [quickstart.en.md](./doc/quickstart.en.md) |
| API reference | [api.zh.md](./doc/api.zh.md) | [api.en.md](./doc/api.en.md) |
| Install guide for AI agents | [ai-install.zh.md](./doc/ai-install.zh.md) | [ai-install.en.md](./doc/ai-install.en.md) |
| Usage guide for AI agents | [ai-usage.zh.md](./doc/ai-usage.zh.md) | [ai-usage.en.md](./doc/ai-usage.en.md) |
| Security policy | [security.zh.md](./doc/security.zh.md) | [security.en.md](./doc/security.en.md) |
| Benchmarks | [benchmarks.zh.md](./doc/benchmarks.zh.md) | [benchmarks.en.md](./doc/benchmarks.en.md) |
| Changelog | [changelog.zh.md](./doc/changelog.zh.md) | [changelog.en.md](./doc/changelog.en.md) |

voscript is a plain HTTP service — no specific client is required. Anything
that can send `multipart/form-data` works (curl, axios, requests, browser
uploads, …).
## Contributing

A typical integration — OpenPlaud(Maple), under Settings → Transcription:
PRs welcome — read [CONTRIBUTING.md](./CONTRIBUTING.md) first.

- **Private transcription base URL**: `http://<host>:8780`
- **Private transcription API key**: the same `API_KEY` as in `.env`
## Star History

After that its worker routes every recording through this service. If
you're writing your own client, the full contract + error table lives in
[`doc/api.en.md`](./doc/api.en.md).
[![Star History Chart](https://api.star-history.com/svg?repos=MapleEve/voscript&type=date)](https://www.star-history.com/#MapleEve/voscript&type=date)

## License

MIT — see [LICENSE](./LICENSE).
Apache 2.0 — [LICENSE](./LICENSE)
Loading
Loading