Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new models for gemini translation #571

Merged
merged 36 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b189683
perf: update gemini package
Jerry23011 May 28, 2024
9661e35
feat: add new models for gemini translation
Jerry23011 May 28, 2024
6d2e66b
fix: type in openai service
Jerry23011 May 28, 2024
93fd77b
fix: type in ezconstkey annotation
Jerry23011 May 28, 2024
e32fa30
fix: type in a few files
Jerry23011 May 28, 2024
659fa9c
perf: update chatgpt link
Jerry23011 May 28, 2024
930e7e5
fix: build error
Jerry23011 May 28, 2024
a9b784e
perf: remove gemini keys in ezschemeparser
Jerry23011 May 28, 2024
53f7ada
format: add back empty line
Jerry23011 May 28, 2024
a13034a
Merge branch 'openai-type-fix' into gemini-update
Jerry23011 May 28, 2024
8df4ebb
perf: rename opanaiusagestatus to serviceusagestatus
Jerry23011 May 28, 2024
a0d3fa7
perf: remove queryTextType since gemini supports all type
Jerry23011 May 28, 2024
cff774a
perf: update xcstring state
Jerry23011 May 28, 2024
cc78117
perf: enable LLMStreamService to change models quickly
tisfeng May 28, 2024
8446f11
fix: typo in gemini service
Jerry23011 May 28, 2024
2d5b9ae
perf: support dictionary and sentence query
Jerry23011 May 28, 2024
e65b2f7
perf: implement results handle for gemini
Jerry23011 May 28, 2024
cbbda54
docs: update sponsor list
tisfeng May 29, 2024
6cd291e
Merge branch 'dev' into gemini-update
tisfeng May 29, 2024
7e6c7a6
perf: remove unused annotation
Jerry23011 May 29, 2024
ad879a4
perf: remove unused code
Jerry23011 May 29, 2024
fcde137
perf: implement systemInstruction and role/model prompt
Jerry23011 May 30, 2024
f6e92b7
perf: remove redundant code
Jerry23011 May 30, 2024
f26ec5c
fix: gemini stream ui
Jerry23011 May 30, 2024
b8bbbcd
Merge branch 'dev' into gemini-update
tisfeng May 31, 2024
e68b614
Merge branch 'dev' into gemini-update
tisfeng May 31, 2024
74d919d
perf: move promptContent to extension GeminiService
Jerry23011 Jun 2, 2024
f0c9bb2
Merge branch 'dev' into gemini-update
Jerry23011 Jun 2, 2024
9c8c34b
perf: optimize usage of handleResult
Jerry23011 Jun 2, 2024
adff638
perf: add bool argument to opt out LLM systemPrompt
Jerry23011 Jun 2, 2024
19c31d4
Merge branch 'dev' into gemini-update
tisfeng Jun 2, 2024
5c0082d
fix: use dict system prompt when querying a word
tisfeng Jun 3, 2024
6301f08
perf: add dynamic variable for gemini available models
Jerry23011 Jun 4, 2024
a6dcd5d
refactor: replace all AI const stored keys with dynamic variables
tisfeng Jun 4, 2024
22bf492
fix: gemini-1.0-pro model cannot use system instruction
tisfeng Jun 4, 2024
67a63f1
perf: change system prompt to user prompt for gemini-1.0-pro model
tisfeng Jun 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Easydict.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3573,7 +3573,7 @@
repositoryURL = "https://github.com/google/generative-ai-swift";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.4.4;
minimumVersion = 0.5.3;
};
};
03022F1A2B35DEBA00B63209 /* XCRemoteSwiftPackageReference "Hue" */ = {
Expand Down
4 changes: 2 additions & 2 deletions Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/generative-ai-swift",
"state" : {
"revision" : "dcbdb5e591e1aa2bb68851dc7515f6b0a59026cd",
"version" : "0.4.7"
"revision" : "5d750b80651da9721c37c5eb1fc0b6750d1884d3",
"version" : "0.5.3"
}
},
{
Expand Down
8 changes: 4 additions & 4 deletions Easydict/App/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -2410,18 +2410,18 @@
}
}
},
"service.configuration.gemini.api_key.title" : {
"service.configuration.gemini.api_key.placeholder" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "API Key"
"value" : "xxxxxxxxxxxxx"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "API Key"
"state" : "needs_review",
tisfeng marked this conversation as resolved.
Show resolved Hide resolved
"value" : "xxxxxxxxxxxxx"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,32 @@ extension Defaults.Keys {
static let aliAccessKeySecret = Key<String?>(EZAliAccessKeySecret)

// Gemni
static let geminiAPIKey = Key<String?>(EZGeminiAPIKey)
static let geminiAPIKey = Key<String?>("EZGeminiAPIKey")
static let geminiTranslation = Key<String>(
translationStoredKey(.gemini),
default: "1"
)
static let geminiDictionary = Key<String>(
dictionaryStoredKey(.gemini),
default: "1"
)
static let geminiSentence = Key<String>(
sentenceStoredKey(.gemini),
default: "1"
)
static let geminiServiceUsageStatus = Key<GeminiUsageStatus>(
serviceUsageStatusStoredKey(.gemini),
default: .default
)
static let geminiModel = Key<String>("geminiModel", default: GeminiModel.gemini1_5_flash.rawValue)
static let geminiAvailableModels = Key<String?>(
"geminiAvailableModels",
tisfeng marked this conversation as resolved.
Show resolved Hide resolved
default: GeminiModel.allCases.map { $0.rawValue }.joined(separator: ",")
)
static let geminiVaildModels = Key<Array>(
"geminiVaildModels",
default: GeminiModel.allCases.map { $0.rawValue }
)
}

/// shortcut
Expand Down
24 changes: 19 additions & 5 deletions Easydict/Swift/Service/Gemini/GeminiService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Defaults
import Foundation
import GoogleGenerativeAI

// TODO: add a LLM stream service base class, make both OpenAI and Gemini inherit from it.
@objc(EZGeminiService)
public final class GeminiService: LLMStreamService {
// MARK: Public
Expand All @@ -28,7 +27,7 @@ public final class GeminiService: LLMStreamService {
}

override public func queryTextType() -> EZQueryTextType {
[.translation]
[.translation, .dictionary, .sentence]
tisfeng marked this conversation as resolved.
Show resolved Hide resolved
}

override public func translate(
Expand All @@ -39,11 +38,14 @@ public final class GeminiService: LLMStreamService {
) {
Task {
do {
result.from = from
result.to = to
result.isStreamFinished = false
let translationPrompt = translationPrompt(text: text, from: from, to: to)
let prompt = LLMStreamService.translationSystemPrompt +
"\n" + translationPrompt
let model = GenerativeModel(
name: "gemini-pro",
name: model,
apiKey: apiKey,
safetySettings: [
harassmentBlockNone,
Expand All @@ -53,8 +55,6 @@ public final class GeminiService: LLMStreamService {
]
)

result.isStreamFinished = false

var resultString = ""

// Gemini Docs: https://github.com/google/generative-ai-swift
Expand Down Expand Up @@ -123,6 +123,20 @@ public final class GeminiService: LLMStreamService {
Defaults[.geminiAPIKey] ?? ""
}

override var availableModels: [String] {
Defaults[.geminiVaildModels]
}

override var model: String {
get {
Defaults[.geminiModel]
}
set {
// easydict://writeKeyValue?EZGeminiModelKey=gemini-1.5-flash
Defaults[.geminiModel] = newValue
}
}

// MARK: Private

// Set Gemini safety level to BLOCK_NONE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,232 @@
// Copyright © 2024 izual. All rights reserved.
//

import Combine
import Defaults
import Foundation
import SwiftUI

// MARK: - GeminiService + ConfigurableService

extension GeminiService: ConfigurableService {
func configurationListItems() -> some View {
ServiceConfigurationSecretSectionView(service: self, observeKeys: [.geminiAPIKey]) {
GeminiServiceConfigurationView(service: self)
}
}

// MARK: - GeminiServiceConfigurationView

private struct GeminiServiceConfigurationView: View {
// MARK: Lifecycle

init(service: GeminiService) {
self.service = service
self.viewModel = GeminiViewModel(service: service)
}

// MARK: Internal

let service: GeminiService

var body: some View {
ServiceConfigurationSecretSectionView(
service: service,
observeKeys: [.geminiAPIKey, .geminiAvailableModels]
) {
ServiceConfigurationSecureInputCell(
textFieldTitleKey: "service.configuration.gemini.api_key.title",
key: .geminiAPIKey
textFieldTitleKey: "service.configuration.openai.api_key.title",
key: .geminiAPIKey,
placeholder: "service.configuration.gemini.api_key.placeholder"
)
// supported models
TextField(
"service.configuration.custom_openai.supported_models.title",
text: viewModel.$availableModels ?? "",
prompt: Text("service.configuration.custom_openai.model.placeholder")
)
.padding(10.0)
Picker(
"service.configuration.openai.model.title",
selection: viewModel.$model
) {
ForEach(viewModel.validModels, id: \.self) { value in
Text(value)
}
}
.padding(10.0)

ServiceConfigurationToggleCell(
titleKey: "service.configuration.openai.translation.title",
key: .geminiTranslation
)
ServiceConfigurationToggleCell(
titleKey: "service.configuration.openai.sentence.title",
key: .geminiSentence
)
ServiceConfigurationToggleCell(
titleKey: "service.configuration.openai.dictionary.title",
key: .geminiDictionary
)
ServiceConfigurationPickerCell(
titleKey: "service.configuration.openai.usage_status.title",
key: .geminiServiceUsageStatus,
values: GeminiUsageStatus.allCases
)
}
.onDisappear {
viewModel.invalidate()
}
}

// MARK: Private

@ObservedObject private var viewModel: GeminiViewModel
}

// MARK: - GeminiViewModel

private class GeminiViewModel: ObservableObject {
// MARK: Lifecycle

init(service: GeminiService) {
self.service = service
Defaults.publisher(.geminiModel, options: [])
.removeDuplicates()
.sink { _ in
self.modelChanged()
}
.store(in: &cancellables)
Defaults.publisher(.geminiAvailableModels)
.removeDuplicates()
.throttle(for: 0.1, scheduler: DispatchQueue.main, latest: true)
.sink { _ in
self.modelsTextChanged()
}
.store(in: &cancellables)
}

// MARK: Internal

let service: GeminiService

@Default(.geminiModel) var model
@Default(.geminiAvailableModels) var availableModels

@Published var validModels: [String] = []

func invalidate() {
cancellables.forEach { $0.cancel() }
cancellables.removeAll()
}

// MARK: Private

private var cancellables: Set<AnyCancellable> = []

private func modelChanged() {
if !validModels.contains(model) {
if model.isEmpty {
availableModels = ""
} else {
if availableModels?.isEmpty == true {
availableModels = model
} else {
availableModels = /* "\(model), " + */ availableModels ?? ""
tisfeng marked this conversation as resolved.
Show resolved Hide resolved
tisfeng marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
serviceConfigChanged()
}

private func modelsTextChanged() {
guard let availableModels else { return }

validModels = availableModels.components(separatedBy: ",")
.map { $0.trim() }.filter { !$0.isEmpty }

if validModels.isEmpty {
model = ""
} else if !validModels.contains(model) {
model = validModels[0]
}

Defaults[.geminiVaildModels] = validModels
}

private func serviceConfigChanged() {
objectWillChange.send()

let userInfo: [String: Any] = [
EZWindowTypeKey: service.windowType.rawValue,
EZServiceTypeKey: service.serviceType().rawValue,
]
let notification = Notification(name: .serviceHasUpdated, object: nil, userInfo: userInfo)
NotificationCenter.default.post(notification)
}
}

// MARK: - GeminiModel

// swiftlint:disable identifier_name
enum GeminiModel: String, CaseIterable {
// Docs: https://ai.google.dev/gemini-api/docs/models/gemini

// RPM: Requests per minute, TPM: Tokens per minute
// RPD: Requests per day, TPD: Tokens per day
case gemini1_0_pro = "gemini-1.0-pro" // Free 15 RPM/32,000 TPM, 1,500 RPD/46,080,000 TPD (n/a context length)
case gemini1_5_flash = "gemini-1.5-flash" // Free 15 RPM/100million TPM, 1500 RPD/ n/a TPD (1048k context length)
case gemini1_5_pro = "gemini-1.5-pro" // Free 2 RPM/32,000 TPM, 50 RPD/46,080,000 TPD (1048k context length)
}

// MARK: EnumLocalizedStringConvertible

// swiftlint:enable identifier_name

extension GeminiModel: EnumLocalizedStringConvertible {
var title: String {
rawValue
tisfeng marked this conversation as resolved.
Show resolved Hide resolved
}
}

// MARK: Defaults.Serializable

extension GeminiModel: Defaults.Serializable {}

// MARK: - GeminiUsageStatus

enum GeminiUsageStatus: String, CaseIterable {
case `default` = "0"
case alwaysOff = "1"
case alwaysOn = "2"
tisfeng marked this conversation as resolved.
Show resolved Hide resolved
}

// MARK: EnumLocalizedStringConvertible

extension GeminiUsageStatus: EnumLocalizedStringConvertible {
var title: String { // Use same xcstring with openai for title
switch self {
case .default:
NSLocalizedString(
"service.configuration.openai.usage_status_default.title",
bundle: .main,
comment: ""
)
case .alwaysOff:
NSLocalizedString(
"service.configuration.openai.usage_status_always_off.title",
bundle: .main,
comment: ""
)
case .alwaysOn:
NSLocalizedString(
"service.configuration.openai.usage_status_always_on.title",
bundle: .main,
comment: ""
)
}
}
}

// MARK: Defaults.Serializable

extension GeminiUsageStatus: Defaults.Serializable {}