Commit a7af231852e2719142eabee6289e5c9b1e2f9ddf
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>
Showing
2 changed files
with
24 additions
and
2 deletions
lifelog-recorder/app/src/main/kotlin/com/lemon/lifelog/summary/SummaryScheduler.kt
| @@ -4,6 +4,7 @@ import android.content.Context | @@ -4,6 +4,7 @@ import android.content.Context | ||
| 4 | import androidx.work.Constraints | 4 | import androidx.work.Constraints |
| 5 | import androidx.work.Data | 5 | import androidx.work.Data |
| 6 | import androidx.work.ExistingPeriodicWorkPolicy | 6 | import androidx.work.ExistingPeriodicWorkPolicy |
| 7 | +import androidx.work.ExistingWorkPolicy | ||
| 7 | import androidx.work.NetworkType | 8 | import androidx.work.NetworkType |
| 8 | import androidx.work.OneTimeWorkRequestBuilder | 9 | import androidx.work.OneTimeWorkRequestBuilder |
| 9 | import androidx.work.PeriodicWorkRequestBuilder | 10 | import androidx.work.PeriodicWorkRequestBuilder |
| @@ -40,6 +41,12 @@ object SummaryScheduler { | @@ -40,6 +41,12 @@ object SummaryScheduler { | ||
| 40 | .setInputData(data) | 41 | .setInputData(data) |
| 41 | .addTag(ONE_SHOT_TAG) | 42 | .addTag(ONE_SHOT_TAG) |
| 42 | .build() | 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,6 +37,10 @@ class SummaryWorker( | ||
| 37 | const val DEFAULT_GAP_MIN = 10 | 37 | const val DEFAULT_GAP_MIN = 10 |
| 38 | const val MIN_SESSION_SPEECH_MS = 30_000L // 会话至少 30s 语音 | 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 | const val INPUT_NOW_TS = "now_ts" // 覆盖 now(测试 / 手动) | 44 | const val INPUT_NOW_TS = "now_ts" // 覆盖 now(测试 / 手动) |
| 41 | const val INPUT_GAP_MS = "gap_ms" // 覆盖会话间隔(手动跑用当前设置) | 45 | const val INPUT_GAP_MS = "gap_ms" // 覆盖会话间隔(手动跑用当前设置) |
| 42 | 46 | ||
| @@ -132,11 +136,22 @@ class SummaryWorker( | @@ -132,11 +136,22 @@ class SummaryWorker( | ||
| 132 | start: Long, | 136 | start: Long, |
| 133 | end: Long, | 137 | end: Long, |
| 134 | ): String? { | 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 | val sb = StringBuilder() | 147 | val sb = StringBuilder() |
| 136 | sb.appendLine("以下是 ${rangeFmt.format(Date(start))} – ${rangeFmt.format(Date(end))} 一次连续录音的语音转写片段(按时间顺序)。") | 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 | sb.appendLine() | 152 | sb.appendLine() |
| 138 | var hasText = false | 153 | var hasText = false |
| 139 | - for (seg in session) { | 154 | + for (seg in segs) { |
| 140 | val transcript = repo.transcripts.get(seg.id)?.text ?: continue | 155 | val transcript = repo.transcripts.get(seg.id)?.text ?: continue |
| 141 | if (transcript.isBlank()) continue | 156 | if (transcript.isBlank()) continue |
| 142 | hasText = true | 157 | hasText = true |