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

Add support of Vision API. #56

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ let package = Package(
"PubSub",
"SecretManager",
"Storage",
"Vision"
]
),
.library(
Expand Down Expand Up @@ -48,6 +49,10 @@ let package = Package(
name: "GoogleCloudPubSub",
targets: ["PubSub"]
),
.library(
name: "GoogleCloudVision",
targets: ["Vision"]
),
],
dependencies: [
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.18.0"),
Expand Down Expand Up @@ -104,6 +109,13 @@ let package = Package(
],
path: "PubSub/Sources/"
),
.target(
name: "Vision",
dependencies: [
.target(name: "Core")
],
path: "Vision/Sources"
),
.testTarget(
name: "CoreTests",
dependencies: [
Expand Down Expand Up @@ -140,5 +152,12 @@ let package = Package(
],
path: "PubSub/Tests/"
),
.testTarget(
name: "VisionTests",
dependencies: [
.target(name: "Vision")
],
path: "Vision/Tests/"
)
]
)
51 changes: 51 additions & 0 deletions Vision/Sources/API/VisionAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// VisionAPI.swift
//
//
// Created by Kostis Stefanou on 12/9/24.
//

import AsyncHTTPClient
import NIO
import Core
import Foundation

public protocol VisionAPI {
func annotateImages(_ imageRequests: [AnnotateImageRequest]) -> EventLoopFuture<VisionAnnotateImageResponse>
}

public extension VisionAPI {
func annotateImage(_ imageRequest: AnnotateImageRequest) -> EventLoopFuture<VisionAnnotateImageResponse> {
annotateImages([imageRequest])
}
}

public final class GoogleCloudVisionAPI: VisionAPI {

let endpoint: String
let request: GoogleCloudVisionRequest
let encoder = JSONEncoder()

init(request: GoogleCloudVisionRequest,
endpoint: String) {
self.request = request
self.endpoint = endpoint
}

private var annotateImagesPath: String {
"\(endpoint)/v1/images:annotate"
}

public func annotateImages(_ imageRequests: [AnnotateImageRequest]) -> EventLoopFuture<VisionAnnotateImageResponse> {
do {
let bodyDict = [
"requests": [imageRequests]
]

let body = try HTTPClient.Body.data(encoder.encode(bodyDict))
return request.send(method: .POST, path: annotateImagesPath, body: body)
} catch {
return request.eventLoop.makeFailedFuture(error)
}
}
}
56 changes: 56 additions & 0 deletions Vision/Sources/Models/AnnotateImageRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// AnnotateImageRequest.swift
//
//
// Created by Kostis Stefanou on 12/9/24.
//

import Foundation

public struct AnnotateImageRequest: Encodable {

public let image: Image
public let features: [Feature]

public init(image: Image, features: [Feature]) {
self.image = image
self.features = features
}

public init(imageUri: String) {
self.image = .init(source: .init(imageUri: imageUri))
self.features = [.init(type: .textDetection)]
}
}

// MARK: - Image

public extension AnnotateImageRequest {

struct Image: Encodable {
let source: ImageResource
}
}

// MARK: - ImageResource

extension AnnotateImageRequest.Image {

struct ImageResource: Encodable {
let imageUri: String
}
}

// MARK: - Feature

public extension AnnotateImageRequest {

struct Feature: Encodable {

enum FeatureType: String, Encodable {
case textDetection = "TEXT_DETECTION"
}

let type: FeatureType
}
}
94 changes: 94 additions & 0 deletions Vision/Sources/Models/VisionAnnotateTextResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// VisionTextToImageResponse.swift
//
//
// Created by Kostis Stefanou on 12/9/24.
//

import Core
import Foundation

public struct VisionAnnotateImageResponse: GoogleCloudModel {
public let responses: [VisionAnnotateImageResponseBody]
}

public struct VisionAnnotateImageResponseBody: GoogleCloudModel {
public let textAnnotations: [TextAnnotation]
public let fullTextAnnotation: FullTextAnnotation
}

public struct FullTextAnnotation: GoogleCloudModel {
public let pages: [Page]
public let text: String
}

public extension FullTextAnnotation {

struct Page: GoogleCloudModel {
public let property: WordProperty
public let width, height: Int
public let blocks: [Block]
}
}

public extension FullTextAnnotation.Page {

struct Block: GoogleCloudModel {
public let boundingBox: Bounding
public let paragraphs: [Paragraph]
public let blockType: String
}
}

public extension FullTextAnnotation.Page.Block {

struct Paragraph: GoogleCloudModel {
public let boundingBox: Bounding
public let words: [Word]
}
}

// MARK : - Common Models

public struct Word: GoogleCloudModel {
public let property: WordProperty?
public let boundingBox: Bounding
public let symbols: [Symbol]
}

public struct WordProperty: GoogleCloudModel {
public let detectedLanguages: [DetectedLanguage]
}

public struct Symbol: GoogleCloudModel {
public let boundingBox: Bounding
public let text: String
public let property: SymbolProperty?
}

public struct SymbolProperty: GoogleCloudModel {
let detectedBreak: DetectedBreak
}

public struct TextAnnotation: GoogleCloudModel {
public let locale: String?
public let description: String
public let boundingPoly: Bounding
}

public struct Bounding: GoogleCloudModel {
public let vertices: [Vertex]
}

public struct DetectedLanguage: GoogleCloudModel {
public let languageCode: String
public let confidence: Double
}

public struct Vertex: GoogleCloudModel {
let x, y: Int
}

public struct DetectedBreak: GoogleCloudModel {
public let type: String
}
63 changes: 63 additions & 0 deletions Vision/Sources/VisionClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//
// VisionClient.swift
//
//
// Created by Kostis Stefanou on 12/9/24.
//

import Core
import Foundation
import AsyncHTTPClient
import NIO

public final class GoogleCloudVisionClient {

public var vision: VisionAPI
var translationRequest: GoogleCloudVisionRequest

/// Initialize a client for interacting with the Google Cloud Translation API
/// - Parameter credentials: The Credentials to use when authenticating with the APIs
/// - Parameter config: The configuration for the Translation API
/// - Parameter httpClient: An `HTTPClient` used for making API requests.
/// - Parameter eventLoop: The EventLoop used to perform the work on.
/// - Parameter base: The base URL to use for the Translation API
public init(credentials: GoogleCloudCredentialsConfiguration,
config: GoogleCloudVisionConfiguration,
httpClient: HTTPClient,
eventLoop: EventLoop,
base: String = "https://vision.googleapis.com") throws {
/// A token implementing `OAuthRefreshable`. Loaded from credentials specified by `GoogleCloudCredentialsConfiguration`.
let refreshableToken = OAuthCredentialLoader.getRefreshableToken(credentials: credentials,
withConfig: config,
andClient: httpClient,
eventLoop: eventLoop)

/// Set the projectId to use for this client. In order of priority:
/// - Environment Variable (GOOGLE_PROJECT_ID)
/// - Environment Variable (PROJECT_ID)
/// - Service Account's projectID
/// - `GoogleCloudTranslationConfigurations` `project` property (optionally configured).
/// - `GoogleCloudCredentialsConfiguration's` `project` property (optionally configured).

guard let projectId = ProcessInfo.processInfo.environment["GOOGLE_PROJECT_ID"] ??
ProcessInfo.processInfo.environment["PROJECT_ID"] ??
(refreshableToken as? OAuthServiceAccount)?.credentials.projectId ??
config.project ?? credentials.project else {
throw GoogleCloudVisionError.projectIdMissing
}

translationRequest = GoogleCloudVisionRequest(httpClient: httpClient,
eventLoop: eventLoop,
oauth: refreshableToken,
project: projectId)

vision = GoogleCloudVisionAPI(request: translationRequest, endpoint: base)
}

/// Hop to a new eventloop to execute requests on.
/// - Parameter eventLoop: The eventloop to execute requests on.
public func hopped(to eventLoop: EventLoop) -> GoogleCloudVisionClient {
translationRequest.eventLoop = eventLoop
return self
}
}
40 changes: 40 additions & 0 deletions Vision/Sources/VisionConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// VisionConfig.swift
//
//
// Created by Kostis Stefanou on 12/9/24.
//

import Core

public struct GoogleCloudVisionConfiguration: GoogleCloudAPIConfiguration {
public var scope: [GoogleCloudAPIScope]
public let serviceAccount: String
public let project: String?
public let subscription: String? = nil

public init(scope: [GoogleCloudTranslationScope], serviceAccount: String, project: String?) {
self.scope = scope
self.serviceAccount = serviceAccount
self.project = project
}

/// Create a new `GoogleCloudTranslationConfiguration` with cloud platform scope and the default service account.
public static func `default`() -> GoogleCloudVisionConfiguration {
return GoogleCloudVisionConfiguration(scope: [.cloudPlatform],
serviceAccount: "default",
project: nil)
}
}

public enum GoogleCloudTranslationScope: GoogleCloudAPIScope {
/// View and manage your data across Google Cloud Platform services

case cloudPlatform

public var value: String {
switch self {
case .cloudPlatform: return "https://www.googleapis.com/auth/cloud-platform"
}
}
}
Loading