feat: iCloud settings sync and cold-start return redesign

- Add iCloud key-value settings sync (engine/language/polish/Flow prefs);
  API keys stay on-device. New "Sync settings via iCloud" toggle.
- Redesign cold-start handoff: bottom-bar left-to-right swipe guidance,
  auto-dismiss on app switch, tap-anywhere to close, retained return link.
- Harden keyboard->app handoff with host-disconnected hint.
- Include prior Unreleased ASR fixes (route-change crash, fallback warning,
  multi-utterance recognition, local ASR diagnostics).

Release 0.5.0 (build 18).
This commit is contained in:
Rocky
2026-07-07 17:56:01 +08:00
parent bf844caa7f
commit 128aab1b02
42 changed files with 1818 additions and 337 deletions
@@ -43,6 +43,19 @@ public enum TranscriptPostProcessor: Sendable {
text.trimmingCharacters(in: .whitespacesAndNewlines)
}
/// Conservative cleanup for raw ASR fallback delivery. This is used when
/// polish/translation cannot run, so it must not rewrite meaning or invent
/// punctuation; it only removes formatting artifacts that ASR/chunking can
/// introduce.
public static func cleanRawASRFallback(_ text: String) -> String {
var result = text.trimmingCharacters(in: .whitespacesAndNewlines)
result = repairMidSentenceLineBreaks(result)
result = collapseHorizontalWhitespace(result)
result = removeCJKBoundarySpaces(result)
result = normalizePunctuationSpacing(result)
return normalizeWhitespaceAndPunctuation(result)
}
// MARK: - Post-LLM pipeline
/// Apply deterministic cleanup and quality gate to LLM output.
@@ -220,6 +233,53 @@ public enum TranscriptPostProcessor: Sendable {
return result.trimmingCharacters(in: .whitespacesAndNewlines)
}
private static func collapseHorizontalWhitespace(_ text: String) -> String {
text.replacingOccurrences(
of: #"[^\S\r\n]+"#,
with: " ",
options: .regularExpression
)
}
private static func removeCJKBoundarySpaces(_ text: String) -> String {
var output = ""
let characters = Array(text)
for index in characters.indices {
let current = characters[index]
if current.isWhitespace,
let previous = previousNonWhitespace(in: characters, before: index),
let next = nextNonWhitespace(in: characters, after: index),
shouldDropSpaceBetween(previous: previous, next: next) {
continue
}
output.append(current)
}
return output
}
private static func normalizePunctuationSpacing(_ text: String) -> String {
var output = ""
let characters = Array(text)
for index in characters.indices {
let current = characters[index]
if current.isWhitespace,
let next = nextNonWhitespace(in: characters, after: index),
isClosingPunctuation(next) {
continue
}
if isOpeningPunctuation(current),
let next = nextNonWhitespace(in: characters, after: index),
next.isWhitespace {
output.append(current)
continue
}
output.append(current)
}
return output
.replacingOccurrences(of: #"([(「“])\s+"#, with: "$1", options: .regularExpression)
.replacingOccurrences(of: #"\s+([,。!?;:、,.!?;:])"#, with: "$1", options: .regularExpression)
}
// MARK: - Prefix / quote cleanup
public static func stripExplanatoryPrefix(from text: String) -> String {
@@ -271,6 +331,41 @@ public enum TranscriptPostProcessor: Sendable {
return (pAscii && nAscii) ? " " : ""
}
private static func previousNonWhitespace(in characters: [Character], before index: Int) -> Character? {
guard index > characters.startIndex else { return nil }
for i in stride(from: index - 1, through: characters.startIndex, by: -1) {
if !characters[i].isWhitespace { return characters[i] }
}
return nil
}
private static func nextNonWhitespace(in characters: [Character], after index: Int) -> Character? {
let nextIndex = index + 1
guard nextIndex < characters.endIndex else { return nil }
for i in nextIndex..<characters.endIndex {
if !characters[i].isWhitespace { return characters[i] }
}
return nil
}
private static func shouldDropSpaceBetween(previous: Character, next: Character) -> Bool {
(isCJKCharacter(previous) && isCJKCharacter(next)) || isClosingPunctuation(next)
}
private static func isCJKCharacter(_ character: Character) -> Bool {
character.unicodeScalars.contains(where: isCJKScalar)
}
private static func isClosingPunctuation(_ character: Character) -> Bool {
let closing: Set<Character> = ["", "", "", "", "", "", "", ",", ".", "!", "?", ";", ":"]
return closing.contains(character)
}
private static func isOpeningPunctuation(_ character: Character) -> Bool {
let opening: Set<Character> = ["", "", ""]
return opening.contains(character)
}
private static func extractEmojis(from text: String) -> [String] {
text.unicodeScalars.filter(isEmojiScalar).map { String($0) }
}