init: ECAPA-TDNN voiceprint embedding service
This commit is contained in:
commit
b41cb09db5
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
venv/
|
||||||
|
logs/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pid
|
||||||
|
*.log
|
||||||
1
ahserver
Submodule
1
ahserver
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 6165795aa742d7539c758ff4ef370d24f3caaee7
|
||||||
12
app/voiceprint_app.py
Normal file
12
app/voiceprint_app.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
def extract_embedding(audio_path):
|
||||||
|
signal = MODEL.load_audio(audio_path)
|
||||||
|
if isinstance(signal, tuple):
|
||||||
|
signal = signal[0]
|
||||||
|
signal = signal.unsqueeze(0).to(DEVICE)
|
||||||
|
with torch.no_grad():
|
||||||
|
emb = MODEL.encode_batch(signal)
|
||||||
|
return emb.squeeze().cpu().numpy().tolist()
|
||||||
|
|
||||||
|
def verify_speakers(audio_path, ref_path):
|
||||||
|
score, pred = MODEL.verify_files(audio_path, ref_path)
|
||||||
|
return float(score), bool(pred)
|
||||||
1
appPublic
Submodule
1
appPublic
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 371fe768cb28525ade1cab87c2f51b8e118a8fb3
|
||||||
33
conf/config.json
Normal file
33
conf/config.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"device": "cuda:1",
|
||||||
|
"redis_url": "redis://127.0.0.1:6379",
|
||||||
|
"worker_cnt": 2,
|
||||||
|
"logger": {
|
||||||
|
"name": "voiceprint",
|
||||||
|
"levelname": "info",
|
||||||
|
"logfile": "$[workdir]$/logs/voiceprint.log"
|
||||||
|
},
|
||||||
|
"website": {
|
||||||
|
"paths": [
|
||||||
|
["$[workdir]$/wwwroot", ""]
|
||||||
|
],
|
||||||
|
"client_max_size": 10000,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 9087,
|
||||||
|
"coding": "utf-8",
|
||||||
|
"indexes": ["index.html"],
|
||||||
|
"startswiths": [
|
||||||
|
{"leading": "/api/voiceprint-submit", "registerfunction": "voiceprint_submit"},
|
||||||
|
{"leading": "/api/voiceprint-status", "registerfunction": "voiceprint_status"},
|
||||||
|
{"leading": "/api/voiceprint/extract", "registerfunction": "voiceprint_extract"},
|
||||||
|
{"leading": "/api/voiceprint/verify", "registerfunction": "voiceprint_verify"}
|
||||||
|
],
|
||||||
|
"processors": [
|
||||||
|
[".tmpl", "tmpl"],
|
||||||
|
[".ui", "bui"],
|
||||||
|
[".dspy", "dspy"]
|
||||||
|
],
|
||||||
|
"session_max_time": 3000,
|
||||||
|
"session_issue_time": 2500
|
||||||
|
}
|
||||||
|
}
|
||||||
24
download_model.py
Normal file
24
download_model.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Download ECAPA-TDNN model via ModelScope to /share/models/ecapa-tdnn/"""
|
||||||
|
import os, sys, time
|
||||||
|
|
||||||
|
MODEL_ID = 'iic/speech_ecapa-tdnn_sv_en_voxceleb_16k'
|
||||||
|
TARGET = '/share/models/ecapa-tdnn'
|
||||||
|
|
||||||
|
os.makedirs(TARGET, exist_ok=True)
|
||||||
|
|
||||||
|
# Try ModelScope first
|
||||||
|
for attempt in range(3):
|
||||||
|
try:
|
||||||
|
from modelscope import snapshot_download
|
||||||
|
print(f"Attempt {attempt+1}: ModelScope download...")
|
||||||
|
snapshot_download(MODEL_ID, local_dir=TARGET)
|
||||||
|
print("SUCCESS via ModelScope")
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ModelScope failed: {e}")
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
# Fallback: try HF mirror
|
||||||
|
print("Trying HF mirror...")
|
||||||
|
os.system(f'export HF_ENDPOINT=https://hf-mirror.com && python3 -c "from huggingface_hub import snapshot_download; snapshot_download(\'speechbrain/spkrec-ecapa-voxceleb\', local_dir=\'{TARGET}\')"')
|
||||||
1
longtasks
Submodule
1
longtasks
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 3497f30c96b4b8beee551a8f2c2144e3581edd73
|
||||||
1
sqlor
Submodule
1
sqlor
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit a9a02eb45bf8b0f5fa17c859eb4478c93a42a0e8
|
||||||
8
start.sh
Executable file
8
start.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
mkdir -p logs
|
||||||
|
setsid python3 app/voiceprint_app.py -p 9087 >> logs/voiceprint.log 2>&1 &
|
||||||
|
echo $! > voiceprint.pid
|
||||||
|
echo "Voiceprint PID=$(cat voiceprint.pid)"
|
||||||
|
sleep 3
|
||||||
|
curl -s http://localhost:9087/api/health && echo "" || echo "(checking...)"
|
||||||
9
stop.sh
Normal file
9
stop.sh
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
if [ -f voiceprint.pid ]; then
|
||||||
|
kill $(cat voiceprint.pid) 2>/dev/null
|
||||||
|
rm -f voiceprint.pid
|
||||||
|
echo "Voiceprint stopped"
|
||||||
|
else
|
||||||
|
pkill -f voiceprint_app.py 2>/dev/null && echo "Voiceprint stopped (via pkill)"
|
||||||
|
fi
|
||||||
7
vp/__init__.py
Normal file
7
vp/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
from vp.engine import VoiceprintEngine
|
||||||
|
|
||||||
|
|
||||||
|
def load_voiceprint():
|
||||||
|
env = ServerEnv()
|
||||||
|
env.voiceprint = VoiceprintEngine()
|
||||||
63
vp/engine.py
Normal file
63
vp/engine.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
"""ECAPA-TDNN Voiceprint Engine — loads model, processes extract/verify tasks."""
|
||||||
|
import torch
|
||||||
|
from longtasks.longtasks import LongTasks
|
||||||
|
from appPublic.worker import awaitify
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
|
from appPublic.log import debug
|
||||||
|
from speechbrain.inference.speaker import SpeakerRecognition
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class VoiceprintEngine(LongTasks):
|
||||||
|
def __init__(self):
|
||||||
|
self.config = getConfig()
|
||||||
|
super().__init__(self.config.redis_url, 'voiceprint', worker_cnt=self.config.worker_cnt)
|
||||||
|
self.load_model()
|
||||||
|
|
||||||
|
def load_model(self):
|
||||||
|
device = self.config.device
|
||||||
|
debug(f'loading ECAPA-TDNN on {device}...')
|
||||||
|
self.verifier = SpeakerRecognition.from_hparams(
|
||||||
|
source="speechbrain/spkrec-ecapa-voxceleb",
|
||||||
|
savedir="/share/models/ecapa-tdnn",
|
||||||
|
run_opts={"device": device}
|
||||||
|
)
|
||||||
|
debug('ECAPA-TDNN loaded')
|
||||||
|
|
||||||
|
async def process_task(self, payload, workerid=None):
|
||||||
|
task_type = payload.get('task_type', 'extract')
|
||||||
|
audio_file = payload.get('audio_file', '')
|
||||||
|
if not audio_file:
|
||||||
|
return {'status': 'FAILED', 'result': 'missing audio_file'}
|
||||||
|
|
||||||
|
if task_type == 'extract':
|
||||||
|
f = awaitify(self._extract)
|
||||||
|
return await f(audio_file)
|
||||||
|
elif task_type == 'verify':
|
||||||
|
ref_file = payload.get('reference_file', '')
|
||||||
|
if not ref_file:
|
||||||
|
return {'status': 'FAILED', 'result': 'missing reference_file'}
|
||||||
|
f = awaitify(self._verify)
|
||||||
|
return await f(audio_file, ref_file)
|
||||||
|
return {'status': 'FAILED', 'result': f'unknown task_type: {task_type}'}
|
||||||
|
|
||||||
|
def _extract(self, audio_path):
|
||||||
|
signal = self.verifier.load_audio(audio_path, 16000)
|
||||||
|
t = torch.tensor(signal).unsqueeze(0).to(self.config.device)
|
||||||
|
emb = self.verifier.encode_batch(t)
|
||||||
|
vec = emb.squeeze().cpu().numpy().tolist()
|
||||||
|
return {
|
||||||
|
'status': 'SUCCEEDED',
|
||||||
|
'embedding': vec,
|
||||||
|
'embedding_dim': len(vec),
|
||||||
|
'usage': {'audio_duration': round(len(signal) / 16000, 2)},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _verify(self, audio_path, ref_path):
|
||||||
|
score, pred = self.verifier.verify_files(audio_path, ref_path)
|
||||||
|
return {
|
||||||
|
'status': 'SUCCEEDED',
|
||||||
|
'similarity': round(float(score), 4),
|
||||||
|
'is_same_speaker': bool(pred),
|
||||||
|
'usage': {},
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user