Skip to content

Commit

Permalink
Add GeminiService Support (#297)
Browse files Browse the repository at this point in the history
* test Google Gemini

* perf: use Task to wrap gemini async stream

* feat: initiate support for Google Gemini

* fix: build error

* fix: missing GeminiTranslateType file

* perf: restore CaiyunService

* perf: improve gemini prompt

from openai translate

* refractor: define static property for translation prompt

Co-Authored-By: Kyle <kyle201817146@gmail.com>

* perf: bump debug version

* fix: resolve deprecated warnings

* refractor: remove GeminiTranslateType

* perf: change min version back to 11.0

* perf: resolve xcode warning

Xcode Autofix

* perf: adjust gemini safety level

#297 (comment)

* refractor: manual error handling

* perf: improve error message for Gemini

* fix: content streaming on macOS 12+

* perf: improve code, remove logs

* perf: update generative-ai-swift to 0.4.7

* perf: rename variable

---------

Co-authored-by: tisfeng <tisfeng@gmail.com>
Co-authored-by: Kyle <kyle201817146@gmail.com>
  • Loading branch information
3 people authored Jan 28, 2024
1 parent 896d04d commit 1a89fb9
Show file tree
Hide file tree
Showing 13 changed files with 261 additions and 3 deletions.
41 changes: 41 additions & 0 deletions Easydict.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
03008B2B2940D3230062B821 /* EZDeepLTranslate.m in Sources */ = {isa = PBXBuildFile; fileRef = 03008B2A2940D3230062B821 /* EZDeepLTranslate.m */; };
03008B2E2941956D0062B821 /* EZURLSchemeHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 03008B2D2941956D0062B821 /* EZURLSchemeHandler.m */; };
03008B3F29444B0A0062B821 /* NSView+EZAnimatedHidden.m in Sources */ = {isa = PBXBuildFile; fileRef = 03008B3E29444B0A0062B821 /* NSView+EZAnimatedHidden.m */; };
03022F192B3591AE00B63209 /* GoogleGenerativeAI in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F182B3591AE00B63209 /* GoogleGenerativeAI */; };
03022F1C2B35DEBA00B63209 /* Hue in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F1B2B35DEBA00B63209 /* Hue */; };
03022F1F2B36CF3100B63209 /* SwiftShell in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F1E2B36CF3100B63209 /* SwiftShell */; };
03022F222B36D1A400B63209 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 03022F212B36D1A400B63209 /* SnapKit */; };
Expand Down Expand Up @@ -83,6 +84,7 @@
03882F9029D95044005B5A52 /* ToastWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 03882F8829D95044005B5A52 /* ToastWindowController.xib */; };
03882F9129D95044005B5A52 /* CTCommon.m in Sources */ = {isa = PBXBuildFile; fileRef = 03882F8929D95044005B5A52 /* CTCommon.m */; };
03882F9229D95044005B5A52 /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 03882F8C29D95044005B5A52 /* Info.plist */; };
038A72402B62C0B9004995E3 /* String+Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038A723F2B62C0B9004995E3 /* String+Regex.swift */; };
038EA1AA2B41169C008A6DD1 /* ZipArchive in Frameworks */ = {isa = PBXBuildFile; productRef = 038EA1A92B41169C008A6DD1 /* ZipArchive */; };
038EA1AD2B41282F008A6DD1 /* MJExtension in Frameworks */ = {isa = PBXBuildFile; productRef = 038EA1AC2B41282F008A6DD1 /* MJExtension */; };
0396D611292C932F006A11D9 /* EZSelectLanguageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 0396D610292C932F006A11D9 /* EZSelectLanguageCell.m */; };
Expand Down Expand Up @@ -260,6 +262,7 @@
9672D7D22B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */; };
A0B65CA0F31AC8ECFB8347CC /* Pods_EasydictTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 378E73A7EA8FC8FB9C975A63 /* Pods_EasydictTests.framework */; };
B87AC7E36367075BA5D13234 /* Pods_Easydict.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6372B33DFF803C7096A82250 /* Pods_Easydict.framework */; };
C415C0AD2B450D4800A9D231 /* GeminiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C415C0AC2B450D4800A9D231 /* GeminiService.swift */; };
C4DD01E92B12B3C80025EE8E /* TencentService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01E82B12B3C80025EE8E /* TencentService.swift */; };
C4DD01EB2B12BA250025EE8E /* TencentResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */; };
C4DD01ED2B12BE9B0025EE8E /* TencentTranslateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */; };
Expand Down Expand Up @@ -445,6 +448,7 @@
03882F8A29D95044005B5A52 /* CTView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CTView.h; sourceTree = "<group>"; };
03882F8B29D95044005B5A52 /* CoolToast.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoolToast.h; sourceTree = "<group>"; };
03882F8C29D95044005B5A52 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
038A723F2B62C0B9004995E3 /* String+Regex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Regex.swift"; sourceTree = "<group>"; };
0396D60F292C932F006A11D9 /* EZSelectLanguageCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZSelectLanguageCell.h; sourceTree = "<group>"; };
0396D610292C932F006A11D9 /* EZSelectLanguageCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZSelectLanguageCell.m; sourceTree = "<group>"; };
0396D613292CC4C3006A11D9 /* EZLocalStorage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZLocalStorage.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -755,6 +759,7 @@
9672D7D02B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "MASShortcutBinder+EZMASShortcutBinder.h"; sourceTree = "<group>"; };
9672D7D12B4008B40023B8FB /* MASShortcutBinder+EZMASShortcutBinder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "MASShortcutBinder+EZMASShortcutBinder.m"; sourceTree = "<group>"; };
A230E9A2358C7FBC7FB26189 /* Pods-EasydictTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-EasydictTests.debug.xcconfig"; path = "Target Support Files/Pods-EasydictTests/Pods-EasydictTests.debug.xcconfig"; sourceTree = "<group>"; };
C415C0AC2B450D4800A9D231 /* GeminiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeminiService.swift; sourceTree = "<group>"; };
C4DD01E82B12B3C80025EE8E /* TencentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentService.swift; sourceTree = "<group>"; };
C4DD01EA2B12BA250025EE8E /* TencentResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentResponse.swift; sourceTree = "<group>"; };
C4DD01EC2B12BE9B0025EE8E /* TencentTranslateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TencentTranslateType.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -806,6 +811,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
03022F192B3591AE00B63209 /* GoogleGenerativeAI in Frameworks */,
2721E4D02AFE920700A059AC /* Alamofire in Frameworks */,
03022F1F2B36CF3100B63209 /* SwiftShell in Frameworks */,
038030952B4106800009230C /* CocoaLumberjack in Frameworks */,
Expand Down Expand Up @@ -1198,6 +1204,14 @@
path = Kit;
sourceTree = "<group>";
};
038A723E2B62C07B004995E3 /* String */ = {
isa = PBXGroup;
children = (
038A723F2B62C0B9004995E3 /* String+Regex.swift */,
);
path = String;
sourceTree = "<group>";
};
0396D612292CBDFD006A11D9 /* Storage */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1317,6 +1331,7 @@
isa = PBXGroup;
children = (
62E2BF462B4082BA00E42D38 /* Ali */,
C415C0AB2B450C4500A9D231 /* Gemini */,
17BCAEF22B0DFF9000A7D372 /* Niutrans */,
2746AEBF2AF95040005FE0A1 /* Caiyun */,
C4DD01E72B12B3B00025EE8E /* Tencent */,
Expand Down Expand Up @@ -2166,6 +2181,14 @@
path = Pods;
sourceTree = "<group>";
};
C415C0AB2B450C4500A9D231 /* Gemini */ = {
isa = PBXGroup;
children = (
C415C0AC2B450D4800A9D231 /* GeminiService.swift */,
);
path = Gemini;
sourceTree = "<group>";
};
C4A40A9B2AC0168400B8E6EF /* Recovered References */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -2250,6 +2273,7 @@
EA9943E62B534D7C00EE7B97 /* Extensions */ = {
isa = PBXGroup;
children = (
038A723E2B62C07B004995E3 /* String */,
EAED41F02B54B1A60005FE0A /* QueryService+ConfigurableService */,
EA9943E72B534D8900EE7B97 /* LanguageDetectOptimizeExtensions.swift */,
EA9943ED2B5353AB00EE7B97 /* WindowTypeExtensions.swift */,
Expand Down Expand Up @@ -2375,6 +2399,7 @@
038EA1A92B41169C008A6DD1 /* ZipArchive */,
038EA1AC2B41282F008A6DD1 /* MJExtension */,
EA3B81FB2B52555C004C0E8B /* Defaults */,
03022F182B3591AE00B63209 /* GoogleGenerativeAI */,
);
productName = Bob;
productReference = C99EEB182385796700FEE666 /* Easydict-debug.app */;
Expand Down Expand Up @@ -2434,6 +2459,7 @@
038EA1A82B41169C008A6DD1 /* XCRemoteSwiftPackageReference "ZipArchive" */,
038EA1AB2B41282F008A6DD1 /* XCRemoteSwiftPackageReference "MJExtension" */,
EA3B81FA2B52555C004C0E8B /* XCRemoteSwiftPackageReference "Defaults" */,
03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */,
);
productRefGroup = C99EEB192385796700FEE666 /* Products */;
projectDirPath = "";
Expand Down Expand Up @@ -2758,8 +2784,10 @@
03B0232229231FA6001C7E63 /* NSImage+MM.m in Sources */,
03BB2DEF29F59C8A00447EDD /* EZSymbolImageButton.m in Sources */,
0A2BA9642B4A3CCD002872A4 /* Notification+Name.swift in Sources */,
C415C0AD2B450D4800A9D231 /* GeminiService.swift in Sources */,
62A2D03F2A82967F007EEB01 /* EZBingRequest.m in Sources */,
03BDA7BE2A26DA280079D04F /* XPMCountedArgument.m in Sources */,
038A72402B62C0B9004995E3 /* String+Regex.swift in Sources */,
03D35DAA2AA6C49B00B023FE /* NSString+EZRegex.m in Sources */,
03B022FE29231FA6001C7E63 /* EZBaseQueryViewController.m in Sources */,
DC6D9C892B3969510055EFFC /* Appearance.swift in Sources */,
Expand Down Expand Up @@ -3318,6 +3346,14 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/google/generative-ai-swift";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.4.4;
};
};
03022F1A2B35DEBA00B63209 /* XCRemoteSwiftPackageReference "Hue" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/zenangst/Hue";
Expand Down Expand Up @@ -3425,6 +3461,11 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
03022F182B3591AE00B63209 /* GoogleGenerativeAI */ = {
isa = XCSwiftPackageProductDependency;
package = 03022F172B3591AE00B63209 /* XCRemoteSwiftPackageReference "generative-ai-swift" */;
productName = GoogleGenerativeAI;
};
03022F1B2B35DEBA00B63209 /* Hue */ = {
isa = XCSwiftPackageProductDependency;
package = 03022F1A2B35DEBA00B63209 /* XCRemoteSwiftPackageReference "Hue" */;
Expand Down
9 changes: 9 additions & 0 deletions Easydict.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@
"version" : "10.19.1"
}
},
{
"identity" : "generative-ai-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/google/generative-ai-swift",
"state" : {
"revision" : "dcbdb5e591e1aa2bb68851dc7515f6b0a59026cd",
"version" : "0.4.7"
}
},
{
"identity" : "googleappmeasurement",
"kind" : "remoteSourceControl",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Gemini.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions Easydict/App/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,23 @@
}
}
},
"gemini_translate" : {
"comment" : "The name of Gemini Translate",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Gemini Translate"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Gemini 翻译"
}
}
}
},
"GitHub:" : {
"localizations" : {
"zh-Hans" : {
Expand Down
139 changes: 139 additions & 0 deletions Easydict/Feature/Service/Gemini/GeminiService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//
// GeminiService.swift
// Easydict
//
// Created by Jerry on 2024-01-02.
// Copyright © 2024 izual. All rights reserved.
//

import Foundation
import GoogleGenerativeAI

@objc(EZGeminiService)
public final class GeminiService: QueryService {
override public func serviceType() -> ServiceType {
.gemini
}

override public func link() -> String? {
"https://bard.google.com/chat"
}

override public func name() -> String {
NSLocalizedString("gemini_translate", comment: "The name of Gemini Translate")
}

// https://ai.google.dev/available_regions
private static let unsupportedLanguages: [Language] = [.persian, .filipino, .khmer, .lao, .malay, .mongolian, .burmese, .telugu, .tamil, .urdu]

override public func supportLanguagesDictionary() -> MMOrderedDictionary<AnyObject, AnyObject> {
// TODO: Replace MMOrderedDictionary.
let orderedDict = MMOrderedDictionary<AnyObject, AnyObject>()
for language in EZLanguageManager.shared().allLanguages {
let value = language.rawValue
if !GeminiService.unsupportedLanguages.contains(language) {
orderedDict.setObject(value as NSString, forKey: language.rawValue as NSString)
}
}

return orderedDict
}

override public func ocr(_: EZQueryModel) async throws -> EZOCRResult {
NSLog("Gemini Translate does not support OCR")
throw QueryServiceError.notSupported
}

override public func needPrivateAPIKey() -> Bool {
true
}

override public func hasPrivateAPIKey() -> Bool {
if apiKey == defaultAPIKey {
return false
}
return true
}

private let defaultAPIKey = "" /* .decryptAES() */

// easydict://writeKeyValue?EZGeminiAPIKey=xxx
private var apiKey: String {
let apiKey = UserDefaults.standard.string(forKey: EZGeminiAPIKey)
if let apiKey, !apiKey.isEmpty {
return apiKey
} else {
return defaultAPIKey
}
}

// Set Gemini safety level to BLOCK_NONE
private static let harassmentSafety = SafetySetting(harmCategory: .harassment, threshold: .blockNone)
private static let hateSpeechSafety = SafetySetting(harmCategory: .hateSpeech, threshold: .blockNone)
private static let sexuallyExplicitSafety = SafetySetting(harmCategory: .sexuallyExplicit, threshold: .blockNone)
private static let dangerousContentSafety = SafetySetting(harmCategory: .dangerousContent, threshold: .blockNone)

private static let translationPrompt = "You are a translation expert proficient in various languages that can only translate text and cannot interpret it. You are able to accurately understand the meaning of proper nouns, idioms, metaphors, allusions or other obscure words in sentences and translate them into appropriate words by combining the context and language environment. The result of the translation should be natural and fluent, you can only return the translated text, do not show additional information and notes."

override public func autoConvertTraditionalChinese() -> Bool {
true
}

override public func translate(_ text: String, from: Language, to: Language, completion: @escaping (EZQueryResult, Error?) -> Void) {
Task {
do {
let prompt = GeminiService.translationPrompt + "Translate the following \(from.rawValue) text into \(to.rawValue): \(text)"
print("gemini prompt: \(prompt)")
let model = GenerativeModel(
name: "gemini-pro",
apiKey: apiKey,
safetySettings: [
GeminiService.harassmentSafety,
GeminiService.hateSpeechSafety,
GeminiService.sexuallyExplicitSafety,
GeminiService.dangerousContentSafety,
]
)

if #available(macOS 12.0, *) {
var resultString = ""
let outputContentStream = model.generateContentStream(prompt)

// stream response
for try await outputContent in outputContentStream {
guard let line = outputContent.text else {
return
}

resultString += line
result.translatedResults = [resultString]
completion(result, nil)
}

} else {
let outputContent = try await model.generateContent(prompt)
guard let resultString = outputContent.text else {
return
}

result.translatedResults = [resultString]
completion(result, nil)
}
} catch {
/**
https://github.com/google/generative-ai-swift/issues/89

String(describing: error)

"internalError(underlying: GoogleGenerativeAI.RPCError(httpResponseCode: 400, message: \"API key not valid. Please pass a valid API key.\", status: GoogleGenerativeAI.RPCStatus.invalidArgument))"
*/
let ezError = EZError(nsError: error)
let errorString = String(describing: error)
let errorMessage = errorString.extract(withPattern: "message: \"([^\"]*)\"") ?? errorString
ezError?.errorDataMessage = errorMessage

completion(result, ezError)
}
}
}
}
1 change: 1 addition & 0 deletions Easydict/Feature/Service/Model/EZConstKey.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ static NSString *const EZNiuTransAPIKey = @"EZNiuTransAPIKey";
static NSString *const EZCaiyunToken = @"EZCaiyunToken";
static NSString *const EZTencentSecretId = @"EZTencentSecretId";
static NSString *const EZTencentSecretKey = @"EZTencentSecretKey";
static NSString *const EZGeminiAPIKey = @"EZGeminiAPIKey";

static NSString *const EZAliAccessKeyId = @"EZAliAccessKeyId";
static NSString *const EZAliAccessKeySecret = @"EZAliAccessKeySecret";
Expand Down
1 change: 1 addition & 0 deletions Easydict/Feature/Service/Model/EZEnumTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ FOUNDATION_EXPORT EZServiceType const EZServiceTypeNiuTrans;
FOUNDATION_EXPORT EZServiceType const EZServiceTypeCaiyun;
FOUNDATION_EXPORT EZServiceType const EZServiceTypeTencent;
FOUNDATION_EXPORT EZServiceType const EZServiceTypeAli;
FOUNDATION_EXPORT EZServiceType const EZServiceTypeGemini;

FOUNDATION_EXPORT NSString *const EZQueryTextTypeKey;
FOUNDATION_EXPORT NSString *const EZIntelligentQueryTextTypeKey;
Expand Down
1 change: 1 addition & 0 deletions Easydict/Feature/Service/Model/EZEnumTypes.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
NSString *const EZServiceTypeCaiyun = @"Caiyun";
NSString *const EZServiceTypeTencent = @"Tencent";
NSString *const EZServiceTypeAli = @"Alibaba";
NSString *const EZServiceTypeGemini = @"Gemini";

NSString *const EZServiceTypeAppleDictionary = @"AppleDictionary";

Expand Down
1 change: 1 addition & 0 deletions Easydict/Feature/Service/Model/EZServiceTypes.m
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ + (instancetype)allocWithZone:(struct _NSZone *)zone {
EZServiceTypeCaiyun, [EZCaiyunService class],
EZServiceTypeTencent, [EZTencentService class],
EZServiceTypeAli, [EZAliService class],
EZServiceTypeGemini, [EZGeminiService class],
nil];
return allServiceDict;
}
Expand Down
Loading

0 comments on commit 1a89fb9

Please sign in to comment.