iPhone版 ChatGPTと話そう!(その2): Ionic版から作る

OpenAIのGPT-4を使って新しいiPhoneアプリを作ります。iPhoneアプリを作るにはMacで動くXcodeという開発環境が必要です。ここではmacOS 13.4上でXcode 14.3を使い、動作確認にはiOSシミュレータ(iOS 16.4)を使いました。

音声入力・出力が動いた「Ionic版 ChatGPTと話そう!(その2)」のソースコードをChatGPTに与えてiPhoneアプリに変換してもらいました。

 

GPT-4とのやり取り

プロンプト

ソースコードは長いので省略しますが実際はファイル名の後にコピペしています。

次のIonicプログラムと同じ機能を持つiOSアプリをSwiftUIで作る方法をステップ1からステップ3にしたがって教えてください。Xcode version=14.3, Swift version=5で、 iOS16以上をサポートしてください。初心者でもわかるように詳しくお願いします。

ステップ1:細かい機能の説明
ステップ2:作り方の概略
ステップ3:初めての人でもわかるような詳しい作り方。ChatGPTとの通信部分はAPIのことを知らない人にもわかるように説明してください

home.page.html

home.page.ts

返答

まず、このIonicアプリは次の機能を持っています:

  1. ユーザがテキストフィールドにメッセージを入力できます。
  2. 音声認識を開始・停止するボタンがあります。
  3. メッセージを送信するボタンがあります。これはユーザのメッセージをOpenAIのGPT-3.5-turbo APIに送り、その結果を受け取って表示します。また、結果を音声で出力します。

次に、これをSwiftUIとiOSで実装する方法を説明します。

ステップ1:細かい機能の説明

このプロジェクトには以下のような主要な部分があります。

  1. テキストフィールド: ユーザがテキストを入力できるようにします。この場合、TextFieldというSwiftUIのコンポーネントを使用します。
  2. 音声認識: ユーザが音声をテキストに変換する機能を提供します。iOSでは、この目的のために SFSpeechRecognizerというクラスがあります。
  3. ボタン: SwiftUIでは、Buttonというコンポーネントを使用します。これを使って音声認識の開始・停止とメッセージの送信の機能を実装します。
  4. OpenAI APIとの通信: OpenAI APIにメッセージを送信し、応答を取得します。これには、SwiftのネットワーキングライブラリであるURLSessionを使用します。
  5. 音声出力: OpenAI APIからの応答を音声で出力します。これにはAVSpeechSynthesizerというiOSのクラスを使用します。

ステップ2:作り方の概略

まず、SwiftUIで新しいプロジェクトを作成します。このプロジェクトには以下のファイルが含まれます。

  1. ContentView.swift: これはメインのビューで、テキストフィールドとボタンを含みます。
  2. SpeechRecognizer.swift: これは音声認識の機能を提供するクラスです。
  3. OpenAIManager.swift: これはOpenAI APIとの通信を管理するクラスです。
  4. SpeechSynthesizer.swift: これは音声出力の機能を提供するクラスです。

各クラスはその目的に応じて必要なメソッドを持つべきです。

ステップ3:初めての人でもわかるような詳しい作り方。

詳細な実装はかなりのコード量となるため、ここでは最初の部分、つまりビューと音声認識の部分だけをカバーします。

  1. 新しいSwiftUIプロジェクトの作成:Xcodeを開き、新しいプロジェクトを作成します。"App"テンプレートを選択し、プロジェクト名(例えば"OpenAIVoiceApp")を入力します。"Interface"として"SwiftUI"、"Life Cycle"として"SwiftUI App"を選択します。最後に、"Language"として"Swift"を選択し、プロジェクトを作成します。
  2. ビューの作成:ContentView.swiftファイルを開き、以下のように変更します。

import SwiftUI

struct ContentView: View {
    @State private var userInput = ""
    @State private var response = ""

    var body: some View {
        VStack {
            TextField("Your message", text: $userInput)
                .padding()

            Button(action: {
                // TODO: Start speech recognition
            }) {
                Text("Start Speech Recognition")
            }
            .padding()

            Button(action: {
                // TODO: Stop speech recognition
            }) {
                Text("Stop Speech Recognition")
            }
            .padding()

            Button(action: {
                // TODO: Send message
            }) {
                Text("Send Message")
            }
            .padding()

            Text(response)
        }
        .padding()
    }
}

これはUIをセットアップするだけで、まだ具体的なアクションは何も実装していません。

3. 音声認識の設定:

まず、音声認識のためにプライバシー設定を追加する必要があります。プロジェクトのInfo.plistファイルを開き、"Privacy – Speech Recognition Usage Description"という新しい項目を追加します。この項目の値は、音声認識を使用する理由をユーザに説明するメッセージです。

次に、新しいSpeechRecognizer.swiftファイルを作成します。このファイルには、音声認識を開始と停止するメソッドと、音声認識の結果を取得するためのデリゲートメソッドが必要です。具体的なコードは次の通りです。



import Foundation
import Speech

class SpeechRecognizer: NSObject, SFSpeechRecognizerDelegate {
    private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))!
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    private let audioEngine = AVAudioEngine()

    // 音声認識の結果を返すコールバック
    var onResult: ((String) -> Void)?

    override init() {
        super.init()
        self.speechRecognizer.delegate = self
    }

    func startRecognition() {
        // TODO: Implement this
    }

    func stopRecognition() {
        // TODO: Implement this
    }
}


startRecognitionメソッドとstopRecognitionメソッドは具体的な実装が必要です。その実装は、Appleの公式ドキュメンテーションであるSFSpeechRecognizerを参照してください。

このように、SwiftUIでの基本的なセットアップと音声認識の初期セットアップが完了しました。次にOpenAI APIとの通信と音声出力の設定を行う必要がありますが、その詳細な説明は長くなるためここでは省略します。

OpenAI APIとの通信については、SwiftのURLSessionを使用することが一般的です。音声出力については、AVSpeechSynthesizerクラスを使用します。

以上がIonicアプリをSwiftUIで実装するための基本的なガイドラインです。具体的な実装はアプリの詳細や要件によりますが、上記のガイドラインを参照にして進めてみてください。

プロンプト

OpenAI APIとの通信と音声出力の部分を実装して

返答

もちろんです。それでは、それぞれの詳細なステップを以下に説明します。

OpenAI APIとの通信:

新しいOpenAIManager.swiftファイルを作成します。これはURLSessionを使用してOpenAIのGPT-3.5-turbo APIにリクエストを送信するためのクラスとなります。具体的なコードは次の通りです。



import Foundation

class OpenAIManager {
    private let apiKey = "YOUR_OPENAI_API_KEY"  // OpenAIのAPIキーに書き換えてください
    private let url = URL(string: "https://api.openai.com/v1/chat/completions")!

    func sendMessage(_ message: String, completion: @escaping (Result<String, Error>) -> Void) {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let data = [
            "model": "gpt-3.5-turbo",
            "messages": [
                [
                    "role": "user",
                    "content": message
                ]
            ]
        ]
        
        let jsonData = try! JSONSerialization.data(withJSONObject: data, options: [])
        request.httpBody = jsonData
        
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                completion(.failure(error))
                return
            }

            if let data = data {
                let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
                let choices = json["choices"] as! [[String: Any]]
                let message = choices[0]["message"] as! [String: Any]
                let content = message["content"] as! String
                completion(.success(content))
            }
        }
        
        task.resume()
    }
}

音声出力:

新しいSpeechSynthesizer.swiftファイルを作成します。これはAVSpeechSynthesizerを使用してテキストを音声に変換するためのクラスとなります。具体的なコードは次の通りです。


import AVFoundation

class SpeechSynthesizer {
    private let synthesizer = AVSpeechSynthesizer()

    func speak(_ text: String) {
        let utterance = AVSpeechUtterance(string: text)
        utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP")
        synthesizer.speak(utterance)
    }
}


以上がそれぞれの具体的な実装になります。これらのクラスをContentViewで使用して、音声認識、OpenAI APIとの通信、音声出力の各機能を完全に接続することができます。

プロンプト

ContentView.swiftだけで動くように書き直して

返答

それでは、OpenAIManager, SpeechSynthesizer, SpeechRecognizerのすべてをContentViewに統合しましょう。以下のように変更します。


import SwiftUI
import AVFoundation
import Speech

struct ContentView: View {
    @State private var userInput = ""
    @State private var response = ""

    private let openAIManager = OpenAIManager()
    private let speechSynthesizer = SpeechSynthesizer()
    private let speechRecognizer = SpeechRecognizer()

    var body: some View {
        VStack {
            TextField("Your message", text: $userInput)
                .padding()

            Button(action: {
                self.speechRecognizer.startRecognition()
            }) {
                Text("Start Speech Recognition")
            }
            .padding()

            Button(action: {
                self.speechRecognizer.stopRecognition()
            }) {
                Text("Stop Speech Recognition")
            }
            .padding()

            Button(action: {
                self.openAIManager.sendMessage(self.userInput) { result in
                    switch result {
                    case .success(let message):
                        self.response = message
                        self.speechSynthesizer.speak(message)
                    case .failure(let error):
                        print(error)
                    }
                }
            }) {
                Text("Send Message")
            }
            .padding()

            Text(response)
        }
        .padding()
        .onAppear {
            self.speechRecognizer.onResult = { result in
                self.userInput = result
            }
        }
    }
}

class OpenAIManager {
    private let apiKey = "YOUR_OPENAI_API_KEY"
    private let url = URL(string: "https://api.openai.com/v1/chat/completions")!

    func sendMessage(_ message: String, completion: @escaping (Result<String, Error>) -> Void) {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        let data = [
            "model": "gpt-3.5-turbo",
            "messages": [
                [
                    "role": "user",
                    "content": message
                ]
            ]
        ]

        let jsonData = try! JSONSerialization.data(withJSONObject: data, options: [])
        request.httpBody = jsonData

        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                completion(.failure(error))
                return
            }

            if let data = data {
                let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
                let choices = json["choices"] as! [[String: Any]]
                let message = choices[0]["message"] as! [String: Any]
                let content = message["content"] as! String
                DispatchQueue.main.async {
                    completion(.success(content))
                }
            }
        }

        task.resume()
    }
}

class SpeechSynthesizer {
    private let synthesizer = AVSpeechSynthesizer()

    func speak(_ text: String) {
        let utterance = AVSpeechUtterance(string: text)
        utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP")
        synthesizer.speak(utterance)
    }
}

class SpeechRecognizer: NSObject, SFSpeechRecognizerDelegate {
    private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))!
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    private let audioEngine = AVAudioEngine()

    var onResult: ((String) -> Void)?

    override init() {
        super.init()
        speechRecognizer.delegate = self
    }

    func startRecognition() {
        if audioEngine.isRunning {
            audioEngine.stop()
            recognitionRequest?.endAudio()
        } else {
            do {
                try startRecording()
            } catch {
                print("Failed to start recording")
            }
        }
    }

    func stopRecognition() {
        if audioEngine.isRunning {
            audioEngine.stop()
            recognitionRequest?.endAudio()
        }
    }

    private func startRecording() throws {
        recognitionTask?.cancel()
        recognitionTask = nil

        recognitionRequest = SFSpeechAudioBufferRecognitionRequest()

        let inputNode = audioEngine.inputNode

        guard let recognitionRequest = recognitionRequest else {
            fatalError("Unable to create a SFSpeechAudioBufferRecognitionRequest object")
        }

        recognitionRequest.shouldReportPartialResults = true

        recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { result, error in
            if let result = result {
                DispatchQueue.main.async {
                    self.onResult?(result.bestTranscription.formattedString)
                }
            }
        }

        let recordingFormat = inputNode.outputFormat(forBus: 0)
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
            self.recognitionRequest?.append(buffer)
        }

        audioEngine.prepare()
        try audioEngine.start()
    }
}

このプログラムは、音声認識でユーザー入力を収集し、それをOpenAI APIに送信し、返されたメッセージを音声として出力する一連のプロセスを実行します。ただし、エラーハンドリングやUIの改良は必要に応じて行う必要があります。また、ユーザーのプライバシーを守るため、APIキーはハードコードせず、安全な方法で管理してください。

Xcodeでビルドしたところエラーが出たので、エラーメッセージをそのまま送ってみます

プロンプト

Heterogeneous collection literal could only be inferred to '[String : Any]’; add explicit type annotation if this is intentional

返答

あなたが受け取ったエラーメッセージは、Swiftがコレクションの型推論に問題があると感じたときに表示されるものです。エラーの根本的な原因は、コードの一部がSwiftにとってあいまいであるということです。

この問題は、"messages"キーに対するデータの形式によるものです。現在、これは2つの異なるタイプのデータ(String[[String: Any]])を混在させているため、コンパイラは型推論に問題を抱えています。

エラーメッセージを解消するために、sendMessageメソッド内のデータオブジェクトを次のように変更します:


let data: [String: Any] = [
    "model": "gpt-3.5-turbo",
    "messages": [
        [
            "role": "user",
            "content": message
        ]
    ]
]


これでコンパイラに明示的にデータの型を伝え、混合型のリテラルが意図的であることを示すことができます。

エラーが消えたので、後はプロンプト表示用のTextFieldが複数行表示できるようにしてボタンを日本語に変更します。

最終的なContentView.swiftです。

import SwiftUI
import AVFoundation
import Speech

struct ContentView: View {
    @State private var userInput = ""
    @State private var response = ""

    private let openAIManager = OpenAIManager()
    private let speechSynthesizer = SpeechSynthesizer()
    private let speechRecognizer = SpeechRecognizer()

    var body: some View {
        VStack {
            TextField("プロンプト", text: $userInput, axis:  .vertical)
                .padding()

            Button(action: {
                self.speechRecognizer.startRecognition()
            }) {
                Text("音声入力開始")
            }
            .padding()

            Button(action: {
                self.speechRecognizer.stopRecognition()
            }) {
                Text("音声入力停止")
            }
            .padding()

            Button(action: {
                self.openAIManager.sendMessage(self.userInput) { result in
                    switch result {
                    case .success(let message):
                        self.response = message
                        self.speechSynthesizer.speak(message)
                    case .failure(let error):
                        print(error)
                    }
                }
            }) {
                Text("送信")
            }
            .padding()

            Text(response)
        }
        .padding()
        .onAppear {
            self.speechRecognizer.onResult = { result in
                self.userInput = result
            }
        }
    }
}

class OpenAIManager {
    private let apiKey = "YOUR_OPENAI_API_KEY"
    private let url = URL(string: "https://api.openai.com/v1/chat/completions")!

    func sendMessage(_ message: String, completion: @escaping (Result<String, Error>) -> Void) {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        let data: [String: Any] = [
            "model": "gpt-3.5-turbo",
            "messages": [
                [
                    "role": "user",
                    "content": message
                ]
            ]
        ]


        let jsonData = try! JSONSerialization.data(withJSONObject: data, options: [])
        request.httpBody = jsonData

        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                completion(.failure(error))
                return
            }

            if let data = data {
                let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
                let choices = json["choices"] as! [[String: Any]]
                let message = choices[0]["message"] as! [String: Any]
                let content = message["content"] as! String
                DispatchQueue.main.async {
                    completion(.success(content))
                }
            }
        }

        task.resume()
    }
}

class SpeechSynthesizer {
    private let synthesizer = AVSpeechSynthesizer()

    func speak(_ text: String) {
        let utterance = AVSpeechUtterance(string: text)
        utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP")
        synthesizer.speak(utterance)
    }
}

class SpeechRecognizer: NSObject, SFSpeechRecognizerDelegate {
    private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))!
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    private let audioEngine = AVAudioEngine()

    var onResult: ((String) -> Void)?

    override init() {
        super.init()
        speechRecognizer.delegate = self
    }

    func startRecognition() {
        if audioEngine.isRunning {
            audioEngine.stop()
            recognitionRequest?.endAudio()
        } else {
            do {
                try startRecording()
            } catch {
                print("Failed to start recording")
            }
        }
    }

    func stopRecognition() {
        if audioEngine.isRunning {
            audioEngine.stop()
            recognitionRequest?.endAudio()
        }
    }

    private func startRecording() throws {
        recognitionTask?.cancel()
        recognitionTask = nil

        recognitionRequest = SFSpeechAudioBufferRecognitionRequest()

        let inputNode = audioEngine.inputNode

        guard let recognitionRequest = recognitionRequest else {
            fatalError("Unable to create a SFSpeechAudioBufferRecognitionRequest object")
        }

        recognitionRequest.shouldReportPartialResults = true

        recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { result, error in
            if let result = result {
                DispatchQueue.main.async {
                    self.onResult?(result.bestTranscription.formattedString)
                }
            }
        }

        let recordingFormat = inputNode.outputFormat(forBus: 0)
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
            self.recognitionRequest?.append(buffer)
        }

        audioEngine.prepare()
        try audioEngine.start()
    }
}

実行方法は「Ionic版 ChatGPTと話そう!(その1)」と同じです。

 

ChatGPT,iPhoneアプリ