6ef9e082a6
- 前端: VAD语音检测(@ricky0123/vad-web) + useVoiceInput双模式(流式WS/REST) - Gateway: VoiceStreamManager代理WS流式STT到voice-service - Voice-service: DashScope REST → Realtime WS → Whisper三级引擎 + ffmpeg转码 - 共享模块: pkg/audio(音频转换) + pkg/dashscope(ASR REST客户端) - 清理: 移除旧plugin-manager和pkg/plugins,完成插件→工具合并 - 文档: 完善gateway-api.md和voice-service.md语音API文档 - 工具: scripts/voice/ 语音转换脚本集 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
从 Honkai: Star Rail 的 .pck (AKPK/Wwise SoundBank) 文件中提取 .wem 音频。
|
|
|
|
用法:
|
|
python extract_pck.py <input.pck> <output_dir>
|
|
python extract_pck.py VoBanks31.pck ./output/
|
|
python extract_pck.py --all <pck_dir> <output_dir>
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import struct
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def find_wem_files(data: bytes) -> list[tuple[int, int, str]]:
|
|
"""
|
|
扫描数据中的所有 RIFF/WAVE 块 (.wem 文件).
|
|
返回 [(offset, size, riff_type), ...] 列表.
|
|
"""
|
|
results = []
|
|
pos = 0
|
|
data_len = len(data)
|
|
while pos < data_len - 12:
|
|
if data[pos:pos + 4] == b'RIFF':
|
|
chunk_size = struct.unpack_from('<I', data, pos + 4)[0]
|
|
riff_type = data[pos + 8:pos + 12]
|
|
# .wem 文件的 riff_type 是 b'WAVE'
|
|
if riff_type == b'WAVE' and chunk_size > 100:
|
|
total_size = chunk_size + 8 # RIFF header + data
|
|
if pos + total_size <= data_len:
|
|
results.append((pos, total_size, riff_type.decode('ascii', errors='replace')))
|
|
# 跳过已匹配的块
|
|
pos += 8 + chunk_size
|
|
continue
|
|
pos += 1
|
|
return results
|
|
|
|
|
|
def extract_pck(pck_path: str, output_dir: str, prefix: str = "") -> list[str]:
|
|
"""
|
|
从单个 .pck 文件提取所有 .wem 文件到 output_dir.
|
|
返回提取的文件路径列表.
|
|
"""
|
|
pck_name = Path(pck_path).stem
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
with open(pck_path, 'rb') as f:
|
|
data = f.read()
|
|
|
|
print(f"[{pck_name}] 文件大小: {len(data):,} bytes, 扫描 WEM 块...")
|
|
wem_entries = find_wem_files(data)
|
|
print(f"[{pck_name}] 找到 {len(wem_entries)} 个音频文件")
|
|
|
|
extracted = []
|
|
for i, (offset, size, riff_type) in enumerate(wem_entries):
|
|
# 使用 offset 作为唯一 ID (Wwise 文件 ID 就是 offset 的某种映射)
|
|
wem_data = data[offset:offset + size]
|
|
|
|
if prefix:
|
|
filename = f"{prefix}_{i:04d}_{offset:08x}.wem"
|
|
else:
|
|
filename = f"{pck_name}_{i:04d}_{offset:08x}.wem"
|
|
|
|
out_path = os.path.join(output_dir, filename)
|
|
with open(out_path, 'wb') as f:
|
|
f.write(wem_data)
|
|
extracted.append(out_path)
|
|
|
|
total_mb = sum(wem_entries[i][1] for i in range(len(wem_entries))) / 1024 / 1024
|
|
print(f"[{pck_name}] 提取完成: {len(extracted)} 文件, {total_mb:.1f} MB")
|
|
return extracted
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="从 HSR .pck 文件提取 .wem 音频")
|
|
parser.add_argument('input', help='输入 .pck 文件路径,或 --all 模式下的目录路径')
|
|
parser.add_argument('output', help='输出目录')
|
|
parser.add_argument('--all', action='store_true', help='批量模式:提取目录中所有 .pck 文件')
|
|
parser.add_argument('--prefix', default='', help='输出文件名前缀')
|
|
args = parser.parse_args()
|
|
|
|
if args.all:
|
|
pck_dir = Path(args.input)
|
|
pck_files = sorted(pck_dir.glob('*.pck'))
|
|
print(f"批量模式: 找到 {len(pck_files)} 个 .pck 文件")
|
|
total_extracted = 0
|
|
for pck_file in pck_files:
|
|
extracted = extract_pck(str(pck_file), args.output, args.prefix)
|
|
total_extracted += len(extracted)
|
|
print(f"\n总计: {total_extracted} 个音频文件")
|
|
else:
|
|
extract_pck(args.input, args.output, args.prefix)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|