diff --git a/Docs/zenzai.md b/Docs/zenzai.md index 4bf88a4..a414d6a 100644 --- a/Docs/zenzai.md +++ b/Docs/zenzai.md @@ -8,7 +8,7 @@ let options = ConvertRequestOptions.withDefaultDictionary( zenzaiMode: .on( weight: url, inferenceLimit: 1, - versionDependentMode: .v2(.init(profile: "三輪/azooKeyの開発者", leftSideContext: "私の名前は")) + versionDependentMode: .v3(.init(profile: "三輪/azooKeyの開発者", leftSideContext: "私の名前は")) ) // ... ) diff --git a/Sources/CliTool/Subcommands/ExperimentalPredict.swift b/Sources/CliTool/Subcommands/ExperimentalPredict.swift index f4b6c6b..dcd221e 100644 --- a/Sources/CliTool/Subcommands/ExperimentalPredict.swift +++ b/Sources/CliTool/Subcommands/ExperimentalPredict.swift @@ -38,7 +38,7 @@ extension Subcommands { shouldResetMemory: false, memoryDirectoryURL: URL(fileURLWithPath: ""), sharedContainerURL: URL(fileURLWithPath: ""), - zenzaiMode: self.zenzWeightPath.isEmpty ? .off : .on(weight: URL(string: self.zenzWeightPath)!, inferenceLimit: .max, versionDependentMode: .v2(.init())), + zenzaiMode: self.zenzWeightPath.isEmpty ? .off : .on(weight: URL(string: self.zenzWeightPath)!, inferenceLimit: .max, versionDependentMode: .v3(.init())), metadata: .init(versionString: "anco for debugging") ) } diff --git a/Sources/CliTool/Subcommands/SessionCommand.swift b/Sources/CliTool/Subcommands/SessionCommand.swift index a6154b1..ea945b5 100644 --- a/Sources/CliTool/Subcommands/SessionCommand.swift +++ b/Sources/CliTool/Subcommands/SessionCommand.swift @@ -25,12 +25,16 @@ extension Subcommands { var roman2kana = false @Option(name: [.customLong("config_zenzai_inference_limit")], help: "inference limit for zenzai.") var configZenzaiInferenceLimit: Int = .max - @Flag(name: [.customLong("config_zenzai_rich_n_best")], help: "enable profile prompting for zenz-v2.") + @Flag(name: [.customLong("config_zenzai_rich_n_best")], help: "enable rich n_best generation for zenzai.") var configRequestRichCandidates = false - @Option(name: [.customLong("config_profile")], help: "enable rich n_best generation for zenzai.") - var configZenzV2Profile: String? + @Option(name: [.customLong("config_profile")], help: "enable profile prompting for zenz-v2 and later.") + var configZenzaiProfile: String? + @Option(name: [.customLong("config_topic")], help: "enable topic prompting for zenz-v3 and later.") + var configZenzaiTopic: String? @Flag(name: [.customLong("zenz_v1")], help: "Use zenz_v1 model.") var zenzV1 = false + @Flag(name: [.customLong("zenz_v2")], help: "Use zenz_v2 model.") + var zenzV2 = false static let configuration = CommandConfiguration(commandName: "session", abstract: "Start session for incremental input.") @@ -192,8 +196,10 @@ extension Subcommands { func requestOptions(memoryDirectory: URL, leftSideContext: String) -> ConvertRequestOptions { let zenzaiVersionDependentMode: ConvertRequestOptions.ZenzaiVersionDependentMode = if self.zenzV1 { .v1 + } else if self.zenzV2 { + .v2(.init(profile: self.configZenzaiProfile, leftSideContext: leftSideContext)) } else { - .v2(.init(profile: self.configZenzV2Profile, leftSideContext: leftSideContext)) + .v3(.init(profile: self.configZenzaiProfile, topic: self.configZenzaiTopic, leftSideContext: leftSideContext)) } var option: ConvertRequestOptions = .withDefaultDictionary( N_best: self.onlyWholeConversion ? max(self.configNBest, self.displayTopN) : self.configNBest, diff --git a/Sources/KanaKanjiConverterModule/Converter/ConvertRequestOptions.swift b/Sources/KanaKanjiConverterModule/Converter/ConvertRequestOptions.swift index 3b79efa..735e19a 100644 --- a/Sources/KanaKanjiConverterModule/Converter/ConvertRequestOptions.swift +++ b/Sources/KanaKanjiConverterModule/Converter/ConvertRequestOptions.swift @@ -154,14 +154,37 @@ public struct ConvertRequestOptions: Sendable { public var leftSideContext: String? } + public struct ZenzaiV3DependentMode: Sendable, Equatable, Hashable { + public init(profile: String? = nil, topic: String? = nil, style: String? = nil, preference: String? = nil, leftSideContext: String? = nil) { + self.profile = profile + self.topic = topic + self.style = style + self.preference = preference + self.leftSideContext = leftSideContext + } + + /// プロフィールコンテクストを設定した場合、プロフィールを反映したプロンプトが自動的に付与されます。プロフィールは10〜20文字程度の長さにとどめることを推奨します。 + public var profile: String? + /// topicを設定した場合、話題にあった変換が自動的に優先されます。話題は10〜20文字程度の長さにとどめることを推奨します。 + public var topic: String? + /// styleを設定した場合、文章のスタイルにあった変換が自動的に優先されます。スタイルは10〜20文字程度の長さにとどめることを推奨します。 + public var style: String? + /// preferenceコンテクストを設定した場合、ユーザの書き方の好みに合わせた変換が自動的に優先されます。preferenceは10〜20文字程度の長さにとどめることを推奨します。 + public var preference: String? + /// 左側の文字列を文脈として与えます。 + public var leftSideContext: String? + } + public enum ZenzVersion: Sendable, Equatable, Hashable { case v1 case v2 + case v3 } public enum ZenzaiVersionDependentMode: Sendable, Equatable, Hashable { case v1 case v2(ZenzaiV2DependentMode) + case v3(ZenzaiV3DependentMode) public var version: ZenzVersion { switch self { @@ -169,6 +192,8 @@ public struct ConvertRequestOptions: Sendable { return .v1 case .v2: return .v2 + case .v3: + return .v3 } } } @@ -179,7 +204,7 @@ public struct ConvertRequestOptions: Sendable { weightURL: URL(fileURLWithPath: ""), inferenceLimit: 10, requestRichCandidates: false, - versionDependentMode: .v2(.init()) + versionDependentMode: .v3(.init()) ) /// activate *Zenzai* - Neural Kana-Kanji Conversiion Engine @@ -188,7 +213,7 @@ public struct ConvertRequestOptions: Sendable { /// - inferenceLimit: applying inference count limitation. Smaller limit makes conversion faster but quality will be worse. (Default: 10) /// - requestRichCandidates: when this flag is true, the converter spends more time but generate richer N-Best candidates for candidate list view. Usually this option is not recommended for live conversion. /// - versionDependentMode: specify zenz model version and its configuration. - public static func on(weight: URL, inferenceLimit: Int = 10, requestRichCandidates: Bool = false, versionDependentMode: ZenzaiVersionDependentMode = .v2(.init())) -> Self { + public static func on(weight: URL, inferenceLimit: Int = 10, requestRichCandidates: Bool = false, versionDependentMode: ZenzaiVersionDependentMode = .v3(.init())) -> Self { ZenzaiMode( enabled: true, weightURL: weight, diff --git a/Sources/KanaKanjiConverterModule/Converter/KanaKanjiConverter.swift b/Sources/KanaKanjiConverterModule/Converter/KanaKanjiConverter.swift index 25a1566..466d854 100644 --- a/Sources/KanaKanjiConverterModule/Converter/KanaKanjiConverter.swift +++ b/Sources/KanaKanjiConverterModule/Converter/KanaKanjiConverter.swift @@ -62,7 +62,7 @@ import SwiftUtils return [] } guard options.zenzaiMode.versionDependentMode.version == .v2 else { - print("next character prediction requires zenz-v2 models, not zenz-v1") + print("next character prediction requires zenz-v2 models, not zenz-v1 nor zenz-v3 and later") return [] } let results = zenz.predictNextCharacter(leftSideContext: leftSideContext, count: count) diff --git a/Sources/KanaKanjiConverterModule/Zenz/ZenzContext.swift b/Sources/KanaKanjiConverterModule/Zenz/ZenzContext.swift index e4bdc62..0d907d0 100644 --- a/Sources/KanaKanjiConverterModule/Zenz/ZenzContext.swift +++ b/Sources/KanaKanjiConverterModule/Zenz/ZenzContext.swift @@ -263,30 +263,75 @@ class ZenzContext { conditions.append("辞書:\(userDictionaryPrompt)") } // プロフィールがある場合はこれを条件に追加 - if case .v2(let mode) = versionDependentConfig, let profile = mode.profile, !profile.isEmpty { - let pf = profile.suffix(25) - conditions.append("プロフィール:\(profile)") + switch versionDependentConfig { + case .v1: break + case .v2(let mode): + if let profile = mode.profile, !profile.isEmpty { + let pf = profile.suffix(25) + conditions.append("プロフィール:\(pf)") + } + case .v3(let mode): + if let profile = mode.profile, !profile.isEmpty { + let pf = profile.suffix(25) + conditions.append("\u{EE03}\(pf)") + } + if let topic = mode.topic, !topic.isEmpty { + let tp = topic.suffix(25) + conditions.append("\u{EE04}\(tp)") + } + if let style = mode.style, !style.isEmpty { + let st = style.suffix(25) + conditions.append("\u{EE05}\(st)") + } + if let preference = mode.preference, !preference.isEmpty { + let pr = preference.suffix(25) + conditions.append("\u{EE06}\(pr)") + } } // 左文脈を取得 // プロフィールがある場合はこれを条件に追加 - let leftSideContext = if case .v2(let mode) = versionDependentConfig, let leftSideContext = mode.leftSideContext { - String(leftSideContext.suffix(40)) - } else { - "" + let leftSideContext: String = switch versionDependentConfig { + case .v1: "" + case .v2(let mode): + if let leftSideContext = mode.leftSideContext { + String(leftSideContext.suffix(40)) + } else { + "" + } + case .v3(let mode): + if let leftSideContext = mode.leftSideContext { + String(leftSideContext.suffix(40)) + } else { + "" + } } let inputTag = "\u{EE00}" let outputTag = "\u{EE01}" let contextTag = "\u{EE02}" // プロンプトを作成 - let prompt: String = if !conditions.isEmpty { - // 条件がemptyでない場合は「・」でつなぎ、「発言:」を末尾に追加 - inputTag + input + contextTag + conditions.joined(separator: "・") + "・発言:\(leftSideContext)" + outputTag - } else if !leftSideContext.isEmpty { - // 条件がemptyの場合、単にleftSideContextを追加 - inputTag + input + contextTag + leftSideContext + outputTag - } else { - // そのまま + let prompt: String = switch versionDependentConfig { + case .v1: inputTag + input + outputTag + case .v2: + if !conditions.isEmpty { + // 条件がemptyでない場合は「・」でつなぎ、「発言:」を末尾に追加 + inputTag + input + contextTag + conditions.joined(separator: "・") + "・発言:\(leftSideContext)" + outputTag + } else if !leftSideContext.isEmpty { + // 条件がemptyの場合、単にleftSideContextを追加 + inputTag + input + contextTag + leftSideContext + outputTag + } else { + // そのまま + inputTag + input + outputTag + } + case .v3: + if !leftSideContext.isEmpty { + // leftSideContextがEmptyでなければ下記の通り処理 + // contextがinputに前置されるように変更された(KV-cachingの効率化のため) + conditions.joined(separator: "") + contextTag + leftSideContext + inputTag + input + outputTag + } else { + // そのまま + conditions.joined(separator: "") + inputTag + input + outputTag + } } // Therefore, tokens = prompt_tokens + candidate_tokens is an appropriate operation. let prompt_tokens = self.tokenize(text: prompt, add_bos: true, add_eos: false)