Inside of LOVOT

GROOVE X 技術ブログ

クリスマスの精霊を妄想してメリークリスマス

この記事は「GROOVE X Advent Calendar 2023」の最後の記事です。

こんにちは、ソフトウェアのエリアプロダクトオーナーのよっきです。

今年のアドベントカレンダーのラストを飾らせていただくことになりました。ありがたい話です。有ることが難しい。有り難い。あってよかった。

実は、去年のアドベントカレンダーもラストを飾らせてもらっていました。去年は「LOVOTと生きがい」についてのポエムを書かせてもらいました。今年はどんなテーマで書こうか悩みましたが、最近考えている「気づきを与える」ということについて考えてみました。

※組織の見解ではなく個人の私見になります。

温かいテクノロジーとしての目指す方向性

2023年5月に弊社代表の林要が書いた「温かいテクノロジー」の本を読んだ方はいますでしょうか?

GROOVE X社員としては必読書で、私はAmazonで予約して買ったのですが、なんと後日社員に配られる事になり、私の手元には2冊あります。それはともかくとして「温かいテクノロジー」の中に書かれていたテクノロジーのゴールの1つが「気づき」に関連する部分なので紹介します。環境問題と同じくらい大きな社会問題という節になります。

 僕ら人類はラーニング(学習)能力が高いため、手に入れた認知を土台に次の学習を積み重ねています。となると、土台から作り直すレベルのアンラーニングというのは、自分の世界観を一変させるほどの大きな変革となります。もはや個々の努力だけでは、どうにもならないことかもしれません。
 (ちょっと中略)
 アンラーニングを促すために必要なのは、新たなことに自ら「気づく」経験です。なので、この課題に対して、ロボット開発者としてのぼくが目指している解決策の1つは、いつかロボットを人類に寄り添う「コーチ」にまで進化させ、「気づき」を得るサポートをすること。
 ぼくは、これこそがテクノロジーのゴールの1つだと捉えています。

めっちゃわかりやすく言うと、社会の価値観が変わっている時に自分も変わらないと生きづらいですよ、と言っているのだと思います。 では、自分が社会に合わせていけばいいのか、と言われるとそれが相当難しい。本書の引用にもあるように、自分を成す土台を作り直さないといけないですからね。今まで学習してきたことは何だったのかと。

では、社会の価値観が変わって自分の土台を作り直す必要が出た時に何に気づけば良いのか。私は自分の感情と向き合うのがいいと思っています。つまり「自分の感情に気づく」ことが大事だと思っています。

自分の感情に気づくとはどういうことか!?

今回は、この自分の感情に気づくとはどういうことかについて、あるクリスマスにまつわる有名な物語に合わせながら考えていきたいと思います。

クリスマス・キャロルから考える自分の感情への気づき

小説「クリスマス・キャロル」はみなさんご存知でしょうか?なんと出版が1843年の180年も前の物語なんですね。 私は最近Audibleで聴きました。我が家では家族で車移動する時にAudibleを聴くのがお気に入りで、12月ということで「こどもの聴く名作シリーズ」からクリスマスらしいお話を探してクリスマス・キャロルにたどり着きました。

この「クリスマス・キャロル」が今回の物語となります。

クリスマス・キャロルのお話はざっとこんな感じです (1840年代の名作で、世の中にストーリーは溢れているのでネタバレになってもいいですよね?)

 スクルージという冷血で金の亡者の初老が、クリスマスに現れた過去・現在・未来の3人の精霊と一緒に時空の旅をして、昔の自分がどんなことにワクワクしていたか、現在の自分が他の人にどう思われているか、そして今後起こる自分がどうなるかを精霊と一緒に見て、自分の行いを反省して改心する

一文で書くと、よくある説教臭いファンタジーなのですが、自分の感情に気づく、という点で非常に参考になる物語でした。

もうちょっと詳しく物語見ていくと、

主人公のスクルージは、冷血で金の亡者なので、クリスマスも関係ない、むしろ、寄付をせがまれたり、従業員が休みたいと言ったりで、超迷惑している。クリスマスを祝うなんでバカバカしい、と思っている。いや、口にも出している。あまりに行動が酷いのでクリスマスの精霊が来て、過去現在未来の状況を見せつけて改心させようとする。 その中で過去の精霊がスクルージに見せつけたことが自分の感情に気づかせるのにとてもよかったです。

過去の精霊はどんなものを見せつけたのか。

それが、スクルージ少年が大好きなアリババの物語の本を読んでニコニコしているところ、スクルージ青年が下働きをしていた雇い主にクリスマスパーティを開催してもらい優しくしてもらっているところ、といった自分がワクワクしたこと、感謝の気持ちを持ったときの思い出でした。で、このシーンを見せている時に精霊は決して口を出さず、黙っています。そして、スクルージが懐かしがったり感動している時に、精霊はスクルージが以前言っていたセリフをそのままスクルージに言います。

「クリスマスを祝うなんてバカバカしい、クリスマスを祝うバカはツリーにぶら下げてやればいい」

まぁまぁ、ひどい仕打ちですよね。スクルージはそのくらい酷い人だったので仕方がないのかもしれませんが。

スクルージの例はかなり極端な例ですが、私たちも普段の生活や仕事をしている中でワクワクがなかったり、感謝の気持ちが芽生えてなかったりすると、もしかしたら自分の感情に気づかず別の行動をしているのかも知れないです。

精霊が見せた過去、その過去の気持ちって自分の純粋な気持ちに近いのかなって思います。そんなピュアな気持ちを持っていたことに過去の自分を通して気づき、そして、現在の自分とのギャップに気づく。このギャップの気づきが自分の行動をどう変えていくと良いのかの道標になると思います。

そして、精霊のすごいところは、これがダメでやるならこうした方がいい、というアドバイスをしていなく、あくまで見せて、感じさせて、自ら気づかせているところ。まさにコーチングって感じです。ダメなことややった方がいいことって、たくさんの書物に書かれていて、それをおすすめすればできることだと思います。それこそChatGPTですでにできているとも言えます。そうじゃなくて、自分の経験から内面と向き合う機会を作り、自ら気づくという経験をさせていくことが、気づきの与え方としては適切なんじゃないかなって思います。 だって、たくさんの書物を読んでもできないものはできないもん!

そんな気づきの与え方は、過去のデータと現在の状況から判断できるので、もしかしたらAIでやろうと思えばできることなのかも!? その人と長く暮らす未来の家族型ロボットなら、そういうデータを集めて、過去の精霊になれるかもしれない!? なんて妄想も膨らむ、クリスマス・キャロルでした。

その後、スクルージは現在と未来の精霊に導かれ、周りにどう思われているか、そして未来はどうなるかを見せつけられ、現在に戻って、クリスマスを最高に楽しめる人になる、というストーリーで終わります。めでたしめでたし!

最後に

ここまで読んで頂きありがとうございます。

クリスマスのストーリーに合わせてLOVOTに関する未来を妄想してみました。気づきを与えるいいコーチにしていけるかしら!?

こんな妄想をくれるきっかけを与えてくれたLOVOTという存在、そして、温かいテクノロジーという本、それを執筆した要(かなめ)さんに感謝です。冒頭に温かいテクノロジーの本が2冊あると書きましたが、そのうちの一冊に社員だけど社長のところに行ってこっそりサインをもらいました。そして、お願いして一言入れてもらいました。

「人生の挑戦者へ」

この言葉は、私が入社した時、その当時出版されていた要さんの初書籍「ゼロイチ」を持って社長にサインください、とミーハー感出してもらった時に書いてもらった言葉です。入社した時はベンチャーは人生の挑戦なんだな、くらいの軽い気持ちで受け取っていましたが、入社して5年ほどたち、すごい挑戦であることを身をもって体験し、同じ言葉だけど重みが違う言葉として今回は受け取ることができました。

GROOVE Xでは引き続き人生の挑戦者を募集しております。一緒に頑張りましょう。

それでは今年も良いクリスマスを! メリークリスマス!! (※人生の挑戦者達へ、から、人生の挑戦者へ、という言葉に変わったってことは、一人の挑戦者として認めてもらえたってことかな!?)

人生の挑戦者は ↓↓こちら↓↓ からどうぞ recruit.jobcan.jp

LOVOTのお出迎え機能の中国向け対応について

こんにちは、APPチームの黒田です。 この記事は GROOVE X Advent Calendar 2023 の24日目の記事です。
LOVOTは2023年より中国での販売を開始しました。今回は、それに関連してLOVOTアプリの中国向け対応、中でも特に位置情報を使った機能の対応について紹介します。

アプリで中国向け対応が必要な機能

アプリの中国対応と聞いてすぐ思い浮かぶことと言えば、Googleのサービスが使えない、ということが挙げられますね。
LOVOTアプリでは、主にLOVOTのお出迎え機能の実現のため、位置情報を取得したりGeofenceの機能を利用する必要がありますが、Androidで位置情報取得といえば、通常、Google Play services の一部である Location API を利用することになります。
我々がよく見るおなじみの位置情報サービスの設定画面、こちらは Google Play services の機能によって実現されているという訳です。

おなじみの画面

さて、これが中国本土向けのAndroid端末だとどうなるかと言えば、端末によってはこの画面がある場合もあるし、無い場合もある、ということになっているようです。
GMS(Google Mobile Service)がインストールされた端末であれば Google Play が利用可能、また Xiaomi 端末にて Basic Google services の設定をオンにすることでGoolge位置情報サービスが有効化されるものもあったりします。

みなれない設定(Xiaomi端末)

なんにせよ、中国本土においては Google の Location API が利用できるという前提で開発ができない、ということになりますので、位置情報サービスが利用できるよう、別の手段を用意しなければなりません。
(なおiOSの場合は、何もしなくても日本国内向けアプリで実装した位置情報取得や Geofence の機能がそのまま使えます。素晴らしい。)

その他、FCM(Firebase Cloud Messaging)が使えない、Firestoreも使えない、Firebase Authentication を直接利用することも出来ない、電話番号によるログインがほぼ必須、などといった問題もあり、それぞれ対応が必要でしたが、以下、この記事では位置情報サービスの利用に話題を絞ります。

中国本土での位置情報サービスの利用

中国本土で位置情報サービスを使いたい、となると、中国で地図サービスを提供している企業がが開発者向けに用意している SDK を利用するのだろう、ということになります。
地図サービスの主なプロバイダーとして、百度地图(Baidu)高德地图(Amap)腾讯地图(Tencent) などがありますが、これらはどれも開発者向け SDK を提供しています。

それらの中から目的に合ったものを選んで利用すれば良さそうですね、という訳で、LOVOTアプリでは Baidu の SDK を利用することとなりました。
今回の記事では位置情報や Geofence の話題に絞っていますが、アプリでの地図データの取得にも Baidu のサービスを利用しています。中国での地図アプリのシェアは高德地图と百度地图が2大巨頭ですので、Baidu の地図を利用しておけばユーザーにとって馴染みのある表示にできる、ということもありますし、クオリティーにも信頼がおけそうです。
Baidu でなく高德地图の利用を検討することも可能でしたが、(今回の記事ではサービスの利用申請に関しては触れませんが)各サービスの利用のため開発者登録を行うためには、企業認証の申請を通すため中国法人側で書類を用意したりと手続きもやや面倒ですし時間もかかります。そういったこともあり、Baidu の SDK が必要な機能を満たしていること、また費用面でも高德地图と同等であるということが確認できたため、そのまま Baidu の SDK を利用しています。

座標系について

ここから実際の SDK の利用について説明したいのですが、その前に中国本土内での位置情報の座標系について触れておきます。
我々が普段地図で見ている緯度経度、開発者であれば Location API で取得したり、Maps API で地図データを取得するのに利用している緯度経度の情報は、WGS-84 と呼ばれる座標系における値となっています。
正直、私もそのようなことは意識したこともなかったのですが、ここで中国本土においては GCJ-02 と呼ばれる別の座標系が採用されている、ということが問題になります。
WGS-84 と GCJ-02 の相互変換アルゴリズムは公式には公開されていないため、簡単に言えば、中国本土内にてある位置の地図を取得しようと思えば、中国国内で提供されているサービスを使わざるを得ない、ということになります。
(例えば、中国本土でどうしても Google Maps API を利用したければ、中国のクラウドサービス上に Google Maps API へアクセスするProxyを実装する、といった荒業も可能かもしれませんが、それで取得できたマップは正しくないので意味がない、ということになります。)
座標系の問題も、Baidu などの SDK を利用せざるを得ない理由の一つという訳です。

左: Google Mapで取得した地図(赤丸が中心点)
右: Baiduで取得した同じ座標の地図(赤丸がGoogle Mapでの中心点)

さて、Baidu の SDK により位置情報を取得した際は、座標系も一緒に返されるようになっています。ここで、必ずしも GCJ-02 で取得できるとは限らない、というのもポイントです。SDK を利用して位置情報の取得をテストしてみると、WGS-84 と GCJ-02、どちらで取得されることもある、ということが分かります。
Geofence の登録時にも座標系を一緒に登録できるようになっており、SDK 内では座標系の相互変換が可能なため、WGS-84 と GCJ-02、どちらで取得された位置情報であっても、正しく Geofence が動作する、ということになっているようです。
(実は、更に BD-09 などという Baidu 独自の座標系もあるのですが、ともかく Baidu の SDK を利用している限りこれ以上気にすることはありません)

SDKの導入

SDKの導入については、ドキュメントにコード例も記載されているので、基本的にそれに従って実装すれば良い、となります。
とはいえ実際に実装してみると上手く動かない、ということもあると多々思いますので、以下、LOVOTアプリの場合のコード例とともに、幾つかポイントについて説明しておきたいと思います。
前提として、LOVOTのお出迎え機能がオンの間は、常にスマホの Geofence のイベントを監視していたいため、Foreground Service を起動するようにしています。以下のコード例は、そのサービスのコードからの関連部分を抜粋し編集したものであり、実際のコードとは異なります。
なお、LOVOTアプリは Unity で作られているため、これらの機能は Unity のネイティブプラグインとして実装されています。

public class LocationUpdateService extends Service {
    private GeoFenceClient geofenceClient;
    private LocationClient locationClient;
    private LocationUpdateCallback currentCallback;
    private GeofenceBroadcastReceiver geofenceBroadcastReceiver;

    interface OnLocationResultListener {
        void onReceiveLocation(BDLocation location);
    }

    public class LocationUpdateCallback extends BDAbstractLocationListener {
        OnLocationResultListener listener;

        public void Initialize(OnLocationResultListener listener) {
            this.listener = listener;
        }

        @Override
        public void onReceiveLocation(BDLocation location) {
            listener.onReceiveLocation(location);
        }
    }

    @Override
    public void onCreate() {
        // LocationClient や GeoFenceClient の初期化を行う
        // 国内向けアプリでは FusedLocationProviderClient の取得のみを行っていたところ

        super.onCreate();

        try {
            LocationClient.setAgreePrivacy(true);
            locationClient = new LocationClient(this);
        } catch (Exception e) {}
            return;
        }

        LocationClientOption option = new LocationClientOption();
        option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);
        option.setCoorType("gcj02");
        option.setFirstLocType(LocationClientOption.FirstLocType.ACCURACY_IN_FIRST_LOC);
        option.setScanSpan(60 * 1000);
        option.setOpenGnss(true);
        option.setLocationNotify(true);
        option.setIgnoreKillProcess(true);
        option.SetIgnoreCacheException(false);
        option.setWifiCacheTimeOut(60 * 1000);
        option.setEnableSimulateGnss(false);
        option.setNeedNewVersionRgc(true);
        locationClient.setLocOption(option);

        IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
        // Geofence イベントの通知を受ける GeofenceBroadcastReceiver は別途実装しておく
        filter.addAction(GeofenceBroadcastReceiver.ACTION_PROCESS_UPDATES);
        geofenceBroadcastReceiver = new GeofenceBroadcastReceiver();
        registerReceiver(geofenceBroadcastReceiver, filter);

        geofenceClient = new GeoFenceClient(this);
        geofenceClient.setActivateAction(GEOFENCE_IN_OUT);
        geofenceClient.createPendingIntent(GeofenceBroadcastReceiver.ACTION_PROCESS_UPDATES);
        // 条件が満たされれば事実上無限に Geofence を発火させるようにしておく
        geofenceClient.setTriggerCount(100000, 100000, 0);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        if (locationClient != null) {
            locationClient.stop();
        }
        if (geofenceBroadcastReceiver != null) {
            try {
                unregisterReceiver(geofenceBroadcastReceiver);
            } catch (Exception ex) {}
        }
        if (geofenceClient != null) {
            geofenceClient.removeGeoFence();
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent.getAction().equals(START_ACTION)) {
            start(intent, startId);
            return START_REDELIVER_INTENT;
        } else {
            stop(intent, startId);
            return START_NOT_STICKY;
        }
    }

    private void start(Intent intent, int startId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            startForegroundWithNotification();
        }
        startGeofencing();
        startLocationUpdates();
    }

    private void stop(Intent intent, int startId) {
        if (geofenceClient != null) {
            // removeGeoFence() で削除するのではなく Geofence 登録を残したまま pause & resume で制御するようにしている
            geofenceClient.pauseGeoFence();
        }
        stopLocationUpdates();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // onStartCommand 後、5秒以内に startForeground が呼ばれないとANRとなる
            startForegroundWithNotification();
            stopForeground(true);
        }

    }

    private void startForegroundWithNotification() {
        Notification notification = createNotification(this);
        startForeground(1, notification);
    }

    public Notification createNotification(Context context) {
        // Notification を生成して返す
    }

    private void startLocationUpdates() {
        if (currentCallback != null) {
            stopLocationUpdates();
        }

        // 実際にはここでGPSオンの確認や、位置情報取得パーミッションのチェックを行う

        final GeoFenceClient geofenceClient = this.geofenceClient;
        final LocationUpdateCallback callback = new LocationUpdateCallback();
        OnLocationResultListener listener = new OnLocationResultListener() {
            @Override
            public void onReceiveLocation(BDLocation location) {
                int locType = location.getLocType();
                // https://mapopen-pub-androidsdk.cdn.bcebos.com/location/doc/v9.4.0/constant-values.html
                // TypeGpsLocation 61
                // TypeNetWorkLocation 161
                // TypeOffLineLocation 66
                boolean isSucceeded = locType == BDLocation.TypeGpsLocation || locType == BDLocation.TypeNetWorkLocation || locType == BDLocation.TypeOffLineLocation;
                if (isSucceeded) {
                    // 適宜ログ出力するなど
                }

                // pause 状態になっていることがあったので、resume するようにしている
                if (geofenceClient.isPause()) {
                    geofenceClient.resumeGeoFence();
                }
            }
        };
        callback.Initialize(listener);
        currentCallback = callback;

        locationClient.registerLocationListener(callback);
        locationClient.start();
        geofenceClient.resumeGeoFence();

    }

    private void startGeofencing() {
        // SharedPreferences に保存済みの設定データ取得し、設定に従って Geofence を登録するようにしている
        // (実際には複数の Geofence 設定が存在する場合があるのでそれぞれ登録)
        // また、削除されていた設定に対応するGeofenceの解除も行うようにしている

        JSONObject config = getGeofenceConfig();
        final double latitude = config.getDouble("Latitude");
        final double longitude = config.getDouble("Longitude");
        final float radius = 100F;

        final GeoFenceClient geofenceClient = this.geofenceClient;
        GeoFenceListener fenceListener = new GeoFenceListener() {
            @Override
            public void onGeoFenceCreateFinished(List<GeoFence> geoFenceList, int errorCode, String customId) {
                geofenceClient.setGeoFenceListener(null);
                if (errorCode == GeoFence.ADDGEOFENCE_SUCCESS) {
                    // 適宜ログ出力するなど
                }
            }
        };

        geoFenceClient.setGeoFenceListener(fenceListener);
        DPoint centerPoint = new DPoint(latitude, longitude);
        geoFenceClient.addGeoFence(centerPoint, GeoFenceClient.WGS84, radius, customId);
    }

    private void stopLocationUpdates() {
        if (locationClient == null || currentCallback == null) {
            return;
        }
        locationClient.stop();
        locationClient.unRegisterLocationListener(currentCallback);
        currentCallback = null;
    }
}

イベント受信側については、BroadcastReceiver で受け Intent の Action や Extra にイベント種別など必要な情報が格納されている、という形なので、Google の Geofencing API を利用する場合と同じような実装となり、特に困ることはないでしょう。

以下、コード内のコメントでは説明できていないポイントについて補足しておきます。

登録した Geofence は、GeoFenceClient オブジェクトが破棄されれば無効となる

当然といえば当然なのですが、国内向けに Google の GeofencingClient を利用していた際は、一度 Geofence を登録すればアプリ側でその生存期間を気にする必要はなかったのですが、Baidu の GeoFenceClient の場合はオブジェクト破棄とともに Geofence が無効となるため、Foregroud Service内の処理側で必要に応じて毎回登録処理を行うようにしています。

LocationClient による位置情報取得を開始していないと Geofence のイベントは発火しない

公式ドキュメントには GeoFenceClient に Geofence を登録すれば内部的に位置情報取得も行われるというような記載があるのですが、それだと全く Geofence イベントが発火しませんでした。よって、明示的に LocationClient の開始を行っておく必要がありました。

Geofence イベントの発火には制限回数が必要

GeoFenceClient が生きている限り Geofence は有効なのですが、初期化時に制限回数をセットする必要があり、実質無限となるように十分大きな数をセットしています。セットしない場合、全くイベントが発火しなくなる現象が発生し、また無制限を指定するような方法は無さそうでした。

位置情報取得を止めない

国内向けのLOVOTアプリでは、状況に応じて位置情報の取得頻度を下げるような処理を行っているのですが、Baidu の LocationClient では位置情報取得の中断/再開処理をアプリ側で制御していると Geofence イベントが極端に発火しづらくなってしまいます。LocationClient の初期化時に位置情報取得の最低間隔を指定できるので、それによって制御するに留めるようにしています。

動作確認

アプリの開発は全て日本国内で行っており、位置情報を利用する機能について、実際に中国で動作させたときの挙動は、最終的には中国国内で確認する必要があります。
これには、中国側のスタッフや中国出張中のメンバーの手を借りることとなりました。
出張メンバーは中国語が分からないため、中国スマホを別のスマホのGoogleアプリで撮影して翻訳しながら作業する、といった技を編み出していたようです。こういった技を駆使しながら中国の街中をうろうろしていてもらっていたかと思うと、感謝しかありません。

スマホを撮影する出張メンバー

最後に

実は、アプリの中国対応をするにあたって何と言っても困るのは、中国語の情報しかない、ということだったりするかも知れません。
Baidu の SDK の開発者ドキュメントも英語版はありません。検索しようにもどう検索すれば良いかが分からなかったり、関連するワードで検索したとしても見つかる情報が少なかったり。もちろん、ChatGPTも正しいことは教えてくれませんでした。普段、大量の日本語や英語の情報に助けられて開発していることを痛感します...。
中国向けに位置情報アプリの対応を行う、といった機会はあまり多くはないかも知れませんが、今回の記事がいつか誰かの役に立てば幸いです。

Google翻訳されたドキュメント。コードなども微妙に翻訳されてしまうあるある。どうにかする良い方法あるんでしょうか


さて、GROOVE X では、一緒にアプリを開発してくれる方も募集中です。
Unity といえばゲームやVRなどの開発が殆どかと思いますが、それらとは一味違う開発の経験が可能です。大量の3Dオブジェクトのリアルタイム制御、美麗なグラフィック、というような開発要素はほぼ無いのですが、アプリと連携するクラウド側やLOVOT内で動作するサービスの開発、ネイティブの機能を利用するためのプラグイン開発などに興味のある方がいらっしゃれば、是非よろしくお願いします。

recruit.jobcan.jp

フィールドを越えると そこはフィールドだった

こんにちは。 GROOVE X フットサル部所属の id: numa-gx です。

テックネタではありませんが、GROOVE Xでコロナ前から3年以上も継続している活動を紹介したいと思います!

昼休みにフットサルやってます

フットサル部では、オフィスの近くにあるコートで昼休みを返上して活動しています!

週1回のペースで12時過ぎから約30分ほどプレイしています。

今週の木曜日の様子

男女関係なくわいわいやってます

フットサル部の危機

長く続けているフットサル部ですが、ある程度時間が経過すると、毎回一定以上の人数を集めて開催することが難しくなっていました。

参加者が集まらない原因としては、

  1. 新しいメンバーを誘っても数回参加して定着しない
  2. メンバーの転職などによって参加できなくなる
  3. 気候の影響(夏場の暑さなど)

などがありました。

社内での周知や新入社員への声かけを積極的にしていましたが、参加者は徐々に減っていき、開催が難しい時期が続いていました。

参加者を求める悲痛な叫び

社内の有志の活動でよくあるパターンですね…。

人数が少ないと参加者の負荷があがって更に参加者が減る悪循環…

おもわぬ仲間の登場

そんな状況でしたが、 「退職したメンバーの転職先で募集すればいいのでは?」 というアイデアが救世主となりました。

実は、奇跡的にもGROOVE X が入っているオフィスの別の階の会社に転職したメンバーがいたので、声をかけたところ参加してくれることに!

フットサル仲間が一気に2倍近くに増えました(わーい)

さらに、参加交渉のハードルが下がったおかげで、その場に居合わせた野生のフットボーラーにも声をかけ、時々参加してもらえています。

フットサルスケジューラーの誕生

週1で開催していましたが、何曜日に開催するかは参加者の都合を社内Slackで相談しながら決めていました。 ところが、社外のフットサル仲間が増えた影響によりスケジュールの調整が難しくなってしまいました。

そこで、Slack Connect を使って社外のメンバーにもSlackに入ってもらって連絡できるようにしています。

slackをEnterprise Gridに変更した話 - Inside of LOVOT で紹介したように弊社はSlackをものすごく使ってます!)

翌週の開催日を決めてくれる翼くんBot。投票が多い曜日に開催されます。

フットサルをやってよかったこと

仕事関係ないよねと思われるかもしれませんが、やっていてよかったことが色々あったのでまとめてみます。

  1. 社内の普段仕事で関わらない人と関わって話がしやすくなる
  2. ほど良い運動で健康に良い
  3. 出社して仕事する動機づけになる
  4. 知り合いが増える

仕事に関係するところでは、職場で普段関わりがない人と話がしやすくなるため、社内のイベントで会ったときなどにコミュニケーションがスムーズになります。「最近どうですか」みたいなことも話しやすいです。

また退社後や休日に運動する時間を取りづらい人にとっては、昼休みを使って運動する機会が得られるのでとても助かってます。

さらに、会社の方針にもよりますが、出社して仕事をして欲しい場合、フットサルがあることで出社日を作りやすくなっています。 特に私はソフトウェアエンジニアでリモートでも仕事はできますが、出社する動機付けになっています。

最後に、フットサルを通じて知り合いが増えるのもありがたいことです。特に社外の人とも仕事関係なく話をしたり交流できるのは貴重な機会になってます。感謝。

仲間募集

というわけで、GROOVE Xではフットサルを一緒にやってくれる仲間も募集しています。 平日の昼間に日本橋浜町あたりに行けるよという方いらっしゃれば気軽にお声がけください。

ボールは友達!

ついでに弊社に興味を持っていただけたら、応募もよろしくおねがいします!

recruit.jobcan.jp

『ゲームさんぽ』でロボットデザインについて(好き勝手に)語りました!

はじめまして!『Inside of LOVOT』初登場 CDO(チーフデザインオフィサー)の 根津 です! 年の瀬も迫り、GROOVE X AdventCalendar 2023 も、ついに22回目となりましたね。。

少し前の話になるのですが、今年の7月、ライブドアニュースさんの YouTube『ゲームさんぽ』で、「ロボット開発者に、ゲームに登場するロボットのデザインについて、いろいろと語ってほしい!」というご依頼をいただいて、『Atomic Heart』というゲームを題材に、好き勝手に語ってきました。ひとりでは心もとないということで、超強力助っ人 アニメーターの中里ひろきさんにもご出演をお願いし、さらに、LOVOT「らむね」も一緒に出演することになったため、撮影スタジオはとても賑やかな絵面になりました。

撮影前準備中!

正直なところ、アトミックハートというゲームのことは、この時に初めて知ったのですが、なかなかに考察意欲をそそる興味深いゲームでした。未来のような、ちょっと昔のような、不思議な世界(「1955年の架空のソビエト連邦」という設定)を舞台に、個性豊かなたくさんのロボットたちが登場します。司会のヤマグチクエストさんが、テンポよくゲーム世界を案内してくださるので、中里さんと楽しいトークをしているうちに、あっという間に時間がすぎていきました。

特に、ロボットデザインでは避けて通ることのできない「不気味の谷」や、「カワイイの正体って何なんだろう?」といった考察は、話しながらいろいろと新しい発見があって、とても有意義でした。LOVOTで最も大切にしていることと言っても過言ではない「生命感」がどこに宿るのか、それが宿っていないロボットや、ちょっと惜しいロボットたちを題材に、その形状やしぐさなど、様々な角度から考察することで、「やっぱりLOVOTはすごいな!」と再認識しました。また、普段、漠然と感じていることを改めて言語化してみることの価値や、考えをぶつけあえる相手のありがたさを痛感する機会にもなりました。(中里さん、ヤマグチクエストさんに感謝なのです!)

スタッフのみなさんも興味シンシン!

自分で言うのもなんですが、いま見てもなかなか面白い内容じゃないかと思いますので、まだの方は、年末年始にでもぜひご覧になってみてください!

↓ ↓ ↓ 各動画はコチラから! ↓ ↓ ↓

ライブドアニュース『ゲームさんぽ』【Atomic Heart】

第1回【どこ見てるの…?】ロボットの愛らしさは「目」と「健気さ」で決まる

www.youtube.com

第2回【感無量】ロボットが反乱を起こしたら感動するかも…?ロボット開発者たちの本音

www.youtube.com

第3回【不気味の谷】ロボットの顔面崩壊シーンはなぜ恐ろしい?開発者たちに見てもらった

www.youtube.com

PythonとGoとRustで複数のHTTPリクエストを同時にやってみた

こんにちは!ソフトウェアチームのエンジニアのふくだです。
この記事は、GROOVE X Advent Calendar 2023の21日目の記事です。

qiita.com

並行処理が大好きです

私は並行処理が大好きです。ただとても難しいです。

私はPythonの asyncio という非同期処理の標準ライブラリ、その中でも最近のわかりやすく使いやすくなった高レベルAPIで好きになった新参者で、日々勉強しています。

弊社GROOVE Xには、さまざまな技術を持ったエンジニアが活躍しています。ソフトウェア領域であれば、CやC++、Go、PythonやRust、TypeScriptなどさまざまなプログラミング言語が使われています。*1

そうなんです。並行処理といえば!と言われている言語が使われているではありませんか!そう、GoとRustです。 ただ、私はGoもRustもあまり書いたことがありません。今回の記事では、PythonのコードをベースにGoとRustでたくさんのHTTPリクエストを行う処理を書いてみました。

簡単な仕様

今回のコードの簡単な仕様は以下の2つです。

  • 特定のエンドポイントに対して複数のHTTPリクエストを同時に行い出力する
  • エラー処理を書く

そんなことしないって?確かにしませんね。ただみなさんのお使いのプロダクトなどに置き換えてみてください。 DBアクセスや外部APIの実行だったり、I/Oの塊だったりしませんか?弊社LOVOTもセンサー情報をたくさんあるため、I/Oがたくさんです。

実行環境

試した環境としては、以下のとおりです。

  • MacBook Air M2

また、外部の公開されているAPIを利用するため、コードをお試しいただく場合には、 計画的 にお願いします。 今回利用するのはPokemon APIです。

pokeapi.co

書いてみた

まずはPython

各種バージョンは以下のとおりです。

  • Python 3.12.0
  • HTTPX 0.25.2
  • trio 0.23.2

Python コード

asyncio版

import asyncio
from dataclasses import dataclass
from time import time

import httpx

URL = "https://pokeapi.co/api/v2/pokemon/"


@dataclass(slots=True)
class PokemonResponse:
    """PokemonResponse はAPIレスポンスのデータクラス"""
    name: str


async def fetch_pokemon(client: httpx.AsyncClient, num: int) -> PokemonResponse:
    """fetch_pokemon は指定された番号のポケモンの名前を取得"""
    response = await client.get(f"{URL}{num}")
    data = response.json()
    return PokemonResponse(name=data["name"])


async def main():
    start = time()
    try:
        async with httpx.AsyncClient() as client:
            async with asyncio.TaskGroup() as tg:
                tasks: [asyncio.Task] = [
                    tg.create_task(fetch_pokemon(client, number))
                    for number in range(1, 2)
                ]
        pokemon_names = [task.result().name for task in tasks]
        print(pokemon_names)
        print(f"Time: {time() - start}s")
    except* BaseException as e:
        print(f"Error: {e.exceptions}")


if __name__ == "__main__":
    asyncio.run(main())

Trio版

弊社では、意思決定エンジンであるneodmにTrioを利用しています。そのため、Trioでも書いてみましょう。

from dataclasses import dataclass
from time import time

import trio
import httpx

URL = "https://pokeapi.co/api/v2/pokemon/"


@dataclass(slots=True)
class PokemonResponse:
    """PokemonResponse はAPIレスポンスのデータクラス"""
    name: str


async def fetch_pokemon(client: httpx.AsyncClient, num: int, results: list) -> None:
    """fetch_pokemon は指定された番号のポケモンの名前を取得"""
    response = await client.get(f"https://pokeapi.co/api/v2/pokemon/{num}")
    pokemon = response.json()
    results.append(PokemonResponse(name=data["name"]))


async def main():
    start = time()
    results = []
    try:
        async with httpx.AsyncClient() as client:
            async with trio.open_nursery() as nursery:
                for number in range(1, 151):
                    nursery.start_soon(fetch_pokemon, client, number, results)
        print(results)
        print(f"Time: {time() - start}s")
    except* BaseException as e:
        print(f"Error: {e.exceptions}")


if __name__ == "__main__":
    trio.run(main)

Python 実行結果

それぞれ実行してみます。

$ python asyncio_pokemon.py
['bulbasaur', 'ivysaur', 'venusaur', ...
Time: 0.9101917743682861s

$ python trio_pokemon.py
['moltres', 'kabuto', 'magikarp', ...
Time: 0.8739862442016602s

時間をおいて3回実行してみました。結果は次のとおりです。

Time: 0.7011699676513672s
Time: 1.0189759731292725s
Time: 0.5281181335449219s

続いてGo

各種バージョンは以下のとおりです。

  • Go 1.21.1

Go のコード

Goのコードです。goroutinesとchannelを使用して、複数のHTTPリクエストを同時に実行します。

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
    "time"
)

const baseURL = "https://pokeapi.co/api/v2/pokemon/"

// PokemonResponse はAPIレスポンスの構造体
type PokemonResponse struct {
    Name string `json:"name"`
}

// fetchPokemon は指定された番号のポケモンの名前を取得
func fetchPokemon(wg *sync.WaitGroup, number int, results chan<- string, errors chan<- error) {
    defer wg.Done()
    url := fmt.Sprintf("%s%d", baseURL, number)

    resp, err := http.Get(url)
    if err != nil {
        errors <- err
        return
    }
    defer resp.Body.Close()

    var pokemon PokemonResponse
    if err := json.NewDecoder(resp.Body).Decode(&pokemon); err != nil {
        errors <- err
        return
    }

    results <- pokemon.Name
}

func main() {
    var wg sync.WaitGroup
    results := make(chan string, 150)
    errors := make(chan error, 150)

    start := time.Now()

    for i := 1; i <= 150; i++ {
        wg.Add(1)
        go fetchPokemon(&wg, i, results, errors)
    }

    wg.Wait()
    close(results)
    close(errors)

    for err := range errors {
        fmt.Println("Error:", err)
    }

    for result := range results {
        fmt.Println(result)
    }

    fmt.Printf("Time: %v\n", time.Since(start))
}

Go 実行結果

それぞれ実行してみます。

$ go run pokemon.go
starmie
clefable
geodude
haunter
...
Time: 1.124114875s

時間をおいて3回実行してみました。結果は次のとおりです。

Time: 1.124114875s
Time: 381.089875ms
Time: 199.999333ms

最後にRust

各種バージョンは以下のとおりです。

  • Rust 1.74.1
  • reqwest 0.11
  • tokio 1

Rust のコード

Rustのコードです。HTTPクライアントのreqwestクレートと並行処理を行うtokioを利用します。

/// `PokemonResponse`は、PokeAPIからの応答を表す構造体
#[derive(Deserialize, Debug)]
struct PokemonResponse {
    name: String,
}

#[tokio::main]
async fn main() {
    let start = std::time::Instant::now();

    let client = reqwest::Client::new();
    let mut handles = vec![];

    for number in 1..=150 {
        let client = client.clone();
        handles.push(tokio::spawn(
            async move { fetch_pokemon(&client, number).await },
        ));
    }

    let mut pokemons = Vec::new();
    for handle in handles {
        match handle.await {
            Ok(Ok(pokemon)) => pokemons.push(pokemon.name),
            Ok(Err(e)) => eprintln!("Error: {}", e),
            Err(e) => eprintln!("Error: {:?}", e),
        }
    }

    println!("{:?}", pokemons);
    println!("Time: {:?}", start.elapsed());
}

/// fetchPokemon は指定された番号のポケモンの名前を取得
async fn fetch_pokemon(client: &reqwest::Client, number: u32) -> Result<PokemonResponse, reqwest::Error> {
    let url = format!("https://pokeapi.co/api/v2/pokemon/{}", number);
    let resp = client.get(&url).send().await?;
    let pokemon = resp.json::<PokemonResponse>().await?;
    Ok(pokemon)
}

Cargo.tomlは以下の通りです。

[package]
name = "poke"
version = "0.1.0"
edition = "2021"

[dependencies]
reqwest = { version = "0.11.22", features = ["json"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Rust 実行結果

それぞれ実行してみます。

$ cargo run --release
❯ cargo run --release
    Finished release [optimized] target(s) in 0.16s
     Running `target/release/poke`
["bulbasaur", "ivysaur", ...
Time: 1.5475795s

時間をおいて3回実行してみました。結果は次のとおりです。

Time: 1.412308208s
Time: 1.120167541s
Time: 1.589511041s

結果

速度としては、次のとおりの結果でした。気にはなるのですが、速度については処理系がマルチコアが使えるか、とか、ネットワークの状況もあるかと思うので、あくまでご参考で :pray:

|Python|約0.749秒| |Go|約0.568秒| |*Rust|約1.374秒|

感想

それよりも気になるのは、それぞれの言語の特徴ですね。気になるところを箇条書きにしてみました。

  • Go/Rustでやっぱり型をカッチリできるのはいい
    • Pythonでかっちりしたい場合には、Pydantic の出番
  • Goの sync.WaitGroup 書きやすい
    • Add() Done() Wait() でのハンドリング。async/awaitを採用していないのもおもしろい!
    • errgroup も気になる〜
  • Pythonのwalrus演算子 := が、Goでは型推論 (walrusはセイウチ。横から見るとセイウチの顔っぽく見える)
  • Rustのコンパイラ優しい ありがたい
  • Rustのキモは tokio::spawn での非同期タスクの生成と async move
    • 特に move での所有権をクロージャに移動して安全に使うところとか
      • タスクの生成は、Pythonの create_task のイメージ
  • Rustの .await?; ?って!?
    • エラーのハンドリングのためのもの
      • Result型 はOkとErrを持つ。 ? でハンドリングする
        • これもおもしろい〜!

な、印象を持ちました。みなさまはどうでしょうか?普段使っていない言語での似たコードはとても刺激的でした。

まとめ

個人的にはGoのファイル名、pokemon.goで優勝ですね。 それぞれの言語にそれぞれの特徴があってとてもおもしろかったです。コードを書いてみて、それぞれのフォーマッターやコードスタイルも気になりました!また色々な比較がかけそうですね。今回は紹介できませんでしたが、もちろんJavaScriptでも書けますし、C++にも std::async があったりC++ 20から co_await が追加になったりという情報もキャッチアップでき、まだまだ広がる並行処理の世界にワクワクが止まりません!

また弊社Slackには、トゲピーやタマザラシ、モルペコがいます!ぜひ会いにきてください!

わちゃついて!とお願いしたら、わちゃついてくれたやさしいポケモンのみなさん

*1:詳しくは、アドベントカレンダー1日目の LOVOTech Night 初開催報告!! - Inside of LOVOT をご参照ください。

ふるまい開発を支える可視化フレームワーク "pura"

こんにちは!ふるまいチームのエンジニア、市川です! 私は主にLOVOTのふるまい開発*1をしています。

LOVOTには半天球カメラ、4個のマイクアレイ、照度センサー、 温度カメラ、タッチセンサー、測距センサーなど、多数のセンサーが搭載されています*2。 それらのセンサー値や、画像認識・音声認識・自己位置推定の結果など、様々な情報を使ってLOVOTに意思決定させています。 LOVOTがどんな情報を得ていて、どのような意思決定をしているかをリアルタイムにモニタリングできるとデバッグが捗ります。 そこでそれらを可視化できるフレームワークとして"pura"というライブラリを使用しています。 puraは弊社が開発したpythonで利用可能なライブラリで、オープンソースで公開しています。 すごく使いやすいので、今回puraについて紹介させてください。

github.com ※ puraが提供しているのはブラウザに絵を描く汎用的な機能であり、LOVOTの情報を描くようなLOVOTに特化した機能は提供していません

可視化例

実際にpuraで描画している様子をいくつかご紹介します。 詳細な説明は割愛させて下さい。雰囲気だけでも伝われば幸いです。

しっぽの位置

慣性センサーから推定されるLOVOTの傾き

メロディの認識状況

LOVOTは周囲の音から、メロディらしきものを識別します。

まぶたの開閉状況

深度カメラの認識状況

最初に見えているのは足で、その後に崖を発見しています。

赤外線発光位置の推定状況

点が推定発光位置(大きな点は統計的に見た推定位置)で、点群が動いているのはLOVOTが動いているためです。

経路計画の状況

puraの導入方法

puraの導入方法について簡単にご説明します。 繰り返しになりますが、puraが提供しているのはブラウザに絵を描く汎用的な機能であり、LOVOTと接続して、LOVOTのデータを見ることができるわけでありません。あしからず!

下記の手順でinstallできます。

git clone https://github.com/groove-x/pura.git
cd pura
pip install -e .

サンプルが用意されており、下記で実行できます。

python examples/web_view_example.py

http://localhost:8080/で下記の画面が出れば成功です。

左上のリストボックスからチャンネルを切り替えることができます。 下記のように、マウス等の入力を受け付ける事も可能です。

web_view_example.pyのコードを見れば分かりますが、非常にシンプルに実装できます。 サンプルコードを更に短くしたものを下記に書いておきます。

import anyio
from pura import WebViewServer, WebViewMixin

# 1. WebViewMixinを継承したクラスを用意する
class Hello(WebViewMixin):
    def __init__(self):
        # 2. ウィンドウサイズやフレームレートを定義
        super().__init__(webview_size=(320, 240), webview_frame_rate=20)

    # 3. draw()に描画内容を記載
    def draw(self, ctx):
        # 背景の塗りつぶし
        ctx.background(200)
        # 色の指示
        ctx.fill(128, 0, 255)
         # マウスの位置に"hello"を描画
        ctx.text("hello", ctx.mouseX, ctx.mouseY)

async def async_main():
    # 4. 非同期にするためTaskGroupのブロックを作る(anyioではなくasyncioやtrioを使っても良いです)
    async with anyio.create_task_group() as tg:
        # 5. WebViewServerを用意してserve
        server = WebViewServer()
        await tg.start(server.serve, "Web view example", 'localhost', 8080)
        # 6. WebViewMixinを継承した独自のクラスをインスタンス化してserveメソッドを呼ぶ
        #     (チャンネルの数だけ下記のように呼べばチャンネルが増えます)
        tg.start_soon(Hello().webview.serve, server)

if __name__ == '__main__':
    anyio.run(async_main)

puraはProcessing APIのサブセットでコーディングされているため、どのようなAPIが用意されているかはProcessing API側のドキュメントを読むのが良さそうです(pura側にはあまり整ったドキュメントがなさそう)。

https://py.processing.org/reference/

puraで具体的にサポートされているAPIは直接下記コードを見れば分かります。

pura/src/pura/_web_view.py at c767f24e6ab260e2565d7e77b797a55cb3965a03 · groove-x/pura · GitHub

その他

弊社のJohn BelmonteさんがPyCon US 2021で紹介したようです。 動画があるため良かったらこちらもご覧ください。

さいごに

一緒にLOVOTを作ってくれる仲間を募集しています。 上記のようなデバッグ環境があり、ロボット開発経験がなくても取っつきやすいと思います。ぜひ一緒に開発しましょう! 様々な分野で募集しているため、是非下記リンクをご確認下さい。

recruit.jobcan.jp

*1:ふるまいに関しては次の記事を参照ください: tech.groove-x.com

*2:下記で紹介しています: lovot.life

非英語話者の海外登壇(EuroPython 2023/PyCon TW 2023)

こんにちは!ソフトウェアチームのエンジニアのふくだです。
この記事は、GROOVE X Advent Calendar 2023の19日目の記事です。

qiita.com

英語をほとんどしゃべれない私ですが、海外のカンファレンスで登壇する機会を得ました。そのチャレンジの様子と、どうやって乗り越えたのか、をお伝えする記事になります。

参加したカンファレンスって

今回参加したのは、Pythonというプログラミング言語のカンファレンスです。Pythonのカンファレンスはは世界中で行われています。

世界中で行われているPyCon。こちらより引用 Join us at PyCon

また、 先日日本でもPyCon APAC 2023が開催され、弊社はスポンサーをさせていただきました!

PyCon APAC 2023でのスポンサーブースの様子はこちらの記事をご参照ください。 tech.groove-x.com

私の登壇したカンファレンス

今回私が登壇したのは、ヨーロッパの各地で年1回開催されているEuroPython 2023と台湾で行われているPyCon TW 2023です。プロポーザルを出し、採択いただき登壇することになりました。

名称 開催場所 開催期間
EuroPython 2023 チェコ・プラハ 7月19日⁠~21日
PyCon TW 台湾・台北 9月1日⁠~2日

どちらのカンファレンスもとてもとても楽しかったです。ここでその話をすると長くなってしまいますので、カンファレンスの様子については、gihyo.jp さんの以下の記事をご参考ください。

日本からひとりで参加した「EuroPython 2023」スピーカー体験レポート | gihyo.jp

https://gihyo.jp/assets/images/article/2023/12/road2euro-python23/002.png

PyCon TW 2023 カンファレンスレポート | gihyo.jp

https://gihyo.jp/assets/images/article/2023/09/pycon-tw2023/007.png

どれくらい英語しゃべれないの?

相当しゃべれないです。辞書を片手にゆっくり読むことはできるのですが、会話となると全く出てこなくなってしまいます。私の所属するふるまいチームには英語ネイティブのスーパーエンジニアがいます。その方は日本語をほとんどしゃべれません。ですが、その方に、日本語を話させてしまうくらいしゃべれないのです。

朝会の様子

非英語話者の海外登壇

どうやって乗り切ったの?トーク編

テクノロジーを頼って乗り越えました。自分の発表の準備や、他のセッションの聞き方について紹介します。

自分の発表(スピーキング)

私はトークをする時、必ず台本(原稿・トークスクリプト)を用意します。おおよそ以下の流れです。

  1. トークの見出しを決める
  2. 大枠の内容を埋める
  3. 台本を用意する
  4. 発表の練習をする
  5. 練習でだめだったところを修正する

3~5を繰り返し行って、クオリティをあげていくのが理想です。またその中で時間調整も行っていきます。

そして今回は英語です。まずは日本語で作った資料と台本を英語に翻訳していきます。翻訳で苦労したのは、「英語として意味が通じるか」といった点です。 また、日本語の時点でも資料・台本に荒さがあるため、日本語を修正→英語翻訳を修正、といった日本語でやる場合の倍の労力もかかりました。

資料と原稿はこんな感じです。MacのKeynoteを使っています。

常に手元に英語の先生がいてくれると嬉しい状況でした。つまらないことも聞きたい。そしてそう、2023年の私たちには手元にあの先生がいますね。今回翻訳にあたり、2つのWebサービスにとてもお世話になりました。

本当にささいいな言い回しをたくさん確認しました。ありがとうChatGPTさん

もちろん、社内で練習することも可能です!ですが、今回バタバタしすぎて、社内での練習はできませんでした。。。

本番の資料、ビデオは公開されておりますので、ご興味ある方は是非そちらからご覧ください(笑ってください。。)。

他の方の発表(リスニング)

普段英語圏の方のトークを見る時は、動画配信がほとんどです。YouTubeであれば、字幕機能を活用しています。ただし、オフラインだとそうはいきません。また英語圏のため、日本語への同時翻訳などもちろんありません。

はじめは、Google翻訳の音声入力を活用しようと思ったのですが、長時間の音声入力とその翻訳となると、うまく使いこなせず、内容が入ってきませんでした。 そこで、次のサービスを利用しました。

Euro Pythonに参加した時はOtterを、PyCon TWの時はOtterとMicrosoft Translatorを利用しました。 どちらもリアルタイム文字起こしが可能です。スマートフォンでアプリを起動し音声入力を行い、リアルタイムでPCのWebサイトで確認できる、というものです。

Otterは翻訳はしてくれないのですが、リアルタイムで英語の文字起こしができるため、サクサク内容をキャッチアップすることができました。

EuroPython 2日目のソフィー・ウィルソン氏のKeynote

Microsoft Translatorは翻訳もできます。PyCon TWでは、中国語でのトークも多かったため、非常に助かりました。

どうやって乗り切ったの?コミュニケーション編

イベント中や現地のコミュニケーションは、定型文とGoogle翻訳で乗り切りました。

また、初対面のエンジニア同士のコミュニケーションは、とても気軽に行うことができました。「どこからきたの?」「どんな仕事をしているの?」といった内容で始まることが多かったです。話をしていると、人種がちがって、バックボーンも国も違っても、同世代のエンジニアである以上、似たような仕事やマインドセットになるのかな、といったこと気づけたのもとてもおもしろかったです。「Django*1で社内システムをやっている」「マネジメントはしたくない」「今のポジションに割と満足している」「家族との時間を大切にしたい」というような話をされていて感じたところでした。

ドイツでデータ分析をやっている方々

Euro Pythonの開催地であったチェコ・プラハは観光地であることもあり、チェコ語ももちろん町中にありましたが、英語も同じくらいあります。また、若い人を中心に英語をしゃべることができます。チェコ語しかしゃべれないスーパーの店員さんともGoogle翻訳で難なくコミュニケーションをとることができました。

連日通ったプラハのレストランにて。英語がとてもお上手だった

また今年のPyCon TWは、日本からの参加者が非常に多く、現地での参加は総勢18名でした。これはこれでとても心強く、英語が話せる方を頼って行動をしてしまいました。お世話になった皆様本当にありがとうございました!

https://gihyo.jp/assets/images/article/2023/09/pycon-tw2023/018.png

海外カンファレンス、英語必須・・・?

英語があまり上手でなくても、海外カンファレンスを楽しむことができることを体験してきました。もちろんしゃべれるに越したことはありませんが、なくても大丈夫です。私が訪れた都市が観光地であったこと、また異なる国からの参加者の多いカンファレンスだったこともありますが、多くの人の優しさ・温かさに助けられた海外カンファレンスへの参加でした。

たくさん写真を載せたいのですが、多すぎてまとめられず...かわりにきれいなプラハの景色をどうぞ。

まとめ

弊社には英語ネイティブのメンバーが開発チームを中心に在籍しており、会社全体としても英語話者は非常に多いと感じています。私も怠けることなく、英語の鍛錬を行いたい!

英語でのコミュニケーションに挑戦したいあなた!また海外カンファレンスを目指したいあなたも、興味ある方はぜひこちらからお気軽にどうぞ!カジュアルにぜひ1度お話ししましょう〜!

recruit.jobcan.jp

おまけ

PyCon TWの会場にジョブボード(会社やプロダクトの紹介を自由に書き込めるボード)があり、そこにLOVOTを描きました。私の書いたLOVOTがこちら!

.........

上手くなりたい。。。心底思いました。。。この記事を読んで練習します!

tech.groove-x.com