
診察室の会話から、カルテ作成するサービスを作ってみた - AIと医師の共存のヒントを添えて -
今注目されているChatGPTを使って、「会話音声を要約してカルテを出力する」というサービスを作ってみました。その体験を通して僕が感じたことや得たことを共有できればと思います。
僕自身としてはエンジニアではないですが、ノーコードでアプリを作ったり、参考になるサイトからコピペでGoogleAppScriptを設定した経験があります。
*ちなみに、示し合わせたわけでもないのにほぼ同時に弊社の原瀬がアプリを作ったnote記事を書いていて、
なぜ作ろうと思ったのか?
シンプルに「AIを使ったイケてるサービスを作ってみたい」という願望を叶えたかったからです。
僕は2018年頃にIBMのワトソンの本と出会い、「医療とAIの時代がくる。もう来ているんだ。」と思いました。その一端を我が手で担えないか、一度試しに作ってみようとandroid studioやjupyter notebookと向きあった日々。呪文のようなコードに頭を散々悩ませたものの、大したものはできず・・・。(コードをコピペしてただけなので基礎的なコード力はないです)
それが今はChatGPTやOpenAIのAPIの力を借りることで簡単にAIなサービスを作れるというではありませんか。
これに加えて、GPT-3のAPIを利用すれば、いかにもAIなアプリを作れるはず。であれば、自分の願望はすぐ叶えられるかも・・・?
開発環境について
chat_GPTに聞いたところ「Xcodeがいいよ!」って言われたのでXcodeにしました。chat_GPTは英語のほうが性能いいので質問は基本的に英語でしてます。

まずは音声文字起こし機能をつくろう
どうやって作るの?とカジュアルに聞きました。
(ちなみになんとなく Google Speech To Text API を使うのかと思って変な感じで聞いてしまってます・・・)
詳細でありがたい!だがしかし僕はコードを知りたいのだ・・・。

「ここまでやったで。でも次どうしたらいいの?」と聞いてくれると丁寧に教えてくれました・・・!

提示されたコードをXcodeの中に記載していきます。
コードに関しては、
エラーがでたら"I got an error : <エラー文> to <対象の文>"と聞く。
全文がわからなくなったら"What is the overall code?"と聞く。
ほぼこれでいけました。
ちなみにデフォルトでは英語設定で日本で使えなかったりして設定の変更を聞いたりしました。

1〜2時間くらいで音声文字起こしはできました。
いよいよAIを使う
この調子だとすぐできそうだ。
「出来上がったコードにchatGPTの機能をつけてくれ!」
と頼んでみます。

しかしここから以下のために時間がかかります。
コード全文が長いためやりとりが長くなる
コード全文が長いためかchatGPTの目が届かずエラーが多くなる
なんとか開始からさらに3時間ほどでビルドできるくらいになりました。
想定通りに動かないコード
よしよし、これでやったか・・・?
文字起こしはできてそうだが、chatGPTによるサマライズがうまくいかない
error:CGSWindowShmemCreateWithPort failed on port 0
このコードがなんか悪そう・・・?failしてるし。
ここから色々きいて「ここが悪いから直そっか」みたいなラリーが続く。

どこかのタイミングで、エラーのことだけじゃなくて明示的に「サマライズされないんだよ」というと、APIの問題かもと。
あと自分が最初にエラーだと思ってたコード問題ないんじゃないと。

ここからさらにでてきたエラーを潰していくことを繰り返して、気がつくと夜の2時。ついに・・・?!
不恰好ではありますが、やっとできたぞ!
最終的には以下のようなコードになりました。
(MY-API-KEYのところにOpenAIのAPI keyをいれてください)
import SwiftUI
import Speech
import AVFoundation
import OpenAISwift
@MainActor class SpeechRecognitionManager: NSObject, ObservableObject {
private(set) var recognizedText = ""
private(set) var summarizedText = ""
@Published var isRecording = false
var recognizedTextBinding: Binding<String> {
Binding(get: { self.recognizedText }, set: { newValue in self.recognizedText = newValue })
}
var summarizedTextBinding: Binding<String> {
Binding(get: { self.summarizedText }, set: { newValue in self.summarizedText = newValue })
}
private let audioEngine = AVAudioEngine()
private let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "ja-JP"))
private var recognitionTask: SFSpeechRecognitionTask?
private var completion: ((String) -> Void)?
func startRecording() {
let node = audioEngine.inputNode
let recordingFormat = node.outputFormat(forBus: 0)
let request = SFSpeechAudioBufferRecognitionRequest()
self.recognitionTask = recognizer?.recognitionTask(with: request) { [weak self] (result, error) in
guard let self = self else { return }
if let result = result {
DispatchQueue.main.async {
self.recognizedText = result.bestTranscription.formattedString
}
}
if error != nil {
print("Error: \(error!)")
}
if self.recognitionTask?.isCancelled == true || self.recognitionTask == nil {
self.audioEngine.stop()
node.removeTap(onBus: 0)
self.recognitionTask = nil
self.isRecording = false
self.completion?(self.recognizedText)
}
}
node.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer, _) in
request.append(buffer)
}
audioEngine.prepare()
do {
try audioEngine.start()
isRecording = true
} catch let error {
print("Error starting recording: \(error.localizedDescription)")
}
}
func stopRecording(completion: @escaping (String) -> Void) {
self.completion = completion
recognitionTask?.finish()
}
func getSummarizedText(prompt: String) async -> String {
do {
try await sendToChatGPT(text: prompt)
} catch {
print(error.localizedDescription)
}
return self.summarizedText
}
func sendToChatGPT(text: String) async {
let prompt = "患者や医師の話を聞き、それをカルテにまとめる専門家です。次のテキストについて、【主訴】【病歴】【医師が認めた所見】【考えられる病気と治療方針】にまとめてください。テキスト:[\(text)]"
let apiKey = "MY-API-KEY" // Replace this with your actual API key
let model = "gpt-3.5-turbo"
let headers = [
"Content-Type": "application/json",
"Authorization": "Bearer \(apiKey)"
]
let json: [String: Any] = [
"messages": [
[
"role": "system",
"content": "You are the expert in listening to the patient and the doctor and putting it all together in the medical record."
],
[
"role": "user",
"content": prompt
]
],
"model": model,
"max_tokens": 2048, // You can adjust this value as needed
"temperature": 0.5
]
guard let jsonData = try? JSONSerialization.data(withJSONObject: json) else { return }
let url = URL(string: "https://api.openai.com/v1/chat/completions")! // Replace this with the actual API endpoint
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = jsonData
do {
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
print("API response status code: \(httpResponse.statusCode)")
}
if let jsonResult = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
print("API response JSON: \(jsonResult)")
if let choices = jsonResult["choices"] as? [[String: Any]],
let firstChoice = choices.first,
let message = firstChoice["message"] as? [String: Any],
let result = message["content"] as? String {
DispatchQueue.main.async {
self.summarizedText = result.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
} else {
print("Error parsing JSON")
}
} catch {
print("Error: \(error)")
}
}
}
struct ContentView: View {
@StateObject private var speechRecognitionManager = SpeechRecognitionManager()
@State private var showingAlert = false
@State private var showingSummarizedAlert = false
var body: some View {
VStack {
// Display an image icon
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
// Display a text label
Text("Hello, world!")
// Create a text field to display and edit the recognized text
TextField("Enter some text", text: speechRecognitionManager.recognizedTextBinding)
TextEditor(text: speechRecognitionManager.summarizedTextBinding)
.frame(height: 15 * 20) // 15 rows with an estimated height of 20 points each
.border(Color.gray, width: /*@START_MENU_TOKEN@*/1/*@END_MENU_TOKEN@*/) // Optional: Add a border to the TextEditor
// Create a horizontal stack to contain the recording buttons
HStack {
// Start Recording button
Button("Start Recording") {
speechRecognitionManager.startRecording()
}.disabled(speechRecognitionManager.isRecording)
// Stop Recording button
Button("Stop Recording") {
speechRecognitionManager.stopRecording { _ in
self.showingAlert = true
}
}.disabled(!speechRecognitionManager.isRecording)
}
// Summarize button
Button("Summarize") {
Task {
await speechRecognitionManager.sendToChatGPT(text: speechRecognitionManager.recognizedText)
self.showingSummarizedAlert = true
}
}
}
.padding()
.alert(isPresented: $showingAlert) {
Alert(title: Text("Recognized Text"), message: Text(speechRecognitionManager.recognizedText), dismissButton: .default(Text("OK")))
}
.alert(isPresented: $showingSummarizedAlert) {
Alert(title: Text("Summarized Text"), message: Text(speechRecognitionManager.summarizedText), dismissButton: .default(Text("OK")))
}
}
}
UI側のファイル
import SwiftUI
@main
struct shirapoyoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
chatGPTとやりとりして作りきって感じたこと
気がつくと1日が溶けてしまいましたが、1日でコードの基礎も知らないような人間がAIを使ったサービスを作りきれたのはすごいことだと感じました。
chatGPTはすごいのだけど、長文になるとミスがでてくるということや、今回のようにやることを増やしていくとミスがでてくるのかなと思いました。
また、きちんとこちらで何が問題なのか認識できていないと、chatGPTに伝えられないので答えがわからない、のだなと思いました。
また、記事にあまり書けていないのですがXcodeのセッティングが不足しており、おそらく専門家にとっては当たり前であろう部分でスタックすることがあった。質問の仕方だったのかもだが、セッティングに言及がないまま永遠にうまくいかないコードを書き換え続けるようなループに入ることがあったので、別のアプローチにしないマズいと思いググってそれらしいもの見つけて解決、みたいなこともしばしばあった。
問題の認識、問題の認識からの質問力、またおそらく当たり前すぎてchatGPTが教えてくれない(ことがある)ような情報など、エンジニアは多くの情報を得て解釈しているのだろうと感じさせられました。
とはいえ、もちろん圧倒的な力があり、うまくディレクションすることで自身の力をブーストしてくれる存在であることは間違いないと思います。
疲れたー。