voice-input 1.0.0__py3-none-any.whl → 1.0.1__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.
voice_input/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  """跨设备语音输入传输系统 - 将手机端语音识别文本传送到电脑"""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.0.1"
4
4
  __author__ = "mofanx"
voice_input/server.py CHANGED
@@ -85,6 +85,54 @@ def create_app(config: AppConfig) -> Flask:
85
85
  app.voice_history_lock = threading.Lock()
86
86
  app.voice_history_counter = 0
87
87
 
88
+ # 预热 keyboard / pyclip,强制完成底层设备初始化
89
+ # keyboard 在 Linux 上首次按键时才创建 /dev/uinput 虚拟设备,
90
+ # 内核注册该设备需要数百毫秒,期间发送的按键事件会丢失。
91
+ # 因此在启动时做一次空按键,等待设备就绪后再提供服务。
92
+ try:
93
+ import pyclip
94
+ pyclip.paste() # 预热剪贴板后端
95
+ logging.info("pyclip 模块已预热")
96
+ except Exception:
97
+ pass
98
+ try:
99
+ import keyboard
100
+ if platform.system() != "Windows":
101
+ keyboard.press_and_release("shift")
102
+ time.sleep(0.5) # 等待内核注册 uinput 虚拟设备
103
+ logging.info("keyboard 模块已预热(uinput 设备已就绪)")
104
+ except Exception:
105
+ pass
106
+
107
+ # ==================== 认证辅助 ====================
108
+
109
+ def _check_auth():
110
+ """对敏感路由执行 IP 白名单 + Token 校验,返回错误响应或 None"""
111
+ cfg = app.voice_config
112
+ client_ip = get_client_ip(request)
113
+ if not is_ip_allowed(client_ip, cfg.allowed_ips):
114
+ logging.warning(f"IP未授权访问: {client_ip}")
115
+ return (
116
+ jsonify({"code": 403, "message": "IP not allowed",
117
+ "error_detail": "Your IP address is not in the whitelist"}),
118
+ 403,
119
+ )
120
+ # 对于非 POST 请求(GET/DELETE),data 为 None,token 仅从 header/query 取
121
+ data = None
122
+ if request.is_json:
123
+ try:
124
+ data = request.get_json(silent=True)
125
+ except Exception:
126
+ pass
127
+ if not is_token_valid(request, data, cfg.token, cfg.require_token):
128
+ logging.warning(f"Token校验失败: {client_ip}")
129
+ return (
130
+ jsonify({"code": 401, "message": "Unauthorized",
131
+ "error_detail": "Invalid or missing token"}),
132
+ 401,
133
+ )
134
+ return None
135
+
88
136
  # ==================== 路由 ====================
89
137
 
90
138
  @app.route("/", methods=["GET"])
@@ -108,7 +156,7 @@ def create_app(config: AppConfig) -> Flask:
108
156
  {
109
157
  "code": 200,
110
158
  "message": "service running",
111
- "version": "2.0.0",
159
+ "version": "1.0.1",
112
160
  "server_ip": local_ip,
113
161
  "port": cfg.port,
114
162
  "platform": platform.system(),
@@ -121,6 +169,9 @@ def create_app(config: AppConfig) -> Flask:
121
169
 
122
170
  @app.route("/history", methods=["GET"])
123
171
  def get_history():
172
+ auth_err = _check_auth()
173
+ if auth_err:
174
+ return auth_err
124
175
  with app.voice_history_lock:
125
176
  items = list(app.voice_history)
126
177
  return jsonify(
@@ -134,6 +185,9 @@ def create_app(config: AppConfig) -> Flask:
134
185
 
135
186
  @app.route("/history/<int:item_id>", methods=["DELETE"])
136
187
  def delete_history_item(item_id):
188
+ auth_err = _check_auth()
189
+ if auth_err:
190
+ return auth_err
137
191
  with app.voice_history_lock:
138
192
  before = len(app.voice_history)
139
193
  app.voice_history = deque(
@@ -145,6 +199,9 @@ def create_app(config: AppConfig) -> Flask:
145
199
 
146
200
  @app.route("/history", methods=["DELETE"])
147
201
  def clear_history():
202
+ auth_err = _check_auth()
203
+ if auth_err:
204
+ return auth_err
148
205
  with app.voice_history_lock:
149
206
  count = len(app.voice_history)
150
207
  app.voice_history.clear()
@@ -152,6 +209,9 @@ def create_app(config: AppConfig) -> Flask:
152
209
 
153
210
  @app.route("/history/export", methods=["GET"])
154
211
  def export_history():
212
+ auth_err = _check_auth()
213
+ if auth_err:
214
+ return auth_err
155
215
  fmt = request.args.get("format", "json")
156
216
  with app.voice_history_lock:
157
217
  items = list(app.voice_history)
@@ -192,20 +252,11 @@ def create_app(config: AppConfig) -> Flask:
192
252
  def handle_input():
193
253
  cfg = app.voice_config
194
254
 
195
- # 1. IP 白名单验证
255
+ # 1. IP + Token 认证
256
+ auth_err = _check_auth()
257
+ if auth_err:
258
+ return auth_err
196
259
  client_ip = get_client_ip(request)
197
- if not is_ip_allowed(client_ip, cfg.allowed_ips):
198
- logging.warning(f"IP未授权访问: {client_ip}")
199
- return (
200
- jsonify(
201
- {
202
- "code": 403,
203
- "message": "IP not allowed",
204
- "error_detail": "Your IP address is not in the whitelist",
205
- }
206
- ),
207
- 403,
208
- )
209
260
 
210
261
  # 2. JSON 解析
211
262
  try:
@@ -223,21 +274,7 @@ def create_app(config: AppConfig) -> Flask:
223
274
  400,
224
275
  )
225
276
 
226
- # 3. Token 校验
227
- if not is_token_valid(request, data, cfg.token, cfg.require_token):
228
- logging.warning(f"Token校验失败: {client_ip}")
229
- return (
230
- jsonify(
231
- {
232
- "code": 401,
233
- "message": "Unauthorized",
234
- "error_detail": "Invalid or missing token",
235
- }
236
- ),
237
- 401,
238
- )
239
-
240
- # 4. 必需字段
277
+ # 3. 必需字段
241
278
  if not data or "text" not in data:
242
279
  logging.error("缺少必需字段 'text'")
243
280
  return (
@@ -450,8 +450,7 @@
450
450
  const action = mode ? mode.value : 'paste';
451
451
  const payload = { text, timestamp: Date.now(), device_id: 'phone_web', action,
452
452
  restore_clipboard: (action !== 'copy' && restoreClipEl.checked) };
453
- const headers = { 'Content-Type': 'application/json' };
454
- if (tokenEl && tokenEl.value.trim()) headers['X-Auth-Token'] = tokenEl.value.trim();
453
+ const headers = Object.assign({'Content-Type': 'application/json'}, authHeaders());
455
454
 
456
455
  sending = true;
457
456
  $('send').disabled = true;
@@ -477,12 +476,19 @@
477
476
  }
478
477
  $('send').addEventListener('click', doSend);
479
478
 
479
+ // ===== Auth helper =====
480
+ function authHeaders() {
481
+ const h = {};
482
+ if (tokenEl && tokenEl.value.trim()) h['X-Auth-Token'] = tokenEl.value.trim();
483
+ return h;
484
+ }
485
+
480
486
  // ===== History =====
481
487
  let allItems = [];
482
488
 
483
489
  async function loadHistory() {
484
490
  try {
485
- const res = await fetch('/history');
491
+ const res = await fetch('/history', { headers: authHeaders() });
486
492
  const j = await res.json();
487
493
  allItems = (j.items || []);
488
494
  renderHistory();
@@ -537,7 +543,7 @@
537
543
  if (!btn) return;
538
544
  const id = btn.getAttribute('data-id');
539
545
  try {
540
- await fetch('/history/' + id, { method: 'DELETE' });
546
+ await fetch('/history/' + id, { method: 'DELETE', headers: authHeaders() });
541
547
  allItems = allItems.filter(i => String(i.id) !== String(id));
542
548
  renderHistory();
543
549
  } catch (ex) { toast('删除失败', false); }
@@ -547,20 +553,28 @@
547
553
  $('histClear').addEventListener('click', async () => {
548
554
  if (!confirm('确定清空全部历史记录?')) return;
549
555
  try {
550
- await fetch('/history', { method: 'DELETE' });
556
+ await fetch('/history', { method: 'DELETE', headers: authHeaders() });
551
557
  allItems = [];
552
558
  renderHistory();
553
559
  toast('已清空', true);
554
560
  } catch (ex) { toast('清空失败', false); }
555
561
  });
556
562
 
557
- // Export
558
- $('histExportJson').addEventListener('click', () => {
559
- window.open('/history/export?format=json', '_blank');
560
- });
561
- $('histExportCsv').addEventListener('click', () => {
562
- window.open('/history/export?format=csv', '_blank');
563
- });
563
+ // Export (use fetch with auth, then trigger download)
564
+ async function doExport(fmt) {
565
+ try {
566
+ const res = await fetch('/history/export?format=' + fmt, { headers: authHeaders() });
567
+ if (!res.ok) { toast('导出失败: ' + res.status, false); return; }
568
+ const blob = await res.blob();
569
+ const a = document.createElement('a');
570
+ a.href = URL.createObjectURL(blob);
571
+ a.download = 'voice_history.' + fmt;
572
+ a.click();
573
+ URL.revokeObjectURL(a.href);
574
+ } catch (e) { toast('导出失败', false); }
575
+ }
576
+ $('histExportJson').addEventListener('click', () => doExport('json'));
577
+ $('histExportCsv').addEventListener('click', () => doExport('csv'));
564
578
 
565
579
  // Auto-focus
566
580
  setTimeout(() => textEl.focus(), 300);
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voice-input
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: 跨设备语音输入传输系统 - 将手机端语音识别文本传送到电脑
5
5
  Author-email: mofanx <yanwuning@live.cn>
6
6
  Maintainer-email: mofanx <yanwuning@live.cn>
@@ -19,14 +19,12 @@ Requires-Python: >=3.8
19
19
  Description-Content-Type: text/markdown
20
20
  Requires-Dist: flask>=2.2
21
21
  Requires-Dist: pyclip>=0.7
22
- Provides-Extra: keyboard
23
- Requires-Dist: keyboard>=0.13; extra == "keyboard"
22
+ Requires-Dist: keyboard>=0.13
24
23
  Provides-Extra: production
25
24
  Requires-Dist: waitress>=2.1; extra == "production"
26
25
  Provides-Extra: config
27
26
  Requires-Dist: pyyaml>=6.0; extra == "config"
28
27
  Provides-Extra: all
29
- Requires-Dist: keyboard>=0.13; extra == "all"
30
28
  Requires-Dist: waitress>=2.1; extra == "all"
31
29
  Requires-Dist: pyyaml>=6.0; extra == "all"
32
30
 
@@ -0,0 +1,12 @@
1
+ voice_input/__init__.py,sha256=mQrEoy4clpVluSKc1FnLX_lPNAPt0832KLmkH7ymKR0,133
2
+ voice_input/__main__.py,sha256=InFnKcGaQVM3WHkf2a4q3dH2IcYIKW8640LQOuYcQNY,79
3
+ voice_input/cli.py,sha256=coH1MGUPC_qe8PO8AIdGDycDCh5KrQXWHm45VQ0AZyY,4904
4
+ voice_input/config.py,sha256=VpW_AyHpn1KnkuzG8ueswDWJagEQXHVgTDz0G7IF_Zs,3223
5
+ voice_input/server.py,sha256=wHKRkCJPTCT6Aro5I6FeAZE8N3CDIzTGk4i2BBp-S8Y,12857
6
+ voice_input/utils.py,sha256=UdjC2AWjAoUEVdd5WmA383HWSZvcZImyUeCtRnke6AU,1737
7
+ voice_input/templates/index.html,sha256=FJqncHE9fJgCEa8VsbVTUn9ad-iL3_fxFCbIF8t8mZg,20972
8
+ voice_input-1.0.1.dist-info/METADATA,sha256=s0cgvHbCTKO8bKCcru8ILFNztXUKnZZS7Rt6pYIoNnk,8559
9
+ voice_input-1.0.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
+ voice_input-1.0.1.dist-info/entry_points.txt,sha256=l75c0p_8NV2TGVTlI3hgH29_u8_MwB9Jj8-MG_OMdnI,53
11
+ voice_input-1.0.1.dist-info/top_level.txt,sha256=WI3OWTYoGskXcN7Ok8vZ7lyRFyV21dPfIbiCsxMIyUY,12
12
+ voice_input-1.0.1.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- voice_input/__init__.py,sha256=mfk9Ug2J6GpSS3U7Go9NDgrXhvnLZR6vPfJkS_80QUw,133
2
- voice_input/__main__.py,sha256=InFnKcGaQVM3WHkf2a4q3dH2IcYIKW8640LQOuYcQNY,79
3
- voice_input/cli.py,sha256=coH1MGUPC_qe8PO8AIdGDycDCh5KrQXWHm45VQ0AZyY,4904
4
- voice_input/config.py,sha256=VpW_AyHpn1KnkuzG8ueswDWJagEQXHVgTDz0G7IF_Zs,3223
5
- voice_input/server.py,sha256=2K2F-xR889sxN28VEDgNu5DAOz--A4oRDgOOhFcn12k,11364
6
- voice_input/utils.py,sha256=UdjC2AWjAoUEVdd5WmA383HWSZvcZImyUeCtRnke6AU,1737
7
- voice_input/templates/index.html,sha256=qzt5zKLH3u-QMpcfntQb_eJkJWo0r7dPX_uURAg2fRU,20346
8
- voice_input-1.0.0.dist-info/METADATA,sha256=nstZ6cBN4X3DcSjhTN5jGyFsZuWtLv-fXkOGYAPW1eg,8651
9
- voice_input-1.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
- voice_input-1.0.0.dist-info/entry_points.txt,sha256=l75c0p_8NV2TGVTlI3hgH29_u8_MwB9Jj8-MG_OMdnI,53
11
- voice_input-1.0.0.dist-info/top_level.txt,sha256=WI3OWTYoGskXcN7Ok8vZ7lyRFyV21dPfIbiCsxMIyUY,12
12
- voice_input-1.0.0.dist-info/RECORD,,