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 +1 -1
- voice_input/server.py +66 -29
- voice_input/templates/index.html +26 -12
- {voice_input-1.0.0.dist-info → voice_input-1.0.1.dist-info}/METADATA +2 -4
- voice_input-1.0.1.dist-info/RECORD +12 -0
- voice_input-1.0.0.dist-info/RECORD +0 -12
- {voice_input-1.0.0.dist-info → voice_input-1.0.1.dist-info}/WHEEL +0 -0
- {voice_input-1.0.0.dist-info → voice_input-1.0.1.dist-info}/entry_points.txt +0 -0
- {voice_input-1.0.0.dist-info → voice_input-1.0.1.dist-info}/top_level.txt +0 -0
voice_input/__init__.py
CHANGED
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": "
|
|
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.
|
|
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 (
|
voice_input/templates/index.html
CHANGED
|
@@ -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 = {
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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.
|
|
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
|
-
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|