---
name: ahserver-pitfalls
description: "POST 405 fix, auth crash, multipart hang. Voiceprint howto."
version: "1.0.0"
---
# ahserver Pitfalls & Voiceprint Integration
## POST 405 for startswiths Routes
aiohttp StaticResource tracks allowed methods in `_allowed_methods` set. `ProcessorResource.__init__` adds POST to `_routes` but not `_allowed_methods`, so POST returns 405 with `Allow: GET,HEAD`.
**Fix:** In `processorResource.py` `__init__`, after `_routes.update` lines:
```python
self._allowed_methods = set(self._routes.keys())
```
## Auth Middleware Crash on Multipart Uploads
`get_session_userinfo` calls `auth.get_auth(request)` which raises `RuntimeError('auth_middleware not installed')` when auth is disabled via `self.user = None`. This crashes `getPostData` during multipart processing.
**Fix in `auth_api.py`:**
```python
async def get_session_userinfo(request):
try:
d = await auth.get_auth(request)
except:
d = None
if d is None:
return DictObject()
```
## client_max_size Too Small → Silent Hang
`conf/config.json` `client_max_size: 10000` (10KB) causes large multipart uploads to hang. Small files work, large files (>client_max_size) never return a response.
**Fix:** Set to >= expected max file size. For audio/video: `104857600` (100MB).
## Voiceprint Service (media.opencomputing.net:10443)
- Location: `ymq@opencomputing.net:/share/ymq/run/voiceprint`
- Start: `PYTHONPATH='.:sqlor:ahserver:appPublic:longtasks' python3 ah.py -p 9087`
- GPU: cuda:1, ECAPA-TDNN model via speechbrain
- Endpoint: `POST /extract/submit` with multipart `file` field
- Response: `{"status":"SUCCEEDED","embedding":[...],"embedding_dim":192}` — no `speakers` field
## speechbrain load_audio Signature
`SpeakerRecognition.load_audio(self, path, savedir=None)` — 2nd arg is `savedir`, NOT sample rate.
- ❌ `load_audio(path, 16000)` → TypeError (int as Path)
- ✅ `load_audio(path)`
## sqlor `IN (${ids}$)` List Expansion Failure
On some sqlor versions, passing a Python list to `${ids}$` for `IN` clauses raises:
```
Illegal parameter data types varchar and row for operation '='
```
**Workaround** — build quoted comma-separated string manually:
```python
id_list = ','.join(["'" + str(x) + "'" for x in doc_ids])
# Then use in raw SQL concatenation:
"... WHERE id IN (" + id_list + ")"
```
Always wrap in `try/except` as fallback.
## DSPY `%%` LIKE Patterns — Avoid Escaped Quotes
Using `\"` inside a `%%...%%` LIKE pattern in a double-quoted Python string causes SyntaxError:
```python
# ❌ BROKEN — \" closes the Python string
"... AND metadata LIKE '%%voiceprint_status%%\"done\"%%' ..."
# ✅ Use simple patterns without quoted substrings:
"... AND metadata LIKE '%%voiceprint_status%%done%%' ..."
```
## SSH Background Process on This Server
`nohup ... &` hangs SSH. Preferred order:
1. `ssh -f user@host "cmd"` — forks background, returns immediately
2. Use `terminal(background=true)` — Hermes's own background mode
## Multipart Handler Pattern
ahserver auto-handles multipart: file saved to FileStorage, params_kw has web_path:
```python
web_path = params_kw.get('file')
fs = FileStorage()
abs_path = fs.realPath(web_path)
```
### Frontend: bricks 上传文件必须用 FormData,JSON.stringify 会静默丢 File 对象
bricks `UiFile` 只把浏览器 `File` 对象存进 `this.value`(内存),**不会自动上传**。`AgentIO`/`TextFiles` 经 `HttpResponseStream.post → bricks_fetch` 发送,当 params 不是 FormData 时走 `JSON.stringify(data)` —— **File 对象被序列化成 `{}`,文件内容静默丢失**(只有 `f.name` 字符串能传出去)。这就是"用户上传了文件但后端 agent 收不到内容"的根因。
修复(`bricks/agent.js` 的 `user_inputed`):有 `add_files` 时构造 FormData 上传文件二进制:
```javascript
var files = params.add_files || [];
var send_params = params;
if (files.length > 0) {
send_params = new FormData();
Object.keys(params).forEach(function(k){
if (k !== 'add_files' && k !== 'file_names') send_params.append(k, params[k]);
});
files.forEach(function(f){ send_params.append('file', f); });
}
var resp = await hr.post(this.opts.url, {params:send_params});
```
`bricks_fetch` 已处理 `data instanceof FormData`(自动 append session、body=FormData)。改完需重新 build `dist/bricks.js`:`bash build.sh`(把 `bricks/*.js` 按 SOURCES 列表 cat 合并到 dist,前端加载的是 dist 打包版,不是源码)。
dspy 后端取文件:`params_kw.get('file')`(单个 web_path 或 list),`FileStorage().realPath()` 拿绝对路径。docx 文本提取:zipfile 读 `word/document.xml` + `re.findall(r']*>(.*?)', xml)`(`cat` 读 docx 是乱码,必须解 zip 提取 ``)。
## DSPY Silent Error Swallowing
DSPY files often wrap logic in `except Exception: return "加载失败"`. This silently hides the real error. When debugging, always replace with:
```python
except Exception as _e:
import traceback
return {"widgettype":"Text","options":{"text":str(_e)+"\n"+traceback.format_exc()[-200:]}}}
```
Common hidden errors: sqlor placeholder mismatch, Python SyntaxError in string concatenation, missing imports.
## Tag Storage: Dual Sources (media_tags table + metadata.tags)
Tags can live in TWO places:
1. `media_tags` + `tags` tables — from face processing, tag assignment
2. `documents.metadata.tags` JSON array — from `add_tag.dspy` UI
When displaying tags on cards, read from BOTH sources:
```python
# Source 1: media_tags table
doc_tags = {}
try:
id_list = ','.join(["'" + str(x) + "'" for x in doc_ids])
mt_recs = await sor.sqlExe(
"SELECT mt.media_id, t.name, t.color FROM media_tags mt " +
"JOIN tags t ON mt.tag_id=t.id " +
"WHERE mt.media_type='document' AND mt.media_id IN (" + id_list + ")",
ns={})
for mt in mt_recs:
doc_tags.setdefault(mt.media_id, []).append({"name": mt.name, "color": mt.color})
except: pass
# Source 2: metadata.tags from add_tag.dspy
for r in rows:
try:
meta = json.loads(r.get("metadata", "{}"))
for t in meta.get("tags", []):
# deduplicate
existing = doc_tags.get(r["id"], [])
if not any(e.get("name")==t for e in existing):
existing.append({"name": t, "color": "#3b82f6"})
doc_tags[r["id"]] = existing
except: pass
```
## Media URLs in DSPY: Use entire_url()
Widgets like VideoPlayer/Image/Audio need absolute URLs. `safe_url()` only prepends `/idfile`:
```python
# ❌ Relative path — breaks in some contexts
media_url = safe_url(h.get("file_path", ""))
# ✅ Absolute URL
media_url = entire_url(safe_url(h.get("file_path", "")))
# Result: https://rag.opencomputing.cn/idfile/117/169/.../file.mp4
```
## Media Cards: Voice Query Including Videos
Voice cards should show BOTH audio files AND videos with extracted voiceprints:
```python
# Query includes videos that have voiceprint_status=done
"WHERE kb_id=${kb_id}$ AND (metadata LIKE '%%voiceprint_status%%done%%' " +
"OR LOWER(file_name) LIKE '%%.mp3' OR LOWER(file_name) LIKE '%%.wav' ...)"
```
## Video Playback: Native `