出现未识别标签保留字幕bug修复:给兜底,与最相似声纹合并
This commit is contained in:
parent
236037a0b0
commit
7892e35d2d
|
|
@ -165,6 +165,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
private val speakerCheckThread = HandlerThread("speaker-window").also { it.start() }
|
private val speakerCheckThread = HandlerThread("speaker-window").also { it.start() }
|
||||||
private val speakerCheckHandler by lazy { Handler(speakerCheckThread.looper) }
|
private val speakerCheckHandler by lazy { Handler(speakerCheckThread.looper) }
|
||||||
private val speakerSwitchDrop = 0.30f // cos(window, reference) below this => switch
|
private val speakerSwitchDrop = 0.30f // cos(window, reference) below this => switch
|
||||||
|
// 最近邻兜底阈值(用户要求:未识别归到最相似的已识别人):
|
||||||
|
// endpoint 精确匹配不中→nearest≥0.35 归入;再不行才注册新人
|
||||||
|
private val verdictFallbackMin = 0.35f
|
||||||
|
// 滚动检测 fast 空间的兜底阈值(仅影响换行标签显示,endpoint 仍权威)
|
||||||
|
private val rollingFallbackMin = 0.30f
|
||||||
// 滚动检测在 fast 空间查"这人是谁"仅用于**显示标签**,阈值放宽到 0.4;
|
// 滚动检测在 fast 空间查"这人是谁"仅用于**显示标签**,阈值放宽到 0.4;
|
||||||
// endpoint 权威判定仍用准确模型 + speakerThreshold(0.5)。
|
// endpoint 权威判定仍用准确模型 + speakerThreshold(0.5)。
|
||||||
private val fastSearchThreshold = 0.4f
|
private val fastSearchThreshold = 0.4f
|
||||||
|
|
@ -176,6 +181,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Volatile private var rollingLowHits = 0
|
@Volatile private var rollingLowHits = 0
|
||||||
// C(最新者胜):每次投递滚动检测都刷新序号,队列里被超额的旧任务直接跳过
|
// C(最新者胜):每次投递滚动检测都刷新序号,队列里被超额的旧任务直接跳过
|
||||||
@Volatile private var rollingSeq = 0L
|
@Volatile private var rollingSeq = 0L
|
||||||
|
// 兜底残料:当前句子槽位最近一次滚动窗口声纹。整句判定因碎片算不出
|
||||||
|
// embedding 时复用它做归属(用户要求 2026-09-11)
|
||||||
|
@Volatile private var rollingEmbSid = -1L
|
||||||
|
@Volatile private var rollingEmb: FloatArray? = null
|
||||||
@Volatile private var pendingSwitch: SpeakerSwitch? = null
|
@Volatile private var pendingSwitch: SpeakerSwitch? = null
|
||||||
@Volatile private var windowRefEmb: FloatArray? = null
|
@Volatile private var windowRefEmb: FloatArray? = null
|
||||||
@Volatile private var activeLabelForWindow: String? = null
|
@Volatile private var activeLabelForWindow: String? = null
|
||||||
|
|
@ -493,6 +502,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
win, sampleRateInHz
|
win, sampleRateInHz
|
||||||
) ?: return@post
|
) ?: return@post
|
||||||
if (rseq != rollingSeq) return@post
|
if (rseq != rollingSeq) return@post
|
||||||
|
rollingEmbSid = owner
|
||||||
|
rollingEmb = emb
|
||||||
// 计算耗时内句子可能已结算:回写前二次核销
|
// 计算耗时内句子可能已结算:回写前二次核销
|
||||||
if (seq != checkSerial) return@post
|
if (seq != checkSerial) return@post
|
||||||
val ref = windowRefEmb
|
val ref = windowRefEmb
|
||||||
|
|
@ -660,35 +671,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
// 整句声纹判定移出录音线程(停摆根因):后台算完
|
// 整句声纹判定移出录音线程(停摆根因):后台算完
|
||||||
// 回主线程 confirmSpeaker 修正归属
|
// 回主线程 confirmSpeaker 修正归属
|
||||||
speakerCheckHandler.post {
|
postSpeakerVerdict(sid, snap)
|
||||||
val emb = SpeakerRecognition.computeEmbedding(
|
|
||||||
snap, sampleRateInHz
|
|
||||||
)
|
|
||||||
// 镜像嵌入:② 用句尾 2s 切片(与滚动检测同视角),
|
|
||||||
// 整句 embedding 会拉低窗口匹配度,导致换行时查不到人
|
|
||||||
val fastEmb = if (emb != null &&
|
|
||||||
SpeakerRecognition.fastReady()
|
|
||||||
) {
|
|
||||||
val tailSlice = if (snap.size > 32000)
|
|
||||||
snap.copyOfRange(
|
|
||||||
snap.size - 32000, snap.size
|
|
||||||
) else snap
|
|
||||||
SpeakerRecognition.computeEmbeddingFast(
|
|
||||||
tailSlice, sampleRateInHz
|
|
||||||
)
|
|
||||||
} else null
|
|
||||||
val label = if (emb == null) null else
|
|
||||||
SpeakerRecognition.registry.search(
|
|
||||||
emb, speakerThreshold
|
|
||||||
)
|
|
||||||
?: SpeakerRecognition.nextAutoLabel().also {
|
|
||||||
SpeakerRecognition.registry.addAuto(it, emb)
|
|
||||||
if (fastEmb != null)
|
|
||||||
SpeakerRecognition.fastRegistry
|
|
||||||
.addAuto(it, fastEmb)
|
|
||||||
}
|
|
||||||
uiHandler.post { confirmSpeaker(sid, label, emb, fastEmb) }
|
|
||||||
}
|
|
||||||
// 定格 + 送检立即走主线程队列,不等声纹
|
// 定格 + 送检立即走主线程队列,不等声纹
|
||||||
uiHandler.post {
|
uiHandler.post {
|
||||||
val accepted = submitRefineTask(
|
val accepted = submitRefineTask(
|
||||||
|
|
@ -702,9 +685,67 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Stop 兜底(软 endpoint):最后一句未完的转写同样走定格→判定→精修,
|
||||||
|
// 否则丢半句 + 灰"未识别"行滞留(用户反馈 2026-09-11)
|
||||||
|
if (sentenceId != -1L) {
|
||||||
|
val raw = onlineRecognizer.getResult(stream).text
|
||||||
|
val disp =
|
||||||
|
if (raw.isNotBlank() && isInAllowedLanguages(raw)) raw else ""
|
||||||
|
onlineRecognizer.reset(stream)
|
||||||
|
val sid = sentenceId
|
||||||
|
sentenceId = -1L
|
||||||
|
windowRefEmb = null
|
||||||
|
if (disp.isNotBlank() && bufferTotal > 0) {
|
||||||
|
val snap = flattenSamples()
|
||||||
|
val dur = bufferTotal / sampleRateInHz.toFloat()
|
||||||
|
resetBuffer()
|
||||||
|
postSpeakerVerdict(sid, snap)
|
||||||
|
uiHandler.post {
|
||||||
|
val accepted = submitRefineTask(
|
||||||
|
sid, snap, dur, "STOP", null, disp, disp
|
||||||
|
)
|
||||||
|
finalizeSentence(sid, accepted)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resetBuffer()
|
||||||
|
uiHandler.post { finalizeSentence(sid, false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
stream.release()
|
stream.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 整句声纹判定:后台算准确 embedding + fast 镜像(句尾 2s 切片,与滚动
|
||||||
|
* 检测同视角),注册后回主线程 confirmSpeaker 修正归属。
|
||||||
|
* 自然 endpoint / 强制断句 / Stop 软 endpoint 共用。
|
||||||
|
*/
|
||||||
|
private fun postSpeakerVerdict(sid: Long, snap: FloatArray) {
|
||||||
|
speakerCheckHandler.post {
|
||||||
|
var emb = SpeakerRecognition.computeEmbedding(snap, sampleRateInHz)
|
||||||
|
if (emb == null && rollingEmbSid == sid) {
|
||||||
|
// 兜底①:句子太碎、整句 embedding 算不出 → 复用该槽滚动窗口残料
|
||||||
|
emb = rollingEmb
|
||||||
|
}
|
||||||
|
val fastEmb = if (emb != null && SpeakerRecognition.fastReady()) {
|
||||||
|
val tailSlice = if (snap.size > 32000)
|
||||||
|
snap.copyOfRange(snap.size - 32000, snap.size) else snap
|
||||||
|
SpeakerRecognition.computeEmbeddingFast(tailSlice, sampleRateInHz)
|
||||||
|
} else null
|
||||||
|
val label = if (emb == null) null else
|
||||||
|
(SpeakerRecognition.registry.search(emb, speakerThreshold)
|
||||||
|
// 兜底②:精确匹配不中 → 归入最相似的已注册身份
|
||||||
|
?: SpeakerRecognition.registry.nearest(
|
||||||
|
emb, verdictFallbackMin
|
||||||
|
))
|
||||||
|
?: SpeakerRecognition.nextAutoLabel().also {
|
||||||
|
SpeakerRecognition.registry.addAuto(it, emb)
|
||||||
|
if (fastEmb != null)
|
||||||
|
SpeakerRecognition.fastRegistry.addAuto(it, fastEmb)
|
||||||
|
}
|
||||||
|
uiHandler.post { confirmSpeaker(sid, label, emb, fastEmb) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Keep only zh/ja/en output when firstPassLanguage == "auto":
|
// Keep only zh/ja/en output when firstPassLanguage == "auto":
|
||||||
// letters must be ASCII latin (en), kana (ja), or CJK hanzi (zh/ja);
|
// letters must be ASCII latin (en), kana (ja), or CJK hanzi (zh/ja);
|
||||||
// everything else (hangul, cyrillic, thai, arabic, accented latin ...)
|
// everything else (hangul, cyrillic, thai, arabic, accented latin ...)
|
||||||
|
|
@ -820,9 +861,11 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun renameSpeaker(oldKey: String, name: String) {
|
private fun renameSpeaker(oldKey: String, name: String) {
|
||||||
// 真名不得长得像内部键/显示名,避免与 S{n}→未识别N 的映射冲突
|
// 真名不得长得像内部键/显示名,避免与 S{n}→已识别人N 的映射冲突
|
||||||
if (autoLabelRegex.matches(name)) {
|
if (autoLabelRegex.matches(name) ||
|
||||||
Toast.makeText(this, "名字不能是 S+数字 形式", Toast.LENGTH_SHORT).show()
|
Regex("^(已识别人?\\d+|未识别\\d*)$").matches(name)) {
|
||||||
|
Toast.makeText(this, "名字不能是 S数字/已识别人N/未识别N 形式",
|
||||||
|
Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 声纹库升级:S{n} 的嵌入转为真名注册(真名可多组嵌入并存,
|
// 声纹库升级:S{n} 的嵌入转为真名注册(真名可多组嵌入并存,
|
||||||
|
|
@ -1246,14 +1289,14 @@ class MainActivity : AppCompatActivity() {
|
||||||
subtitleRecycler.visibility = View.VISIBLE
|
subtitleRecycler.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
// 内部标签 S0/S1/…(注册键,可被命名为真名)对外显示为 未识别1/未识别2…;
|
// 内部标签 S0/S1/…(已注册、能区分但尚未起名的身份)对外显示"已识别人1/2…";
|
||||||
// 已命名者直接显示真名。
|
// 已命名者显示真名;label==null(完全无法归属)才显示灰色"未识别"。
|
||||||
private val autoLabelRegex = Regex("^S(\\d+)$")
|
private val autoLabelRegex = Regex("^S(\\d+)$")
|
||||||
|
|
||||||
private fun displayName(label: String?): String {
|
private fun displayName(label: String?): String {
|
||||||
if (label == null) return "未识别"
|
if (label == null) return "未识别"
|
||||||
val m = autoLabelRegex.matchEntire(label)
|
val m = autoLabelRegex.matchEntire(label)
|
||||||
return if (m != null) "未识别${m.groupValues[1].toInt() + 1}" else label
|
return if (m != null) "已识别人${m.groupValues[1].toInt() + 1}" else label
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun colorIdxFor(label: String): Int =
|
private fun colorIdxFor(label: String): Int =
|
||||||
|
|
@ -1524,8 +1567,14 @@ class MainActivity : AppCompatActivity() {
|
||||||
private fun rollingSpeakerSearch(emb: FloatArray): String? =
|
private fun rollingSpeakerSearch(emb: FloatArray): String? =
|
||||||
if (SpeakerRecognition.fastReady())
|
if (SpeakerRecognition.fastReady())
|
||||||
SpeakerRecognition.fastRegistry.search(emb, fastSearchThreshold)
|
SpeakerRecognition.fastRegistry.search(emb, fastSearchThreshold)
|
||||||
|
?: SpeakerRecognition.fastRegistry.nearest(
|
||||||
|
emb, rollingFallbackMin
|
||||||
|
)
|
||||||
else
|
else
|
||||||
SpeakerRecognition.registry.search(emb, speakerThreshold)
|
SpeakerRecognition.registry.search(emb, speakerThreshold)
|
||||||
|
?: SpeakerRecognition.registry.nearest(
|
||||||
|
emb, verdictFallbackMin
|
||||||
|
)
|
||||||
|
|
||||||
// ③ 滚动检测查到了已知人、但当前行还没有归属(占位"未识别"):
|
// ③ 滚动检测查到了已知人、但当前行还没有归属(占位"未识别"):
|
||||||
// 直接把活跃归属采纳为他,等 endpoint 权威判定给行冠名,不再多切一行
|
// 直接把活跃归属采纳为他,等 endpoint 权威判定给行冠名,不再多切一行
|
||||||
|
|
@ -1533,6 +1582,15 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (lastConfirmedLabel == null) {
|
if (lastConfirmedLabel == null) {
|
||||||
lastConfirmedLabel = label
|
lastConfirmedLabel = label
|
||||||
activeLabelForWindow = label
|
activeLabelForWindow = label
|
||||||
|
// 修"灰色未识别滞留"来源之一:滚动已匹配到人,顺带给当前灰色
|
||||||
|
// 占位行冠名(能并回上方同人行则并),不必等 endpoint 权威判定
|
||||||
|
subtitleLines.lastOrNull()
|
||||||
|
?.takeIf { it.open && it.label == null }?.let { gray ->
|
||||||
|
gray.label = label
|
||||||
|
gray.colorIdx = colorIdxFor(label)
|
||||||
|
if (!mergeLineIntoPrevIfSame(gray, label)) notifyLine(gray)
|
||||||
|
Log.i(TAG, "adopt named gray line -> ${displayName(label)}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1695,15 +1753,21 @@ class MainActivity : AppCompatActivity() {
|
||||||
copyMinutesButton.isEnabled = true
|
copyMinutesButton.isEnabled = true
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.e(TAG, "SUMMARIZE failed", t)
|
if (t is SummarizeEmptyException) Log.i(TAG, "SUMMARIZE empty: ${t.message}")
|
||||||
|
else Log.e(TAG, "SUMMARIZE failed", t)
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
minutesStatus.visibility = View.VISIBLE
|
minutesStatus.visibility = View.VISIBLE
|
||||||
minutesStatus.text = if (t is SummarizeCancelledException)
|
minutesStatus.text = when (t) {
|
||||||
"已取消生成(保留上一次的纪要)"
|
is SummarizeCancelledException -> "已取消生成(保留上一次的纪要)"
|
||||||
else "生成失败:${t.message}"
|
is SummarizeEmptyException ->
|
||||||
Toast.makeText(this,
|
"已自动取消:${t.message}(上次纪要未覆盖)"
|
||||||
if (t is SummarizeCancelledException) "已取消生成"
|
else -> "生成失败:${t.message}"
|
||||||
else "纪要生成失败:${t.message}", Toast.LENGTH_LONG).show()
|
}
|
||||||
|
Toast.makeText(this, when (t) {
|
||||||
|
is SummarizeCancelledException -> "已取消生成"
|
||||||
|
is SummarizeEmptyException -> "已自动取消生成"
|
||||||
|
else -> "纪要生成失败:${t.message}"
|
||||||
|
}, Toast.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
summarizeEngine = null
|
summarizeEngine = null
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,26 @@ class SpeakerRegistry {
|
||||||
return best
|
return best
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最近邻兜底(用户要求 2026-09-11):"未识别"归到与 e 最相似的已注册者
|
||||||
|
* (命名条目优先,其后自动条目;相似度须 ≥ minScore)。全空/不达标返回 null。
|
||||||
|
*/
|
||||||
|
@Synchronized
|
||||||
|
fun nearest(e: FloatArray, minScore: Float): String? {
|
||||||
|
val q = l2normalize(e)
|
||||||
|
var bestName: String? = null
|
||||||
|
var best = -1f
|
||||||
|
for ((name, emb) in named) {
|
||||||
|
val s = dot(q, emb)
|
||||||
|
if (s > best) { best = s; bestName = name }
|
||||||
|
}
|
||||||
|
for ((name, emb) in auto) {
|
||||||
|
val s = dot(q, emb)
|
||||||
|
if (s > best) { best = s; bestName = name }
|
||||||
|
}
|
||||||
|
return if (best >= minScore) bestName else null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 点击标签命名用:把内部键 S{n}(自动注册、未命名)升级为真名,
|
* 点击标签命名用:把内部键 S{n}(自动注册、未命名)升级为真名,
|
||||||
* 或把已有真名改成另一个真名。同名允许多条嵌入并存 —— 之前误判成
|
* 或把已有真名改成另一个真名。同名允许多条嵌入并存 —— 之前误判成
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ import org.json.JSONObject
|
||||||
|
|
||||||
class SummarizeCancelledException : Exception("summarize cancelled")
|
class SummarizeCancelledException : Exception("summarize cancelled")
|
||||||
|
|
||||||
|
/** 空笔记防线(用户要求 2026-09-11):笔记/成稿为空 → 提示并自动取消,不落盘。 */
|
||||||
|
class SummarizeEmptyException(msg: String) : Exception(msg)
|
||||||
|
|
||||||
data class Prompts(
|
data class Prompts(
|
||||||
val p1System: String, val p1UserTpl: String,
|
val p1System: String, val p1UserTpl: String,
|
||||||
val p2System: String, val p2UserTpl: String,
|
val p2System: String, val p2UserTpl: String,
|
||||||
|
|
@ -167,6 +170,14 @@ class SummarizePipeline(
|
||||||
else -> "English (formal written English)"
|
else -> "English (formal written English)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 笔记是否无任何实质条目(四类条目全空;"前回末尾"不算)。解析失败按空处理。 */
|
||||||
|
internal fun notesEntriesEmpty(notes: String): Boolean = runCatching {
|
||||||
|
val o = JSONObject(notes)
|
||||||
|
listOf("已定事項", "討論中", "行動項目", "人物").all { k ->
|
||||||
|
o.optJSONArray(k)?.length() == 0
|
||||||
|
}
|
||||||
|
}.getOrDefault(true)
|
||||||
|
|
||||||
fun mmss(ms: Long): String {
|
fun mmss(ms: Long): String {
|
||||||
val t = ms / 1000
|
val t = ms / 1000
|
||||||
return "%02d:%02d".format(t / 60, t % 60)
|
return "%02d:%02d".format(t / 60, t % 60)
|
||||||
|
|
@ -318,7 +329,7 @@ class SummarizePipeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param labelOf 内部说话人键(S{n}/真名)→ 显示名(未识别N/真名)
|
* @param labelOf 内部说话人键(S{n}/真名)→ 显示名(已识别人N/真名/未识别)
|
||||||
* @param entries SessionTranscript 快照(定格顺序,不可为空)
|
* @param entries SessionTranscript 快照(定格顺序,不可为空)
|
||||||
* @param resumeNotes/resumeFromIndex M2:会中滚动笔记已覆盖前 resumeFromIndex 句,
|
* @param resumeNotes/resumeFromIndex M2:会中滚动笔记已覆盖前 resumeFromIndex 句,
|
||||||
* 仅补跑尾部(进度、成稿复用同一份笔记)
|
* 仅补跑尾部(进度、成稿复用同一份笔记)
|
||||||
|
|
@ -345,6 +356,13 @@ class SummarizePipeline(
|
||||||
for (e in entries.drop(startIdx)) lines.add(renderLine(e))
|
for (e in entries.drop(startIdx)) lines.add(renderLine(e))
|
||||||
val chunks = chunk(lines)
|
val chunks = chunk(lines)
|
||||||
val total = chunks.size
|
val total = chunks.size
|
||||||
|
// 用户要求(2026-09-11):没有新句可消化(再点生成纪要,断点已覆盖全部
|
||||||
|
// 转写)时不再空跑"成稿 0/0",直接提示并自动取消
|
||||||
|
if (total == 0) {
|
||||||
|
throw SummarizeEmptyException(if (resumeNotes != null)
|
||||||
|
"本场会议没有新增转写,纪要已是最新"
|
||||||
|
else "没有可分析的转写内容")
|
||||||
|
}
|
||||||
Log.i(TAG, "SUMMARIZE: ${entries.size} utterances -> $total chunks, " +
|
Log.i(TAG, "SUMMARIZE: ${entries.size} utterances -> $total chunks, " +
|
||||||
"lang=$lang, resumeIdx=$startIdx")
|
"lang=$lang, resumeIdx=$startIdx")
|
||||||
|
|
||||||
|
|
@ -375,7 +393,15 @@ class SummarizePipeline(
|
||||||
)
|
)
|
||||||
parsed = extractJson(out)
|
parsed = extractJson(out)
|
||||||
}
|
}
|
||||||
if (parsed != null) notes = parsed else failed++
|
if (parsed != null) notes = parsed
|
||||||
|
else {
|
||||||
|
failed++
|
||||||
|
// 及时止损:第一块都解析不出来,笔记链条已断,后面块只会白烧 token
|
||||||
|
if (idx == 0) {
|
||||||
|
Log.w(TAG, "P1 first chunk unparsable, abort. raw=${out.take(120)}")
|
||||||
|
throw SummarizeEmptyException("开头转写片段无法生成有效笔记(内容过少或输出不可解析)")
|
||||||
|
}
|
||||||
|
}
|
||||||
// 测量点(baseline,不改行为):raw≫json 说明解码浪费在废话/思考上;
|
// 测量点(baseline,不改行为):raw≫json 说明解码浪费在废话/思考上;
|
||||||
// ms 是单块真实耗时,决定 diff 化/大块合并的参数
|
// ms 是单块真实耗时,决定 diff 化/大块合并的参数
|
||||||
Log.i(TAG, "P1 chunk=${idx + 1}/$total prompt_len=${user.length} " +
|
Log.i(TAG, "P1 chunk=${idx + 1}/$total prompt_len=${user.length} " +
|
||||||
|
|
@ -385,6 +411,10 @@ class SummarizePipeline(
|
||||||
}
|
}
|
||||||
Log.i(TAG, "P1 phase total_ms=${System.currentTimeMillis() - tP1Start} " +
|
Log.i(TAG, "P1 phase total_ms=${System.currentTimeMillis() - tP1Start} " +
|
||||||
"chunks=$total failed=$failed")
|
"chunks=$total failed=$failed")
|
||||||
|
// 空笔记终检:全部块失败,或笔记仍是空壳(total==0 已在入口取消)
|
||||||
|
if (failed == total || notesEntriesEmpty(notes)) {
|
||||||
|
throw SummarizeEmptyException("整场转写未能形成任何笔记条目")
|
||||||
|
}
|
||||||
|
|
||||||
// ---- 成稿(§5.3)----
|
// ---- 成稿(§5.3)----
|
||||||
if (listener.isCancelled()) throw SummarizeCancelledException()
|
if (listener.isCancelled()) throw SummarizeCancelledException()
|
||||||
|
|
@ -406,6 +436,7 @@ class SummarizePipeline(
|
||||||
// 纯统计日志(零成本),纪要中的混语留给人工处理。
|
// 纯统计日志(零成本),纪要中的混语留给人工处理。
|
||||||
val viol = scriptViolation(lang, md)
|
val viol = scriptViolation(lang, md)
|
||||||
if (viol != null) Log.i(TAG, "SUMMARIZE langcheck NOTE($lang): $viol (no rewrite)")
|
if (viol != null) Log.i(TAG, "SUMMARIZE langcheck NOTE($lang): $viol (no rewrite)")
|
||||||
|
if (md.isBlank()) throw SummarizeEmptyException("成稿输出为空")
|
||||||
Log.i(TAG, "SUMMARIZE wall clock total_ms=${System.currentTimeMillis() - tP1Start}")
|
Log.i(TAG, "SUMMARIZE wall clock total_ms=${System.currentTimeMillis() - tP1Start}")
|
||||||
return MeetingSummary(md, lang, notes, total, failed)
|
return MeetingSummary(md, lang, notes, total, failed)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue