wangli commited on
Commit
9b44474
·
verified ·
1 Parent(s): 5aaec5c

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -77,6 +77,8 @@ OPENAI_BASE_URL=http://127.0.0.1:8001/v1 # 本地 OpenAI 协议服务时设置
77
  OPENAI_MODEL=AXERA-TECH/Qwen3-1.7B
78
  HOST=0.0.0.0
79
  PORT=8000
 
 
80
  ```
81
 
82
  依赖提示(WebSocket):
@@ -85,6 +87,18 @@ PORT=8000
85
  设备权限提示:
86
  - 如果遇到 `/dev/axcl_host` 权限错误,请用有权限的账号或 `sudo` 运行
87
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  ![meeting_demo.png](assert/meeting_demo.png)
89
 
90
 
 
77
  OPENAI_MODEL=AXERA-TECH/Qwen3-1.7B
78
  HOST=0.0.0.0
79
  PORT=8000
80
+ SSL_CERT=cert.pem
81
+ SSL_KEY=key.pem
82
  ```
83
 
84
  依赖提示(WebSocket):
 
87
  设备权限提示:
88
  - 如果遇到 `/dev/axcl_host` 权限错误,请用有权限的账号或 `sudo` 运行
89
 
90
+ HTTPS(推荐,便于浏览器麦克风权限):
91
+
92
+ ```bash
93
+ openssl req -x509 -newkey rsa:2048 -nodes \\
94
+ -keyout key.pem -out cert.pem -days 365 \\
95
+ -subj "/CN=<你的IP>"
96
+ ```
97
+
98
+ ```bash
99
+ SSL_CERT=cert.pem SSL_KEY=key.pem python -m app.server
100
+ ```
101
+
102
  ![meeting_demo.png](assert/meeting_demo.png)
103
 
104
 
app/cli_batch.py CHANGED
@@ -9,7 +9,8 @@ import soundfile as sf
9
  from app.model_bundle import ModelBundle
10
  from app.summarizer import IncrementalSummarizer
11
  from utils.vad_utils import merge_vad
12
- from utils.ax_cam_bin import do_clustering, distribute_spk, get_trans_sentence_sensevoice, chunk
 
13
  from app.config import MERGE_VAD_MAX_LEN_MS
14
 
15
 
@@ -46,47 +47,24 @@ def diar_asr(bundle: ModelBundle, speech: np.ndarray, fs: int = 16000) -> str:
46
  embeddings = bundle.speaker_infer(speech, fs, chunks=chunks)
47
  _, diar_results = do_clustering(chunks, embeddings, speaker_num=None)
48
 
49
- audio_duration = speech.shape[0] / fs
50
- all_metadata = {}
51
-
52
  for i, segment in enumerate(vad_segments):
53
  segment_start, segment_end = segment
54
  start_sample = int(segment_start / 1000 * fs)
55
  end_sample = min(int(segment_end / 1000 * fs), speech.shape[0])
56
  segment_speech = speech[start_sample:end_sample]
57
- time_offset_sec = segment_start / 1000.0
58
 
59
- _, segment_meta = bundle.asr_infer(
60
  segment_speech,
61
- output_timestamp=True,
62
  key=f"segment_{i}",
63
  )
 
 
64
 
65
- if "merged_words" in segment_meta:
66
- all_metadata.setdefault("merged_words", []).extend(segment_meta["merged_words"])
67
-
68
- if "merged_timestamps" in segment_meta:
69
- adjusted = [
70
- [
71
- min(ts[0] + time_offset_sec, audio_duration),
72
- min(ts[1] + time_offset_sec, audio_duration),
73
- ]
74
- for ts in segment_meta["merged_timestamps"]
75
- ]
76
- all_metadata.setdefault("merged_timestamps", []).extend(adjusted)
77
-
78
- output_asr = {
79
- "merged_words": all_metadata.get("merged_words", []),
80
- "merged_timestamps": all_metadata.get("merged_timestamps", []),
81
- }
82
-
83
- asr_timestamps = get_trans_sentence_sensevoice(output_asr)
84
- sentence_info_with_spk = distribute_spk(asr_timestamps, diar_results)
85
-
86
- lines = []
87
- for text_string, timeinterval, spk in sentence_info_with_spk:
88
  lines.append(
89
- f"Speaker_{spk}: [{timeinterval[0]:.3f} {timeinterval[1]:.3f}] {text_string}"
90
  )
91
  return "\n".join(lines)
92
 
 
9
  from app.model_bundle import ModelBundle
10
  from app.summarizer import IncrementalSummarizer
11
  from utils.vad_utils import merge_vad
12
+ from utils.ax_cam_bin import do_clustering, chunk
13
+ from app.diar_utils import pick_speaker
14
  from app.config import MERGE_VAD_MAX_LEN_MS
15
 
16
 
 
47
  embeddings = bundle.speaker_infer(speech, fs, chunks=chunks)
48
  _, diar_results = do_clustering(chunks, embeddings, speaker_num=None)
49
 
50
+ lines = []
 
 
51
  for i, segment in enumerate(vad_segments):
52
  segment_start, segment_end = segment
53
  start_sample = int(segment_start / 1000 * fs)
54
  end_sample = min(int(segment_end / 1000 * fs), speech.shape[0])
55
  segment_speech = speech[start_sample:end_sample]
 
56
 
57
+ text, _ = bundle.asr_infer(
58
  segment_speech,
59
+ output_timestamp=False,
60
  key=f"segment_{i}",
61
  )
62
+ if not text or not text.strip():
63
+ continue
64
 
65
+ spk = pick_speaker(segment_start / 1000.0, segment_end / 1000.0, diar_results)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  lines.append(
67
+ f"Speaker_{spk}: [{segment_start/1000.0:.3f} {segment_end/1000.0:.3f}] {text.strip()}"
68
  )
69
  return "\n".join(lines)
70
 
app/diar_utils.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ def pick_speaker(seg_st: float, seg_ed: float, diar_results) -> int:
4
+ if not diar_results:
5
+ return 0
6
+ best_spk = diar_results[0][2]
7
+ best_overlap = 0.0
8
+ for st_spk, ed_spk, spk in diar_results:
9
+ overlap = min(seg_ed, ed_spk) - max(seg_st, st_spk)
10
+ if overlap > best_overlap:
11
+ best_overlap = overlap
12
+ best_spk = spk
13
+ return int(best_spk)
app/model_bundle.py CHANGED
@@ -8,7 +8,7 @@ from utils.ax_model_bin import AX_SenseVoiceSmall
8
  from utils.ax_vad_bin import AX_Fsmn_vad
9
  from utils.ax_cam_bin import AX_SpeakerEmbeddingInference
10
  from utils.sentencepiece_tokenizer import SentencepiecesTokenizer
11
- import re
12
 
13
 
14
  class ModelBundle:
@@ -76,39 +76,5 @@ class ModelBundle:
76
  key=[key],
77
  )
78
  text = "".join([r.get("text", "") for r in results])
79
- text = self._clean_asr_text(text)
80
  return text, meta
81
-
82
- @staticmethod
83
- def _clean_asr_text(text: str) -> str:
84
- if not text:
85
- return ""
86
- # Remove token tags like <|zh|>, <|SPEECH|> or broken sequences
87
- text = re.sub(r"<\\|[^>]*?\\|>", "", text)
88
- text = re.sub(r"<\\|[^|>]*\\|", "", text)
89
- text = text.replace("<|", "").replace("|>", "")
90
- # Remove plain token chains like zh|NEUTRAL|Speech|withitn|
91
- text = re.sub(
92
- r"(?:\\b(zh|en|yue|ja|ko|speech|happy|sad|angry|neutral|emo_unknown|withitn|woitn)\\b\\|)+",
93
- "",
94
- text,
95
- flags=re.IGNORECASE,
96
- )
97
- # Remove leftover meta tokens joined by pipes anywhere
98
- text = re.sub(
99
- r"\\b(zh|en|yue|ja|ko|speech|happy|sad|angry|neutral|emo_unknown|withitn|woitn)\\b",
100
- "",
101
- text,
102
- flags=re.IGNORECASE,
103
- )
104
- text = text.replace("|", "")
105
- # Remove standalone metadata tokens (case-insensitive)
106
- meta_re = r"\\b(zh|en|yue|ja|ko|speech|happy|sad|angry|neutral|emo_unknown|withitn|woitn)\\b"
107
- text = re.sub(meta_re, "", text, flags=re.IGNORECASE)
108
- # Remove concatenated metadata tokens like zhEMO_UNKNOWNSpeechwithitn
109
- meta_tokens = ["zh","en","yue","ja","ko","speech","happy","sad","angry","neutral","emo_unknown","withitn","woitn"]
110
- meta_concat_re = r"(?:%s)+" % "|".join(meta_tokens)
111
- text = re.sub(meta_concat_re, "", text, flags=re.IGNORECASE)
112
- # Normalize spaces
113
- text = re.sub(r"\\s+", " ", text).strip()
114
- return text
 
8
  from utils.ax_vad_bin import AX_Fsmn_vad
9
  from utils.ax_cam_bin import AX_SpeakerEmbeddingInference
10
  from utils.sentencepiece_tokenizer import SentencepiecesTokenizer
11
+ from app.text_cleaner import clean_asr_text
12
 
13
 
14
  class ModelBundle:
 
76
  key=[key],
77
  )
78
  text = "".join([r.get("text", "") for r in results])
79
+ text = clean_asr_text(text)
80
  return text, meta
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/pipeline.py CHANGED
@@ -14,12 +14,8 @@ from app.config import (
14
  MERGE_VAD_MAX_LEN_MS,
15
  )
16
  from utils.vad_utils import merge_vad
17
- from utils.ax_cam_bin import (
18
- do_clustering,
19
- distribute_spk,
20
- get_trans_sentence_sensevoice,
21
- chunk,
22
- )
23
 
24
 
25
  @dataclass
@@ -145,48 +141,23 @@ class StreamingMeetingSession:
145
  embeddings = self.model_bundle.speaker_infer(speech, self.sample_rate, chunks=chunks)
146
  _, diar_results = do_clustering(chunks, embeddings, speaker_num=None)
147
 
148
- audio_duration = speech.shape[0] / self.sample_rate
149
- all_metadata: Dict[str, List] = {}
150
-
151
  for i, segment in enumerate(vad_segments):
152
  segment_start, segment_end = segment
153
  start_sample = int(segment_start / 1000 * self.sample_rate)
154
  end_sample = min(int(segment_end / 1000 * self.sample_rate), speech.shape[0])
155
  segment_speech = speech[start_sample:end_sample]
156
- time_offset_sec = segment_start / 1000.0
157
 
158
- _, segment_meta = self.model_bundle.asr_infer(
159
  segment_speech,
160
- output_timestamp=True,
161
  key=f"segment_{i}",
162
  )
163
-
164
- if "merged_words" in segment_meta:
165
- all_metadata.setdefault("merged_words", []).extend(segment_meta["merged_words"])
166
-
167
- if "merged_timestamps" in segment_meta:
168
- adjusted = [
169
- [
170
- min(ts[0] + time_offset_sec, audio_duration),
171
- min(ts[1] + time_offset_sec, audio_duration),
172
- ]
173
- for ts in segment_meta["merged_timestamps"]
174
- ]
175
- all_metadata.setdefault("merged_timestamps", []).extend(adjusted)
176
-
177
- output_asr = {
178
- "merged_words": all_metadata.get("merged_words", []),
179
- "merged_timestamps": all_metadata.get("merged_timestamps", []),
180
- }
181
-
182
- asr_timestamps = get_trans_sentence_sensevoice(output_asr)
183
- sentence_info_with_spk = distribute_spk(asr_timestamps, diar_results)
184
-
185
- lines = []
186
- for text_string, timeinterval, spk in sentence_info_with_spk:
187
- if not text_string or not text_string.strip():
188
  continue
 
 
189
  lines.append(
190
- f"Speaker_{spk}: [{timeinterval[0]:.3f} {timeinterval[1]:.3f}] {text_string}"
191
  )
192
  return "\n".join(lines)
 
14
  MERGE_VAD_MAX_LEN_MS,
15
  )
16
  from utils.vad_utils import merge_vad
17
+ from utils.ax_cam_bin import do_clustering, chunk
18
+ from app.diar_utils import pick_speaker
 
 
 
 
19
 
20
 
21
  @dataclass
 
141
  embeddings = self.model_bundle.speaker_infer(speech, self.sample_rate, chunks=chunks)
142
  _, diar_results = do_clustering(chunks, embeddings, speaker_num=None)
143
 
144
+ lines = []
 
 
145
  for i, segment in enumerate(vad_segments):
146
  segment_start, segment_end = segment
147
  start_sample = int(segment_start / 1000 * self.sample_rate)
148
  end_sample = min(int(segment_end / 1000 * self.sample_rate), speech.shape[0])
149
  segment_speech = speech[start_sample:end_sample]
 
150
 
151
+ text, _ = self.model_bundle.asr_infer(
152
  segment_speech,
153
+ output_timestamp=False,
154
  key=f"segment_{i}",
155
  )
156
+ if not text or not text.strip():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  continue
158
+
159
+ spk = pick_speaker(segment_start / 1000.0, segment_end / 1000.0, diar_results)
160
  lines.append(
161
+ f"Speaker_{spk}: [{segment_start/1000.0:.3f} {segment_end/1000.0:.3f}] {text.strip()}"
162
  )
163
  return "\n".join(lines)
app/server.py CHANGED
@@ -85,6 +85,8 @@ if __name__ == "__main__":
85
 
86
  host = os.getenv("HOST", "0.0.0.0")
87
  port = int(os.getenv("PORT", "8000"))
 
 
88
  try:
89
  s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
90
  s.connect(("8.8.8.8", 80))
@@ -92,5 +94,13 @@ if __name__ == "__main__":
92
  s.close()
93
  except Exception:
94
  local_ip = "127.0.0.1"
95
- print(f"Local URL: http://{local_ip}:{port}")
96
- uvicorn.run("app.server:app", host=host, port=port, reload=False)
 
 
 
 
 
 
 
 
 
85
 
86
  host = os.getenv("HOST", "0.0.0.0")
87
  port = int(os.getenv("PORT", "8000"))
88
+ ssl_cert = os.getenv("SSL_CERT")
89
+ ssl_key = os.getenv("SSL_KEY")
90
  try:
91
  s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
92
  s.connect(("8.8.8.8", 80))
 
94
  s.close()
95
  except Exception:
96
  local_ip = "127.0.0.1"
97
+ scheme = "https" if ssl_cert and ssl_key else "http"
98
+ print(f"Local URL: {scheme}://{local_ip}:{port}")
99
+ uvicorn.run(
100
+ "app.server:app",
101
+ host=host,
102
+ port=port,
103
+ reload=False,
104
+ ssl_certfile=ssl_cert,
105
+ ssl_keyfile=ssl_key,
106
+ )
app/static/app.js CHANGED
@@ -66,6 +66,11 @@ async function startMeeting() {
66
  finalTranscript.textContent = '';
67
  liveLog.innerHTML = '';
68
 
 
 
 
 
 
69
  const wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
70
  ws = new WebSocket(`${wsProtocol}://${location.host}/ws`);
71
  ws.binaryType = 'arraybuffer';
@@ -87,7 +92,13 @@ async function startMeeting() {
87
  };
88
 
89
  ws.onopen = async () => {
90
- mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
 
 
 
 
 
 
91
  audioCtx = new (window.AudioContext || window.webkitAudioContext)();
92
  sourceNode = audioCtx.createMediaStreamSource(mediaStream);
93
 
 
66
  finalTranscript.textContent = '';
67
  liveLog.innerHTML = '';
68
 
69
+ if (!window.isSecureContext && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
70
+ logLine('麦克风权限需要 HTTPS 或 localhost 访问。');
71
+ alert('麦克风权限需要 HTTPS 或 localhost 访问。请使用 https 或在本机用 127.0.0.1 访问。');
72
+ }
73
+
74
  const wsProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
75
  ws = new WebSocket(`${wsProtocol}://${location.host}/ws`);
76
  ws.binaryType = 'arraybuffer';
 
92
  };
93
 
94
  ws.onopen = async () => {
95
+ try {
96
+ mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true });
97
+ } catch (err) {
98
+ logLine(`麦克风权限请求失败: ${err && err.name ? err.name : err}`);
99
+ alert('麦克风权限请求失败。请检查浏览器权限或使用 HTTPS/localhost。');
100
+ return;
101
+ }
102
  audioCtx = new (window.AudioContext || window.webkitAudioContext)();
103
  sourceNode = audioCtx.createMediaStreamSource(mediaStream);
104
 
app/text_cleaner.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import re
3
+
4
+
5
+ def clean_asr_text(text: str) -> str:
6
+ if not text:
7
+ return ""
8
+ # Remove token tags like <|zh|>, <|SPEECH|> or broken sequences
9
+ text = re.sub(r"<\|[^>]*?\|>", "", text)
10
+ text = re.sub(r"<\|[^|>]*\|", "", text)
11
+ text = text.replace("<|", "").replace("|>", "")
12
+
13
+ # Remove plain token chains like zh|NEUTRAL|Speech|withitn|
14
+ text = re.sub(
15
+ r"(?:\b(zh|en|yue|ja|ko|speech|happy|sad|angry|neutral|emo_unknown|withitn|woitn)\b\|)+",
16
+ "",
17
+ text,
18
+ flags=re.IGNORECASE,
19
+ )
20
+
21
+ # Remove leftover meta tokens joined by pipes anywhere
22
+ text = re.sub(
23
+ r"\b(zh|en|yue|ja|ko|speech|happy|sad|angry|neutral|emo_unknown|withitn|woitn)\b",
24
+ "",
25
+ text,
26
+ flags=re.IGNORECASE,
27
+ )
28
+ text = text.replace("|", "")
29
+
30
+ # Remove standalone metadata tokens (case-insensitive)
31
+ meta_re = r"\b(zh|en|yue|ja|ko|speech|happy|sad|angry|neutral|emo_unknown|withitn|woitn)\b"
32
+ text = re.sub(meta_re, "", text, flags=re.IGNORECASE)
33
+
34
+ # Remove concatenated metadata tokens like zhEMO_UNKNOWNSpeechwithitn
35
+ meta_tokens = ["zh","en","yue","ja","ko","speech","happy","sad","angry","neutral","emo_unknown","withitn","woitn"]
36
+ meta_concat_re = r"(?:%s)+" % "|".join(meta_tokens)
37
+ text = re.sub(meta_concat_re, "", text, flags=re.IGNORECASE)
38
+
39
+ # Normalize spaces
40
+ text = re.sub(r"\s+", " ", text).strip()
41
+ return text
cert.pem ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDETCCAfmgAwIBAgIUPI3rAC24IoJ7FUydYijEOp7GeygwDQYJKoZIhvcNAQEL
3
+ BQAwGDEWMBQGA1UEAwwNMTAuMTI2LjMzLjE0MDAeFw0yNjAyMjcwMzE3MzRaFw0y
4
+ NzAyMjcwMzE3MzRaMBgxFjAUBgNVBAMMDTEwLjEyNi4zMy4xNDAwggEiMA0GCSqG
5
+ SIb3DQEBAQUAA4IBDwAwggEKAoIBAQChzhKivPsGUSjfzFWwocMg1FT56iDFo8yy
6
+ tba/LvbP2i2BpujTqK5/6iInLh6N9ZptJg4PsLSEQ2HWfLdYupEYvMrXDy4nYwsX
7
+ gAbrjAz3uhsJV2+LSVF/0g8PNhwidDN4WNWLQoVf5g9FCxl0SneCoyKpQdpIB10r
8
+ J0ZXtKe5SY9Ydq0EdjS+5898U83XgOIFQfKDRdakPuxKLX00DFd6S5Xz4Yw148As
9
+ ypAOfNSXCqX0+2wtSyfAednwlPea+VxQPExQBpx7yOYe0eDrMDDPP7z6UUdzMdoD
10
+ ZOlWTgtIarBCthD9HnhZhqK6iom9sxm9hvHOV+kM/pje1iMeaFbpAgMBAAGjUzBR
11
+ MB0GA1UdDgQWBBRhENjhWth7MneI5GxGFzCu2RjUbTAfBgNVHSMEGDAWgBRhENjh
12
+ Wth7MneI5GxGFzCu2RjUbTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA
13
+ A4IBAQAXi8HfQF1LyACZSkilE6emnB0zh31NMR7oimhc9rlKgX5yPD4RQp+OKr8l
14
+ APeIFeWLiHadvxKpjnT+MQwzODs5i+Qai0vDaRoy/OLPiaI3rH8yaAMxMI0hrh4c
15
+ Rfyf6bWBvcTpuUTFO/hC1KxfVVCjCwMcV/Jmv0xukCMCXCHNqDdkG2m24RSmKUHl
16
+ Af85bduX8PabtZxEDESkLc2Z8G8w5VvEWJizQ7OF0EcYJIA3JEDcr50o5kFpu7OQ
17
+ ZkRfWopQ3I1DOUOfSrc4d5gMy3b1rvDBbeLTRsNnec9WciwP4U/+Hbxs2P+BALo+
18
+ WSq6D7FC0WTTXtYdGFv2oY5s38Dr
19
+ -----END CERTIFICATE-----
key.pem ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQChzhKivPsGUSjf
3
+ zFWwocMg1FT56iDFo8yytba/LvbP2i2BpujTqK5/6iInLh6N9ZptJg4PsLSEQ2HW
4
+ fLdYupEYvMrXDy4nYwsXgAbrjAz3uhsJV2+LSVF/0g8PNhwidDN4WNWLQoVf5g9F
5
+ Cxl0SneCoyKpQdpIB10rJ0ZXtKe5SY9Ydq0EdjS+5898U83XgOIFQfKDRdakPuxK
6
+ LX00DFd6S5Xz4Yw148AsypAOfNSXCqX0+2wtSyfAednwlPea+VxQPExQBpx7yOYe
7
+ 0eDrMDDPP7z6UUdzMdoDZOlWTgtIarBCthD9HnhZhqK6iom9sxm9hvHOV+kM/pje
8
+ 1iMeaFbpAgMBAAECggEAQjovPYX9dy3z/XpM3pGvZPoT2AEBLfQn/kPLS4CFDDlg
9
+ o+814CBsYDXsib3iSreq4B8R5VEt6e8MljaQ8xPV/NqVaaYwfXWYHiPMcU/vJNx7
10
+ YXz0zn2RirBncpHyvRVz1cACk9AD+GcZe+iZoBQ0y3dLYhzuo8nD1Dxsmcx7VCaF
11
+ VTAVKGsCSW3ZHfXXxMDkB/3tiRknb4KNpvVzLo2GNgj5fiygkkgf9hM1hRfqt7/P
12
+ nejF3A0laWQc0N0OluM6SF+2A4uNnWQwIeyLK/AdM14Me5rvYjiOG/xE1hNVBvup
13
+ utkjVyVYP1pCoK0eCqaApd61DuDcagFPi6gSmKeY4wKBgQDXF0cGE6tbohNUa/GU
14
+ r8jCb51F1gWuLJNl4y6SMDr9/iku1OKkJtbK0InMmfeyeKeg/xeuXSc5QJY5hcrd
15
+ A/Y4mTRWlpfzpYCj5qos7f1HVl/mtwBqN2VUVi2Tid1OrfDgTup0ZZlN1IjszH3/
16
+ 37IQnJTs/sfVxwwh+ij+VKvP6wKBgQDAlFDTq8lRtbc/nVAqLPnnc2HnLtrAzeLg
17
+ N1qD7QQvcfWP9gxSg0ZBYJf52uYRjLDB4b6LBPV5Ys9SIcXz8G6VYwUESkJcnQ+s
18
+ yTNUuJolzAFq0NoMLAAN0vKV2yyIdGP/oupTYEfSRz1bRtIGchhY/XMdVjj5Ppxc
19
+ w3CfAAcTewKBgCI4nuE1oe7bU437+py4dw2Qaopg6dhzWSQ9x/wUVl5w4KaF0mVh
20
+ lI0CLtpxqLopfiocS+0+/u2Z/Ay837DYX4VTwsMABL8MFvJ80ZiCaOi/slRny1Ya
21
+ 6DFJ4Mh3h9Fr1UYq6ByKyaBbb0mVo3phYdhIwV0PkEXP/HsvbPRCDm/vAoGBALng
22
+ bgNgk/giBLWKCY4ryyny3FRfjRT7pDf2NY+QfbGttO829b3Op0kDCq1G8zmNKi54
23
+ zYkxSB3ZmXIU1xQUxSe7Y2Q4qMTrc+26ZakoZOCGf/exjkShU4wER9EMs3choENl
24
+ 4/aFv8zepgIr4RwHlCiQuUNfra4lGJcQrOtLA4lxAoGAWn8ovOGTEo5Seuq5CoCQ
25
+ HbDdMmd6z9kLInKcqaGsuclFaEZjG6CHznD7B+SRtJFBN3+L0K3UbwNeAka6yEqg
26
+ 3TUXNBXMypOKYPYDAbGE8t7wrLL7WEtgHiqpkN/zZoxX61f3nLOIoku817dW3KtW
27
+ PTUCWpm7u5IQWl6T+ThOJmU=
28
+ -----END PRIVATE KEY-----
requirements.txt CHANGED
@@ -1,7 +1,5 @@
1
- torch
2
- tqdm
3
- funasr==1.2.7
4
- torchaudio
5
  kaldi_native_fbank
6
  fastcluster
7
  hdbscan
@@ -10,5 +8,5 @@ loguru
10
  gradio
11
  fastapi
12
  uvicorn
13
- soundfile
14
  openai
 
1
+ numpy
2
+ soundfile
 
 
3
  kaldi_native_fbank
4
  fastcluster
5
  hdbscan
 
8
  gradio
9
  fastapi
10
  uvicorn
11
+ websockets
12
  openai
tests/test_lightweight.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import unittest
3
+
4
+ from app.text_cleaner import clean_asr_text
5
+ from app.diar_utils import pick_speaker
6
+
7
+
8
+ class TestTextCleaner(unittest.TestCase):
9
+ def test_angle_tokens(self):
10
+ s = "<|zh|<|NEUTRAL|<|Speech|<|withitn|嗯,你好你好你好。"
11
+ self.assertEqual(clean_asr_text(s), "嗯,你好你好你好。")
12
+
13
+ def test_pipe_tokens(self):
14
+ s = "zh|NEUTRAL|Speech|withitn|他相当于把一个平台就把一个拼行能力拆分掉了。"
15
+ self.assertEqual(clean_asr_text(s), "他相当于把一个平台就把一个拼行能力拆分掉了。")
16
+
17
+ def test_concat_tokens(self):
18
+ s = "zhEMO_UNKNOWNSpeechwithitn房止追后了对这种方式掉。"
19
+ self.assertEqual(clean_asr_text(s), "房止追后了对这种方式掉。")
20
+
21
+ def test_empty_after_clean(self):
22
+ s = "zh|NEUTRAL|Speech|withitn|"
23
+ self.assertEqual(clean_asr_text(s), "")
24
+
25
+
26
+ class TestPickSpeaker(unittest.TestCase):
27
+ def test_pick_speaker_overlap(self):
28
+ diar = [
29
+ [0.0, 2.0, 0],
30
+ [2.0, 5.0, 1],
31
+ ]
32
+ spk = pick_speaker(1.5, 3.5, diar)
33
+ self.assertEqual(spk, 1)
34
+
35
+ def test_pick_speaker_no_overlap(self):
36
+ diar = [[0.0, 1.0, 0]]
37
+ spk = pick_speaker(2.0, 3.0, diar)
38
+ self.assertEqual(spk, 0)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ unittest.main()
utils/ax_model_bin.py CHANGED
@@ -6,12 +6,7 @@
6
  import os.path
7
  from pathlib import Path
8
  from typing import List, Union, Tuple
9
- try:
10
- import torch
11
- TORCH_AVAILABLE = True
12
- except Exception:
13
- torch = None
14
- TORCH_AVAILABLE = False
15
  import numpy as np
16
  import axengine as axe
17
 
@@ -34,7 +29,6 @@ from utils.infer_utils import (
34
  read_yaml,
35
  )
36
  from utils.frontend import WavFrontend
37
- from utils.ctc_alignment import ctc_forced_align
38
 
39
  logging = get_logger()
40
 
@@ -222,95 +216,10 @@ class AX_SenseVoiceSmall:
222
  if ibest_writer is not None:
223
  ibest_writer["text"][key[j]] = text
224
 
225
- if output_timestamp and tokenizer is not None:
226
- if not TORCH_AVAILABLE:
227
- # Torch is required for CTC forced align. Skip timestamps in torch-free mode.
228
- output_timestamp = False
229
- result_i = {"key": key[j] if j < len(key) else f"result_{j}", "text": text}
230
- results.append(result_i)
231
- continue
232
- # Process timestamps similar to model.py
233
- from itertools import groupby
234
- timestamp = []
235
- tokens = tokenizer.text2tokens(text)[4:] if hasattr(tokenizer, 'text2tokens') else []
236
- # If ctc_forced_align is available, calculate timestamps
237
- if 'ctc_forced_align' in globals() or 'ctc_forced_align' in locals():
238
- softmax = torch.nn.Softmax(dim=-1)
239
- torch_ctc = torch.from_numpy(ctc_logits).float()
240
- enc_len_item = encoder_out_lens[j].item() if hasattr(encoder_out_lens[j], "item") else int(encoder_out_lens[j])
241
- logits_speech = softmax(torch_ctc[j, 4:enc_len_item, :])
242
-
243
- pred = logits_speech.argmax(-1).cpu()
244
- logits_speech[pred == self.blank_id, self.blank_id] = 0
245
-
246
- try:
247
-
248
- # Convert numpy types to PyTorch tensors where needed
249
- # Make sure encoder_out_lens is a torch.Tensor before calling .long()
250
- tokens_tensor = torch.Tensor(token_int[4:]).unsqueeze(0).long()
251
-
252
- # Handle numpy int64 by converting to torch tensor first
253
- if isinstance(encoder_out_lens[j], (np.integer, np.int64)):
254
- lens_tensor = torch.tensor(int(encoder_out_lens[j]-4))
255
- else:
256
- lens_tensor = (encoder_out_lens[j]-4)
257
- if hasattr(lens_tensor, 'long'):
258
- lens_tensor = lens_tensor.long()
259
-
260
- token_len = torch.tensor(len(token_int)-4).unsqueeze(0)
261
- #token_len = torch.tensor(len(token_int)).unsqueeze(0)
262
-
263
- if hasattr(token_len, 'long'):
264
- token_len = token_len.long()
265
-
266
- align = ctc_forced_align(
267
- logits_speech.unsqueeze(0).float(),
268
- tokens_tensor,
269
- lens_tensor,
270
- token_len,
271
- ignore_id=0
272
- )
273
- # 时间戳处理完成
274
- # Process alignment
275
- # Handle potential numpy type for slicing
276
- if isinstance(encoder_out_lens[j], (np.integer, np.int64)):
277
- end_idx = int(encoder_out_lens[j]-4)
278
- else:
279
- end_idx = encoder_out_lens[j]-4
280
- if hasattr(end_idx, 'item'):
281
- end_idx = end_idx.item()
282
-
283
- pred = groupby(align[0, :end_idx])
284
- _start = 0
285
- token_id = 0
286
- # Convert ts_max to the right type for calculation
287
- if isinstance(encoder_out_lens[j], (np.integer, np.int64)):
288
- ts_max = int(encoder_out_lens[j] - 4)
289
- else:
290
- ts_max = encoder_out_lens[j] - 4
291
- if hasattr(ts_max, 'item'):
292
- ts_max = ts_max.item()
293
- #timestamps_one_sentence = [] # Store timestamps for the current sentence
294
- for pred_token, pred_frame in pred:
295
- _end = _start + len(list(pred_frame))
296
- if pred_token != 0 and token_id < len(tokens):
297
- # 计算时间戳,加上时间偏移以保持连续,精确保留两位小数
298
- ts_left = round(max((_start*60-30)/1000, 0) + time_offset, 2)
299
- ts_right = round(min((_end*60-30)/1000, (ts_max*60-30)/1000) + time_offset, 2)
300
-
301
- merged_timestamps.append([ts_left, ts_right])
302
- merged_words.append(tokens[token_id])
303
- #timestamps_one_sentence.append(ts_entry)
304
- token_id += 1
305
- _start = _end
306
- #merged_timestamps.append(timestamps_one_sentence)
307
- # 时间戳处理完成
308
- except (ImportError, Exception) as e:
309
- logging.warning(f"Timestamp calculation failed: {e}")
310
-
311
- result_i = {"key": key[j] if j < len(key) else f"result_{j}", "text": text, "timestamp": timestamp}
312
- else:
313
- result_i = {"key": key[j] if j < len(key) else f"result_{j}", "text": text}
314
 
315
  # 直接添加结果,重复处理将在export.py中进行
316
  results.append(result_i)
 
6
  import os.path
7
  from pathlib import Path
8
  from typing import List, Union, Tuple
9
+ TORCH_AVAILABLE = False
 
 
 
 
 
10
  import numpy as np
11
  import axengine as axe
12
 
 
29
  read_yaml,
30
  )
31
  from utils.frontend import WavFrontend
 
32
 
33
  logging = get_logger()
34
 
 
216
  if ibest_writer is not None:
217
  ibest_writer["text"][key[j]] = text
218
 
219
+ if output_timestamp:
220
+ # Torch-free build: skip timestamp generation
221
+ output_timestamp = False
222
+ result_i = {"key": key[j] if j < len(key) else f"result_{j}", "text": text}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
  # 直接添加结果,重复处理将在export.py中进行
225
  results.append(result_i)
utils/vad_utils.py CHANGED
@@ -1,5 +1,4 @@
1
- import torch
2
- from torch.nn.utils.rnn import pad_sequence
3
 
4
 
5
  def slice_padding_fbank(speech, speech_lengths, vad_segments):
@@ -13,16 +12,21 @@ def slice_padding_fbank(speech, speech_lengths, vad_segments):
13
  speech_lengths_i = end_idx - bed_idx
14
  speech_list.append(speech_i)
15
  speech_lengths_list.append(speech_lengths_i)
16
- feats_pad = pad_sequence(speech_list, batch_first=True, padding_value=0.0)
17
- speech_lengths_pad = torch.Tensor(speech_lengths_list).int()
 
 
 
 
 
 
 
18
  return feats_pad, speech_lengths_pad
19
 
20
 
21
  def slice_padding_audio_samples(speech, speech_lengths, vad_segments):
22
  speech_list = []
23
  speech_lengths_list = []
24
- import pdb
25
- pdb.set_trace()
26
  for i, segment in enumerate(vad_segments):
27
  bed_idx = int(segment[0][0] * 16)
28
  end_idx = min(int(segment[0][1] * 16), speech_lengths)
@@ -58,4 +62,4 @@ def merge_vad(vad_result, max_length=15000, min_length=0):
58
  # new_result.append([bg + j * spl_l, bg + (j + 1) * spl_l])
59
  bg = time
60
  new_result.append([bg, time_step[-1]])
61
- return new_result
 
1
+ import numpy as np
 
2
 
3
 
4
  def slice_padding_fbank(speech, speech_lengths, vad_segments):
 
12
  speech_lengths_i = end_idx - bed_idx
13
  speech_list.append(speech_i)
14
  speech_lengths_list.append(speech_lengths_i)
15
+ max_len = max(speech_lengths_list) if speech_lengths_list else 0
16
+ feats_pad = []
17
+ for arr in speech_list:
18
+ pad_len = max_len - arr.shape[0]
19
+ if pad_len > 0:
20
+ arr = np.pad(arr, (0, pad_len), mode="constant")
21
+ feats_pad.append(arr)
22
+ feats_pad = np.stack(feats_pad, axis=0) if feats_pad else np.zeros((0, 0), dtype=np.float32)
23
+ speech_lengths_pad = np.array(speech_lengths_list, dtype=np.int32)
24
  return feats_pad, speech_lengths_pad
25
 
26
 
27
  def slice_padding_audio_samples(speech, speech_lengths, vad_segments):
28
  speech_list = []
29
  speech_lengths_list = []
 
 
30
  for i, segment in enumerate(vad_segments):
31
  bed_idx = int(segment[0][0] * 16)
32
  end_idx = min(int(segment[0][1] * 16), speech_lengths)
 
62
  # new_result.append([bg + j * spl_l, bg + (j + 1) * spl_l])
63
  bg = time
64
  new_result.append([bg, time_step[-1]])
65
+ return new_result