LoginSignup
42
21

More than 1 year has passed since last update.

僕のワンダフルライフ 〜ARのAI犬が友達〜

Last updated at Posted at 2019-12-13

この記事は NewsPicks Advent Calendar 2019 の13日目の記事です。

はじめに

いらっしゃいませ、NewsPicksでエンジニアをしているkohemonです。

突然ですが僕のワンダフルライフという映画をご存知でしょうか?
最近では続編の僕のワンダフル・ジャーニーも有名でした。
犬と飼い主の絆の物語なのですが、これがまあ泣ける映画で試写会では**満足度100%**という驚異的な数値を叩き出しました。
劇中では犬の健気な飼い主を愛する気持ちがセリフとなり、伝わってきます。

犬好きな自分はよりこう思うわけです、犬飼いたいと。犬と話したいと。
しかし、私が住む築20年のアパートはそんなことを許してくれるはずがありません。だったらARで犬を表示して、さらに会話をできるようにしましょう。

要件

ARで犬を表示する
ARKitでいけそう

音声入力で犬に話しかける
SFSpeechRecognizerでいけそう

AIで返事をする
→chat系のAPIがあればできそう

その返事を読み上げる
AVSpeechSynthesizerでいけそう

あれ、これ1日でいける!?

ということで早速実装していきます。

ARで犬を表示する

まず3Dデータを準備します。.obj.daeのファイルをXcodeで.scnに変えてあげる必要があります。

Free 3D
こちらのサイトにフリーのいい感じの3Dファイルがたくさんあるので好きなものを選びましょう。今回はもちろん犬です。

準備ができたらNew FileからSceneKit Catalogを選択します。
スクリーンショット 2019-12-12 10.43.15.png

この中にさきほどダウンロードしてきた.objファイルたちを入れてあげます。
スクリーンショット 2019-12-12 10.43.51.png

そうしたらobjにカーソルを合わせてEditorからConvert to SceneKit file formatを選択して.scnファイルに変換しましょう。
スクリーンショット 2019-12-12 10.47.25.png

すると.scnファイルが作られます。中身をみていくと
スクリーンショット 2019-12-12 10.48.00.png
とんでもない向きになってますね。可愛いうちのわんちゃんを地面にめり込ませるわけにはいかないので向きを変更しましょう。

3Dのオブジェクトを選択してNode Inspectortransformで向きを調整します。
スクリーンショット 2019-12-12 10.50.03.png

うん、可愛い。名前付けたい
スクリーンショット 2019-12-12 10.51.39.png
ニンテンドッグスとかにはまっちゃう私としてはたまりません。
早速このわんちゃんをARで表示していきます。

ARを表示するためのARSCNViewをviewにおき、シーンのセットとデバッグ情報を表示できるようにします。

ViewController.swift

    @IBOutlet weak var sceneView: ARSCNView!

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let scene = SCNScene()
        sceneView.scene = scene
        sceneView.delegate = self
        sceneView.showsStatistics = true
        sceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints
    }

次にviewが表示されたらconfigurationを設定します。今回は水平な面にわんちゃんをおきたいので水平を検知する設定をいれます。

ViewController.swift
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = .horizontal
        sceneView.session.run(configuration)
    }

これで水平な面が検知されたらdelegateで受け取ることができるのでそこでわんちゃんを置いていきましょう。

ViewController.swift
extension ViewController: ARSCNViewDelegate {
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        guard let planeAnchor = anchor as? ARPlaneAnchor else {
            fatalError()
        }
        guard let scene = SCNScene(named: "dog.scn", inDirectory: "art.scnassets/dog") else {
            fatalError()
        }
        guard let catNode = scene.rootNode.childNode(withName: "Dog", recursively: true) else {
            fatalError()
        }
        
        let magnification = 0.005
        catNode.scale = SCNVector3(magnification, magnification, magnification)
        catNode.position = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z)

        DispatchQueue.main.async(execute: {
            node.addChildNode(catNode)
        })

    }
}

IMG_0340.jpg

かんわぃぃぃぃ
ペット禁止の我が家についにわんちゃんが現れました。
可愛すぎてさわれないのが惜しいですがこのままやっていきましょう。
次はこのわんちゃんに話しかけられるようにします。

音声入力で犬に話しかけたい

次に音声認識で声から文字列を取得していきます。
SFSpeechRecognizerを使用します。

ViewController.swift
    private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))!
    private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
    private var recognitionTask: SFSpeechRecognitionTask?
    private let audioEngine = AVAudioEngine()

    override func viewDidLoad() {
        super.viewDidLoad()
                
        speechRecognizer.delegate = self
    }

    @IBAction func tapeedRecording(_ sender: UILongPressGestureRecognizer) {
        switch sender.state{
        case .began:
            try! start()
            recordingLabel.text = "認識中..."
            isRecording = true
        case .ended:
            recordImageView.isUserInteractionEnabled = false
            audioEngine.stop()
            recognitionRequest?.endAudio()
            audioEngine.inputNode.removeTap(onBus: 0)
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                self.recordImageView.isUserInteractionEnabled = true
            }
        default:
            break
        }
    }

    private func start() throws {
        guard !isRecording else {
            return
        }
        if let recognitionTask = recognitionTask {
            recognitionTask.cancel()
            self.recognitionTask = nil
        }

        let audioSession = AVAudioSession.sharedInstance()
        try audioSession.setCategory(.record, mode: .measurement, options: [])
        try audioSession.setActive(true, options: .notifyOthersOnDeactivation)

        let recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
        self.recognitionRequest = recognitionRequest
        recognitionRequest.shouldReportPartialResults = true
        recognitionTask = speechRecognizer.recognitionTask(with: recognitionRequest) { [weak self] (result, error) in
            guard let `self` = self else {
                return
            }
            guard self.isRecording else {
                return
            }
            var isFinal = false
            if let result = result {
                isFinal = result.isFinal
                self.recordingLabel.text = result.bestTranscription.formattedString
            }

            if error != nil || isFinal {
                self.audioEngine.stop()
                self.audioEngine.inputNode.removeTap(onBus: 0)
                self.recognitionRequest = nil
                self.recognitionTask = nil
            }
        }

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

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

これで音声入力で文字列を取得できるようになります。

IMG_0349.jpg

IMG_0350.jpg

次はここで取得したテキストをChatのAPIに投げていきましょう。

AIで返事をする

今回はユーザーローカルさんのSupportChatbotのAPIを使っていきましょう。
個人利用の場合は申請フォームから申し込むだけですぐにAPIのKeyが発行され、それと元に叩くだけで利用できます。
早速サンプルで投げてみましょう。

https://chatbot-api.userlocal.jp/api/chat?message=こんにちは&key=APIKey

レスポンスがこちら

{
    "status": "success",
    "result": "なーにー?"
}

めちゃくちゃ簡単。今回はこのmessageに音声認識で取得した文字列をセットして投げていきます。

ChatStruct.swift
struct ChatStruct: Codable {
    var status: String
    var result: String
}
ChatApi.swift
class ChatApi {
    static func getMessage(message: String, completion: @escaping (ChatStruct) -> Swift.Void) {
        let url =  URL(string: "https://chatbot-api.userlocal.jp/api/chat?message=\(message)&key=your_api_key".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)
        let task = URLSession.shared.dataTask(with: url!) { data, response, error in
            guard let jsonData = data else {
                return
            }
            do {
                let chat = try JSONDecoder().decode(ChatStruct.self, from: jsonData)
                completion(chat)
            } catch {
                print(error.localizedDescription)
            }
        }
        task.resume()
    }
}

こんな感じでStructとModelを作成して音声認識が終わったらAPIを叩くようにして、結果を出力してみましょう。

ViewController.swift
    @IBAction func tapeedRecording(_ sender: UILongPressGestureRecognizer) {
        switch sender.state{
        case .began:
            try! start()
            recordingLabel.text = "認識中..."
            isRecording = true
        case .ended:
            recordImageView.isUserInteractionEnabled = false
            audioEngine.stop()
            recognitionRequest?.endAudio()
            audioEngine.inputNode.removeTap(onBus: 0)
            loadingView.isHidden = false
            // 追加
            ChatApi.getMessage(message: inputMessage) { [unowned self] (chat) in
                self.chat = chat
                DispatchQueue.main.async {
                    self.loadingView.isHidden = true
                    self.recordingLabel.text = nil
                    // 出力してみる
                    print("result:", chat.result)
                }
                self.isRecording = false
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                self.recordImageView.isUserInteractionEnabled = true
            }
        default:
            break
        }
    }

こんにちはと話しかけた結果がこちら

result: すごーっ

絶妙に会話にはなっていないですがまあいいでしょう。最後にこの結果を読み上げてもらいましょう。

その返事を読み上げる

AVSpeechSynthesizerを使用して文字列を読み上げてもらいます。

ViewController.swift
    private let speechSynthesizer = AVSpeechSynthesizer()

    private func speak(message: String) {
        defer {
            disableAVSession()
        }
        do {
            try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: .defaultToSpeaker)
            try AVAudioSession.sharedInstance().setActive(true, options: .notifyOthersOnDeactivation)
        } catch {
            print("audioSession properties weren't set because of an error.")
        }
        let utterance = AVSpeechUtterance(string: message)
        utterance.voice = AVSpeechSynthesisVoice(language: "ja-JP")
        utterance.pitchMultiplier = 1
        self.speechSynthesizer.speak(utterance)
    }
    
    private func disableAVSession() {
        do {
            try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
        } catch {
            print("audioSession properties weren't disable.")
        }
    }

これで一通りの機能は完成したので早速試してみましょう。

hachi.gif

声が機械的すぎてあれだけどちゃんと慰めてくれてますね。Qiitaに動画載せられなかったので音声は伝わらないのですが。。

結果

これで僕にも友達ができました。僕のワンダフルライフはここから始まります。
gitにもあげたので全体のソースはそこで確認してください。

明日は弊社のAndroidの神こと@kozmatsの記事です、お楽しみに!!

参考

ARKitで簡単ARやってみた
【ARKit】今日からはじめる AR プログラミング Part.1「現実空間をトラッキングする」
Swiftでリアルタイム音声認識するための最小コード
【Speech Framework】音声認識してテキストを入力する
[Swift] AVSpeechSynthesizerで読み上げ機能を使ってみる

上記とてもわかりやすくて参考にしました、ありがとうございますm(_ _)m

42
21
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
21