Commit a7af231852e2719142eabee6289e5c9b1e2f9ddf

Authored by yibo
1 parent 5bd1bf78

feat(lifelog/summary): 纪要去重 + 超长会话抽样

- SummaryScheduler.runNow 改 enqueueUniqueWork(KEEP):在跑/排队时重复触发直接忽略,
  避免并发跑出重复纪要
- SummaryWorker:单篇喂 LLM 的转写条数上限 150,超长会话均匀抽样(覆盖整段、时间戳
  保真),防止 prompt 撑爆上下文 / 端侧 prefill 过慢

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/summary/SummaryScheduler.kt
... ... @@ -4,6 +4,7 @@ import android.content.Context
4 4 import androidx.work.Constraints
5 5 import androidx.work.Data
6 6 import androidx.work.ExistingPeriodicWorkPolicy
  7 +import androidx.work.ExistingWorkPolicy
7 8 import androidx.work.NetworkType
8 9 import androidx.work.OneTimeWorkRequestBuilder
9 10 import androidx.work.PeriodicWorkRequestBuilder
... ... @@ -40,6 +41,12 @@ object SummaryScheduler {
40 41 .setInputData(data)
41 42 .addTag(ONE_SHOT_TAG)
42 43 .build()
43   - WorkManager.getInstance(ctx).enqueue(request)
  44 + // 唯一作业 + KEEP:若已有一次在跑/排队(含 RETRY),重复触发直接忽略,
  45 + // 避免并发跑出重复纪要。
  46 + WorkManager.getInstance(ctx).enqueueUniqueWork(
  47 + ONE_SHOT_TAG,
  48 + ExistingWorkPolicy.KEEP,
  49 + request,
  50 + )
44 51 }
45 52 }
... ...
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/summary/SummaryWorker.kt
... ... @@ -37,6 +37,10 @@ class SummaryWorker(
37 37 const val DEFAULT_GAP_MIN = 10
38 38 const val MIN_SESSION_SPEECH_MS = 30_000L // 会话至少 30s 语音
39 39  
  40 + /** 单篇纪要喂给 LLM 的最大转写条数;超长会话均匀抽样到这个数,
  41 + * 避免 prompt 撑爆上下文 / prefill 过慢(端侧热降频下尤其重要)。 */
  42 + const val MAX_SEGMENTS_PER_SUMMARY = 150
  43 +
40 44 const val INPUT_NOW_TS = "now_ts" // 覆盖 now(测试 / 手动)
41 45 const val INPUT_GAP_MS = "gap_ms" // 覆盖会话间隔(手动跑用当前设置)
42 46  
... ... @@ -132,11 +136,22 @@ class SummaryWorker(
132 136 start: Long,
133 137 end: Long,
134 138 ): String? {
  139 + // 超长会话均匀抽样,保证 prompt 有界(覆盖整段时间而非只取头部)。
  140 + val segs = if (session.size > MAX_SEGMENTS_PER_SUMMARY) {
  141 + val step = session.size.toDouble() / MAX_SEGMENTS_PER_SUMMARY
  142 + (0 until MAX_SEGMENTS_PER_SUMMARY).map { session[(it * step).toInt()] }
  143 + } else {
  144 + session
  145 + }
  146 +
135 147 val sb = StringBuilder()
136 148 sb.appendLine("以下是 ${rangeFmt.format(Date(start))} – ${rangeFmt.format(Date(end))} 一次连续录音的语音转写片段(按时间顺序)。")
  149 + if (segs.size < session.size) {
  150 + sb.appendLine("(会话较长,共 ${session.size} 段,已均匀抽样 ${segs.size} 段,时间戳仍为真实值。)")
  151 + }
137 152 sb.appendLine()
138 153 var hasText = false
139   - for (seg in session) {
  154 + for (seg in segs) {
140 155 val transcript = repo.transcripts.get(seg.id)?.text ?: continue
141 156 if (transcript.isBlank()) continue
142 157 hasText = true
... ...