使わなくなったiPhoneをMLサーバー化する

:::note info
この記事は[ZOZO Advent Calendar 2023](https://qiita.com/advent-calendar/2023/zozo)Vol.5の7日目の記事です。
:::

# はじめに
こんにちは。株式会社ZOZOでiOSエンジニアをしている[@rei_nakaoka](https://twitter.com/rei_nakaoka)です。１年ぶりの投稿です。使わなくなったiPhoneで何かしてみるシリーズ第二弾として今回はiPhoneを簡易的なMLサーバーにしてみました。

# 使用したライブラリ・フレームワーク

* [Vapor](https://github.com/vapor/vapor)
* [Vision](https://developer.apple.com/documentation/vision/)

# Vaporとは
VaporとはSwiftでWebアプリを作るためのフレームワークです。実はこのVaporですが、iOSに対応しており、アプリ上でWebサーバーを構築することができます。リポジトリの[Package.swift](https://github.com/vapor/vapor/blob/main/Package.swift)の中身を見るとわかります。

```swift
let package = Package(
    name: "vapor",
    platforms: [
        .macOS(.v10_15),
        .iOS(.v13),
        .tvOS(.v13),
        .watchOS(.v6)
    ],
```

# iOSアプリ上でWebサーバーを構築する
まずはiOSアプリ上でWebサーバーを起動してみます。他のWebフレームワークと同じような感じでサーバーの設定なりエンドポイントを記述します。

```Server.swift
import Vapor

final class Server {

    var app: Application
    let port: Int

    init(port: Int) {
        self.port = port
        app = Application(.development)
    }

    func start() throws {
        Task(priority: .background) {
            do {
                try configure(app)
                try app.run()
            } catch {
                fatalError(error.localizedDescription)
            }
        }
    }

    func configure(_ app: Application) throws {
        app.http.server.configuration.hostname = "0.0.0.0"
        app.http.server.configuration.port = port

        try defineRoutes(app)
    }

    func defineRoutes(_ app: Application) throws {
        app.get { req in
            "Hello, world!"
        }
    }
}
```

そして上記のstartメソッドをアプリ起動時に呼ぶようにして、ビルドします。

```swift
@main
struct Vapor_with_iOSApp: App {

    let server = Server(port: 8080)

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
                    try? server.start()
                }
        }
    }
}
```

そして、iPhoneのプライベートIPを調べてcurlコマンドでリクエストを送ってみると「Hello, world!」が無事返ってきます。

```
$ curl http://（プライベートIP）:8080
Hello, world!
```

# 画像認識エンドポイントの追加
無事にサーバーが起動できていることが確認できたので、次は画像認識エンドポイントを追加します。今回は送られてきた画像に何の文字が書かれているのかを判定してレスポンスとして返します。文字の判定にはVisionフレームワークを使用します。

まずはテキスト認識クラスを定義します。

```TextRecognizer.swift
import Vapor
import Vision

class TextRecognizer {
    func recognizeText(from imageData: Data, on eventLoop: EventLoop) -> EventLoopFuture<String> {
        let promise = eventLoop.makePromise(of: String.self)

        let handler = VNImageRequestHandler(data: imageData, options: [:])
        let request = VNRecognizeTextRequest { (request, error) in
            guard let observations = request.results as? [VNRecognizedTextObservation],
                  error == nil else {
                promise.fail(error ?? Abort(.internalServerError))
                return
            }

            let recognizedText = observations.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n")
            promise.succeed(recognizedText)
        }

        do {
            try handler.perform([request])
        } catch {
            promise.fail(error)
        }

        return promise.futureResult
    }
}
```

次にテキスト認識のエンドポイントを追加します。

```Server.swift
let let textRecognizer = TextRecognizer()

func defineRoutes(_ app: Application) throws {
    app.get { req in
        "Hello, world!"
    }

    app.post("recognize-text") { [self] req -> EventLoopFuture<String> in
        let imageBase64Data = try req.content.decode(ImageData.self)
        guard let imageData = Data(base64Encoded: imageBase64eData.image) else {
            throw Abort(.badRequest)
        }
        return textRecognizer.recognizeText(from: imageData, on: req.eventLoop)
    }
}
```

再ビルドしてcurlコマンドでBase64にエンコードした以下の画像を送ってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256520/862742f7-fa7d-0b2b-27c4-f5fcd4b412ae.png)

```
$ base64 -i sample.png -o image.txt
$ curl -X POST -H "Content-Type: application/json" \
     -d '{"image": "'$(cat image.txt)'"}' \
     http://（プライベートIP）:8080/recognize-text
ZOZOTOWN
```

無事に認識結果が返ってきました。

# まとめ
今回はVapor+VisionフレームワークでiPhoneをMLサーバー化してみました。iOS上で動かすことができるのでVisionなどのAppleの公式のフレームワークと組み合わせて使用することができるのが魅力的ですね。
全体の実装は以下のリポジトリにあるので気になる方はこちらから見てみてください。

https://github.com/NakaokaRei/Vapor-with-iOS.git

# 参考
https://www.kodeco.com/31498014-running-a-web-server-on-ios-with-vapor?page=1#toc-anchor-001