[Swift] 55 visionOSアプリ製作 その3 データ永続化 / SwiftData

[Mac M2 Pro 12CPU, Ventura 13.6, visionOS 1.0 (21N5300a), Xcode 15.1 beta 3]

visionOSで使える簡単なユーティリティアプリを製作しています。

iOS 17から利用可能になったSwiftDataを試しています。まだ基本的な操作しか修得していませんが、まずまずの感触です。

現段階で大方の機能は実装しました。あとは自動回転などお遊び機能を入れてみようかと思います。

[Swift] 54 visionOSアプリ製作 その2 タップ検知 / CollisionComponent

[Mac M2 Pro 12CPU, Ventura 13.6, visionOS 1.0 (21N5300a), Xcode 15.1 beta 3]

ようやく3Dモデルへのタップを検知できるようになりました。

11/4にタップ検知実装に着手してから3週間も掛かりました。あまりの開発効率の悪さに我ながら引いています。

解決後、よく調べてみるとAppleが提供しているサンプルコード HappyBeamでCollisionComponentを扱っていました。

単なるEntityではなくusdzファイルを読み込ませたModelEntityでないと上手くいかないようです。デフォルトのSceneエンティティはただのEntityなのでハナから無理筋だったのか。

UnityでもvisionOSアプリを開発できるものの、Unity Pro(年額26.8万円)の登録が必須になっています。2年分でVision Proを購入できます。まあ個人開発者には厳しいですね。

そのうちUnity Personal(無料)に登録してMeta Quest用アプリを作ってみたいですが、visionOSアプリ開発にしばらく注力します。macOSやWindowsのように複数のアプリを起動できるvisionOSの方が私には魅力的です。

[Swift] 53 visionOSアプリ製作 その1 ECSの分割管理

[Mac M2 Pro 12CPU, Ventura 13.6, visionOS 1.0 (21N5300a), Xcode 15.1 beta 3]

visionOSアプリを製作しています。

Entity Component Systemアーキテクチャ(ECS)の扱いに手間取っています。初めはReality Composer Proを使ってRealityKitContent内でECSを一元管理しようと試みましたが、ContentViewにあるボタンとの連携がうまく出来ません。そこで自製ComponentについてはPackages外にECSディレクトリを設けて別管理することにしました。

[Python] 362 OpenAI DALL-E 3による画像生成

[Mac M2 Pro 12CPU, Ventura 13.6, Python 3.10.4]

DALL-E 3からAPIで使えるようになったので、早速試してみました。

生駒山近景を出力したところ存在しないロープウェイを含む景色でした。2回目は信仰の山らしく祠の画像でした。六甲山も出してみました。

どうも画のタッチが好みではありません。そこら辺を調整できれば、使い出がありそうです。

# openaiライブラリを導入済みの場合はアップグレードする(旧Verにはopenai.OpenAIがない)
pip install --upgrade openai
import os
from openai import OpenAI

# 環境変数からAPIキーを取得
api_key = os.getenv('CHATGPT_API_KEY')
if not api_key:
    raise ValueError("OpenAI API Keyが環境変数に設定されていません")

# APIキーを設定
os.environ["OPENAI_API_KEY"] = api_key

client = OpenAI()

response = client.images.generate(
  model="dall-e-3",
  prompt="大阪府・奈良県にある生駒山の近景",
  size="1024x1024",
  quality="standard",
  n=1,
)

image_url = response.data[0].url
print(image_url)
生駒山近景1
生駒山近景2
六甲山近景

[C++] 355 ChatGPTアプリの製作 その40 GPT-4 Turbo with visionへの対応

[Mac M2 Pro 12CPU, MacOS Ventura 13.6, clang 15.0.0]

自製ChatGPTアプリをGPT-4 Turbo with visionに対応させました。

画像のURLを送ると内容を読み取り、プロンプトに対応します。

漢字、ひらがな、カタカナなど非ラテン文字は読めません。これらが画像に含まれているとレスポンスしなくなるので、モザイクを掛けるなり前処理が必要です。

プログラミングでUIについて質問する際に画像を使うとやりやすいです。特に見てほしいところを色線で囲むなど、工夫を入れるとより深い分析をしてくれます。ここまでできるとプログラミングスクールにとどまらず教育業界全体が相当な危機でしょう。

GPT-4までは心強いパートナーという感じでしたが、with visionになって凄まじい眼力を身に付け、いささか脅威を覚えるようになりました。

今のAIがどのような状況になっているのか、もっと周知しないとヤバい気がします。

[JavaScript] 20 Adobe XDのアイテムデータを取得するプラグイン作成 その6 Pythonスクリプトとの統合 / GPT-4 Turbo

[Mac M2 Pro 12CPU, Ventura 13.6]

ChatGPTがGPT-4 Turboになってトークン数に余裕ができたので、早速仕事をしてもらいました。プラグインのmain.jsにtxtTojson.pyの内容を取り込みました。

自力でもある程度プログラミングできるレベルであれば、ChatGPTによる恩恵は絶大だと思います。

const fs = require("uxp").storage.localFileSystem;

async function myCommand(selection) {
    let items_list = [];
    selection.items.forEach(function (value, index) {
        // ここでPythonスクリプトの処理に相当するデータ抽出を行う
        const name = value.name;
        const bounds = value.globalBounds; // globalBoundsオブジェクトを取得
        const x = bounds.x;
        const y = bounds.y;
        const w = bounds.width; // globalBoundsからwidthを取得
        const h = bounds.height; // globalBoundsからheightを取得
        items_list.push({ name, bounds: [x, y, w, h] });
    });

    // JSONオブジェクトを作成
    const json_obj = {};
    items_list.forEach(item => {
        json_obj[item.name] = item.bounds;
    });

    // JSON文字列に変換
    const json_str = JSON.stringify(json_obj, null, 2);

    // ファイルを保存する
    const file = await fs.getFileForSaving("output.json", { types: ["json"] });
    await file.write(json_str);
}

module.exports = {
    commands: {
        GetItemXY: myCommand
    }
};
AdobeXDプラグインを使って2つの方法でアイテムデータを取得し、比較しています
1.プラグインのmain.jsでアイテムデータをtxtファイル化し、txtTojson.pyでjsonファイルに変換する
2.プラグインのmain.jsでアイテムデータをjsonファイル化する

1の方法では成功しますが、2の方法ではデータの欠損が生じます。

以下にそれぞれのスクリプトを記述しますので、これらを参考に2の方法のmain.jsを修正してください

1の方法
-- main.js
const fs = require("uxp").storage.localFileSystem;

async function myCommand(selection) {
    let items_list = "";
    var num = 1;
    selection.items.forEach(function(value){
        if (num < selection.items.length){
            items_list += value + ";";
        } else {
            items_list += value;
        }
        num += 1;
    });

    // ファイルを保存する
    const file = await fs.getFileForSaving("output.txt", { types: ["txt"] });
    await file.write(items_list);
}

module.exports = {
    commands: {
        GetItemXY: myCommand
    }
};
--

-- txtTojson.py
import json

with open("items.txt", "r") as tf:
    items_list = tf.read().replace("\n","").split(';')
    
# print(items_list)
# print(len(items_list))

# listからデータ抽出し、JSON文字列作成
json_str = ""
num = 0
for item in items_list:
    name = item.split("'")[1]
    print(name + "\n")
    
    xy = (item.split("global X,Y:")[1]).split("parent")[0]
    x = xy.split(",")[0]
    y = xy.split(",")[1]
    print(x + "\n")
    print(y + "\n")
    
    wh = (item.split("{")[1]).split("global")[0]
    w = (wh.split("width:")[1]).split(", height")[0]
    h = wh.split("height:")[1]
    print(w + "\n")
    print(h + "\n")
    
    if num == 0:
        json_str += "{\"" + name + "\"" + ":[" + x + ", " + y + ", " + w + ", " + h + "],\n"
    elif num < len(items_list) -1:
        json_str += "\"" + name + "\"" + ":[" + x + ", " + y + ", " + w + ", " + h + "],\n"
    else:
        json_str += "\"" + name + "\"" + ":[" + x + ", " + y + ", " + w + ", " + h + "]}"

    print(json_str + "\n")
    
    num += 1

# JSONファイル作成
file = open('items.json', mode='w')
file.write(json_str.replace(" ",""))
file.close()
--

-- items.json
{"input":{"x":20,"y":23,"w":14,"h":18},
"長方形11":{"x":6,"y":50,"w":30,"h":30},
"inputLine":{"x":40,"y":30,"w":370,"h":130},
"showBtn":{"x":40,"y":10,"w":370,"h":20},
"showBtn":{"x":20,"y":165,"w":160,"h":30},
"長方形7":{"x":20,"y":200,"w":460,"h":320},
"Codeカウント":{"x":196,"y":165,"w":40,"h":30},
"Codeカウント":{"x":252,"y":165,"w":60,"h":30},
"showBtn":{"x":327,"y":165,"w":28,"h":30},
"Codeカウント":{"x":340,"y":525,"w":40,"h":20},
"長方形9":{"x":20,"y":550,"w":460,"h":82},
"showBtn":{"x":370,"y":165,"w":28,"h":30},
"showBtn":{"x":387,"y":525,"w":26,"h":20}}
--

2の方法
-- main.js
const fs = require("uxp").storage.localFileSystem;

async function myCommand(selection) {
    let items_list = [];
    selection.items.forEach(function (value) {
        // ここでPythonスクリプトの処理に相当するデータ抽出を行う
        const name = value.name;
        const x = value.globalBounds.x;
        const y = value.globalBounds.y;
        const w = value.width;
        const h = value.height;
        items_list.push({ name, bounds: [x, y, w, h] });
    });

    // JSONオブジェクトを作成
    const json_obj = {};
    items_list.forEach(item => {
        json_obj[item.name] = item.bounds;
    });

    // JSON文字列に変換
    const json_str = JSON.stringify(json_obj, null, 2);

    // ファイルを保存する
    const file = await fs.getFileForSaving("output.json", { types: ["json"] });
    await file.write(json_str);
}

module.exports = {
    commands: {
        GetItemXY: myCommand
    }
};
--

-- output.json(データが欠損している)
{
  "長方形 7": [
    20,
    200,
    460,
    320
  ],
  "showBtn": [
    327,
    165,
    null,
    null
  ],
  "長方形 9": [
    20,
    550,
    460,
    82
  ],
  "Codeカウント": [
    252,
    165,
    null,
    null
  ]
}
--
2の方法でデータの欠損が生じている原因は、同じ名前のアイテムが複数存在する場合、JSONオブジェクトにおいて後から追加されたアイテムで以前のアイテムのデータが上書きされてしまうためです。JSONオブジェクトのキーは一意でなければならないため、同じ名前のアイテムがあると問題が発生します。

この問題を解決するためには、アイテムの名前が重複しないように一意のキーを生成するか、アイテムのリストを配列としてJSONに保存する必要があります。以下に修正したスクリプトを示します。

```javascript
const fs = require("uxp").storage.localFileSystem;

async function myCommand(selection) {
    let items_list = [];
    selection.items.forEach(function (value, index) {
        // ここでPythonスクリプトの処理に相当するデータ抽出を行う
        const name = value.name;
        const x = value.globalBounds.x;
        const y = value.globalBounds.y;
        const w = value.width;
        const h = value.height;
        // 同じ名前のアイテムがある場合に備えて、インデックスを名前に追加
        const uniqueName = `${name}_${index}`;
        items_list.push({ name: uniqueName, bounds: [x, y, w, h] });
    });

    // JSONオブジェクトを作成
    const json_obj = {};
    items_list.forEach(item => {
        json_obj[item.name] = item.bounds;
    });

    // JSON文字列に変換
    const json_str = JSON.stringify(json_obj, null, 2);

    // ファイルを保存する
    const file = await fs.getFileForSaving("output.json", { types: ["json"] });
    await file.write(json_str);
}

module.exports = {
    commands: {
        GetItemXY: myCommand
    }
};
```

この修正により、各アイテムに一意の名前が付けられ、JSONオブジェクトに保存される際にデータの上書きが発生しなくなります。ただし、この方法では元のアイテム名とインデックスが結合された形で保存されるため、元の名前だけを知りたい場合は後処理が必要になるかもしれません。それを避けるためには、アイテムを配列として保存する方法も考えられます。

[C++] 354 ChatGPTアプリの製作 その39 GPT-4 Turboへの対応

[Mac M2 Pro 12CPU, MacOS Ventura 13.6, clang 15.0.0]

自製ChatGPTアプリを本日11/7未明よりサービス開始となったGPT-4 Turboに対応させました。

たまに英語で回答が返ってくるため、最初の役割設定(role : sys)で日本語回答を必須にしました。

トレーニングデータは23年4月までですから、AppleのvisionOSに関する情報はカバーしていません。

トークンの上限がGPT-4の8,192から128,000になりました。ChatGPTでのやり取りが途切れることも大分減りそうです。また料金はGPT-4の3分の1なので、ある程度気にせず利用できます。

[JavaScript] 19 Adobe XDのアイテムデータを取得するプラグイン作成 その5 作業手順 / UXP for Adobe XD / Pythonでjson化

[Mac M2 Pro 12CPU, Ventura 13.6]

作業手順をまとめておきます。

1.Adobe XDのデザインタブでXY座標を取得したい項目を選択する

2. メニューのプラグインからItemXYGetterを選択する。出力ファイルを指定して実行する。

3.出力されたtxtファイルをtxtTojson.pyで処理し、jsonファイルにする。

時間があればJavaScriptとPythonのスクリプトを統合し、JavaScriptのプラグインだけで処理できるようにしたいです。

※ ItemXYGetterのjsスクリプトの内容等は過去記事で紹介しています。画面左の検索窓で探してみてください。

Pythonスクリプトでjsonファイルに変換

[Swift] 52 BBS閲覧アプリiOS版製作 その6 スレッド表示 / datファイル取得

[Mac M2 Pro 12CPU, Ventura 13.6, iOS 17.0.3, Xcode 15.0]

掲示板サイトからdatファイルをダウンロードするところで、結構時間がかかりました。

ちょっとした手違いで非同期関数が非同期的に動作せず、ダウンロード完了の手前で終わっていました。

今はdatファイルの内容をそのまま表示しています。次はこれをHTMLに加工します。

datファイルはiPhone内のコンテナに保存しています。エディタで内容を確認するため可視ファイルにしたいのですが、今のところできていません。

掲示板名(横線)スレッドタイトル(横線)スレッド内容、を縦に並べている
import SwiftUI

struct datView: View {
    var board: Board?
    var thread: Thread?
    @State var datPath: String = ""
    @State var datContent: String?

    var body: some View {
        VStack(spacing: 8) {
            if let board = board {
                Text(board.name)
                    .font(.system(size: 16))
                Divider() // 横線
            }
            if let thread = thread {
                Text(thread.threadTitle)
                    .font(.system(size: 16))
                Divider()
            }
            // datファイルの内容を表示する
            if let content = datContent {
               Text(content)
            }
            Spacer()
        }
        .padding(.horizontal, 0)
        .padding(.top, 0)
        .padding(.bottom, 0)
        .task {
            if let thread = thread, let board = board {
                datPath = await makeDat(id: thread.threadID, url: board.url)
                datContent = await readDatFile(path: datPath)
                if datContent?.isEmpty ?? true {
                    print("datなし")
                }
            }
        }
        .refreshable {
            if let thread = thread, let board = board {
                datPath = await makeDat(id: thread.threadID, url: board.url)
                datContent = await readDatFile(path: datPath)
                if datContent?.isEmpty ?? true {
                    print("datなし")
                }
            }
        }
    }
}

struct datView_Previews: PreviewProvider {
    static var previews: some View {
        datView()
    }
}
今のところアプリ本体は852KB

[Swift] 51 BBS閲覧アプリiOS版製作 その5 文字列分割 / TestFlight登録 / Vision Pro

[Mac M2 Pro 12CPU, Ventura 13.6, iOS 17.0.3, Xcode 15.0]

手掛けている掲示板のHTMLから取得する文字列はスレッド番号、タイトル、投稿数が結合したものです。これを分割して、Viewに横並びさせました。

このアプリを公開する予定はないのですが、未経験のTestFlightに登録だけしてみました。内部テスターとして自分を登録し、iOSのTestFlightアプリからインストールしました。非公開のまま知り合いだけ内部テスターとして登録し、配布しても面白そうです。内部テスターは100人、外部テスター(アプリ要審査承認)は10000人まで登録できます。

Xcodeベータ版のVision Proシミュレータでも使ってみました。Vision ProではiPad版UIで使用できます。

import Foundation

struct ThreadContent{
    let threadNum: Int
    let threadTitle: String
    let threadPostNum: Int
}

func splitTitle(threadString: String) async -> ThreadContent {
    var threadContent: ThreadContent
    var colonIndex: String.Index?
    var openParenIndex: String.Index?
    var number: Int = 0
    var title: String = ""
    var postCount: Int = 0
    
    // 番号を取り出す(最初のコロンの左側)
    if let index = threadString.firstIndex(of: ":") {
        colonIndex = index
        if let colonIndex = colonIndex {
            number = Int(threadString[..<colonIndex]) ?? 0
            print("番号: \(number)")
        }
    }
    
    if let index = threadString.lastIndex(of: "(") {
        // タイトルを取り出す(最初のコロンの右が始点、末尾から最初の開き括弧の左が終点)
        openParenIndex = index
        if let colonIndex = colonIndex, let openParenIndex = openParenIndex {
            title = String(threadString[threadString.index(after: colonIndex)..<openParenIndex])
            print("タイトル: \(title)")
        }
        
        // 投稿数を取り出す(開き括弧右から末尾1文字を削除した数字)
        if let openParenIndex = openParenIndex {
            let postCountString = threadString[threadString.index(after: openParenIndex)..<threadString.index(before: threadString.endIndex)]
            postCount = Int(postCountString) ?? 0
            print("投稿数: \(postCount)")
        }

    }
    
    threadContent = ThreadContent(threadNum: number, threadTitle: title, threadPostNum: postCount)
    
    return threadContent
}
TestFlightにアルファ版を登録、配布可能にした
Vision Proシミュレータでテスト