chore(release): bump version to 1.0.1 (build 27)

Release the current PiP reliability, polish style, macOS dictionary, and UI updates.
This commit is contained in:
Rocky
2026-07-27 00:09:30 +08:00
parent 5b1283c3ed
commit 956331a2af
41 changed files with 1241 additions and 295 deletions
@@ -0,0 +1,101 @@
// DictionaryAliasGenerator.swift
// OSGKeyboard · Shared
//
// After the user manually adds or edits a personal-dictionary term,
// asks the built-in DeepSeek endpoint for common ASR misrecognitions.
// Shared by the iOS and macOS dictionary editors; persisted aliases are
// available to the keyboard extension on the next polish / correction call.
import Foundation
public struct DictionaryAliasGenerator: Sendable {
private let client: LLMClient?
private let timeout: TimeInterval
public init(client: LLMClient? = nil, timeout: TimeInterval = 12) {
self.client = client
self.timeout = timeout
}
public func generateAliases(for term: String) async -> [String] {
let trimmed = term.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return [] }
do {
let client = try resolveClient()
let prompt = Self.makePrompt(for: trimmed)
let raw = try await withThrowingTaskGroup(of: String.self) { group in
group.addTask {
try await client.polish(trimmed, systemPrompt: prompt)
}
group.addTask {
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
throw CancellationError()
}
let result = try await group.next()!
group.cancelAll()
return result
}
return Self.parseAliases(from: raw, excludingTerm: trimmed)
} catch {
#if DEBUG
print("⚠️ [DictionaryAliasGenerator] alias generation failed: \(error)")
#endif
return []
}
}
private func resolveClient() throws -> LLMClient {
if let client {
return client
}
guard PreconfiguredKeys.isDeepseekConfigured else {
throw LLMError.noAPIKey
}
let preset = LLMProvider.provider(id: "deepseek")
return OpenAICompatibleClient(
baseURL: preset.defaultBaseURL,
apiKey: PreconfiguredKeys.deepseek,
model: preset.defaultModel
)
}
private static func makePrompt(for term: String) -> String {
"""
你是语音识别纠错助手。用户把专有词汇「\(term)」加入了个人词库。
请列出该词在中文或英文语音输入时最常见的 3–6 个误识别写法(同音字、近音字、拼音混淆、英文误听等)。
不要包含正确词「\(term)」本身。
只输出 JSON 字符串数组,例如 ["1","2"]。若无合理别名则输出 []。
"""
}
public static func parseAliases(from raw: String, excludingTerm term: String) -> [String] {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
let jsonSlice = extractJSONArray(from: trimmed) ?? trimmed
guard let data = jsonSlice.data(using: .utf8),
let decoded = try? JSONDecoder().decode([String].self, from: data)
else { return [] }
let termLower = term.lowercased()
var seen = Set<String>()
var result: [String] = []
for alias in decoded {
let cleaned = alias.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty else { continue }
let key = cleaned.lowercased()
guard key != termLower, !seen.contains(key) else { continue }
seen.insert(key)
result.append(cleaned)
if result.count >= 6 { break }
}
return result
}
private static func extractJSONArray(from text: String) -> String? {
guard let start = text.firstIndex(of: "["),
let end = text.lastIndex(of: "]"),
start < end
else { return nil }
return String(text[start...end])
}
}
@@ -218,14 +218,14 @@ private final class AdaptiveDownsampler: @unchecked Sendable {
// duplicate the audio ~6× (stuttering ASR input). After the
// single feed we report "ran dry", so the expected status is
// `.inputRanDry` (output not full), not `.haveData`.
var provided = false
let provided = OSAllocatedUnfairLock(initialState: false)
var error: NSError?
let status = current.converter.convert(to: current.scratch, error: &error) { _, outStatus in
if provided {
if provided.withLock({ $0 }) {
outStatus.pointee = .noDataNow
return nil
}
provided = true
provided.withLock { $0 = true }
outStatus.pointee = .haveData
return buffer
}
+21
View File
@@ -383,6 +383,27 @@ public enum Keychain: @unchecked Sendable {
private static let onboardingService = "com.osgkeyboard.onboarding"
private static let onboardingAccount = "hasCompletedOnboarding"
/// Survives reboots but is wiped with the app container (unlike Keychain).
private static let installIdentityKey = "osgkeyboard.installIdentity"
/// Call once at config init. Returns `true` when this is a brand-new app
/// container (first launch or reinstall after delete). Clears a stale
/// Keychain onboarding flag so deleted installs show the welcome flow again.
@discardableResult
public static func beginInstallIdentityIfNeeded() -> Bool {
let standard = UserDefaults.standard
if standard.string(forKey: installIdentityKey) != nil {
return false
}
standard.set(UUID().uuidString, forKey: installIdentityKey)
if hasCompletedOnboarding() {
setOnboardingCompleted(false)
OSGLog.config.info("[onboarding] fresh install: cleared stale Keychain onboarding flag")
} else {
OSGLog.config.info("[onboarding] fresh install: install identity created")
}
return true
}
public static func hasCompletedOnboarding() -> Bool {
var query: [String: Any] = [