# -*- coding: utf-8 -*- """ 讯飞 录音文件转写(标准版)- 本地运行脚本 ================================================= 用途:将节目音频转为带时间戳的文本,用于TPS中台三方融合demo。 接口:https://raasr.xfyun.cn/v2/api/upload / getResult 签名:signa = base64(HmacSHA1(MD5(appid + ts), secretKey)) 特性: - 支持热词列表(hotWord),提升专业术语识别率 - 支持军事领域参数(pd=mil) - 支持顺滑+口语规整(输出更接近书面语) - 默认语种 cn(中文普通话),免费包标配,无需 autodialect 依赖:pip install requests 输出文件(与脚本同目录): asr_result_raw.json - 讯飞原始返回 asr_result_full.txt - 纯文本 asr_result_timed.txt - 带时间戳的文本(格式同B稿) """ import base64 import hashlib import hmac import json import os import re import time import wave from urllib.parse import quote import requests # ======================================================================== # 配置区 —— 填写凭证 + 音频路径 # ======================================================================== # 老版只用 APPID + SecretKey 两个字段 APP_ID = "84eff996" SECRET_KEY = "在此填入您的SecretKey" # 音频文件——可以用之前转好的WAV,也可以直接用mp3(讯飞标准版mp3也支持) AUDIO_FILE = "现代防空反导大对决音频_16k_mono.wav" # 自动去除空白 APP_ID = APP_ID.strip() SECRET_KEY = SECRET_KEY.strip() # ======================================================================== # 接口配置 # ======================================================================== HOST = "https://raasr.xfyun.cn/v2/api" UPLOAD_URL = HOST + "/upload" RESULT_URL = HOST + "/getResult" # 业务参数 LANGUAGE = "cn" # 中文普通话 PD = "mil" # 军事领域优化 ENG_SMOOTHPROC = "true" # 顺滑(去掉"嗯/那个") ENG_COLLOQPROC = "true" # 口语规整 # 热词列表——节目里出现的关键专业术语(讯飞限制200个热词,每个2-16字) # 我从A稿提取了下面这批,能显著提升识别准确率 HOT_WORDS = [ # 武器装备 "萨德", "爱国者", "标准-6", "铁穹", "塔米尔", "大卫投石索", "箭式", "箭-2", "箭-3", "霍拉姆沙赫尔-4", "见证者-136", "卡桑", "泥石", "C-17", "飞毛腿", "M270", # 技术概念 "高超音速", "弹道导弹", "巡航导弹", "无人机蜂群", "中段反导", "饱和攻击", "动能撞击", "诱饵弹", "相控阵雷达", "电子战", "外大气层", # 事件军方 "真实承诺-4", "伊斯兰革命卫队", "本·古里安", "拉斐尔", "黎巴嫩真主党", "波斯湾", # 节目栏目 "军事科技", ] # 轮询配置 POLL_INTERVAL_SECONDS = 30 MAX_WAIT_MINUTES = 30 # ======================================================================== # 签名+工具 # ======================================================================== def make_signa(app_id, secret_key, ts): """ 讯飞老版签名:signa = base64(HmacSHA1(MD5(appid + ts), secretKey)) """ base_string = (app_id + ts).encode("utf-8") md5_str = hashlib.md5(base_string).hexdigest() # 32位小写hex mac = hmac.new( secret_key.encode("utf-8"), md5_str.encode("utf-8"), digestmod=hashlib.sha1, ) signa = base64.b64encode(mac.digest()).decode("utf-8") return signa def get_audio_duration_ms(filepath): """获取音频时长(毫秒)。WAV用内置,MP3用mutagen。""" ext = os.path.splitext(filepath)[1].lower() if ext == ".wav": with wave.open(filepath, "rb") as wf: n_frames = wf.getnframes() sample_rate = wf.getframerate() duration_ms = int(round(n_frames / sample_rate * 1000)) print(f"[info] WAV规格: {sample_rate}Hz, 声道={wf.getnchannels()}, 位深={wf.getsampwidth()*8}bit") return duration_ms if ext == ".mp3": try: from mutagen.mp3 import MP3 return int(MP3(filepath).info.length * 1000) except ImportError: print("[warn] mutagen未安装,时长按0传——讯飞文档说该字段不严格校验") return 0 raise ValueError(f"不支持的音频格式: {ext}") # ======================================================================== # 上传 # ======================================================================== def upload_audio(filepath): """上传音频,返回 orderId""" if not os.path.exists(filepath): raise FileNotFoundError(f"音频文件不存在: {filepath}") file_size = os.path.getsize(filepath) file_name = os.path.basename(filepath) duration_ms = get_audio_duration_ms(filepath) ts = str(int(time.time())) signa = make_signa(APP_ID, SECRET_KEY, ts) print(f"[info] ts={ts}, signa={signa}") print(f"[info] 文件: {file_name}, {file_size/1024/1024:.2f}MB, {duration_ms/1000:.1f}秒") # 构建URL参数 params = { "appId": APP_ID, "signa": signa, "ts": ts, "fileSize": str(file_size), "fileName": file_name, "duration": str(duration_ms), "language": LANGUAGE, "pd": PD, "eng_smoothproc": ENG_SMOOTHPROC, "eng_colloqproc": ENG_COLLOQPROC, } # 热词,用 | 分隔 if HOT_WORDS: hot_word_str = "|".join(HOT_WORDS) params["hotWord"] = hot_word_str print(f"[info] 热词数量: {len(HOT_WORDS)} 个") url_parts = [f"{quote(k, safe='')}={quote(str(v), safe='')}" for k, v in params.items()] url = f"{UPLOAD_URL}?{'&'.join(url_parts)}" headers = { "Content-Type": "application/json", } with open(filepath, "rb") as f: audio_bytes = f.read() print(f"[info] 正在上传... ({file_size/1024/1024:.1f}MB)") resp = requests.post(url, headers=headers, data=audio_bytes, timeout=300) print(f"[info] HTTP状态: {resp.status_code}") data = resp.json() print(f"[info] 响应: {json.dumps(data, ensure_ascii=False, indent=2)}") if data.get("code") != "000000": raise RuntimeError(f"上传失败: code={data.get('code')}, desc={data.get('descInfo')}") order_id = data["content"]["orderId"] estimate_ms = data["content"].get("taskEstimateTime", 0) print(f"\n[ok] orderId = {order_id}") print(f"[ok] 预估完成时间: {estimate_ms/1000:.1f}秒\n") return order_id # ======================================================================== # 查询结果 # ======================================================================== def query_result(order_id): """单次查询""" ts = str(int(time.time())) signa = make_signa(APP_ID, SECRET_KEY, ts) params = { "appId": APP_ID, "signa": signa, "ts": ts, "orderId": order_id, "resultType": "transfer", } url_parts = [f"{quote(k, safe='')}={quote(str(v), safe='')}" for k, v in params.items()] url = f"{RESULT_URL}?{'&'.join(url_parts)}" resp = requests.post(url, timeout=30) return resp.json() def poll_until_done(order_id): """轮询直到完成""" print(f"[info] 开始轮询(每 {POLL_INTERVAL_SECONDS} 秒一次)...") start_time = time.time() while True: elapsed = time.time() - start_time if elapsed > MAX_WAIT_MINUTES * 60: raise TimeoutError(f"超过 {MAX_WAIT_MINUTES} 分钟未完成") data = query_result(order_id) order_info = data.get("content", {}).get("orderInfo", {}) status = order_info.get("status") fail_type = order_info.get("failType", 0) elapsed_min = elapsed / 60 print(f"[poll] 已等 {elapsed_min:.1f} 分钟, status={status}, failType={fail_type}") if status == 4: print(f"\n[ok] 转写完成!耗时 {elapsed_min:.1f} 分钟") return data if status == -1: raise RuntimeError(f"转写失败: failType={fail_type}, 数据: {data}") time.sleep(POLL_INTERVAL_SECONDS) # ======================================================================== # 结果解析 # ======================================================================== def parse_order_result(order_result_str): """ 解析嵌套JSON,返回 [(sentence_start_ms, sentence_end_ms, text), ...] """ if not order_result_str: return [] cleaned = re.sub(r"\\\\", r"\\", order_result_str) outer = json.loads(cleaned) sentences = [] for item in outer.get("lattice", []): inner_str = item.get("json_1best", "") if not inner_str: continue inner = json.loads(inner_str) st = inner.get("st", {}) bg = int(st.get("bg", 0)) ed = int(st.get("ed", 0)) words = [] for rt in st.get("rt", []): for ws in rt.get("ws", []): for cw in ws.get("cw", []): w = cw.get("w", "").strip() wp = cw.get("wp", "n") if w and wp != "g": words.append(w) sentence = "".join(words).strip() if sentence: sentences.append((bg, ed, sentence)) return sentences def format_timestamp(ms): total_sec = ms // 1000 return f"{total_sec // 60}m{total_sec % 60}s" def write_outputs(sentences, raw_data): with open("asr_result_raw.json", "w", encoding="utf-8") as f: json.dump(raw_data, f, ensure_ascii=False, indent=2) print(f"[ok] 已写 asr_result_raw.json") timed_lines = [f"[{format_timestamp(bg)}] {text}" for bg, _, text in sentences] with open("asr_result_timed.txt", "w", encoding="utf-8") as f: f.write("\n".join(timed_lines)) print(f"[ok] 已写 asr_result_timed.txt ({len(timed_lines)} 句)") full_text = "".join(t for _, _, t in sentences) with open("asr_result_full.txt", "w", encoding="utf-8") as f: f.write(full_text) print(f"[ok] 已写 asr_result_full.txt ({len(full_text)} 字)") # ======================================================================== # 入口 # ======================================================================== def main(): print("=" * 60) print("讯飞 录音文件转写(标准版) - 本地脚本") print("=" * 60) if SECRET_KEY.startswith("在此填入"): print("[error] 请先填写 SECRET_KEY") return print(f"[diag] APP_ID = {APP_ID} (长度 {len(APP_ID)})") print(f"[diag] SECRET_KEY 长度 {len(SECRET_KEY)}, 首4位={SECRET_KEY[:4]}, 末4位={SECRET_KEY[-4:]}") print(f"[diag] AUDIO_FILE = {AUDIO_FILE}") print() # 1. 上传 order_id = upload_audio(AUDIO_FILE) # 2. 轮询 final_data = poll_until_done(order_id) # 3. 解析+写文件 order_result_str = final_data["content"]["orderResult"] sentences = parse_order_result(order_result_str) write_outputs(sentences, final_data) print("\n" + "=" * 60) print(f"完成!共识别 {len(sentences)} 句") print(f"请把 asr_result_timed.txt 发给 Claude 进行三方融合") print("=" * 60) if __name__ == "__main__": main()