earshot-cli 0.1.0__py3-none-any.whl
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.
- earshot/__init__.py +7 -0
- earshot/capture.py +148 -0
- earshot/cli.py +545 -0
- earshot/compose.py +195 -0
- earshot/config.py +88 -0
- earshot/deliver.py +219 -0
- earshot/extract.py +88 -0
- earshot/live.py +76 -0
- earshot/models.py +72 -0
- earshot/subtitle.py +207 -0
- earshot/transcribe.py +220 -0
- earshot/transcribe_cloud.py +76 -0
- earshot_cli-0.1.0.dist-info/METADATA +140 -0
- earshot_cli-0.1.0.dist-info/RECORD +17 -0
- earshot_cli-0.1.0.dist-info/WHEEL +4 -0
- earshot_cli-0.1.0.dist-info/entry_points.txt +2 -0
- earshot_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
earshot/__init__.py
ADDED
earshot/capture.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""系统声环回采集(soundcard):平台差异只允许存在于此模块。
|
|
2
|
+
|
|
3
|
+
- Windows: WASAPI loopback,免驱
|
|
4
|
+
- Linux: PulseAudio ``.monitor``,免驱
|
|
5
|
+
- macOS: 无原生环回,需安装 BlackHole 并(建议用多输出设备)设为输出
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import platform
|
|
11
|
+
import warnings
|
|
12
|
+
import wave
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
# soundcard 在 macOS import 时必发一条"不支持环回"警告,属已知事实(BlackHole 方案的前提),
|
|
20
|
+
# 压掉以免吓到用户 —— 必须在 import soundcard 之前过滤
|
|
21
|
+
warnings.filterwarnings("ignore", message=".*loopback recording functionality.*", category=Warning)
|
|
22
|
+
import soundcard as sc # noqa: E402
|
|
23
|
+
|
|
24
|
+
SAMPLE_RATE = 48000 # 环回设备主流采样率;重采样交给 ffmpeg
|
|
25
|
+
BLOCK_FRAMES = 4800 # 100ms/块,兼顾电平刷新频率与 CPU
|
|
26
|
+
BLACKHOLE_HINT = "blackhole"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def list_capture_devices() -> list[tuple[str, bool]]:
|
|
30
|
+
"""列出全部输入设备。
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
``(设备名, 是否环回)`` 列表。
|
|
34
|
+
"""
|
|
35
|
+
return [(m.name, bool(m.isloopback)) for m in sc.all_microphones(include_loopback=True)]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def blackhole_missing_hint() -> str | None:
|
|
39
|
+
"""macOS 上未发现 BlackHole 设备时的安装指引,其他平台返回 None。"""
|
|
40
|
+
if platform.system() != "Darwin":
|
|
41
|
+
return None
|
|
42
|
+
names = " ".join(name for name, _ in list_capture_devices()).lower()
|
|
43
|
+
if BLACKHOLE_HINT in names:
|
|
44
|
+
return None
|
|
45
|
+
return (
|
|
46
|
+
"未检测到 BlackHole。安装: brew install blackhole-2ch;"
|
|
47
|
+
"再在「音频 MIDI 设置」建多输出设备(BlackHole+扬声器)并设为系统输出,"
|
|
48
|
+
"即可边听边录。"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pick_loopback(hint: str | None = None) -> Any:
|
|
53
|
+
"""选采集设备:默认取当前默认扬声器的同名环回/输入设备。
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
hint: 设备名子串,大小写不敏感;None 时走默认逻辑。
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
soundcard 的 Mic 对象。
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
LookupError: 找不到匹配设备。
|
|
63
|
+
"""
|
|
64
|
+
mics = sc.all_microphones(include_loopback=True)
|
|
65
|
+
if hint:
|
|
66
|
+
low = hint.lower()
|
|
67
|
+
for m in mics:
|
|
68
|
+
if low in m.name.lower():
|
|
69
|
+
return m
|
|
70
|
+
msg = f"未找到匹配「{hint}」的输入设备(earshot devices 查看)"
|
|
71
|
+
raise LookupError(msg)
|
|
72
|
+
speaker = sc.default_speaker()
|
|
73
|
+
for m in mics:
|
|
74
|
+
if m.name == speaker.name: # macOS:BlackHole 输出即同名输入;Win/Linux:同名环回
|
|
75
|
+
return m
|
|
76
|
+
msg = f"默认扬声器「{speaker.name}」没有可采集的环回设备。{blackhole_missing_hint() or ''}"
|
|
77
|
+
raise LookupError(msg)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def pick_device(hint: str | None = None) -> tuple[Any, str]:
|
|
81
|
+
"""选采集设备,带麦录兜底(零门槛优先于零音质损失)。
|
|
82
|
+
|
|
83
|
+
优先级:hint 匹配 > 默认扬声器环回 > 任意环回 > 默认麦克风(外放收音,豆包同款)。
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
hint: 设备名子串;None 走默认回退链。
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
``(mic, mode_label)``,mode_label 用于向用户说明当前采集方式。
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
LookupError: 全链条找不到可用设备。
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
return pick_loopback(hint), "环回内录(音质无损)"
|
|
96
|
+
except LookupError:
|
|
97
|
+
if hint:
|
|
98
|
+
raise # 显式指定的设备找不到,不擅自换
|
|
99
|
+
for m in sc.all_microphones(include_loopback=True):
|
|
100
|
+
if m.isloopback:
|
|
101
|
+
return m, f"环回内录(音质无损,设备:{m.name})"
|
|
102
|
+
mic = sc.default_microphone()
|
|
103
|
+
if mic is None:
|
|
104
|
+
msg = "未找到任何可用输入设备(earshot devices 查看)"
|
|
105
|
+
raise LookupError(msg)
|
|
106
|
+
fallback = f"麦克风收音(外放收音,豆包同款;配 BlackHole 可内录)设备:{mic.name}"
|
|
107
|
+
return mic, fallback
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def record_to_wav(
|
|
111
|
+
mic: Any,
|
|
112
|
+
out_wav: Path,
|
|
113
|
+
on_level: Callable[[float], None] | None = None,
|
|
114
|
+
on_audio: Callable[[np.ndarray], None] | None = None,
|
|
115
|
+
) -> float:
|
|
116
|
+
"""录制到 wav(pcm16/48k/mono),Ctrl-C 停止。
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
mic: pick_device 返回的设备。
|
|
120
|
+
out_wav: 输出路径。
|
|
121
|
+
on_level: 可选电平回调(rms,0~1 区间参考),用于终端电平条。
|
|
122
|
+
on_audio: 可选音频块回调(float32 mono),实时转写经此喂入。
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
实际录制时长(秒)。
|
|
126
|
+
"""
|
|
127
|
+
total_frames = 0
|
|
128
|
+
with wave.open(str(out_wav), "wb") as w:
|
|
129
|
+
w.setnchannels(1)
|
|
130
|
+
w.setsampwidth(2)
|
|
131
|
+
w.setframerate(SAMPLE_RATE)
|
|
132
|
+
try:
|
|
133
|
+
with mic.recorder(samplerate=SAMPLE_RATE) as rec:
|
|
134
|
+
while True:
|
|
135
|
+
data = rec.record(numframes=BLOCK_FRAMES)
|
|
136
|
+
mono = data.mean(axis=1) if data.ndim > 1 else np.asarray(data, dtype=np.float32)
|
|
137
|
+
if mono.size == 0:
|
|
138
|
+
continue
|
|
139
|
+
if on_level is not None:
|
|
140
|
+
on_level(float(np.sqrt(np.mean(np.square(mono)))))
|
|
141
|
+
if on_audio is not None:
|
|
142
|
+
on_audio(mono)
|
|
143
|
+
pcm16 = (np.clip(mono, -1.0, 1.0) * 32767.0).astype("<i2")
|
|
144
|
+
w.writeframes(pcm16.tobytes())
|
|
145
|
+
total_frames += mono.size
|
|
146
|
+
except KeyboardInterrupt:
|
|
147
|
+
pass # Ctrl-C = "点完成"
|
|
148
|
+
return total_frames / SAMPLE_RATE
|