@ww_nero/media 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +22 -6
- package/package.json +1 -1
- package/scripts/asr_srt.py +57 -21
package/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const {
|
|
|
11
11
|
} = require('@modelcontextprotocol/sdk/types.js');
|
|
12
12
|
|
|
13
13
|
const ASR_API_KEY = process.env.ASR_API_KEY || '';
|
|
14
|
-
const ASR_UPLOAD_URL =
|
|
14
|
+
const ASR_UPLOAD_URL = 'http://fsheep.com:10808/upload';
|
|
15
15
|
|
|
16
16
|
const SUPPORTED_AUDIO_TYPES = ['.mp3', '.wav'];
|
|
17
17
|
const MAX_AUDIO_DURATION_SECONDS = 60;
|
|
@@ -279,7 +279,26 @@ const runAsrScript = (audioPath, outputPath, uploadUrl, apiKey) => {
|
|
|
279
279
|
if (code === 0) {
|
|
280
280
|
resolve(stdout.trim());
|
|
281
281
|
} else {
|
|
282
|
-
|
|
282
|
+
// 尝试解析结构化的 ASR 错误信息
|
|
283
|
+
const stderrContent = stderr.trim();
|
|
284
|
+
const asrErrorMatch = stderrContent.match(/ASR_ERROR:\s*(\{.*\})/);
|
|
285
|
+
if (asrErrorMatch) {
|
|
286
|
+
try {
|
|
287
|
+
const errorInfo = JSON.parse(asrErrorMatch[1]);
|
|
288
|
+
const parts = [errorInfo.error || '识别失败'];
|
|
289
|
+
if (errorInfo.audio_url) {
|
|
290
|
+
parts.push(`静态资源地址: ${errorInfo.audio_url}`);
|
|
291
|
+
}
|
|
292
|
+
if (errorInfo.details) {
|
|
293
|
+
parts.push(`服务器错误信息: ${errorInfo.details}`);
|
|
294
|
+
}
|
|
295
|
+
reject(new Error(parts.join('\n')));
|
|
296
|
+
return;
|
|
297
|
+
} catch {
|
|
298
|
+
// JSON 解析失败,使用原始错误信息
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const errorMsg = stderrContent || stdout.trim() || `进程退出码: ${code}`;
|
|
283
302
|
reject(new Error(errorMsg));
|
|
284
303
|
}
|
|
285
304
|
});
|
|
@@ -299,9 +318,6 @@ const asr = async ({ working_directory, audio_file }) => {
|
|
|
299
318
|
if (!ASR_API_KEY) {
|
|
300
319
|
throw new Error('请配置 ASR_API_KEY 环境变量');
|
|
301
320
|
}
|
|
302
|
-
if (!ASR_UPLOAD_URL) {
|
|
303
|
-
throw new Error('请配置 ASR_UPLOAD_URL 环境变量(完整的上传接口 URL,如 http://server.domain.com/upload)');
|
|
304
|
-
}
|
|
305
321
|
|
|
306
322
|
const workingDir = resolveWorkingDirectory(working_directory);
|
|
307
323
|
const audioPath = resolveAudioFile(workingDir, audio_file);
|
|
@@ -327,7 +343,7 @@ const asr = async ({ working_directory, audio_file }) => {
|
|
|
327
343
|
const server = new Server(
|
|
328
344
|
{
|
|
329
345
|
name: 'media',
|
|
330
|
-
version: '1.0.
|
|
346
|
+
version: '1.0.3',
|
|
331
347
|
},
|
|
332
348
|
{
|
|
333
349
|
capabilities: {
|
package/package.json
CHANGED
package/scripts/asr_srt.py
CHANGED
|
@@ -88,22 +88,16 @@ def upload_audio(upload_url: str, audio_path: str) -> str:
|
|
|
88
88
|
files = {'file': (audio_path.name, f)}
|
|
89
89
|
response = requests.post(upload_url, files=files, timeout=60)
|
|
90
90
|
|
|
91
|
-
if response.status_code == 409:
|
|
92
|
-
# 文件已存在,从响应中获取文件名或根据原文件名推断
|
|
93
|
-
data = response.json()
|
|
94
|
-
if 'fileName' in data:
|
|
95
|
-
return data['fileName']
|
|
96
|
-
# 如果没有返回文件名,使用原始文件名的扩展名
|
|
97
|
-
raise Exception(f"文件已存在: {data.get('message', '未知错误')}")
|
|
98
|
-
|
|
99
91
|
if response.status_code != 200:
|
|
100
92
|
raise Exception(f"上传失败: {response.status_code} - {response.text}")
|
|
101
93
|
|
|
102
94
|
data = response.json()
|
|
103
|
-
|
|
95
|
+
|
|
96
|
+
# 响应格式: {'success': True, 'data': {'path': '/tmp/xxx.wav'}}
|
|
97
|
+
if not data.get('success') or 'data' not in data or 'path' not in data['data']:
|
|
104
98
|
raise Exception(f"上传响应格式错误: {data}")
|
|
105
99
|
|
|
106
|
-
return data['
|
|
100
|
+
return Path(data['data']['path']).name
|
|
107
101
|
|
|
108
102
|
|
|
109
103
|
def get_static_url(upload_url: str, filename: str) -> str:
|
|
@@ -125,6 +119,14 @@ def get_static_url(upload_url: str, filename: str) -> str:
|
|
|
125
119
|
return f"{base_url.rstrip('/')}/{filename}"
|
|
126
120
|
|
|
127
121
|
|
|
122
|
+
class AsrError(Exception):
|
|
123
|
+
"""ASR 识别错误,包含静态资源 URL 和错误详情"""
|
|
124
|
+
def __init__(self, message: str, audio_url: str = None, details: str = None):
|
|
125
|
+
self.audio_url = audio_url
|
|
126
|
+
self.details = details
|
|
127
|
+
super().__init__(message)
|
|
128
|
+
|
|
129
|
+
|
|
128
130
|
def transcribe_audio(audio_url: str, api_key: str) -> list:
|
|
129
131
|
"""
|
|
130
132
|
调用阿里云 ASR 接口进行语音识别
|
|
@@ -139,14 +141,17 @@ def transcribe_audio(audio_url: str, api_key: str) -> list:
|
|
|
139
141
|
dashscope.api_key = api_key
|
|
140
142
|
|
|
141
143
|
# 发起异步识别请求
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
try:
|
|
145
|
+
transcribe_response = Transcription.async_call(
|
|
146
|
+
model='paraformer-v2',
|
|
147
|
+
file_urls=[audio_url],
|
|
148
|
+
language_hints=['zh', 'en', 'ja']
|
|
149
|
+
)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
raise AsrError(f"ASR 请求失败: {e}", audio_url=audio_url, details=str(e))
|
|
147
152
|
|
|
148
153
|
if not transcribe_response or not hasattr(transcribe_response, 'output'):
|
|
149
|
-
raise
|
|
154
|
+
raise AsrError("ASR 请求失败: 无效的响应", audio_url=audio_url)
|
|
150
155
|
|
|
151
156
|
# 轮询等待识别完成
|
|
152
157
|
while True:
|
|
@@ -157,10 +162,26 @@ def transcribe_audio(audio_url: str, api_key: str) -> list:
|
|
|
157
162
|
transcribe_response = Transcription.fetch(task=transcribe_response.output.task_id)
|
|
158
163
|
|
|
159
164
|
if transcribe_response.status_code != HTTPStatus.OK:
|
|
160
|
-
|
|
165
|
+
error_msg = getattr(transcribe_response, 'message', str(transcribe_response.status_code))
|
|
166
|
+
raise AsrError(
|
|
167
|
+
f"ASR 识别失败: {transcribe_response.status_code}",
|
|
168
|
+
audio_url=audio_url,
|
|
169
|
+
details=error_msg
|
|
170
|
+
)
|
|
161
171
|
|
|
162
172
|
if transcribe_response.output.task_status == 'FAILED':
|
|
163
|
-
|
|
173
|
+
# 尝试获取更详细的错误信息
|
|
174
|
+
error_details = None
|
|
175
|
+
results = transcribe_response.output.get('results', [])
|
|
176
|
+
for result in results:
|
|
177
|
+
if result.get('subtask_status') == 'FAILED':
|
|
178
|
+
error_details = result.get('message', '未知错误')
|
|
179
|
+
break
|
|
180
|
+
raise AsrError(
|
|
181
|
+
"ASR 识别任务失败",
|
|
182
|
+
audio_url=audio_url,
|
|
183
|
+
details=error_details
|
|
184
|
+
)
|
|
164
185
|
|
|
165
186
|
# 获取识别结果
|
|
166
187
|
results = transcribe_response.output.get('results', [])
|
|
@@ -196,6 +217,7 @@ def main():
|
|
|
196
217
|
print("错误: 请通过 --api-key 参数或 ASR_API_KEY 环境变量提供 API Key", file=sys.stderr)
|
|
197
218
|
sys.exit(1)
|
|
198
219
|
|
|
220
|
+
audio_url = None
|
|
199
221
|
try:
|
|
200
222
|
# 1. 上传音频文件
|
|
201
223
|
print(f"正在上传音频文件: {args.audio}")
|
|
@@ -211,8 +233,7 @@ def main():
|
|
|
211
233
|
transcriptions = transcribe_audio(audio_url, api_key)
|
|
212
234
|
|
|
213
235
|
if not transcriptions:
|
|
214
|
-
|
|
215
|
-
sys.exit(1)
|
|
236
|
+
raise AsrError("未获取到识别结果", audio_url=audio_url)
|
|
216
237
|
|
|
217
238
|
# 4. 生成 SRT 文件
|
|
218
239
|
subtitle_count = asr_to_srt(transcriptions, args.output)
|
|
@@ -222,8 +243,23 @@ def main():
|
|
|
222
243
|
except FileNotFoundError as e:
|
|
223
244
|
print(f"错误: {e}", file=sys.stderr)
|
|
224
245
|
sys.exit(1)
|
|
246
|
+
except AsrError as e:
|
|
247
|
+
# 输出结构化的错误信息,包含静态资源 URL
|
|
248
|
+
error_info = {
|
|
249
|
+
'error': str(e),
|
|
250
|
+
'audio_url': e.audio_url or audio_url,
|
|
251
|
+
'details': e.details
|
|
252
|
+
}
|
|
253
|
+
print(f"ASR_ERROR: {json.dumps(error_info, ensure_ascii=False)}", file=sys.stderr)
|
|
254
|
+
sys.exit(1)
|
|
225
255
|
except Exception as e:
|
|
226
|
-
|
|
256
|
+
# 其他错误也尝试包含静态资源 URL
|
|
257
|
+
error_info = {
|
|
258
|
+
'error': str(e),
|
|
259
|
+
'audio_url': audio_url,
|
|
260
|
+
'details': None
|
|
261
|
+
}
|
|
262
|
+
print(f"ASR_ERROR: {json.dumps(error_info, ensure_ascii=False)}", file=sys.stderr)
|
|
227
263
|
sys.exit(1)
|
|
228
264
|
|
|
229
265
|
|