notes(asr): LLM 顺手出会议标题,覆盖默认时间戳 title
deploy notes / build-and-deploy (push) Successful in 1m50s

prompt 加要求:第一行 `TITLE: <主题>` + `---` + 正文;backend parse
头两行,覆盖 recordings.title;summary 字段不含 title 行。
失败 fallback 不动 title。前端 sidebar/主视图自带 5s 轮询自动刷新。
This commit is contained in:
Fam Zheng
2026-05-17 23:01:28 +01:00
parent d964b46dbe
commit e7912f3547
+44 -5
View File
@@ -427,8 +427,8 @@ async fn process_recording(s: AppState, id: i64) {
); );
} }
// LLM:生成会议纪要 // LLM:生成会议纪要 + 标题
let summary = match call_llm_summary(&s, &transcript).await { let raw = match call_llm_summary(&s, &transcript).await {
Ok(t) => t, Ok(t) => t,
Err(e) => { Err(e) => {
tracing::error!(%id, error = %e, "LLM failed"); tracing::error!(%id, error = %e, "LLM failed");
@@ -436,14 +436,50 @@ async fn process_recording(s: AppState, id: i64) {
return; return;
} }
}; };
let (new_title, summary_body) = parse_title_from_summary(&raw);
{ {
let conn = s.db.lock().unwrap(); let conn = s.db.lock().unwrap();
if let Some(t) = new_title.as_deref() {
let _ = conn.execute(
"UPDATE recordings SET title = ?1, summary = ?2, status = 'done', error = NULL WHERE id = ?3",
params![t, &summary_body, id],
);
} else {
let _ = conn.execute( let _ = conn.execute(
"UPDATE recordings SET summary = ?1, status = 'done', error = NULL WHERE id = ?2", "UPDATE recordings SET summary = ?1, status = 'done', error = NULL WHERE id = ?2",
params![&summary, id], params![&summary_body, id],
); );
} }
tracing::info!(%id, "done"); }
tracing::info!(%id, title = ?new_title, "done");
}
/// 从 LLM 输出剥离 `TITLE: ...\n---\n` 头部。
/// 返回 (Option<title>, summary_body)title 失败时返回 None + 原文。
fn parse_title_from_summary(raw: &str) -> (Option<String>, String) {
let mut lines = raw.lines();
let first = lines.next().unwrap_or("").trim();
let Some(rest) = first
.strip_prefix("TITLE:")
.or_else(|| first.strip_prefix("Title:"))
.or_else(|| first.strip_prefix("标题:"))
.or_else(|| first.strip_prefix("标题:"))
else {
return (None, raw.to_string());
};
let title: String = rest.trim().chars().take(80).collect();
if title.is_empty() {
return (None, raw.to_string());
}
// 吃掉接下来的 `---` separator + 空行
let body: String = lines
.skip_while(|l| {
let t = l.trim();
t.is_empty() || t == "---" || t.starts_with("---")
})
.collect::<Vec<_>>()
.join("\n");
(Some(title), body)
} }
fn set_status(s: &AppState, id: i64, status: &str, transcript: Option<&str>, error: Option<&str>) { fn set_status(s: &AppState, id: i64, status: &str, transcript: Option<&str>, error: Option<&str>) {
@@ -501,8 +537,11 @@ async fn call_llm_summary(s: &AppState, transcript: &str) -> Result<String, Stri
"model": s.llm_model, "model": s.llm_model,
"messages": [ "messages": [
{ "role": "system", "content": { "role": "system", "content":
"你是一个会议纪要助手。根据语音转写整理一份结构化纪要(markdown 格式)\n\ "你是一个会议纪要助手。根据语音转写输出\n\
\n\ \n\
第一行:`TITLE: <8-20 字符的会议主题>`(不含日期/时间,提取核心议题)\n\
第二行:`---`\n\
之后是 markdown 纪要:\n\
1. **概要**1-2 句话总结\n\ 1. **概要**1-2 句话总结\n\
2. **关键讨论点**bullet 列出\n\ 2. **关键讨论点**bullet 列出\n\
3. **决定 / 结论**\n\ 3. **决定 / 结论**\n\