1 line
8.9 KiB
JSON
1 line
8.9 KiB
JSON
{"repo": "15mika88-cmd/iflytek-skills", "files": {"scripts/iflytek_asr.py": {"a98bfd9d31bcdfea4249413d264c1c3e0566d96d": {"t": 1785674324.449259, "n": 8434, "h": "3da056c14599d262", "c": "#!/usr/bin/env python3\n\"\"\"\niFLYTEK IAT (Intelligent Audio Transcription) - Speech to Text\nUses the iFLYTEK Global WebSocket API to transcribe audio files.\n\nUsage:\n python iflytek_asr.py <wav_file> [language_code]\n\nCredentials are loaded from ~/.config/opencode/iflytek_auth.json\n\nAudio MUST be pre-converted to: mono, 16-bit PCM, 8000 or 16000 Hz WAV.\nUse ffmpeg before calling this script:\n ffmpeg -i input.mp3 -ar 16000 -ac 1 -acodec pcm_s16le output.wav -y\n\"\"\"\n\nimport asyncio\nimport base64\nimport hashlib\nimport hmac\nimport json\nimport os\nimport sys\nimport time\nimport wave\nfrom pathlib import Path\nfrom urllib.parse import quote, urlparse\n\ntry:\n import websockets\nexcept ImportError:\n print(\n \"ERROR: 'websockets' module not found. Install it with: pip install websockets\",\n file=sys.stderr,\n )\n sys.exit(1)\n\n# --- Constants ---\nIAT_ENDPOINT = \"wss://iat-api-sg.xf-yun.com/v2/iat\"\nAUTH_CONFIG_NAME = \"iflytek_auth.json\"\nFRAME_SIZE = 12800 # Bulk-send mode: ~800ms per frame at 16kHz\nSEND_INTERVAL = 0.005 # Minimal delay between frames to avoid buffer overflow\nDEFAULT_LANGUAGE = \"zh_cn\"\n\n\ndef find_auth_config() -> Path:\n \"\"\"\n Locate iflytek_auth.json in standard config directories.\n Search order:\n 1. ~/.config/opencode/iflytek_auth.json\n 2. $APPDATA/opencode/iflytek_auth.json (Windows fallback)\n \"\"\"\n candidates = []\n\n # XDG / Unix-like\n xdg_config = os.environ.get(\"XDG_CONFIG_HOME\", os.path.expanduser(\"~/.config\"))\n candidates.append(Path(xdg_config) / \"opencode\" / AUTH_CONFIG_NAME)\n\n # Windows APPDATA\n appdata = os.environ.get(\"APPDATA\")\n if appdata:\n candidates.append(Path(appdata) / \"opencode\" / AUTH_CONFIG_NAME)\n\n for path in candidates:\n if path.is_file():\n return path\n\n msg = (\n f\"ERROR: Credential file '{AUTH_CONFIG_NAME}' not found.\\n\"\n f\"Searched locations:\\n\"\n + \"\\n\".join(f\" - {p}\" for p in candidates)\n + \"\\n\\nCreate it with the following content:\\n\"\n '{\\n \"appid\": \"<YOUR_APPID>\",\\n'\n ' \"api_key\": \"<YOUR_API_KEY>\",\\n'\n ' \"api_secret\": \"<YOUR_API_SECRET>\"\\n}'\n )\n print(msg, file=sys.stderr)\n sys.exit(1)\n\n\ndef load_credentials() -> dict:\n \"\"\"Load and validate iFLYTEK credentials from config file.\"\"\"\n config_path = find_auth_config()\n with open(config_path, \"r\", encoding=\"utf-8\") as f:\n creds = json.load(f)\n\n required_keys = (\"appid\", \"api_key\", \"api_secret\")\n missing = [k for k in required_keys if not creds.get(k)]\n if missing:\n print(\n f\"ERROR: Missing keys in {config_path}: {', '.join(missing)}\",\n file=sys.stderr,\n )\n sys.exit(1)\n\n return creds\n\n\ndef generate_auth_url(endpoint: str, api_key: str, api_secret: str) -> str:\n \"\"\"Build the authenticated WebSocket URL using HMAC-SHA256.\"\"\"\n parsed = urlparse(endpoint)\n host = parsed.netloc\n path = parsed.path\n date = time.strftime(\"%a, %d %b %Y %H:%M:%S GMT\", time.gmtime())\n\n sign_string = f\"host: {host}\\ndate: {date}\\nGET {path} HTTP/1.1\"\n signature = hmac.new(\n api_secret.encode(\"utf-8\"),\n sign_string.encode(\"utf-8\"),\n digestmod=hashlib.sha256,\n ).digest()\n signature_b64 = base64.b64encode(signature).decode(\"utf-8\")\n\n auth_origin = (\n f'api_key=\"{api_key}\", algorithm=\"hmac-sha256\", '\n f'headers=\"host date request-line\", signature=\"{signature_b64}\"'\n )\n authorization = base64.b64encode(auth_origin.encode(\"utf-8\")).decode(\"utf-8\")\n\n params = (\n f\"host={quote(host)}&date={quote(date)}&authorization={quote(authorization)}\"\n )\n return f\"{endpoint}?{params}\"\n\n\ndef read_wav(file_path: str) -> tuple:\n \"\"\"Read a WAV file and return (raw_pcm_bytes, sample_rate).\"\"\"\n with wave.open(file_path, \"rb\") as wf:\n channels = wf.getnchannels()\n sample_width = wf.getsampwidth()\n framerate = wf.getframerate()\n nframes = wf.getnframes()\n audio_data = wf.readframes(nframes)\n\n if channels != 1:\n print(\n f\"WARNING: Audio has {channels} channels. Expected mono (1 channel).\",\n file=sys.stderr,\n )\n print(\n \"Please convert first: ffmpeg -i input -ar 16000 -ac 1 -acodec pcm_s16le output.wav -y\",\n file=sys.stderr,\n )\n if sample_width != 2:\n print(\n f\"WARNING: Sample width is {sample_width * 8}-bit. Expected 16-bit.\",\n file=sys.stderr,\n )\n if framerate not in (8000, 16000):\n print(\n f\"WARNING: Sample rate is {framerate}Hz. Recommended: 8000 or 16000 Hz.\",\n file=sys.stderr,\n )\n\n return audio_data, framerate\n\n\nasync def recognize(audio_file: str, language: str = DEFAULT_LANGUAGE) -> str:\n \"\"\"\n Perform speech-to-text recognition via iFLYTEK IAT WebSocket API.\n Returns the transcription text.\n \"\"\"\n creds = load_credentials()\n auth_url = generate_auth_url(IAT_ENDPOINT, creds[\"api_key\"], creds[\"api_secret\"])\n audio_data, framerate = read_wav(audio_file)\n\n audio_format = f\"audio/L16;rate={framerate}\"\n\n async with websockets.connect(auth_url) as ws:\n # First frame: business params + empty audio\n await ws.send(\n json.dumps(\n {\n \"common\": {\"app_id\": creds[\"appid\"]},\n \"business\": {\n \"language\": language,\n \"domain\": \"iat\",\n \"ptt\": 1,\n },\n \"data\": {\n \"status\": 0,\n \"format\": audio_format,\n \"encoding\": \"raw\",\n \"audio\": \"\",\n },\n }\n )\n )\n\n result_text = \"\"\n\n async def sender():\n \"\"\"Send audio data in bulk frames.\"\"\"\n for i in range(0, len(audio_data), FRAME_SIZE):\n chunk = audio_data[i : i + FRAME_SIZE]\n status = 2 if i + FRAME_SIZE >= len(audio_data) else 1\n await ws.send(\n json.dumps(\n {\n \"data\": {\n \"status\": status,\n \"format\": audio_format,\n \"encoding\": \"raw\",\n \"audio\": base64.b64encode(chunk).decode(\"utf-8\"),\n }\n }\n )\n )\n await asyncio.sleep(SEND_INTERVAL)\n\n async def receiver():\n \"\"\"Receive and assemble transcription results.\"\"\"\n nonlocal result_text\n try:\n async for msg in ws:\n res = json.loads(msg)\n code = res.get(\"code\")\n if code != 0:\n print(\n f\"Server error: code={code}, message={res.get('message')}\",\n file=sys.stderr,\n )\n break\n\n data = res.get(\"data\", {})\n if \"result\" in data and data[\"result\"]:\n for w_item in data[\"result\"].get(\"ws\", []):\n for cw in w_item.get(\"cw\", []):\n result_text += cw.get(\"w\", \"\")\n\n if data.get(\"status\") == 2:\n break\n except websockets.exceptions.ConnectionClosed:\n pass # Server closed after final result\n\n await asyncio.gather(sender(), receiver())\n\n return result_text\n\n\ndef main():\n if len(sys.argv) < 2:\n print(\n \"Usage: python iflytek_asr.py <wav_file> [language_code]\", file=sys.stderr\n )\n print(f\"Default language: {DEFAULT_LANGUAGE}\", file=sys.stderr)\n print(\n \"Supported: zh_cn, en_us, ms_my, ja_jp, ko_kr, fr_fr, es_es, ...\",\n file=sys.stderr,\n )\n sys.exit(1)\n\n audio_path = sys.argv[1]\n language = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_LANGUAGE\n\n if not os.path.isfile(audio_path):\n print(f\"ERROR: Audio file not found: {audio_path}\", file=sys.stderr)\n sys.exit(1)\n\n text = asyncio.run(recognize(audio_path, language))\n print(text)\n\n\nif __name__ == \"__main__\":\n main()\n"}}}} |