notes: 新建 notes.famzheng.me — 录音 → ASR → LLM 会议纪要
deploy articulate / build-and-deploy (push) Successful in 1m21s
deploy cube / build-and-deploy (push) Successful in 1m44s
deploy karaoke / build-and-deploy (push) Successful in 1m13s
deploy music / build-and-deploy (push) Successful in 2m23s
deploy notes / build-and-deploy (push) Successful in 2m16s
deploy simpleasm / build-and-deploy (push) Successful in 1m44s
deploy werewolf / build-and-deploy (push) Successful in 1m7s

- 后端 axum + sqlite (recordings 表):上传 multipart 流式落 PVC;spawn worker pending → transcribing (调 mochi 那边 ASR endpoint, fireredasr2 token, Whisper-style multipart) → summarizing (调 gemma-4-31b-it OpenAI 兼容接口) → done
- 鉴权 middleware:Authorization: token <PASSPHRASE>;audio 流播放 ?token= query 兜底;passphrase 走 k8s Secret 不写死
- 前端 Vue3:首次访问弹 passphrase modal;sidebar 录音列表(带状态 chip)+ content 选中显示音频 + 转写 + markdown 纪要;5s polling 进度
- k8s manifest: ns cube-notes / PVC 30Gi / Ingress notes.famzheng.me / bodylimit 600M;Secret notes-creds = {passphrase, asr_token, llm_token}
- portal apps.ts 加 notes entry
This commit is contained in:
Fam Zheng
2026-05-17 21:43:44 +01:00
parent 802d5beae9
commit 61abd3f560
15 changed files with 2687 additions and 1 deletions
+48
View File
@@ -0,0 +1,48 @@
// 鉴权:每个请求加 Authorization: token <pass><audio> 用 ?token= 兜底。
const KEY = 'notes.pass'
export function getPass() {
return localStorage.getItem(KEY) || ''
}
export function setPass(v) {
localStorage.setItem(KEY, v || '')
}
export function clearPass() {
localStorage.removeItem(KEY)
}
async function jreq(path, opts = {}) {
const pass = getPass()
const h = { 'Authorization': 'token ' + pass, ...(opts.headers || {}) }
if (opts.body && !(opts.body instanceof FormData) && !h['Content-Type']) {
h['Content-Type'] = 'application/json'
}
const r = await fetch(path, { ...opts, headers: h })
if (r.status === 401) {
const err = new Error('unauthorized')
err.unauthorized = true
throw err
}
if (!r.ok) {
const t = await r.text().catch(() => '')
throw new Error(t || `${r.status}`)
}
return r.json()
}
export function listRecordings() { return jreq('/api/recordings') }
export function getRecording(id) { return jreq('/api/recordings/' + id) }
export function deleteRecording(id) { return jreq('/api/recordings/' + id, { method: 'DELETE' }) }
export function retryRecording(id) { return jreq('/api/recordings/' + id + '/retry', { method: 'POST' }) }
export function uploadRecording(title, file) {
const fd = new FormData()
if (title) fd.append('title', title)
fd.append('audio', file, file.name)
return jreq('/api/recordings', { method: 'POST', body: fd })
}
export function audioUrl(id) {
return `/api/recordings/${id}/audio?token=${encodeURIComponent(getPass())}`
}