[C++,Python] 315 BBS閲覧アプリの製作 その1 DATファイルの保存

[M1 Mac, MacOS Ventura 13.3.1, clang 14.0.3]

とあるBBSのDATファイルへのアクセスが可能になったようなので、早速遊んでみることにしました。

とりあえずDATファイルをダウンロードしてみます。PythonスクリプトをChatGPTに変換してもらったコードがそのまま使えました。

DATファイルの文字コードがシフトJISですから、Macの場合はUTF-8に変換する必要がありますね。

#include <iostream>
#include <fstream>
#include <curl/curl.h>

size_t WriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
    std::ofstream* file = static_cast<std::ofstream*>(userp);
    file->write(static_cast<char*>(contents), size * nmemb);
    return size * nmemb;
}

int main() {
    std::string url = "DATファイルのurl";
    std::string filename = "保存先DATファイルのパス";

    CURL* curl = curl_easy_init();
    if (curl) {
        std::ofstream file(filename, std::ios::binary);
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, &file);
        CURLcode res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            std::cerr << "Error: " << curl_easy_strerror(res) << std::endl;
        }
        curl_easy_cleanup(curl);
    } else {
        std::cerr << "Failed to initialize curl" << std::endl;
    }

    return 0;
}

[Swift] 32 ChatGPTアプリ製作 その3 指示文のソート

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

指示文が連続する会話形式ではデータをJSONファイルにするなど保存するデータがややこしくなるため、一問一答形式でブラッシュアップしました。

指示文が作成順にうまく並ばないので、EntityのAttributeに生成日時を追加しこれをキーとしてソートさせています。常に先頭の指示文を送信する仕様になっています。

また、指示済かつ回答受信済の内容については送信ボタンを押すとアラートが表示されます。

保存するデータがEntityの形式に縛られるのが何とももどかしいです。JSONファイルとしてiCloudやローカルに出力できないか調べてみます。

これで土台はできあがったので、後はのんびり進めていきます。気が向いたら会話形式にも着手します。

アラート表示
import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        entity: Interaction.entity(),
        sortDescriptors: [NSSortDescriptor(key: "creationDate", ascending: false)])
    private var pairs: FetchedResults<Interaction>

    @State var isShowAlert = false

    var body: some View {
        NavigationView {
            VStack{
                List {
                    ForEach(pairs) { pair in
                        NavigationLink{
                            if((pair.instruction?.isEmpty) == false){
                                Draft(text:pair.instruction!, interaction: pair)
                            }
                        }
                        label:{
                            if((pair.instruction?.isEmpty) == false){
                                Text(pair.instruction!)
                            }
                        }
                    }

                    .onDelete(perform: deleteInteraction)
                }
                .navigationTitle("ChatGPTSwift")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar{
                    // 新規作成
                    ToolbarItem(placement:.navigationBarTrailing){
                        NavigationLink{
                            Draft()
                        }label:{
                            Text("+")
                                .font(.system(size: 30))
                        }
                    }
                }

                List {
                    ForEach(pairs, id: \.self) { pair in
                        if let res = pair.res , !res.isEmpty {
                            Text(res)
                            .foregroundColor(.white)
                            .background(Color.blue)
                        }
                    }


                    .onDelete(perform: deleteInteraction)
                }

                Button(action:{
                    if pairs.first != nil{
                        let instruction = pairs.first
                        
                        if instruction!.res == nil{
                            sendRequest(pairs:pairs)
                        }else{
                            isShowAlert = true
                            print("指示文を入力して下さい")
                        }
                        
                    }else{
                        print("pairsは空です")
                        
                    }
                }){
                    Text("送信")
                    .font(.system(size: 24))
                }
                .alert("指示文を入力して下さい", isPresented: $isShowAlert) {
                    Button("OK") {
                    }
                } message: {
                    Text("")
                }
            }
        }
    }
<以下略>
Core Data(Data Model)の内容
creationDateを追加

[Swift] 31 ChatGPTアプリ製作 その2 HTTPRequest

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

GPT-3.5のAPIとやりとりできるようになりました。

ChatGPTの力を借り色々試行錯誤しながらで約半日掛かりました。取りあえず送受信を1往復できるようにしています。

チャットのように交互に表示させるのは難しいため、上下段に振り分けました。

次回以降、複数回の送受信に対応させます。

OpenAIがiOS, iPadOSアプリを5/26にリリースしているので、比較しながら機能を増やしていきたいです。

iOS版
iPadOS版
import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @FetchRequest(sortDescriptors:[])
    private var pairs: FetchedResults<Interaction>

    var body: some View {
        NavigationView {
            VStack{
                List {
                    ForEach(pairs) { pair in
                        NavigationLink{
                            if((pair.instruction?.isEmpty) == false){
                                Draft(text:pair.instruction!, interaction: pair)
                            }
                        }
                        label:{
                            if((pair.instruction?.isEmpty) == false){
                                Text(pair.instruction!)
                            }
                        }
                    }

                    .onDelete(perform: deleteInteraction)
                }
                .navigationTitle("ChatGPT リクエスト")
                .navigationBarTitleDisplayMode(.inline)
                .toolbar{
                    // 新規リクエスト作成
                    ToolbarItem(placement:.navigationBarTrailing){
                        NavigationLink{
                            Draft()
                        }label:{
                            Text("+")
                        }
                    }
                }

                List {
                    ForEach(pairs, id: \.self) { pair in
                        if let res = pair.res, !res.isEmpty {
                            Text(res)
                                .foregroundColor(.white)
                                .background(Color.blue)
                        }
                    }


                    .onDelete(perform: deleteInteraction)
                }

                Button(action:{
                    if pairs.last != nil{
                        sendRequest()
                    }else{
                        print("pairsは空です")
                        
                    }
                }){
                    Text("送信")
                    .font(.system(size: 24))
                }
            }
        }
    }

    func deleteInteraction(offsets:IndexSet){
        for offset in offsets{
            viewContext.delete(pairs[offset])
        }
            
        do{
            try viewContext.save()
        }catch{
            fatalError("セーブに失敗")
        }
    }

    func sendRequest(){
        let urlAPI = "https://api.openai.com/v1/chat/completions";
        let apiKey = "API key";
        let model = "gpt-3.5-turbo"
        let systemStr: String = "あなたは根拠が明確に存在することのみ発言するチャットボットです。"

        let authHeader = "Bearer \(apiKey)"
        var headers = [String: String]()
        headers["Authorization"] = authHeader
        headers["Content-Type"] = "application/json"

        guard let url = URL(string: urlAPI) else {
            fatalError("Invalid URL")
        }

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.allHTTPHeaderFields = headers

        // requestData作成・送受信
        if let lastInteraction = pairs.last {
            if let instruction:String? = lastInteraction.instruction {
                print("instruction:")
                print(instruction)

                let requestData:String = "{\"model\":\"\(model)\", \"messages\":[{\"role\":\"system\",\"content\":\"\(systemStr)\"},{\"role\":\"user\",\"content\":\"\(instruction!)\"}], \"temperature\":0.0}";
                print(requestData)
                
                var responseData: [String: Any] = [:]
                responseData = sendHTTPRequest(url: url, headers: headers, requestData: requestData)
                
                print(responseData)

                if let choices = responseData["choices"] as? [[String: Any]],
                let message = choices.first?["message"] as? [String: Any],
                var content = message["content"] as? String {
                    print(content)
                    lastInteraction.res = content
                }

            } else {
                print("instruction is nil")
                return
            }
        } else {
            print("pairs is empty")
            return
        }
    }

    func sendHTTPRequest(url: URL, headers: [String: String], requestData: String) -> [String: Any] {
        var responseData: [String: Any] = [:]
        var timeoutBool = false

        let semaphore = DispatchSemaphore(value: 0)

        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.allHTTPHeaderFields = headers
        request.httpBody = requestData.data(using: .utf8) // requestDataをData型に変換

        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            if let error = error {
                timeoutBool = true
                print("HTTP request failed: \(error)")
                return
            }

            if let data = data {
                do {
                    responseData = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
                } catch {
                    print("Failed to parse response data: \(error)")
                }
            }

            semaphore.signal()
        }

        task.resume()
        _ = semaphore.wait(timeout: DispatchTime.now() + 90)

        if timeoutBool {
            <中略>
            return [:]
        }

        return responseData
    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
Core Data(Data Model)の内容

[Swift] 29 ChatGPTアプリ製作 その1 クリップボード貼付

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

ChatGPTアプリiOS版の製作に着手しました。

取りあえずリクエスト文を入力できるようにしました。メモアプリのコードを一部流用しています。

クリップボードからテキストをペーストできます。macOSなどからOSをまたいでのペーストは次回以降対応するつもりです。

次にこれをAPIに送信してレスポンスを表示するようにします。

2023/7/4追記
TextEditorのタップ時にPasteやSelect Allなどが選択できるため、緑字のペーストボタンは削除しました。

アプリ画面
TextEditor画面
import SwiftUI

struct Draft: View {
    @State var text = ""
    @FocusState var nameFieldIsForcused: Bool
    @Environment(\.managedObjectContext)var viewContext
    var interaction: Interaction?

    var body: some View {
        TextEditor(text:$text)
            .frame(minHeight: 0, maxHeight: .infinity) // 行数無制限
            .focused($nameFieldIsForcused)
            .onAppear{
                DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+0.5){
                    nameFieldIsForcused = true
                }
            }
            .toolbar{
                ToolbarItem(placement:.navigationBarLeading){
                    if(nameFieldIsForcused){
                        Button(action:{
                            pasteText()

                        }){
                            Text("ペースト")
                            .foregroundColor(.green)
                        }
                    }
                }

                ToolbarItem(placement:.navigationBarTrailing){
                    if(nameFieldIsForcused){
                        Button(action:{
                            if interaction != nil{
                                updateContent(interaction: self.interaction!)
                            }else{
                                addContent()
                            }

                        }){
                            Text("完了")
                        }
                    }
                }
            }
    }

    func updateContent(interaction:Interaction){
        interaction.request = text
        
        do{
            try viewContext.save()
        }catch{
            fatalError("セーブに失敗")
        }

        nameFieldIsForcused = false
    }

    func addContent(){
        let newContent = Interaction(context:viewContext)
        newContent.request = text
        
        do{
            try viewContext.save()
            
        }catch{
            fatalError("セーブに失敗")
        }

        nameFieldIsForcused = false

    }

    func pasteText(){
        let pasteboard = UIPasteboard.general

        if let clipboardString = pasteboard.string {
            text += clipboardString
        }
    }
}

struct Draft_Previews: PreviewProvider {
    static var previews: some View {
        Draft()
    }
}

[AI] プログラミングLLM phi-1が公開予定 Microsoft

Microsoftが開発しているプログラミングに特化したLLM “phi-1″がHugging Faceに公開されるらしいです。

モデルサイズはGPT-3.5の100分の1以下、1.3Bです。能力はGPT-4には及ばないものの、GPT-3.5を少し上回っています(HumanEval比較)。

RTX 4070Tiで動かせるのか分かりませんが、今から待ちどおしいです。

GPT-3.5の100分の1以下のモデルサイズでより高いプログラミング処理能力持つ「phi-1」Microsoftが開発

[Swift] 26 Apple WatchのComplication改良 カレンダー / 元号を追加

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

改良カレンダーをタップした時の表示に元号を追加しました。フォントサイズを大きくすると赤色でも問題なく読めます。

今回は日本固有の内容だったためかChatGPTの回答がかなり怪しく、大幅な修正を加えました。

import SwiftUI

struct ContentView: View {
    let date: Date
    
    var body: some View {
        VStack (spacing: 15){
            Text(getFormattedDate())
            .font(.system(size: 30))
            .foregroundColor(.green)
            
            Text(getFormattedYear())
            .font(.system(size: 30))
            .foregroundColor(.red)
            
            Text(getFormattedWeekday() + "曜日")
            .font(.system(size: 30))
            .foregroundColor(.yellow)
            
            Text(getFormattedTime())
            .font(.system(size: 30))
            .foregroundColor(.blue)

        }
        .padding()
    }
    
    func getFormattedDate() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy/MM/dd"
        return dateFormatter.string(from: date)
    }
    
    func getFormattedYear() -> String {
        let calendar = Calendar(identifier: .japanese)
        let year = calendar.component(.year, from: date)
        return "令和" + String(year) + "年"
    }

    
    func getFormattedWeekday() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "E"
        dateFormatter.locale = Locale(identifier: "ja_JP")
        return dateFormatter.string(from: date)
    }
    
    func getFormattedTime() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "HH:mm:ss"
        return dateFormatter.string(from: date)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(date: Date())
    }
}

[Swift] 25 Apple WatchのComplication改良 カレンダー / 時計の針で読めない際の対策

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

Complicationの改良カレンダーですが、アナログ時計のため時針や分針が重なると読みにくくなります。

カレンダーが読みにくい

そこでタップすると日時を表示するようにしました。デジタル時計は止まったまま動きません。

タップすると表示
import SwiftUI

struct ContentView: View {
    let date: Date
    
    var body: some View {
        VStack (spacing: 20){
            Text(getFormattedDate())
            .font(.system(size: 30))
            .foregroundColor(.green)
            
            Text(getFormattedWeekday() + "曜日")
            .font(.system(size: 30))
            .foregroundColor(.yellow)
            
            Text(getFormattedTime())
            .font(.system(size: 30))
            .foregroundColor(.blue)

        }
        .padding()
    }
    
    func getFormattedDate() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy/MM/dd"
        return dateFormatter.string(from: date)
    }
    
    func getFormattedWeekday() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "E"
        dateFormatter.locale = Locale(identifier: "ja_JP")
        return dateFormatter.string(from: date)
    }
    
    func getFormattedTime() -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "HH:mm:ss"
        return dateFormatter.string(from: date)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(date: Date())
    }
}

[Swift] 24 Apple WatchのComplication改良 カレンダー / 月表示追加

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

改良カレンダーのフォントサイズを調整して3行にし、月表示を追加しました。

ChatGPTが考えたコードを微修正しています。

struct DateToolComplicationEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack (spacing: -5){
            Text(getWeekday(entry.date))
            .font(.system(size: 18))
            .foregroundColor(.yellow)
             
            Text(getMonth(entry.date))
            .font(.system(size: 18))
            .foregroundColor(.green)

            Text(getDay(entry.date))
            .font(.system(size: 20))
            .foregroundColor(.white)
        }
    }
    
    func getWeekday(_ date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "E"
        dateFormatter.locale = Locale(identifier: "ja_JP")
        return dateFormatter.string(from: date)
    }
    
    func getMonth(_ date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "M"
        return dateFormatter.string(from: date)
    }

    func getDay(_ date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "d"
        return dateFormatter.string(from: date)
    }

}

[Swift] 23 Apple WatchのComplication改良 カレンダーの視認性向上

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

Complicationのカレンダーがとても見にくいカラー構成になっているため、フォントサイズと合わせて改良しました。以下の手順になります。

1.watchOS Appのプロジェクトを作成する。
2.Hello World表示のままコードに手を加えずにWidget Extensionを追加する。
3.swiftコードのEntryViewを以下のように書き換える。今回コードはChatGPTに考えさせた。

struct DateToolComplicationEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text(getWeekday(entry.date))
            .font(.largeTitle)
            .foregroundColor(.yellow)
            
            Text(getDay(entry.date))
            .font(.largeTitle)
            .foregroundColor(.white)
        }
    }
    
    func getWeekday(_ date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "E"
        dateFormatter.locale = Locale(identifier: "ja_JP")
        return dateFormatter.string(from: date)
    }

    func getDay(_ date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "d"
        return dateFormatter.string(from: date)
    }
}

4.ビルド後、Apple Watch実機にてComplicationに改良カレンダーを登録し文字盤に表示させる。

既存カレンダー
改良カレンダー

Apple Watchは21年1月から使用していますが、当初からカレンダーの視認性が低いと思っていました。2年と6ヶ月が経ち、ようやくChatGPTの力を借りて解決できました。

ただ画面ロック時にメモアプリと共に表示がブランクになってしまいます。ChatGPTのGPT-4でも解決策は分からず、Stack Overflow英語版やApple開発者フォーラムにも情報はありませんでした。

ロック時の文字盤

プログラミングはAIを使いこなすことでかなりハードルが下がりますね。記憶力よりも応用力の時代になっていくのでしょうか。

[Swift] 22 メモアプリ製作 その8 Complications対応

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

メモアプリを以下の方法でComplicationsに対応させ、Apple Watch文字盤にアイコンを表示させてタップで呼び出せるようにしました。

1.Xcodeの[File]-[New]-[Target]でwatchOSタブにあるWidget Extensionを選択し進めていくと、新ターゲットとしてプロジェクト内にディレクトリが作成される。”Include Configuration Intent”のチェックは外しておく。

2.swiftコードのEntryViewの内容を適当に書き換える。デフォルトではデジタル時計になっている。今回はChatGPTに考えてもらったコードをペーストした。アイコンとしてシステム画像”square.and.pencil”を表示する。

struct MemoToolAW_ComplicationsEntryView : View {
    var entry: Provider.Entry // 他のコードで使うので消さずに残しておく

    var body: some View {
        Image(systemName: "square.and.pencil")
        .resizable()
        .aspectRatio(contentMode: .fit)
        .foregroundColor(.white)
        .background(Color.blue)

//        Text(entry.date, style: .time) // デフォルトはデジタル時計
    }
}

3.ビルド後、Apple Watch実機にてComplicationにメモアプリを登録し文字盤に表示させる。

メモアプリは左の青いアイコン

これまで使っていたメモアプリNotebookはComplicationから起動させると最初に音声入力画面になり内容を表示させるにはこれをキャンセルする必要がありました。今回作ったメモアプリはいきなり内容表示になるので手間が減りました。

参考サイト