daihui.zhang
commited on
Commit
·
2a2d4ba
1
Parent(s):
7289e30
add frontend demo
Browse files- .gitignore +1 -1
- api_model.py +23 -0
- config.py +1 -1
- frontend/index.html +169 -0
- pyproject.toml +1 -0
- transcribe/transcription.py +14 -12
- transcribe/translator.py +1 -1
- transcribe/whisper_llm_serve.py +83 -21
- uv.lock +103 -0
.gitignore
CHANGED
|
@@ -3,7 +3,7 @@ __pycache__/
|
|
| 3 |
*$py.class
|
| 4 |
# C extensions
|
| 5 |
*.so
|
| 6 |
-
|
| 7 |
# Distribution / packaging
|
| 8 |
.Python
|
| 9 |
build/
|
|
|
|
| 3 |
*$py.class
|
| 4 |
# C extensions
|
| 5 |
*.so
|
| 6 |
+
.DS_Store
|
| 7 |
# Distribution / packaging
|
| 8 |
.Python
|
| 9 |
build/
|
api_model.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from uuid import UUID
|
| 3 |
+
|
| 4 |
+
class TransResult(BaseModel):
|
| 5 |
+
# trans_pattern: str
|
| 6 |
+
seg_id: int
|
| 7 |
+
context: str
|
| 8 |
+
from_: str = Field(alias="from")
|
| 9 |
+
to: str
|
| 10 |
+
tran_content: str = Field(alias="tranContent")
|
| 11 |
+
partial: bool = True
|
| 12 |
+
|
| 13 |
+
class Config:
|
| 14 |
+
populate_by_name = True
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Message(BaseModel):
|
| 20 |
+
result: TransResult = {}
|
| 21 |
+
# action: str = "recognition"
|
| 22 |
+
error_code: int = 0
|
| 23 |
+
request_id: UUID
|
config.py
CHANGED
|
@@ -9,7 +9,7 @@ PAUSE_END_MARKERS = [',', ',', '、']
|
|
| 9 |
|
| 10 |
# whisper推理参数
|
| 11 |
WHISPER_PROMPT_ZH = "以下是简体中文普通话的句子。"
|
| 12 |
-
MAX_LENTH_ZH =
|
| 13 |
|
| 14 |
WHISPER_PROMPT_EN = "The following is an English sentence."
|
| 15 |
MAX_LENGTH_EN= 1
|
|
|
|
| 9 |
|
| 10 |
# whisper推理参数
|
| 11 |
WHISPER_PROMPT_ZH = "以下是简体中文普通话的句子。"
|
| 12 |
+
MAX_LENTH_ZH = 4
|
| 13 |
|
| 14 |
WHISPER_PROMPT_EN = "The following is an English sentence."
|
| 15 |
MAX_LENGTH_EN= 1
|
frontend/index.html
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="zh">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<title>试试翻译</title>
|
| 6 |
+
<style>
|
| 7 |
+
body {
|
| 8 |
+
background-color: #f9f9fc;
|
| 9 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 2rem;
|
| 12 |
+
}
|
| 13 |
+
.translation-box {
|
| 14 |
+
background: #f2f2f8;
|
| 15 |
+
border-radius: 12px;
|
| 16 |
+
padding: 1.5rem;
|
| 17 |
+
max-width: 800px;
|
| 18 |
+
margin: 0 auto;
|
| 19 |
+
min-height: 200px;
|
| 20 |
+
}
|
| 21 |
+
.entry {
|
| 22 |
+
margin-bottom: 1.5rem;
|
| 23 |
+
}
|
| 24 |
+
.timestamp {
|
| 25 |
+
font-size: 0.75rem;
|
| 26 |
+
color: #999;
|
| 27 |
+
}
|
| 28 |
+
.original {
|
| 29 |
+
font-size: 1rem;
|
| 30 |
+
color: #333;
|
| 31 |
+
}
|
| 32 |
+
.translation {
|
| 33 |
+
font-size: 1rem;
|
| 34 |
+
font-weight: bold;
|
| 35 |
+
color: #000;
|
| 36 |
+
}
|
| 37 |
+
.footer {
|
| 38 |
+
display: flex;
|
| 39 |
+
justify-content: space-between;
|
| 40 |
+
align-items: center;
|
| 41 |
+
margin-top: 2rem;
|
| 42 |
+
}
|
| 43 |
+
.lang-select {
|
| 44 |
+
background: white;
|
| 45 |
+
border-radius: 9999px;
|
| 46 |
+
padding: 0.4rem 1rem;
|
| 47 |
+
border: none;
|
| 48 |
+
font-size: 1rem;
|
| 49 |
+
box-shadow: 0 0 0 1px #ddd;
|
| 50 |
+
}
|
| 51 |
+
.record-button {
|
| 52 |
+
background-color: #1e40af;
|
| 53 |
+
color: white;
|
| 54 |
+
border: none;
|
| 55 |
+
padding: 0.6rem 1.2rem;
|
| 56 |
+
border-radius: 9999px;
|
| 57 |
+
font-size: 1rem;
|
| 58 |
+
cursor: pointer;
|
| 59 |
+
}
|
| 60 |
+
</style>
|
| 61 |
+
</head>
|
| 62 |
+
<body>
|
| 63 |
+
<div class="translation-box" id="translationBox">
|
| 64 |
+
<!-- 实时内容将插入这里 -->
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<div class="footer">
|
| 68 |
+
<select class="lang-select">
|
| 69 |
+
<option>中文 » 英语</option>
|
| 70 |
+
</select>
|
| 71 |
+
<button class="record-button" onclick="startRecording()">🎤 录音</button>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<script>
|
| 75 |
+
let ws;
|
| 76 |
+
let mediaRecorder;
|
| 77 |
+
|
| 78 |
+
function formatTimestamp(ms) {
|
| 79 |
+
const sec = ms / 1000;
|
| 80 |
+
const min = Math.floor(sec / 60);
|
| 81 |
+
const s = (sec % 60).toFixed(1);
|
| 82 |
+
return `${String(min).padStart(2, '0')}:${s.padStart(4, '0')}`;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
let lastSegId = null; // 用于存储上一个 seg_id
|
| 86 |
+
|
| 87 |
+
function addTranslation(result) {
|
| 88 |
+
const box = document.getElementById('translationBox');
|
| 89 |
+
|
| 90 |
+
// 创建一个新的 div 来显示翻译
|
| 91 |
+
const entry = document.createElement('div');
|
| 92 |
+
entry.className = 'entry';
|
| 93 |
+
|
| 94 |
+
console.log(result);
|
| 95 |
+
|
| 96 |
+
const start = formatTimestamp(result.bg);
|
| 97 |
+
const end = formatTimestamp(result.ed);
|
| 98 |
+
|
| 99 |
+
// 判断是否是新的一行
|
| 100 |
+
if (result.seg_id === lastSegId) {
|
| 101 |
+
// 如果 seg_id 相同,更新该行内容
|
| 102 |
+
const existingEntry = box.querySelector(`.entry[data-seg-id="${result.seg_id}"]`);
|
| 103 |
+
if (existingEntry) {
|
| 104 |
+
const translationDiv = existingEntry.querySelector('.translation');
|
| 105 |
+
translationDiv.innerHTML = result.tranContent;
|
| 106 |
+
}
|
| 107 |
+
} else {
|
| 108 |
+
// 如果 seg_id 不同,表示是新的行,添加新行
|
| 109 |
+
entry.setAttribute('data-seg-id', result.seg_id); // 设置 seg_id
|
| 110 |
+
entry.innerHTML = `
|
| 111 |
+
<div class="original">${result.context}</div>
|
| 112 |
+
<div class="translation">${result.tranContent}</div>
|
| 113 |
+
`;
|
| 114 |
+
box.appendChild(entry);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// 更新 lastSegId 以便下一次判断
|
| 118 |
+
lastSegId = result.seg_id;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
async function startRecording() {
|
| 124 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 125 |
+
const audioContext = new AudioContext({ sampleRate: 16000 });
|
| 126 |
+
const source = audioContext.createMediaStreamSource(stream);
|
| 127 |
+
const processor = audioContext.createScriptProcessor(4096, 1, 1);
|
| 128 |
+
|
| 129 |
+
const wsUrl = "ws://localhost:9090?from=zh&to=en";
|
| 130 |
+
ws = new WebSocket(wsUrl);
|
| 131 |
+
|
| 132 |
+
ws.binaryType = "arraybuffer";
|
| 133 |
+
|
| 134 |
+
ws.onopen = () => {
|
| 135 |
+
console.log("WebSocket opened");
|
| 136 |
+
source.connect(processor);
|
| 137 |
+
processor.connect(audioContext.destination);
|
| 138 |
+
|
| 139 |
+
processor.onaudioprocess = (e) => {
|
| 140 |
+
const input = e.inputBuffer.getChannelData(0);
|
| 141 |
+
const buffer = new Int16Array(input.length);
|
| 142 |
+
for (let i = 0; i < input.length; i++) {
|
| 143 |
+
buffer[i] = Math.max(-1, Math.min(1, input[i])) * 0x7FFF;
|
| 144 |
+
}
|
| 145 |
+
ws.send(buffer);
|
| 146 |
+
};
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
ws.onmessage = (event) => {
|
| 150 |
+
try {
|
| 151 |
+
const msg = JSON.parse(event.data);
|
| 152 |
+
if (msg.result) {
|
| 153 |
+
addTranslation(msg.result);
|
| 154 |
+
}
|
| 155 |
+
} catch (e) {
|
| 156 |
+
console.error("Parse error:", e);
|
| 157 |
+
}
|
| 158 |
+
};
|
| 159 |
+
|
| 160 |
+
ws.onerror = (e) => console.error("WebSocket error:", e);
|
| 161 |
+
ws.onclose = () => {
|
| 162 |
+
console.log("WebSocket closed");
|
| 163 |
+
processor.disconnect();
|
| 164 |
+
source.disconnect();
|
| 165 |
+
};
|
| 166 |
+
}
|
| 167 |
+
</script>
|
| 168 |
+
</body>
|
| 169 |
+
</html>
|
pyproject.toml
CHANGED
|
@@ -11,6 +11,7 @@ dependencies = [
|
|
| 11 |
"numpy>=2.1.3",
|
| 12 |
"onnxruntime>=1.21.0",
|
| 13 |
"pyaudio>=0.2.14",
|
|
|
|
| 14 |
"pywhispercpp>=1.3.0",
|
| 15 |
"setuptools>=78.1.0",
|
| 16 |
"soundfile>=0.13.1",
|
|
|
|
| 11 |
"numpy>=2.1.3",
|
| 12 |
"onnxruntime>=1.21.0",
|
| 13 |
"pyaudio>=0.2.14",
|
| 14 |
+
"pydantic>=2.11.2",
|
| 15 |
"pywhispercpp>=1.3.0",
|
| 16 |
"setuptools>=78.1.0",
|
| 17 |
"soundfile>=0.13.1",
|
transcribe/transcription.py
CHANGED
|
@@ -13,7 +13,7 @@ from .vad import VoiceActivityDetector
|
|
| 13 |
from urllib.parse import urlparse, parse_qsl
|
| 14 |
from websockets.exceptions import ConnectionClosed
|
| 15 |
from websockets.sync.server import serve
|
| 16 |
-
|
| 17 |
logging.basicConfig(level=logging.INFO)
|
| 18 |
|
| 19 |
|
|
@@ -174,14 +174,19 @@ class TranscriptionServer:
|
|
| 174 |
frame_data = websocket.recv()
|
| 175 |
if frame_data == b"END_OF_AUDIO":
|
| 176 |
return False
|
| 177 |
-
return np.frombuffer(frame_data, dtype=np.float32)
|
|
|
|
| 178 |
|
| 179 |
def handle_new_connection(self, websocket):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
try:
|
| 181 |
logging.info("New client connected")
|
| 182 |
-
options = websocket.recv()
|
| 183 |
-
options = json.loads(options)
|
| 184 |
-
|
| 185 |
if self.client_manager is None:
|
| 186 |
max_clients = options.get('max_clients', 4)
|
| 187 |
max_connection_time = options.get('max_connection_time', 600)
|
|
@@ -195,7 +200,9 @@ class TranscriptionServer:
|
|
| 195 |
self.vad_detector = VoiceActivityDetector(frame_rate=self.RATE)
|
| 196 |
|
| 197 |
self.initialize_client(websocket, options)
|
| 198 |
-
|
|
|
|
|
|
|
| 199 |
return True
|
| 200 |
except json.JSONDecodeError:
|
| 201 |
logging.error("Failed to decode JSON from client")
|
|
@@ -240,12 +247,7 @@ class TranscriptionServer:
|
|
| 240 |
self.backend = backend
|
| 241 |
if not self.handle_new_connection(websocket):
|
| 242 |
return
|
| 243 |
-
|
| 244 |
-
from_lang, to_lang = query_parameters_dict.get('from'), query_parameters_dict.get('to')
|
| 245 |
-
|
| 246 |
-
if from_lang and to_lang:
|
| 247 |
-
self.set_lang(websocket, from_lang, to_lang)
|
| 248 |
-
logging.info(f"Source lange: {from_lang} -> Dst lange: {to_lang}")
|
| 249 |
|
| 250 |
try:
|
| 251 |
while not self.client_manager.is_client_timeout(websocket):
|
|
|
|
| 13 |
from urllib.parse import urlparse, parse_qsl
|
| 14 |
from websockets.exceptions import ConnectionClosed
|
| 15 |
from websockets.sync.server import serve
|
| 16 |
+
from uuid import uuid1
|
| 17 |
logging.basicConfig(level=logging.INFO)
|
| 18 |
|
| 19 |
|
|
|
|
| 174 |
frame_data = websocket.recv()
|
| 175 |
if frame_data == b"END_OF_AUDIO":
|
| 176 |
return False
|
| 177 |
+
return np.frombuffer(frame_data, dtype=np.int16).astype(np.float32) / 32768.0
|
| 178 |
+
|
| 179 |
|
| 180 |
def handle_new_connection(self, websocket):
|
| 181 |
+
query_parameters_dict = dict(parse_qsl(urlparse(websocket.request.path).query))
|
| 182 |
+
from_lang, to_lang = query_parameters_dict.get('from'), query_parameters_dict.get('to')
|
| 183 |
+
|
| 184 |
+
|
| 185 |
try:
|
| 186 |
logging.info("New client connected")
|
| 187 |
+
# options = websocket.recv()
|
| 188 |
+
# options = json.loads(options)
|
| 189 |
+
options = {"language": from_lang, "uid": str(uuid1())}
|
| 190 |
if self.client_manager is None:
|
| 191 |
max_clients = options.get('max_clients', 4)
|
| 192 |
max_connection_time = options.get('max_connection_time', 600)
|
|
|
|
| 200 |
self.vad_detector = VoiceActivityDetector(frame_rate=self.RATE)
|
| 201 |
|
| 202 |
self.initialize_client(websocket, options)
|
| 203 |
+
if from_lang and to_lang:
|
| 204 |
+
self.set_lang(websocket, from_lang, to_lang)
|
| 205 |
+
logging.info(f"Source lange: {from_lang} -> Dst lange: {to_lang}")
|
| 206 |
return True
|
| 207 |
except json.JSONDecodeError:
|
| 208 |
logging.error("Failed to decode JSON from client")
|
|
|
|
| 247 |
self.backend = backend
|
| 248 |
if not self.handle_new_connection(websocket):
|
| 249 |
return
|
| 250 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
try:
|
| 253 |
while not self.client_manager.is_client_timeout(websocket):
|
transcribe/translator.py
CHANGED
|
@@ -28,7 +28,7 @@ class QwenTranslator:
|
|
| 28 |
message = self.to_message(prompt, src_lang, dst_lang)
|
| 29 |
start_time = time.monotonic()
|
| 30 |
output = self.llm.create_chat_completion(messages=message, temperature=0.9)
|
| 31 |
-
logger.info(f"
|
| 32 |
return output['choices'][0]['message']['content']
|
| 33 |
|
| 34 |
def __call__(self, prompt,*args, **kwargs):
|
|
|
|
| 28 |
message = self.to_message(prompt, src_lang, dst_lang)
|
| 29 |
start_time = time.monotonic()
|
| 30 |
output = self.llm.create_chat_completion(messages=message, temperature=0.9)
|
| 31 |
+
logger.info(f"Translate time: {time.monotonic() - start_time:.2f}s.")
|
| 32 |
return output['choices'][0]['message']['content']
|
| 33 |
|
| 34 |
def __call__(self, prompt,*args, **kwargs):
|
transcribe/whisper_llm_serve.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
|
| 2 |
|
| 3 |
import soundfile
|
| 4 |
-
from concurrent.futures import ProcessPoolExecutor as
|
| 5 |
import multiprocessing as mp
|
| 6 |
import numpy as np
|
| 7 |
from logging import getLogger
|
|
@@ -14,10 +14,17 @@ import threading
|
|
| 14 |
from functools import partial
|
| 15 |
from .server import ServeClientBase
|
| 16 |
from .translator import QwenTranslator
|
|
|
|
| 17 |
from pywhispercpp.model import Model
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
logger = getLogger(__name__)
|
| 20 |
|
|
|
|
|
|
|
| 21 |
|
| 22 |
class TripleTextBuffer:
|
| 23 |
def __init__(self, size=2):
|
|
@@ -108,6 +115,7 @@ class SegmentManager:
|
|
| 108 |
class PywhisperInference:
|
| 109 |
whisper_model = None
|
| 110 |
llm_model = None
|
|
|
|
| 111 |
|
| 112 |
@classmethod
|
| 113 |
def initializer(cls, event:mp.Event, warmup=True):
|
|
@@ -124,6 +132,7 @@ class PywhisperInference:
|
|
| 124 |
|
| 125 |
# init llamacpp
|
| 126 |
cls.llm_model = QwenTranslator(config.LLM_MODEL_PATH, config.LLM_SYS_PROMPT)
|
|
|
|
| 127 |
event.set()
|
| 128 |
|
| 129 |
@classmethod
|
|
@@ -155,6 +164,12 @@ class PywhisperInference:
|
|
| 155 |
@classmethod
|
| 156 |
def translate(cls, context: str, src_lang, dst_lang):
|
| 157 |
return cls.llm_model.translate(context, src_lang, dst_lang)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
|
| 160 |
class PyWhiperCppServe(ServeClientBase):
|
|
@@ -171,16 +186,21 @@ class PyWhiperCppServe(ServeClientBase):
|
|
| 171 |
self.lock = threading.Lock()
|
| 172 |
self.frames_np = None
|
| 173 |
self.sample_rate = 16000
|
| 174 |
-
|
|
|
|
| 175 |
self._ready_state = mp.Event()
|
| 176 |
-
self._pool =
|
| 177 |
max_workers=1, initializer=partial(PywhisperInference.initializer, event=self._ready_state))
|
| 178 |
-
|
| 179 |
logger.info('Create a process to process audio.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
self.trans_thread = threading.Thread(target=self.speech_to_text)
|
| 181 |
self.trans_thread.daemon = True
|
| 182 |
self.trans_thread.start()
|
| 183 |
-
self.send_ready_state()
|
| 184 |
|
| 185 |
def send_ready_state(self):
|
| 186 |
while not self._ready_state:
|
|
@@ -196,13 +216,29 @@ class PyWhiperCppServe(ServeClientBase):
|
|
| 196 |
self.language = src_lang
|
| 197 |
self.dst_lang = dst_lang
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
def add_frames(self, frame_np):
|
|
|
|
| 200 |
with self.lock:
|
| 201 |
if self.frames_np is None:
|
| 202 |
self.frames_np = frame_np.copy()
|
| 203 |
else:
|
| 204 |
self.frames_np = np.append(self.frames_np,frame_np)
|
| 205 |
-
|
|
|
|
| 206 |
|
| 207 |
def update_audio_buffer(self, last_offset):
|
| 208 |
with self.lock:
|
|
@@ -273,6 +309,7 @@ class PyWhiperCppServe(ServeClientBase):
|
|
| 273 |
return None, left_watch_string, right_watch_string, is_end_sentence
|
| 274 |
|
| 275 |
def speech_to_text(self):
|
|
|
|
| 276 |
while True:
|
| 277 |
if self.exit:
|
| 278 |
logger.info("Exiting speech to text thread")
|
|
@@ -284,13 +321,17 @@ class PyWhiperCppServe(ServeClientBase):
|
|
| 284 |
continue
|
| 285 |
|
| 286 |
audio_buffer = self.get_audio_chunk_for_processing()
|
| 287 |
-
|
| 288 |
# segments = self.transcribe_audio(audio_buffer)
|
|
|
|
|
|
|
|
|
|
| 289 |
try:
|
| 290 |
logger.info(f"Processing audio with duration: {len(audio_buffer) / self.sample_rate:.2f}s")
|
| 291 |
segments = self.transcribe_audio(audio_buffer)
|
| 292 |
-
for
|
| 293 |
-
|
|
|
|
| 294 |
except KeyboardInterrupt:
|
| 295 |
break
|
| 296 |
except Exception as e:
|
|
@@ -300,6 +341,8 @@ class PyWhiperCppServe(ServeClientBase):
|
|
| 300 |
|
| 301 |
def handle_transcription_output(self, segments, audio_buffer):
|
| 302 |
texts = "".join(i.text for i in segments)
|
|
|
|
|
|
|
| 303 |
self._segment_manager.handle(texts)
|
| 304 |
# 分析句子
|
| 305 |
last_cut_index, left_string, right_string, is_end_sentence = self.analysis_segments(segments, audio_buffer)
|
|
@@ -313,26 +356,45 @@ class PyWhiperCppServe(ServeClientBase):
|
|
| 313 |
if is_end_sentence and last_cut_index:
|
| 314 |
message = self._segment_manager.segment
|
| 315 |
seg_id = self._segment_manager.get_seg_id() - 1
|
| 316 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
if self._segment_manager.string.strip():
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
else:
|
| 321 |
seg_id = self._segment_manager.get_seg_id()
|
| 322 |
message = self._segment_manager.short_sentence + self._segment_manager.string
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
|
| 328 |
-
def send_to_client(self,
|
| 329 |
-
content = {
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
}
|
| 333 |
try:
|
| 334 |
self.websocket.send(
|
| 335 |
-
|
| 336 |
)
|
| 337 |
except Exception as e:
|
| 338 |
logger.error(f"Sending data to client: {e}")
|
|
|
|
| 1 |
|
| 2 |
|
| 3 |
import soundfile
|
| 4 |
+
from concurrent.futures import ProcessPoolExecutor as PPool
|
| 5 |
import multiprocessing as mp
|
| 6 |
import numpy as np
|
| 7 |
from logging import getLogger
|
|
|
|
| 14 |
from functools import partial
|
| 15 |
from .server import ServeClientBase
|
| 16 |
from .translator import QwenTranslator
|
| 17 |
+
from .vad import VoiceActivityDetector
|
| 18 |
from pywhispercpp.model import Model
|
| 19 |
+
from queue import Queue
|
| 20 |
+
from scipy.io.wavfile import write
|
| 21 |
+
from api_model import TransResult, Message
|
| 22 |
+
|
| 23 |
|
| 24 |
logger = getLogger(__name__)
|
| 25 |
|
| 26 |
+
def save_to_wave(filename, data:np.ndarray, sample_rate=16000):
|
| 27 |
+
write(filename, sample_rate, data)
|
| 28 |
|
| 29 |
class TripleTextBuffer:
|
| 30 |
def __init__(self, size=2):
|
|
|
|
| 115 |
class PywhisperInference:
|
| 116 |
whisper_model = None
|
| 117 |
llm_model = None
|
| 118 |
+
vad_model = None
|
| 119 |
|
| 120 |
@classmethod
|
| 121 |
def initializer(cls, event:mp.Event, warmup=True):
|
|
|
|
| 132 |
|
| 133 |
# init llamacpp
|
| 134 |
cls.llm_model = QwenTranslator(config.LLM_MODEL_PATH, config.LLM_SYS_PROMPT)
|
| 135 |
+
cls.vad_model = VoiceActivityDetector()
|
| 136 |
event.set()
|
| 137 |
|
| 138 |
@classmethod
|
|
|
|
| 164 |
@classmethod
|
| 165 |
def translate(cls, context: str, src_lang, dst_lang):
|
| 166 |
return cls.llm_model.translate(context, src_lang, dst_lang)
|
| 167 |
+
|
| 168 |
+
@classmethod
|
| 169 |
+
def voice_detect(cls, audio_buffer):
|
| 170 |
+
audio_buffer = np.frombuffer(audio_buffer, dtype=np.float32)
|
| 171 |
+
return cls.vad_model(audio_buffer)
|
| 172 |
+
|
| 173 |
|
| 174 |
|
| 175 |
class PyWhiperCppServe(ServeClientBase):
|
|
|
|
| 186 |
self.lock = threading.Lock()
|
| 187 |
self.frames_np = None
|
| 188 |
self.sample_rate = 16000
|
| 189 |
+
# self._audio_queue = Queue()
|
| 190 |
+
# 进程初始化后再开始收音频
|
| 191 |
self._ready_state = mp.Event()
|
| 192 |
+
self._pool = PPool(
|
| 193 |
max_workers=1, initializer=partial(PywhisperInference.initializer, event=self._ready_state))
|
| 194 |
+
|
| 195 |
logger.info('Create a process to process audio.')
|
| 196 |
+
self.send_ready_state()
|
| 197 |
+
# self.load_frame_thread = threading.Thread(target=self.load_frame_from_queue)
|
| 198 |
+
# self.load_frame_thread.daemon = True
|
| 199 |
+
# self.load_frame_thread.start()
|
| 200 |
+
|
| 201 |
self.trans_thread = threading.Thread(target=self.speech_to_text)
|
| 202 |
self.trans_thread.daemon = True
|
| 203 |
self.trans_thread.start()
|
|
|
|
| 204 |
|
| 205 |
def send_ready_state(self):
|
| 206 |
while not self._ready_state:
|
|
|
|
| 216 |
self.language = src_lang
|
| 217 |
self.dst_lang = dst_lang
|
| 218 |
|
| 219 |
+
# def load_frame_from_queue(self):
|
| 220 |
+
# while True:
|
| 221 |
+
# frame_np = self._audio_queue.get()
|
| 222 |
+
# fut = self._pool.submit(PywhisperInference.voice_detect, frame_np.tobytes())
|
| 223 |
+
# output = fut.result()
|
| 224 |
+
# logger.info(f"VAD: {output}")
|
| 225 |
+
# if output == True:
|
| 226 |
+
# with self.lock:
|
| 227 |
+
# if self.frames_np is None:
|
| 228 |
+
# self.frames_np = frame_np.copy()
|
| 229 |
+
# else:
|
| 230 |
+
# self.frames_np = np.append(self.frames_np,frame_np)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
def add_frames(self, frame_np):
|
| 234 |
+
# self._audio_queue.put(frame_np)
|
| 235 |
with self.lock:
|
| 236 |
if self.frames_np is None:
|
| 237 |
self.frames_np = frame_np.copy()
|
| 238 |
else:
|
| 239 |
self.frames_np = np.append(self.frames_np,frame_np)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
|
| 243 |
def update_audio_buffer(self, last_offset):
|
| 244 |
with self.lock:
|
|
|
|
| 309 |
return None, left_watch_string, right_watch_string, is_end_sentence
|
| 310 |
|
| 311 |
def speech_to_text(self):
|
| 312 |
+
# c = 0
|
| 313 |
while True:
|
| 314 |
if self.exit:
|
| 315 |
logger.info("Exiting speech to text thread")
|
|
|
|
| 321 |
continue
|
| 322 |
|
| 323 |
audio_buffer = self.get_audio_chunk_for_processing()
|
| 324 |
+
logger.info(f"Processing audio with duration: {len(audio_buffer)}")
|
| 325 |
# segments = self.transcribe_audio(audio_buffer)
|
| 326 |
+
# c+= 1
|
| 327 |
+
# name = f"dev-{c}.wav"
|
| 328 |
+
# save_to_wave(name, audio_buffer)
|
| 329 |
try:
|
| 330 |
logger.info(f"Processing audio with duration: {len(audio_buffer) / self.sample_rate:.2f}s")
|
| 331 |
segments = self.transcribe_audio(audio_buffer)
|
| 332 |
+
for tran_result in self.handle_transcription_output(segments, audio_buffer):
|
| 333 |
+
self.send_to_client(tran_result)
|
| 334 |
+
|
| 335 |
except KeyboardInterrupt:
|
| 336 |
break
|
| 337 |
except Exception as e:
|
|
|
|
| 341 |
|
| 342 |
def handle_transcription_output(self, segments, audio_buffer):
|
| 343 |
texts = "".join(i.text for i in segments)
|
| 344 |
+
if not len(texts):
|
| 345 |
+
return
|
| 346 |
self._segment_manager.handle(texts)
|
| 347 |
# 分析句子
|
| 348 |
last_cut_index, left_string, right_string, is_end_sentence = self.analysis_segments(segments, audio_buffer)
|
|
|
|
| 356 |
if is_end_sentence and last_cut_index:
|
| 357 |
message = self._segment_manager.segment
|
| 358 |
seg_id = self._segment_manager.get_seg_id() - 1
|
| 359 |
+
logger.info(f"{seg_id}, {message}")
|
| 360 |
+
yield TransResult(
|
| 361 |
+
seg_id=seg_id,
|
| 362 |
+
context=message,
|
| 363 |
+
from_=self.language,
|
| 364 |
+
to=self.dst_lang,
|
| 365 |
+
tran_content="this is english sample",
|
| 366 |
+
partial=False
|
| 367 |
+
)
|
| 368 |
if self._segment_manager.string.strip():
|
| 369 |
+
logger.info(f"{seg_id + 1}, {self._segment_manager.string}")
|
| 370 |
+
yield TransResult(
|
| 371 |
+
seg_id=seg_id+1,
|
| 372 |
+
context=self._segment_manager.string,
|
| 373 |
+
from_=self.language,
|
| 374 |
+
to=self.dst_lang,
|
| 375 |
+
tran_content="this is english sample",
|
| 376 |
+
)
|
| 377 |
|
| 378 |
else:
|
| 379 |
seg_id = self._segment_manager.get_seg_id()
|
| 380 |
message = self._segment_manager.short_sentence + self._segment_manager.string
|
| 381 |
+
logger.info(f"{seg_id}, {message}")
|
| 382 |
+
yield TransResult(
|
| 383 |
+
seg_id=seg_id,
|
| 384 |
+
context=message,
|
| 385 |
+
from_=self.language,
|
| 386 |
+
to=self.dst_lang,
|
| 387 |
+
tran_content="this is english sample",
|
| 388 |
+
)
|
| 389 |
|
| 390 |
+
def send_to_client(self, data:TransResult):
|
| 391 |
+
# content = {
|
| 392 |
+
# "uid": self.client_uid,
|
| 393 |
+
# **data_dict
|
| 394 |
+
# }
|
| 395 |
try:
|
| 396 |
self.websocket.send(
|
| 397 |
+
Message(result=data, request_id=self.client_uid).model_dump_json(by_alias=True)
|
| 398 |
)
|
| 399 |
except Exception as e:
|
| 400 |
logger.error(f"Sending data to client: {e}")
|
uv.lock
CHANGED
|
@@ -7,6 +7,15 @@ resolution-markers = [
|
|
| 7 |
"python_full_version < '3.12'",
|
| 8 |
]
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
[[package]]
|
| 11 |
name = "audioop-lts"
|
| 12 |
version = "0.2.1"
|
|
@@ -763,6 +772,86 @@ wheels = [
|
|
| 763 |
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
|
| 764 |
]
|
| 765 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
[[package]]
|
| 767 |
name = "pyreadline3"
|
| 768 |
version = "3.5.4"
|
|
@@ -1067,6 +1156,7 @@ dependencies = [
|
|
| 1067 |
{ name = "numpy" },
|
| 1068 |
{ name = "onnxruntime" },
|
| 1069 |
{ name = "pyaudio" },
|
|
|
|
| 1070 |
{ name = "pywhispercpp" },
|
| 1071 |
{ name = "setuptools" },
|
| 1072 |
{ name = "soundfile" },
|
|
@@ -1084,6 +1174,7 @@ requires-dist = [
|
|
| 1084 |
{ name = "numpy", specifier = ">=2.1.3" },
|
| 1085 |
{ name = "onnxruntime", specifier = ">=1.21.0" },
|
| 1086 |
{ name = "pyaudio", specifier = ">=0.2.14" },
|
|
|
|
| 1087 |
{ name = "pywhispercpp", specifier = ">=1.3.0" },
|
| 1088 |
{ name = "setuptools", specifier = ">=78.1.0" },
|
| 1089 |
{ name = "soundfile", specifier = ">=0.13.1" },
|
|
@@ -1112,6 +1203,18 @@ wheels = [
|
|
| 1112 |
{ url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 },
|
| 1113 |
]
|
| 1114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
[[package]]
|
| 1116 |
name = "urllib3"
|
| 1117 |
version = "2.3.0"
|
|
|
|
| 7 |
"python_full_version < '3.12'",
|
| 8 |
]
|
| 9 |
|
| 10 |
+
[[package]]
|
| 11 |
+
name = "annotated-types"
|
| 12 |
+
version = "0.7.0"
|
| 13 |
+
source = { registry = "https://pypi.org/simple" }
|
| 14 |
+
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
| 15 |
+
wheels = [
|
| 16 |
+
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
[[package]]
|
| 20 |
name = "audioop-lts"
|
| 21 |
version = "0.2.1"
|
|
|
|
| 772 |
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
|
| 773 |
]
|
| 774 |
|
| 775 |
+
[[package]]
|
| 776 |
+
name = "pydantic"
|
| 777 |
+
version = "2.11.2"
|
| 778 |
+
source = { registry = "https://pypi.org/simple" }
|
| 779 |
+
dependencies = [
|
| 780 |
+
{ name = "annotated-types" },
|
| 781 |
+
{ name = "pydantic-core" },
|
| 782 |
+
{ name = "typing-extensions" },
|
| 783 |
+
{ name = "typing-inspection" },
|
| 784 |
+
]
|
| 785 |
+
sdist = { url = "https://files.pythonhosted.org/packages/b0/41/832125a41fe098b58d1fdd04ae819b4dc6b34d6b09ed78304fd93d4bc051/pydantic-2.11.2.tar.gz", hash = "sha256:2138628e050bd7a1e70b91d4bf4a91167f4ad76fdb83209b107c8d84b854917e", size = 784742 }
|
| 786 |
+
wheels = [
|
| 787 |
+
{ url = "https://files.pythonhosted.org/packages/bf/c2/0f3baea344d0b15e35cb3e04ad5b953fa05106b76efbf4c782a3f47f22f5/pydantic-2.11.2-py3-none-any.whl", hash = "sha256:7f17d25846bcdf89b670a86cdfe7b29a9f1c9ca23dee154221c9aa81845cfca7", size = 443295 },
|
| 788 |
+
]
|
| 789 |
+
|
| 790 |
+
[[package]]
|
| 791 |
+
name = "pydantic-core"
|
| 792 |
+
version = "2.33.1"
|
| 793 |
+
source = { registry = "https://pypi.org/simple" }
|
| 794 |
+
dependencies = [
|
| 795 |
+
{ name = "typing-extensions" },
|
| 796 |
+
]
|
| 797 |
+
sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 }
|
| 798 |
+
wheels = [
|
| 799 |
+
{ url = "https://files.pythonhosted.org/packages/d6/7f/c6298830cb780c46b4f46bb24298d01019ffa4d21769f39b908cd14bbd50/pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24", size = 2044224 },
|
| 800 |
+
{ url = "https://files.pythonhosted.org/packages/a8/65/6ab3a536776cad5343f625245bd38165d6663256ad43f3a200e5936afd6c/pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30", size = 1858845 },
|
| 801 |
+
{ url = "https://files.pythonhosted.org/packages/e9/15/9a22fd26ba5ee8c669d4b8c9c244238e940cd5d818649603ca81d1c69861/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595", size = 1910029 },
|
| 802 |
+
{ url = "https://files.pythonhosted.org/packages/d5/33/8cb1a62818974045086f55f604044bf35b9342900318f9a2a029a1bec460/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e", size = 1997784 },
|
| 803 |
+
{ url = "https://files.pythonhosted.org/packages/c0/ca/49958e4df7715c71773e1ea5be1c74544923d10319173264e6db122543f9/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a", size = 2141075 },
|
| 804 |
+
{ url = "https://files.pythonhosted.org/packages/7b/a6/0b3a167a9773c79ba834b959b4e18c3ae9216b8319bd8422792abc8a41b1/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505", size = 2745849 },
|
| 805 |
+
{ url = "https://files.pythonhosted.org/packages/0b/60/516484135173aa9e5861d7a0663dce82e4746d2e7f803627d8c25dfa5578/pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f", size = 2005794 },
|
| 806 |
+
{ url = "https://files.pythonhosted.org/packages/86/70/05b1eb77459ad47de00cf78ee003016da0cedf8b9170260488d7c21e9181/pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77", size = 2123237 },
|
| 807 |
+
{ url = "https://files.pythonhosted.org/packages/c7/57/12667a1409c04ae7dc95d3b43158948eb0368e9c790be8b095cb60611459/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961", size = 2086351 },
|
| 808 |
+
{ url = "https://files.pythonhosted.org/packages/57/61/cc6d1d1c1664b58fdd6ecc64c84366c34ec9b606aeb66cafab6f4088974c/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1", size = 2258914 },
|
| 809 |
+
{ url = "https://files.pythonhosted.org/packages/d1/0a/edb137176a1f5419b2ddee8bde6a0a548cfa3c74f657f63e56232df8de88/pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c", size = 2257385 },
|
| 810 |
+
{ url = "https://files.pythonhosted.org/packages/26/3c/48ca982d50e4b0e1d9954919c887bdc1c2b462801bf408613ccc641b3daa/pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896", size = 1923765 },
|
| 811 |
+
{ url = "https://files.pythonhosted.org/packages/33/cd/7ab70b99e5e21559f5de38a0928ea84e6f23fdef2b0d16a6feaf942b003c/pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83", size = 1950688 },
|
| 812 |
+
{ url = "https://files.pythonhosted.org/packages/4b/ae/db1fc237b82e2cacd379f63e3335748ab88b5adde98bf7544a1b1bd10a84/pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89", size = 1908185 },
|
| 813 |
+
{ url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 },
|
| 814 |
+
{ url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 },
|
| 815 |
+
{ url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 },
|
| 816 |
+
{ url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 },
|
| 817 |
+
{ url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 },
|
| 818 |
+
{ url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 },
|
| 819 |
+
{ url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 },
|
| 820 |
+
{ url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 },
|
| 821 |
+
{ url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 },
|
| 822 |
+
{ url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 },
|
| 823 |
+
{ url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 },
|
| 824 |
+
{ url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 },
|
| 825 |
+
{ url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 },
|
| 826 |
+
{ url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 },
|
| 827 |
+
{ url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 },
|
| 828 |
+
{ url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 },
|
| 829 |
+
{ url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 },
|
| 830 |
+
{ url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 },
|
| 831 |
+
{ url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 },
|
| 832 |
+
{ url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 },
|
| 833 |
+
{ url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 },
|
| 834 |
+
{ url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 },
|
| 835 |
+
{ url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 },
|
| 836 |
+
{ url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 },
|
| 837 |
+
{ url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 },
|
| 838 |
+
{ url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 },
|
| 839 |
+
{ url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 },
|
| 840 |
+
{ url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 },
|
| 841 |
+
{ url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 },
|
| 842 |
+
{ url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 },
|
| 843 |
+
{ url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 },
|
| 844 |
+
{ url = "https://files.pythonhosted.org/packages/0b/76/1794e440c1801ed35415238d2c728f26cd12695df9057154ad768b7b991c/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a", size = 2042858 },
|
| 845 |
+
{ url = "https://files.pythonhosted.org/packages/73/b4/9cd7b081fb0b1b4f8150507cd59d27b275c3e22ad60b35cb19ea0977d9b9/pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc", size = 1873745 },
|
| 846 |
+
{ url = "https://files.pythonhosted.org/packages/e1/d7/9ddb7575d4321e40d0363903c2576c8c0c3280ebea137777e5ab58d723e3/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b", size = 1904188 },
|
| 847 |
+
{ url = "https://files.pythonhosted.org/packages/d1/a8/3194ccfe461bb08da19377ebec8cb4f13c9bd82e13baebc53c5c7c39a029/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe", size = 2083479 },
|
| 848 |
+
{ url = "https://files.pythonhosted.org/packages/42/c7/84cb569555d7179ca0b3f838cef08f66f7089b54432f5b8599aac6e9533e/pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5", size = 2118415 },
|
| 849 |
+
{ url = "https://files.pythonhosted.org/packages/3b/67/72abb8c73e0837716afbb58a59cc9e3ae43d1aa8677f3b4bc72c16142716/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761", size = 2079623 },
|
| 850 |
+
{ url = "https://files.pythonhosted.org/packages/0b/cd/c59707e35a47ba4cbbf153c3f7c56420c58653b5801b055dc52cccc8e2dc/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850", size = 2250175 },
|
| 851 |
+
{ url = "https://files.pythonhosted.org/packages/84/32/e4325a6676b0bed32d5b084566ec86ed7fd1e9bcbfc49c578b1755bde920/pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544", size = 2254674 },
|
| 852 |
+
{ url = "https://files.pythonhosted.org/packages/12/6f/5596dc418f2e292ffc661d21931ab34591952e2843e7168ea5a52591f6ff/pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5", size = 2080951 },
|
| 853 |
+
]
|
| 854 |
+
|
| 855 |
[[package]]
|
| 856 |
name = "pyreadline3"
|
| 857 |
version = "3.5.4"
|
|
|
|
| 1156 |
{ name = "numpy" },
|
| 1157 |
{ name = "onnxruntime" },
|
| 1158 |
{ name = "pyaudio" },
|
| 1159 |
+
{ name = "pydantic" },
|
| 1160 |
{ name = "pywhispercpp" },
|
| 1161 |
{ name = "setuptools" },
|
| 1162 |
{ name = "soundfile" },
|
|
|
|
| 1174 |
{ name = "numpy", specifier = ">=2.1.3" },
|
| 1175 |
{ name = "onnxruntime", specifier = ">=1.21.0" },
|
| 1176 |
{ name = "pyaudio", specifier = ">=0.2.14" },
|
| 1177 |
+
{ name = "pydantic", specifier = ">=2.11.2" },
|
| 1178 |
{ name = "pywhispercpp", specifier = ">=1.3.0" },
|
| 1179 |
{ name = "setuptools", specifier = ">=78.1.0" },
|
| 1180 |
{ name = "soundfile", specifier = ">=0.13.1" },
|
|
|
|
| 1203 |
{ url = "https://files.pythonhosted.org/packages/e0/86/39b65d676ec5732de17b7e3c476e45bb80ec64eb50737a8dce1a4178aba1/typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5", size = 45683 },
|
| 1204 |
]
|
| 1205 |
|
| 1206 |
+
[[package]]
|
| 1207 |
+
name = "typing-inspection"
|
| 1208 |
+
version = "0.4.0"
|
| 1209 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1210 |
+
dependencies = [
|
| 1211 |
+
{ name = "typing-extensions" },
|
| 1212 |
+
]
|
| 1213 |
+
sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
|
| 1214 |
+
wheels = [
|
| 1215 |
+
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
|
| 1216 |
+
]
|
| 1217 |
+
|
| 1218 |
[[package]]
|
| 1219 |
name = "urllib3"
|
| 1220 |
version = "2.3.0"
|