feat: add streaming cloud ASR, polish routing, and settings card layout

Unify Bailian/Volcengine/OpenAI realtime streaming, ABE polish routing with
fun styles, and a shared card-page Settings hierarchy; bump to 1.1 (build 32).
This commit is contained in:
Rocky
2026-07-28 20:25:17 +08:00
parent 956331a2af
commit d656bac8c3
58 changed files with 4153 additions and 1396 deletions
@@ -19,8 +19,15 @@ public enum TranscriptPostProcessor: Sendable {
// MARK: - Short-circuit gate (skip LLM)
/// Returns `true` when the transcript is short enough and lacks
/// structural signals so calling the LLM would add latency without
/// meaningful benefit (e.g. "", "OK", "").
/// structural / communicative signals so calling the LLM would add
/// latency without meaningful benefit.
///
/// Two tiers:
/// - **Tier 1 (4 CJK / short English token):** always skip when
/// structure-free (e.g. "", "OK", "").
/// - **Tier 2 (510 CJK):** skip only low-value acks / closings
/// (e.g. "", ""); keep questions, invites,
/// and contentful short lines for polish / ASR repair.
public static func shouldSkipLLM(for text: String) -> Bool {
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return false }
@@ -28,8 +35,15 @@ public enum TranscriptPostProcessor: Sendable {
let cjkCount = trimmed.unicodeScalars.filter(isCJKScalar).count
if cjkCount > 0 {
// e.g. , , ,
return trimmed.count <= 4 && cjkCount <= 4
// Tier 1 ultra-short
if trimmed.count <= 4 && cjkCount <= 4 {
return true
}
// Tier 2 short ack / closing only
if trimmed.count <= 10 && cjkCount <= 10 {
return isTier2SkipUtterance(trimmed)
}
return false
}
// e.g. OK, yes, thanks single short token only
@@ -37,6 +51,54 @@ public enum TranscriptPostProcessor: Sendable {
return words.count == 1 && trimmed.count <= 10
}
/// Tier-2 skip: 510 character Chinese that is only a confirmation,
/// status, or closing not a question, invite, or contentful line.
public static func isTier2SkipUtterance(_ text: String) -> Bool {
let stripped = stripLeadingFillers(text)
if stripped.isEmpty { return true }
let cjk = stripped.unicodeScalars.filter(isCJKScalar).count
if stripped.count <= 4 && cjk <= 4 { return true }
if PolishRouter.hasCommunicativeSignal(stripped) { return false }
if PolishRouter.hasOpponentQuote(stripped) { return false }
if PolishRouter.hasConcreteEntity(stripped) { return false }
for pattern in tier2SkipPatterns {
if stripped.range(of: pattern, options: .regularExpression) != nil {
return true
}
}
return false
}
private static let tier2SkipPatterns: [String] = [
#"^(好的?|行|可以|收到|谢谢|麻烦了|没事|不用了|知道了|明白了|没问题|辛苦了|对的?)(啦|了|啊|呢|哦|呀)?$"#,
#"^(好的?)?(我)?(知道|明白)了$"#,
#"^(好的我知道了|收到谢谢|麻烦你了)$"#,
#"^(那就)?先这样(吧|了|啦)?$"#,
#"^(晚点|待会|一会儿|呆会)(再)?(说|联系|聊|讲)(吧|了|啊)?$"#,
#"^(我)?(马上|立刻|这就)?(就)?到了$"#,
#"^(好的?|嗯)?(收到|谢谢)(你|啦|了|啊)?$"#,
#"^(没事)?(不用|别)(了|啦)?(谢谢)?$"#,
#"^(晚安|早安|早上好|拜拜|再见)(啦|了|啊)?$"#,
#"^(晚点再说|待会联系|先这样吧|马上到了)$"#,
]
private static let leadingFillers = [
"怎么说呢", "就是说", "然后那个", "嗯那个", "那个", "", "",
]
private static func stripLeadingFillers(_ text: String) -> String {
var result = text.trimmingCharacters(in: .whitespacesAndNewlines)
for filler in leadingFillers.sorted(by: { $0.count > $1.count }) {
if result.hasPrefix(filler) {
result = String(result.dropFirst(filler.count))
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
return result
}
/// Local-only cleanup when the LLM is skipped. Keeps the speaker's
/// words verbatim no punctuation invention beyond trimming.
public static func localClean(_ text: String) -> String {