--- 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 `