KINTOテクノロジーズのブログ - TECH PLAY

TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

1113

この記事は KINTOテクノロジーズアドベントカレンダー2024 の11日目の記事です🎅🎄 メリークリスマス✌️🎅 KINTOテクノロジーズで my route(iOS) を開発しているRyommです。 本記事ではカスタムスタイルの紹介をします。 はじめに 私がこの書き方を知ったのはApp Dev Tutorialsがきっかけです。 https://developer.apple.com/tutorials/app-dev-training/creating-a-card-view#Customize-the-label-style なんてスタイリッシュなんだ…! カスタムスタイルを使うことで、SwiftUIのコードが格段に読みやすく、洗練されたコードになる…!私もスタイリッシュなコードを書きたい! そんな衝動に駆られて使い始めましたが、実際かなり便利で読みやすいのでおすすめです。 特に、用途別に乱立した構造体名を覚えていなくても ~~Style() にドットで候補を探せるところが気に入っています。 カスタムスタイルのつくりかた 例えばLabelのカスタムスタイルを作成する場合、 LabelStyle を継承した構造体を作成し、プロトコルに準拠したメソッド(ここでは makeBody(configuration:) )内にスタイルを定義します。 configurationに含まれる値はものによって異なるので都度調べる必要がありますが、LabelStyleConfigurationに関してはTextとImageのViewが入っています。 /// 文字+アイコン のラベルスタイル struct TrailingIconLabelStyle: LabelStyle { func makeBody(configuration: Configuration) -> some View { HStack { configuration.title configuration.icon } } } さらに LabelStyle を拡張して、作成したカスタムスタイルを静的プロパティとして追加すると、呼び出し時に .labelStyle(.trailingIcon) のように呼び出すことができて可読性が向上します。ン〜スタイリッシュ! extension LabelStyle where Self == TrailingIconLabelStyle { static var trailingIcon: Self { Self() } } もし「spaceを指定したい」など、引数を持たせたい場合はカスタムスタイルにメンバプロパティを追加することで実現できます。 /// 文字+アイコン のラベルスタイル struct TrailingIconLabelStyle: LabelStyle { // デフォルト値を設定しておくとドット始まりの呼び出し方法もキープできる var spacing: CGFloat = 4 func makeBody(configuration: Configuration) -> some View { HStack(spacing: spacing) { configuration.title configuration.icon } } } // 呼び出し Label().labelStyle(.trailingIcon) // spaceにはデフォルト値が使われる Label().labelStyle(TrailingIconLabelStyle(spacing: 2)) // spaceを2に指定 使いどころ アプリ全体で広く使う共通デザインや、上記の TrailingIconLabelStyle のように普遍的なカスタムスタイルに使うと良いでしょう。 たとえば、my routeではProgressViewで使っています。 ProgressView自体のスタイル設定もですが、ProgressViewを表示中に背景をグレーっぽくするのもスタイルに含めることができます。 struct CommonProgressViewStyle: ProgressViewStyle { func makeBody(configuration: Configuration) -> some View { ZStack { ProgressView(configuration) .tint(Color(.gray)) .controlSize(.large) Color(.loadingBackground) .frame(maxWidth: .infinity, maxHeight: .infinity) } } } extension ProgressViewStyle where Self == CommonProgressViewStyle { static var common: Self { Self() } } ちなみに、ProgressViewに background() を指定するとProgressViewに必要なサイズのみしか描画されないので、ZStackでColorをProgressViewの下に敷き、背景色が与えられたサイズ全体に広がるようにしています。 このようにスタイルを作成することで、以下のように簡潔でスタイリッシュに書けるようになりました。 struct SomeView: View { @State var loadingStatus: LoadingStatus var body: some View { SomeContentView .overlay { if loadingStatus == .loading { ProgressView() .progressViewStyle(.common) } } .disabled(loadingStatus == .loading) } } おわりに カスタムスタイルの紹介でした! 以下のページにあるものはカスタムスタイルを作成できます。 https://developer.apple.com/documentation/swiftui/view-styles よりスタイリッシュなコードを目指して一歩前進 🦌 🎄
This article is the entry for day 4 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello. My name is Nakaguchi, and I am the team leader of the iOS team in the Mobile App Development Group. In my day-to-day job, I work on: the KINTO Kantan Moushikomi App (“the app”) and Prism Japan ( Smartphone app version / Recently released web version ) As an iOS developer. After deciding to migrate one of the apps I work on to SwiftUI, I’m using this article to share the process we followed and the principles that informed the decision. I hope this article appeals to: iOS engineers, those interested in SwiftUI architecture, and teams considering adopting SwiftUI. I’d love for readers in these groups to find value in it. This article is also based on a presentation I gave at the KINTO Technologies × RIZAP Technologies Mobile Tips event held recently. It does not contain specific, actual examples of converting to SwiftUI that use source code, etc. Instead, I have primarily focused on detailing the process the team followed to arrive at the decision to transition to SwiftUI. I hope it will help people who are having trouble with SwiftUI conversion in their own teams. Choosing the Architecture for the First Release The application app was released in September 2023. The main architectures chosen were: UIKit VIPER Combine Development of the app started around March 2023, but creating a new iOS app in 2023 presented a challenging decision: UIKit or SwiftUI, wouldn’t you agree? At the time, SwiftUI was rapidly gaining popularity within the broader development community. However, we chose UIKit for the Moushikomi app. The reasons for this were the app’s tight delivery deadline , and the lack of team members proficient in SwiftUI . We prioritized achieving a stable release using a technology we were used to rather than risk using a new. Now, I’ll discuss the factors that prompted us to transition to SwiftUI after the app's first release. Wanting to Shift to SwiftUI: The First Wave After the initial release in 2023, we quietly focused on bug fixes, minor updates, and refactoring. However, during this period, some team members began expressing a desire to explore something new. Several options were proposed, but SwiftUI emerged as the most popular choice. The first wave of the SwiftUI conversion came around March 2024. When we discussed within the team whether to adopt SwiftUI, opinions such as the following were shared. ● Reasons in favor of doing it: Interest in SwiftUI ● Reasons against doing it: No one on the team had prior experience with SwiftUI. At the time, we didn’t feel a pressing need to adopt SwiftUI. The team was also experiencing significant changes, with many new members joining due to replacements and other factors. This left us lacking both the personnel and time resources to start learning and adopting SwiftUI. As the team leader, I wasn’t confident in my ability to successfully lead a SwiftUI conversion. At that time, there were plenty of reasons not to proceed with it. For these reasons, we decided that a SwiftUI conversion would need to be postponed. The Desire to Shift to SwiftUI: The Second Wave Around six months went by after that. During one-on-ones, many team members expressed a strong interest in exploring SwiftUI, prompting us to revisit the idea of migrating to it. The second wave of the SwiftUI conversion came around August 2024. The situation had evolved since the first wave, and when we revisited the idea, the following opinions were shared: ● Reasons in favor of doing it Interest in SwiftUI had gradually evolved into a passion for it. Some SwiftUI experts had joined the team as a result of in-house organizational changes. The new team members were quickly becoming core contributors, and we felt that the team as a whole now had sufficient time and human resources to take on the challenge. ● Reasons against doing it There were still concerns about whether it was truly the right time to start a SwiftUI conversion. This time, there were plenty of reasons supporting the decision to move forward with it Considering these circumstances, we decided to proceed with the SwiftUI conversion. Never Get Your Goals Wrong Thus, the team unanimously agreed to move forward with the SwiftUI conversion. However, I firmly believe it’s crucial to never lose sight of your goals. [NO] The goal should not be to pursue a SwiftUI conversion purely out of technical curiosity. [YES] Focus on improving future maintainability, aligning with de facto industry standards, and addressing the complexity of using Combine in development, which we aim to move away from. [NO] The SwiftUI conversion must not compromise the app’s quality. [YES] Ensure the app's quality is maintained at least at its previous level, if not improved. [NO] Avoid misplacing work priorities, such as sidelining original release tasks in favor of the SwiftUI conversion. [YES] Continue delivering additional features at the same pace as before. With the above points firmly in mind, we engaged in team discussions on how to approach the SwiftUI conversion. Choosing the Architecture for the SwiftUI Conversion We discussed the type of architecture we wanted to adopt within the team, and the key opinions were as follows: Not wanting to use libraries Not wanting to use a view model Not wanting to use libraries This primarily referred to The Composable Architecture(TCA) .Many team members expressed a preference to avoid using TCA if possible, citing concerns such as the need to constantly monitor for updates and the potential challenges if support for the library were to be discontinued. Additionally, other projects within the company using TCA had reported usability issues, including a steep learning curve, the challenge of keeping up with the library's rapid update cycle, and an overreliance on parent reducers. Taking these factors into account, we decided to forgo using TCA. Not wanting to use a view model The decision to adopt a view model as the architecture for SwiftUI is a topic of much debate. In our case, several team members noted that SwiftUI's built-in binding capabilities make using MVVM less optimal, as it does not fully leverage SwiftUI's inherent strengths. Consequently, we agreed on a policy of not using a view model. Adopting an MV Architecture — As a Result, Our Team Opted for an MV Architecture . The following figure will give you a picture of what it is like. MV architecture Ideally, views should interact directly with the model. Similarly, data retrieved from APIs is passed to the views through the model. Currently, we are discovering that an MV architecture, makes things simpler and will lead to better maintainability in the future (moving away from Combine); does not depend on libraries; and lets us get the most out of SwiftUI’s features. We are experiencing advantages like these, which suggests that the chosen architecture effectively addresses the concerns raised during our discussions about which approach to adopt. Guidelines for Deciding Which Parts to Convert to SwiftUI Regarding our policy for determining which parts to convert to SwiftUI, the team deliberated on which of the following approaches to adopt: The first approach involves converting individual views to SwiftUI. First, convert the parts related to screen transitions to SwiftUI. As a result, we decided to proceed with the conversion to SwiftUI based on a policy of first converting the parts related to screen transitions . The reasons for this included the following: In our experience, we frequently encountered challenges with screen transitions later in the process If the view responsible for managing transitions remains in UIKit, it often necessitates (temporarily) wrapping individual views in UIKit, even after they have been converted to SwiftUI. The Path Forward for SwiftUI Conversion So far, I have outlined the process and policies for converting the application to SwiftUI. However, the actual conversion process is still in its early stages. As of December 2024, at the time of posting this article, the production code does not yet include any SwiftUI code. Currently, we are dedicating more time to discussions about the SwiftUI conversion through various initiatives. These include utilizing approximately 20 minutes left over in our daily morning meetings and holding a dedicated one-hour meeting each week to focus specifically on this topic. As we progress, we have begun establishing coding rules to foster a shared understanding within the team regarding the SwiftUI conversion. For instance, some team members have been creating sample code and conducting lectures for the entire team based on those examples. In the future, as the SwiftUI conversion gains momentum, we plan to introduce pair and mob programming to enhance the team's overall expertise in SwiftUI.
はじめに こんにちは!Webサービスやモバイルアプリの開発において、必要となる共通機能=会員プラットフォームや決済プラットフォームなどの企画・開発を手がける共通サービス開発グループの中谷( @tksnakatani )です。 本記事では、多くの方が一度は経験したことがある「ヒヤッとするインシデント」の中から、決済プラットフォームで実際に本番環境で発生したAurora MySQLのデッドロックの事例をご紹介します。 デッドロックが発生した状況 2024年の某日、ログ監視システムからインシデント通知が届きました。通知内容を確認したところ、クレジットカード決済を実行している処理で以下のエラーログが記録されていました。 Deadlock found when trying to get lock; try restarting transaction さらに、Slackにはプロダクト担当者から「クレジットカード決済が失敗した」という問い合わせが寄せられていました。この時点で、非常に深刻な状況だと直感し、冷や汗をかいたことを今でも鮮明に覚えています。 原因調査 ロジック確認 デッドロック自体は約30分後に自然と解消されました。 エラーが発生した時間帯には、人気商品の発売があり、その商品への購入申し込みが集中していたことが判明しました。 通常、デッドロックとは、複数のトランザクションが互いに必要なリソースを保持し合い、いずれの処理も進行できなくなる状態を指します。このような状況を想定し、負荷試験を実施していたにもかかわらず、デッドロックが発生した点は謎でした。 当初は、リソースが競合するような処理が見当たらず、理論的にはデッドロックが発生する原因を特定するのが困難な状況でした。 再現確認 次にデッドロックが発生した前後のAPIへのリクエストをローカル環境で再現することを試みました。 本番環境で問題が発生したリクエストパラメータを使用し、curlコマンドで以下の2つのリクエストをほぼ同時に送信しました。その結果、本番環境と同様に1つのリクエストは成功しましたが、もう1つのリクエストではシステムエラーが返却されました。 :::details curlコマンドの例 curl --location 'http://localhost:8080/payments/cards' \ --header 'Content-Type: application/json' \ --data '{ "amount": 10, "capture": false, "request_id": "ITEM-20240101-0000001" } curl --location 'http://localhost:8080/payments/cards' \ --header 'Content-Type: application/json' \ --data '{ "amount": 10, "capture": false, "request_id": "ITEM-20240101-0000002" } ::: またエラーログには次のメッセージが出力されていました。 Deadlock found when trying to get lock; try restarting transaction が出力されていました。 再現できたことで調査の糸口が見え、ひとまず安堵しました。 SHOW ENGINE INNODB STATUS さらに、MySQLのSHOW ENGINE INNODB STATUSコマンドを使い、InnoDBストレージエンジンの状態を確認しました。 SHOW ENGINE INNODB STATUSコマンドでは、InnoDBストレージエンジンの状態に関する広範囲な情報を提供します。 この情報を基に、ロックやトランザクションの詳細を調べ、デッドロック発生の具体的な原因を特定するための手がかりを得ることができます。 mysql> set GLOBAL innodb_status_output=ON; mysql> set GLOBAL innodb_status_output_locks=ON; ・・・再びcurlコマンドを使って、2つのリクエストを送信。 mysql> SHOW ENGINE INNODB STATUS; その時の結果は以下の通りです。 ※一部抜粋、マスキング処理をしています。 ===================================== 2024-xx-xx 10:05:27 0x7fe300290700 INNODB MONITOR OUTPUT ===================================== Per second averages calculated from the last 2 seconds ----------------- BACKGROUND THREAD ----------------- srv_master_thread loops: 463 srv_active, 0 srv_shutdown, 7176 srv_idle srv_master_thread log flush and writes: 0 ---------- SEMAPHORES ---------- OS WAIT ARRAY INFO: reservation count 318 OS WAIT ARRAY INFO: signal count 440 RW-shared spins 290, rounds 306, OS waits 16 RW-excl spins 1768, rounds 5746, OS waits 48 RW-sx spins 0, rounds 0, OS waits 0 Spin rounds per wait: 1.06 RW-shared, 3.25 RW-excl, 0.00 RW-sx ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2024-04-18 10:04:02 0x7fe3059a4700 *** (1) TRANSACTION: TRANSACTION 12085, ACTIVE 6 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 7 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 3 MySQL thread id 70, OS thread handle 140612935517952, query id 28138 192.168.65.1 user update insert into payments (.... *** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12085 lock_mode X locks gap before rec Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12085 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** (2) TRANSACTION: TRANSACTION 12084, ACTIVE 7 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 7 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 3 MySQL thread id 69, OS thread handle 140612935812864, query id 28139 192.168.65.1 user update insert into payments (.... *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12084 lock_mode X locks gap before rec Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12084 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** WE ROLL BACK TRANSACTION (2) ---------------------------- END OF INNODB MONITOR OUTPUT ============================ ここから読み取れたこととしては TRANSACTION 12085 とTRANSACTION 12084 が存在する。 TRANSACTION 12085 とTRANSACTION 12084 が同じ「ギャップロック」を取得した。 TRANSACTION 12085 がインサートする前に「挿入インテンションギャップロック」を取得しようとしたが、TRANSACTION 12084のギャップロックと競合し待ちになった。 TRANSACTION 12084 がインサートする前に「挿入インテンションギャップロック」を取得しようとしたが、TRANSACTION 12085のギャップロックと競合し待ちになった。 MySQLがデッドロックを検知してTRANSACTION 12084をロールバックした。 ギャップロック・挿入インテンションギャップロックとは? ギャップロック ギャップロックは、インデックスレコード間のギャップのロック、または最初のインデックスレコードの前または最後のインデックスレコードの後のギャップのロックです。 たとえば、 SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; では、範囲内の既存のすべての値間のギャップがロックされているため、カラムにそのような値がすでに存在するかどうかにかかわらず、 他のトランザクションが 15 の値をカラム t.c1 に挿入できなくなります。 https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html#innodb-gap-locks 挿入インテンションギャップロック 挿入意図ロックは、行の挿入前に INSERT 操作によって設定されるギャップロックのタイプです。 このロックは、同じインデックスギャップに挿入する複数のトランザクションは、そのギャップ内の同じ場所に挿入しなければ相互に待機する必要がないように、意図的に挿入することを示しています。 値が 4 と 7 のインデックスレコードが存在すると仮定します。 5 と 6 の値をそれぞれ挿入しようとする個別のトランザクションでは、挿入された行の排他ロックを取得する前に、 挿入意図ロックを使用して 4 と 7 のギャップがロックされますが、行が競合していないため相互にブロックされません。 https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html#innodb-insert-intention-locks ギャップロックが今回の問題につながったことが判明したため、次にクレジットカード決済処理の中でギャップロックを取得している箇所を特定する作業を進めました。 決済処理の全体の流れは、以下の3つのステップに分かれています。 同じリクエストIDで決済が行われていないか確認する 決済代行会社に決済を依頼する 決済代行会社からの結果をデータベースに書き込み、レスポンスを返す SQLが発行される箇所を中心にブレークポイントを設定し、ローカル環境でデバッグを行ったところ、以下のクエリの実行直後にギャップロックが取得されていることが確認されました。 SELECT * FROM PAYMENTS where request_id = '' FOR UPDATE; その時のperformance_schema.data_locksのデータが以下です。 原因 全ての情報が揃い、原因の特定が完了しました。 決済プラットフォームでは、リクエストが重複していないかを確認するために request_id をリクエスト元から受け取り、この値を後続の参照にも利用するため、ユニークなインデックスを付与していました。 一方、プロダクト側では request_id を以下のルールで生成していました: 商品ID-YYYYMMDD-連番 デッドロックが発生した際には、人気商品の発売により同一商品の購入リクエストが短時間に集中して送信されていました。その結果、 request_id の連番部分が急速にカウントアップされたリクエストが大量に送信されました。 :::details curlコマンドの例 curl --location 'http://localhost:8080/payments/cards' \ --header 'Content-Type: application/json' \ --data '{ "amount": 10, "capture": false, "request_id": "ITEM-20240101-0000001" } ::: 前述の通りクレジットカード決済処理の主な流れは以下となっています。 同じリクエストIDで決済が行われていないか確認する 決済代行会社に決済を依頼する 決済代行会社からの結果をデータベースに書き込み、レスポンスを返す 問題は、1つ目の処理で発生しました: SELECT * FROM PAYMENTS where request_id = '' FOR UPDATE; このクエリは、通常は同じ request_id のリクエストが来ない前提で実行されます。しかし、当該 request_id のデータのINSERT前であるため、クエリが空振りしギャップロックを取得しました。 その後、3つ目の処理で結果を書き込むINSERT処理が発生し、挿入インテンションギャップロックを取得しようとしました。しかし、すでに取得されていたギャップロックと競合し、待ちが発生。その結果、MySQLがデッドロックを検知し、1つのトランザクションがロールバックされました。 解決策 重複決済の確認用として使用していた SELECT FROM ... FOR UPDATE が原因であることが判明したため、このクエリを廃止しました。代わりに、リクエストを受け付けた時点でデータを仮登録し、トランザクションをコミットする設計に変更しました。 コミットのタイミングが増えることでパフォーマンス劣化が懸念されましたが、負荷テストにより必要な性能を担保できることが確認されたため、この仕様でリリースしました。 反省 ギャップロックについての理解が十分ではありませんでした。 SELECT FROM ... FOR UPDATE で結果が0件の場合にギャップロックが取得されることを十分に理解していませんでした。普段からマニュアルをよく読み、設計に取り入れているつもりでしたが、すべてを知った気になっていたと反省しています。 https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html また、テストで気付けるポイントがあったことも残念に感じています。 負荷テストではインデックスの断片化や再構築によるパフォーマンス劣化を懸念し、 request_id にランダムな値(UUID)を使用していました。このためデッドロックが発生せず、テストは正常に完了していました。 まとめ MySQLやInnoDBストレージエンジンを扱う際には、トランザクションやロックの動作を深く理解することが非常に重要です。日頃からドキュメントや仕様をしっかり読み込み、必要に応じて有識者のサポートを得ることの重要性を改めて認識しました。 また、本番環境で実際に利用されているパラメータを調査し、それに基づいたリクエスト値を使用してテストを行うことが、問題の早期発見や品質向上につながると学びました。
こんにちは、学びの道の駅チームのHOKAです。 学びの道の駅チームは、部活動のような感じで業務時間でありながら、本業+オンで活動していました。が、この秋(9月?)から技術広報グループにジョインしました。 その詳細についてはこちらのブログを御覧ください↓↓ https://blog.kinto-technologies.com/posts/2024-12-03-the-next-goal/ 技術広報グループにジョインしたこともあり、12月のアドベントカレンダーを学びの道の駅チームからも書こうということになりました。 以前、共同でTech Blogを書いたので、今回も同様に気軽なノリでMTGをセッティングしたら、KINTOテクノロジーズ Tech Blogの発起人である中西さんが「15本、書こう」と息まいております。 「あれ、そんな話だったかな~」と思って、まずは15本のテーマを伺いました。 それがこちら ポッドキャスト 10本 まなびぃ 1本 学びの道の駅ポータル1本 もうすぐ一年 技術広報グループでこんなことやるよ このテーマを聞いて改めて、「書くことある?面白い?」と思ってしまった私。 「いやいや、どんどん書いていこう。例えば、春にBlog書いたじゃん。社内の反響とか、雰囲気が変わったとか、あるじゃん。」と自信のある中西。 もともと広報をやっていたので、なんらか文章にすることは出来ると思って、「はぁ、じゃあ、まぁやってみます。」というテンションで終話しようしたところ、きんちゃんが 「HOKAさん、納得してないんじゃないですか?無理していませんか?」と声をかけてくれました。 仕事なので、納得していないことも無理することもあるのは当たり前だろうと思い、正直に「YES」と答えました。そして、「書くほどのネタがないのに、なぜ書くのか?」ということも尋ねました。私なら、現状の活動内容を特に伝えるべきとも思わないし、読んでも面白いとも思わないのです。(言いたい放題) ここから対話形式でお届けします。 中西「KTCに入る前の自分に語りかけるように書いてほしい。こういう社内の雰囲気だったら入社したいと思うかもしれないじゃん。」 HOKA「うーん。全然、読みたいと思わないな...。」 中西「そもそも、テックブログは、1年に一人か二人にしか刺さらない記事で良いんです。」 HOKA「!?」 中西「正直、TechBlogの中にはどこの会社でも起きている当たり前のことが書かれた記事もあります。でも、TechBlogがなかったら、外部の人からはKTCで今何が起きているかは見えないんです。だから、大発見でなくても良い。会社で起きたことをただ書けば良い。それを読んだ人には、そんなことがあったんだと伝わるから。つまり、やったことを文章に残すだけで良いんです」 HOKA「!?!?!?!?!?!?!?」 中西「自分がやったことをただ書くだけ。それならハードルが低いし、誰でも書ける。そして、その内容がたとえ会社で起きたことの一部分だったとしても、各自がそれをやっていけば、集合したときにKTCってこんな会社だってことが見えるようになる。」 HOKA「めっちゃ理解しました。(頭にパッチワークの図を浮かべながら)」 きん「それが中西さんの戦略ですよね。他社のTechBlogとの差別化ポイントなんです。ちなみに、HOKAさんの悩みはTechBlogを書いていないエンジニアの悩みと同じです。私もHOKAさんの悩みを聞いてスッキリしました。」 ちなみに、私はというと、過去に企業広報を10年やっており、「いかに私的感情を省き、端的に業績やブランドイメージを伝えていくか」ということをゴールに置いて文章を書いて来ました。読み手は時間のない記者さんや編集者だったからです。 エンジニアが中心の会社で、エンジニアのコミュニケーションに触れ、学ぶことができたと感じる一日でした。入社してから一番と言っても良いくらい衝撃だったので、早速Blogにしたためました。 まとめ TechBlogは起きたことをただ書けば良い。 学びの道の駅は、正直な気持ちを話せる、素晴らしいチームです。 そして、メンバーの「分からない」に寄り添ってくれる素晴らしい仲間です。 参加している人も学んでいます。
This article is part of day 11 of KINTO Technologies Advent Calendar 2024 Merry Christmas! ✌ I'm Ryomm, and I work on developing the My Route iOS app at KINTO Technologies. In this article, I will introduce custom styles. Introduction App Dev Tutorials were the reason I learned to create custom styles. https://developer.apple.com/tutorials/app-dev-training/creating-a-card-view#Customize-the-label-style How stylish...! Using custom styles significantly enhances the readability and sophistication of SwiftUI code...! "I want to write stylish code, too!" Initially, that was what inspired me to start using it, but now I recommend it because it’s genuinely convenient and makes the code much easier to read. What I particularly like is that you can search for options using dots in ~~Style() even if you don’t remember the specific structure names, as they are organized based on their purpose. How to create a custom style For example, if you want to create a custom style for a Label, create a structure that inherits LabelStyle and define the style in a protocol-compliant method (in this case makeBody(configuration:) ). The values within the configuration object vary depending on what you're creating, so it's important to check each time. For LabelStyleConfiguration, it includes Text and Image views. /// Character + Icon LabelStyle struct TrailingIconLabelStyle: LabelStyle { func makeBody(configuration: Configuration) -> some View { HStack { configuration.title configuration.icon } } } You can also extend LabelStyle to add your custom style as a static property, which can be called as .labelStyle(.trailingIcon) when invoked, and improve readability. So~ stylish! extension LabelStyle where Self == TrailingIconLabelStyle { static var trailingIcon: Self { Self() } } If you want to have a parameter, such as "specify a space," you can do this by adding a member property to your custom style. /// Character + Icon LabelStyle struct TrailingIconLabelStyle: LabelStyle { // you can set the default value to preserve the way the dot starts are called var spacing: CGFloat = 4 func makeBody(configuration: Configuration) -> some View { HStack(spacing: spacing) { configuration.title configuration.icon } } } // call The default value is used in Label().labelStyle(.trailingIcon) // space Label().labelStyle(TrailingIconLabelStyle(spacing: 2)) // Set space to 2 Uses You can use it for common designs that you use widely throughout apps, or for universal custom styles like TrailingIconLabelStyle above. For example, my route uses it in ProgressView. While ProgressView itself is styled, you can also include a grayish background when ProgressView is displayed. struct CommonProgressViewStyle: ProgressViewStyle { func makeBody(configuration: Configuration) -> some View { ZStack { ProgressView(configuration) .tint(Color(.gray)) .controlSize(.large) Color(.loadingBackground) .frame(maxWidth: .infinity, maxHeight: .infinity) } } } extension ProgressViewStyle where Self == CommonProgressViewStyle { static var common: Self { Self() } } By the way, when you use background() with a ProgressView, it only applies to the area required by the ProgressView. To ensure the background color covers a larger area, you can use a ZStack to place the color beneath the ProgressView, allowing the background to expand to the desired size. By defining a style in this way, you can achieve concise and elegant code, as shown in the example below. struct SomeView: View { @State var loadingStatus: LoadingStatus var body: some View { SomeContentView .overlay { if loadingStatus == .loading { ProgressView() .progressViewStyle(.common) } } .disabled(loadingStatus == .loading) } } Conclusion That wraps up this introduction to custom styles! You can create custom styles on the following page. https://developer.apple.com/documentation/swiftui/view-styles Take a step toward writing more stylish and elegant code!
Introduction Hello, we are Chang and Hosaka, in charge of the my route by KINTO iOS app development in the Mobile App Development Group. In our mobile app development group, we usually use GitHub Actions as a CI/CD tool. This time, we introduced Bitrise for the first time to the my route by KINTO iOS app, so we would like to talk about it. What is Bitrise ? Bitrise is a cloud-based CI/CD (continuous integration / continuous delivery) service for the automated building, testing, and deployment of mobile apps. Bitrise is designed to streamline mobile app development, and it supports major mobile app development frameworks such as iOS, Android, React Native, and Flutter. Some of the key features of Bitrise include: Build Automation Builds are automatically triggered when code in the repository is updated. Builds can be easily configured using the visual interface. Test Automation Tests are automatically run after the builds are complete. Bitrise supports integration with a variety of testing tools, allowing automation at different test levels, including unit testing and UI testing. Automated Deployment If the test passes, Bitrise will automatically take steps to deploy the app. Bitrise supports deployment to app stores such as the App Store and Google Play. Variety of Integration Bitrise supports integration with a variety of tools and services, including GitHub, Bitbucket, Slack, Jira, Firebase, and more. Cloud-based Services Bitrise is a cloud-based service that does not require infrastructure configuration or management. Developers can easily take advantage of Bitrise’s features. Bitrise is a powerful tool for streamlining and improving the quality of mobile app development, and is a very valuable CI/CD service to developers and development teams. The Reason for Introducing Bitrise There are two reasons why we implemented Bitrise in the my route by KINTO iOS app. We had an opportunity to hear from Bitrise Ltd. before building a CI/CD environment, and at that time all the team members had replaced their PCs from Intel to M1, so we were fascinated by Bitrise, which can be built in the same M1 environment. The results of the experiment below, comparing Bitrise and Github Actions on an Intel Medium machine (the lowest performance), show that the cost can be reduced by about 30% and the processing time can be shortened by about 50%. Bitrise and GitHub Actions performance comparison experiment (tested in a different app): Processing Time Comparison Experimental Attempt / Machine Name Bitrise (Intel Medium) Github Actions 1 07:48 16:24 2 11:42 16:18 3 06:53 16:09 Average 08:48 16:17 Cost Comparison Github Actions cost per minute is $0.08 Bitrise cost per minute is $0.106666 Bitrise calculation: Given that 1 credit (cr.) = elapsed minutes (min.) × machine spec (2)... (i), and $400/7,500 credit = apprrx. 0.05333($/cr)... (ii), For (i) and (ii), apprx. 0.05333($/cr.) × 2(cr./min.) = apprx. 0.106666 ($/min.) Experimental Count / Machine Name Bitrise (Intel Medium) Github Actions 1 $0.85 $1.36 2 $1.28 $1.36 3 $0.75 $1.36 Average $0.96 $1.36 Using Bitrise in my route by KINTO To implement Bitrise in my route KINTO, we signed up for Bitrise's Teams pricing plan and adopted an M1 Medium machine consuming 2 credits per minute. The Teams plan has a credit limit set according to the price, and exceeding that limit incurs additional costs, so we aimed to optimize costs by also using GitHub Actions. With GitHub Actions, Linux is 1/10 the cost of macOS . Therefore, we use GitHub Actions for steps that can run on Linux (no app build required) and Bitrise for steps that require macOS (app build required). Bitrise Workflow my route by KINTO, mainly automates the following: unit testing, deployment to App Store and TestFlight, and build result notifications to Slack. Currently, builds are triggered by pushing to the develop and release branches, and scheduled builds are done on weekday mornings. We have observed that a single build takes about 6-11 minutes (12-22 credits). GitHub Actions Workflow GitHub Actions automates the static analysis flow. SwiftLint: A static analysis tool for Swift that automatically points out any code violations in the PR. SonarQube: A static analysis tool that analyzes code duplication and other issues that SwiftLint cannot cover. Summary and Future Prospects Looking ahead, Bitrise is expected to continue to expand and improve its features to meet the needs of mobile app development. For example, we can expect more advanced testing and deployment options, more flexible workflow settings, and further expansion of cloud-based resources. It is also expected to provide a more seamless development experience, including collaboration with the developer community and improved integration with other tools. KINTO Technologies would like to keep a close eye on the trends and lead to further utilization of this technology. Here is the review. https://findy-tools.io/products/bitrise/18/39
GitHub Actionsだけで実現するKubernetesアプリケーションのContinuous Delivery こんにちは。Toyota Woven City Payment Solution開発グループの楢崎と申します。 我々は、 Woven by Toyota で Toyota Woven City で利用される決済基盤アプリケーションの開発に携わっており、バックエンドからWebフロントエンド、モバイルアプリケーションまで決済に関わる機能を横断的に開発しています。 決済バックエンドはKubernetes上で動作し、いわゆるクラウドネイティブなツール群を使って開発しています。 今回はKubernetesアプリケーションを構築・安定運用していく上でキーとなる、GitOps(Gitでインフラ構成ファイルを管理し変更を適用指定する運用方法)を踏襲しつつ、CD(Continuous Delivery)プロセスに関して、一般に用いられている、いわゆる「クラウドネイティブなツール」ではなく、GitHub Actionsだけで構築することを目指します。 ここでいうCDの機能はあくまで Kubernetesの構成管理ファイルの変更の適用 コンテナイメージのアップデート です。他にもBlue / GreenやCanaryデプロイなど応用的なCDプロセスはありますが、「小さくスタート」することを想定しています。既にDevOpsチームが組成されていて、その恩恵にあやかれる人ではなく、最小の開発人数で、かつ新たなツールなしに普段利用しているGitHub Actionsのみを利用してKubernetes上で動作するアプリケーションを生産性高く継続的デリバリーさせたい人が対象になります。 レポジトリもアプリケーションのコードとKubernetesの構成管理ファイルを同じレポジトリで管理していることを想定しています。(権限の設定次第ではレポジトリをまたいで実行可能だとは思いますがここでは触れません) (Gitlabを普段お使いの方であれば Auto DevOps という非常に優秀なツールがあるので、決してGitHub及びGitHub Actions最高!というつもりはありませんので悪しからず) クラウドネイティブなKubernetes向けCI/CDツール Kubernetes向けアプリケーションのCI/CDと聞いて読者の皆さんはどのようなツールを思いつきますか? Argo CD Flux CD PipeCD Tekton などが挙げられます。 いずれのツールも非常に高機能で、Kubernetesの機能をフルに活かすために非常に有用です。 またGitOpsを実践する上で、Kubernetesの構成ファイルやアプリケーションイメージを柔軟に安全に更新できます。 一方でツール特有の知識や運用も必要で、DevOpsに専門の人員がいるような大きな組織ではないと継続的に運用するのは難しいのではないでしょうか? CDツールの運用そのものにKubernetesが必要で、Kubernetesの構成ファイルを管理するツールのためにKubernetesの構成ファイルが必要ということで、少人数の組織では導入や運用の敷居も非常に高いと思っています。 この記事では以下の図のようなパイプラインを、GitHub Actionsだけで構成することを考えます。 Kubernetesは特定のクラウドではなく汎用的なクラスタを想定しています。何かしらのコンテナレジストリがあることを想定しています。構成管理ファイルはKustomizeを例にしますが、Helm, Terraformなど何でも応用可能だと思います。 flowchart TD A[コードの変更] -->|ビルドパイプラインを実行| B[コンテナイメージのビルド、プッシュ] B -->|コンテナイメージ更新用のパイプラインが起動| C[新しいコンテナイメージでkustomizationを書き換えたプルリクエストができる] C -->|プルリクエストのレビュー| D[新しいコンテナイメージをKubernetesにデプロイ] linkStyle default stroke-width:2px,color:blue,stroke-dasharray:0 デモ Kubernetesの構成ファイルを管理するフォルダとアプリケーションのフォルダが入っているレポジトリを考えます。 フォルダ構成は以下になります。ここでは具体的なコードやDockerfileの中身、各アプリケーションのソース等は省略します。 ├── .github │   ├── actions │   │   └── image-tag-update │   │   └── action.yaml │   └── workflows │   ├── build-go.yaml │   ├── build-java.yaml │   ├── build-node.yaml │   └── kubectl.yaml ├── go-app │   ├── src/ │   └── Dockerfile ├── java-app │   ├── src/ │   └── Dockerfile ├── k8s │   ├── service-go.yaml │   ├── service-java.yaml │   ├── service-node.yaml │   └── kustomization.yaml └── node-app    ├── src/ └── Dockerfile それぞれのアプリケーションは以下のような形でplaceholderとし、 apiVersion: apps/v1 kind: Deployment metadata: name: app spec: ... template: ... spec: containers: - name: some-server image: go-placeholder # placeholderとしてkustomizationと同じ文字列を入れておく その複数のplaceholderをkustomization.yaml上で一元管理します。 apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: techblog resources: - service-go.yaml - service-java.yaml - service-node.yaml images: - name: go-placeholder newName: go-app newTag: v1.1.1 - name: java-placeholder newName: java-app newTag: v2.7.9alpha - name: node-placeholder newName: node-app newTag: latest まずKubernetesの構成ファイルを適用するために、以下のようなyamlのGitHub Actionsを構成します。 name: kubectl on: pull_request: branches: - "**" paths: - "k8s/**" #Kubernetesのmanifest fileが入っている場所 push: branches: - main paths: - "k8s/**" jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: azure/setup-kubectl@v4 - env: KUBECONFIG_CONTENTS: ${{ secrets.KUBECONFIG_CONTENTS }} #事前にkubeconfigをGitHubのシークレットに格納しておく run: | echo "${KUBECONFIG_CONTENTS}" > $HOME/.kube/config chmod 600 $HOME/.kube/config - run: kubectl apply --dry-run=server -k ./k8s >> $GITHUB_STEP_SUMMARY - if: github.ref == 'refs/heads/main # mainブランチだったら実際に適用 run: kubectl apply -k k8s/ これは管理者権限を持ったkubeconfigが手元にある場合の一般的なKubernetesの構成ファイルを適用するパイプラインです。 各クラウドなどのクラスタの設定方法に応じてconfigの取得方法は変更してください。 次に各アプリケーションをpushする際に自動でプルリクエストを作るか、コンテナのイメージタグを書き換えるcompositeを作成します。 name: image-tag-update description: 'コンテナイメージの更新時にkustomizationのイメージタグを書き換えるタスク' inputs: target_app: description: '対象のアプリケーション' required: true tag_value: description: '新しいコンテナイメージタグ' required: true token: description: 'PRや内容の更新の権限を持ったトークン' required: true runs: using: 'composite' steps: - uses: actions/checkout@v4 id: check-branch-exists continue-on-error: true with: ref: "image-tag-update" # タグ更新用のデフォルトブランチ名 - uses: actions/checkout@v4 # checkoutはブランチが存在しないとデフォルトブランチにフォールバック、みたいなことはできないのでこういう書き方 if: steps.check-branch-exists.outcome == 'failure' with: ref: main - uses: mikefarah/yq@master # yqで対象のplaceholderのタグの値を置換 with: cmd: yq eval '(.images[] | select(.name == "'"${{ inputs.target_app }}-placeholder"'")).newTag = "'"${{ inputs.tag_value }}"'"' -i k8s/kustomization.yaml - uses: peter-evans/create-pull-request@v6 if: steps.check-branch-exists.outcome == 'failure' # プルリクエストがないと新しいプルリクエストを作成 with: title: 'コンテナイメージの更新' body: | `${{ inputs.target_app }}` を更新します branch: "image-tag-update" - uses: stefanzweifel/git-auto-commit-action@v5 if: steps.check-branch-exists.outcome == 'success' # チェックアウトが成功したら、既存のブランチにコミットを追加 with: commit_message: "Image update for ${{ inputs.target_app }}" 各アプリケーションのイメージを作成する過程で、上記のcompositeを呼びます。複数のアプリケーションを管理している場合は、それぞれのアプリケーションの後に付け加えるとよいでしょう。 ... - uses: docker/setup-buildx-action@v3 - uses: docker/build-push-action@v6 with: file: ./Dockerfile push: true tags: ${{ env.tag }} # なにかしらのタグ - uses: ./.github/actions/image_update if: github.ref == 'refs/heads/main' with: target_app: go tag_value: ${{ env.tag }} token: ${{ secrets.GITHUB_TOKEN }} # コンテンツ、プルリクエスト編集権限を持ったgithub token これで、アプリケーションを実行するタイミングでコンテナイメージが自動でアップデートされ、プルリクエストベースで新しいコンテナイメージがデプロイできるようになります! (タグの導出方法は各自のワークフローにおまかせします。下記の例はマイナーバージョンをインクリメントした例です) - name: go-placeholder newName: go-app - newTag: v1.1.1 + newTag: v1.1.2 運用上の注意点 デプロイのタイミング イメージ更新用のプルリクエストが、マージされた瞬間にデプロイされます。インフラの構成ファイルの修正と合わせてリリースしたい場合は、このブランチに修正を追加するか、マージするタイミングを合わせて適用するといいでしょう。 新規コンテナアプリケーションの追加 例えば上記の例でPythonのアプリケーションを足したいという時に、イメージ更新用のプルリクエストがそのまま残っていると、Pythonのイメージタグをどれだけ更新してもプルリクエスト自体に最新版の変更が反映されてない限り、空振りし続けるので注意が必要です。 切り戻し Commitをrevertすれば戻せるので非常にシンプルです。 Reconcileのタイミング GitOps Toolの多くがドリフトを抑制するためのリコンサイルをほぼリアルタイムで実施できるのに対して、このやり方だとCDパイプラインが動作したタイミングでないと実施できません。 Kubernetesのクラスタに更新権限をどれくらいのチームメイトが保有して権限を行使しているかにも応じてツールの使い分けは大事だと思います。 Container Registryを直接見ているわけではない コンテナレジストリから直接コンテナイメージの最新版を取得するものもありますが、この方法では実際に見ているわけではありません。確実にコンテナが存在するか、確認するステップをコンテナレジストリごとに実装したほうが良さそうです。 GitHub Actionsの権限設定に関して contents と pull-requests の更新権限が必要になってきます。Actionsのパーミッション、GitHub Appなどに権限をアサインして使ってください。詳しくは こちら 。 後に実行されたコンテナイメージで上書きされる CDツールには、コンテナイメージのタグの値をみて、Semantic Versioningなどの規則に従ってどちらが新しいバージョンか判別する仕組みがあります。 上記で示したworkflowはタグの値に関係なく、後に実行されたパイプラインでイメージタグを上書きします。 この挙動が問題であれば値を検証して、上書きすべきか判定する必要があります。 まとめ このやり方を用いることで、GitOpsがGitHub上で完結して、非常にシンプルにKubernetesアプリケーションの継続的デリバリーが実践できるのではないのかなと思います。 CDツールのエラーもGitHub Actions上に集約できるので、普段のCIプロセスと同じ方法で実行結果やエラーの内容が確認できるのは非常に嬉しいですね。 色々なツールが多く存在し、目移りすることも多いKubernetesのツール選定ですが、身の丈にあったツールを利用してKubernetesアプリケーション開発の生産性を高めていきたいですね。
この記事は KINTOテクノロジーズアドベントカレンダー2024 の10日目の記事です🎅🎄 Background When developing the KINTO かんたん申し込みアプリ App, we implemented some shared code using KMP (Kotlin Multiplatform) and published it as a Swift Package. This approach allowed us to efficiently share code across platforms and simplify the development process by avoiding code duplication. Our iOS Team currently uses XcodeGen to manage dependencies, and importing KMP code can be as simple as making a 4-line modification to the project.yml file. Here is an example of such a modification: packages: + Shared: + url: https://github.com/[your organization]/private-android-repository + minorVersion: 1.0.0 targets: App: dependencies: + - package: Shared - package: ... However, since our code resides in private repositories, some additional setup is required. This blog will outline those steps and explain how we streamlined the process. About Package.swift Here’s a brief explanation of how we publish KMP code as a Swift Package: Compile the KMP code into an .xcframework . Package the .xcframework into a zip file and calculate its checksum. Create a new release page on GitHub and upload the zip file as part of the release assets. Obtain the zip file’s URL from the release page. Generate the Package.swift file based on the URL and checksum. Commit the Package.swift file and add a git tag to mark the release. Associate the git tag with the release page and officially publish the GitHub release. The resulting Package.swift file will look something like this: // swift-tools-version: 5.10 import PackageDescription let packageName = "Shared" let package = Package( name: packageName, ... targets: [ .binaryTarget( name: packageName, url: "https://api.github.com/repos/[your organization]/private-android-repository/releases/assets/<asset_id>.zip", checksum: "<checksum>" ) ] ) Permission Setup for Development Environment Since the URL resides in a private repository, you will encounter the following error if no permission configuration is done: To resolve this, we explore two options: .netrc files and Keychain. Option 1: Using a .netrc File You can store your GitHub credentials in a .netrc file, which is a simple way to authenticate API requests: #Example: echo "machine api.github.com login username password ghp_AbCdEf1234567890" >> ~/.netrc echo "machine api.github.com login <Your Github Username> password <Your Personal Access Token>" >> ~/.netrc This method is quick and effective but may pose security risks since the token is stored in plaintext. Option 2: Using Keychain If you prefer not to store the token in plaintext, you can use Keychain to securely store your credentials: Open Keychain Access.app . Select ①, the login keychain. Select ②, to create a new Password Item. In the dialog box, enter the following information: Keychain Item Name: https://api.github.com Account Name: Your GitHub username Password: Your Personal Access Token This approach is more secure and integrates seamlessly with macOS authentication mechanisms. For SSH Users The above instructions assume you cloned the iOS repository using the https protocol. If you did, you already have the necessary permissions for github.com configured. However, if you cloned the repository using the ssh protocol, you might lack permissions for github.com , leading to permission-related errors during the resolveDependencies phase. To fix this, you can add an entry for the domain github.com in the .netrc file: #Example: echo "machine github.com login username password ghp_AbCdEf1234567890" >> ~/.netrc echo "machine github.com login <Your Github Username> password <Your Personal Access Token>" >> ~/.netrc Alternatively, use Keychain Access to add an item with the name https://github.com . Either method ensures your system has the required permissions. GitHub Actions After resolving the local development environment issues, you also need to address permission issues in the CI environment to ensure smooth automation during builds. Retrieving Tokens in GitHub Actions Using a Personal Token One straightforward approach is to create a Personal Access Token (PAT) with access to private repositories and pass it to the CI environment via Actions secrets. While effective, this method has several drawbacks: Token Expiration Tokens with an expiration date require periodic updates, and forgetting to update them may cause CI failures. Tokens without an expiration date pose long-term security risks. Broad Permissions A personal account usually has access to multiple private repositories, making it difficult to restrict PAT permissions to a single repository. Personal Dependency If the account owner loses access to private repositories due to role changes, CI workflows will fail. Using a GitHub App Using a GitHub App is a more robust solution, offering several advantages: Fine-grained permissions for repositories No dependency on individual accounts Temporary tokens that enhance security Setting Up a GitHub App We ultimately used a GitHub App to configure access permissions. Here is the process: Create a GitHub App in your organization. Install the App in both iOS and Android projects to manage repository access. Configure the App’s AppID and Private Key in the iOS project’s Actions secrets. Add code in the workflows to retrieve a temporary Access Token. Here’s an example: steps: - name: create app token uses: actions/create-github-app-token@v1 id: app-token with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} owner: "YourOrgName" - name: set access token for private repository shell: bash env: ACCESS_TOKEN: ${{ steps.app-token.outputs.token }} run: | git config --global url."https://x-access-token:$ACCESS_TOKEN@github.com/".insteadOf "https://github.com/" touch ~/.netrc echo "machine github.com login x-access-token password $ACCESS_TOKEN" >> ~/.netrc echo "machine api.github.com login x-access-token password $ACCESS_TOKEN" >> ~/.netrc By using a GitHub App, we ensure our CI workflows are secure, efficient, and free from dependency on individual user accounts. This approach minimizes risk and streamlines development across teams.
この記事は KINTOテクノロジーズアドベントカレンダー2024 の10日目の記事です🎅🎄 「Mobility Night」は、モビリティ領域のソフトウェア技術者、ビジネスパーソン、研究者、プロダクトマネージャーなどが気軽に集まり、業界特有の知見や課題を共有する勉強会シリーズです。#0(初回)のクローズド開催を経て、いよいよ第1回(#1)をオープンな形で開催することができました。 今回のテーマは、モビリティサービスの基盤技術である「GPS・位置情報」。カーナビや地図アプリ、オンデマンド交通、そして将来の自動運転やスマートシティ基盤まで、「いまどこにいるか」を正確に把握し、活用することはサービス価値の根幹を支えます。 当日は以下の5つのセッションが行われ、GPS・位置情報技術を軸に、それぞれが独自の切り口から課題と可能性を示しました。 この記事は技術広報グループでMobility Nightの企画運営も行っている中西が執筆しています。 1. Exploring New Google Places API 登壇者: KINTOテクノロジーズ株式会社 numaさん Google Places APIは地図プラットフォームの中核機能の一つであり、周辺検索や施設情報取得を効率的に行うための重要なインターフェースです。このセッションでは、テキスト入力中から即時に候補を提示するAutocomplete機能の強化や、Fieldsパラメータによる必要情報の絞り込みなど、最新の改善点が紹介されました。 ポイント: パフォーマンスとコスト最適化: Fields指定で不要なデータ取得を削減し、APIコストを抑えるとともにレスポンス高速化が可能。 ユーザーエクスペリエンス向上: 欲しい情報に素早くアクセスできる体験は、移動中のユーザーにとって大きな利点。Autocomplete強化で検索負荷を軽減し、UXを磨き上げる。 将来への展望: 現在は位置情報取得が中心だが、将来的にはIoTセンサーや行動履歴分析と組み合わせたパーソナライズ戦略も期待できる。 https://speakerdeck.com/kintotechdev/exploring-new-google-places-api 2. AIドラレコサービスの走行データで作る位置情報データプロダクト 登壇者: GO株式会社 松浦慎平さん ドライブレコーダーは事故記録用のデバイスという印象が強いですが、このセッションでは「走行データ=街をセンサー化するプラットフォーム」として再解釈されました。映像+GPSデータをAI解析することで、道路標識や信号、舗装工事などの情報を動的に地図へ反映できる可能性が示唆されました。 ポイント: ダイナミックな地図更新: 静的だった地図を“生きた情報基盤”へ進化させ、道路インフラ変化をほぼリアルタイムで反映。 複数車両データの統合: 異なる車両から得られるデータを突き合わせることで、一時的な標識や工事箇所などを高精度に検出。 プライバシー対策: 個人情報が映り込む映像を適切に匿名化しつつ道路情報を保持する技術・運用が必須。 将来的応用: 自動運転用HDマップ整備、スマートシティ計画、新サービス創出など、多面的なビジネス展開が期待。 https://speakerdeck.com/pemugi/aidorarekosabisunozou-xing-deta-dezuo-ruwei-zhi-qing-bao-detapurodakuto-wei-zhi-qing-bao-jing-du-xiang-shang-nogong-fu 3. GPSモジュールを触って学ぶ、衛星測位技術の概要 登壇者: チャリチャリ株式会社 VP of Engineering 蛭田慎也さん GPSは当たり前に利用されていますが、都市環境では電波反射や視界不良、衛星数の偏りなど多くの実務的課題が存在します。このセッションでは基礎的な衛星測位技術を理解し、精度向上の可能性と対策を探りました。 ポイント: 環境依存課題: ビル街でのマルチパス、トンネル下での衛星ロストなど、ロケーションごとの特殊要件が精度を左右。 マルチGNSS活用: GPS単独でなくGLONASS、Galileo、BeiDou、みちびき(QZSS)など複数システムを組み合わせて精度底上げ。 ハイブリッド手法: 加速度・ジャイロセンサ、Wi-Fi/Bluetoothビーコン、マップマッチングなど補完技術で精度改善。 基礎知識が指針に: こうした理解が将来のプロダクトデザインや品質保証、データ分析を行う際の指針となる。 4. 後処理で位置情報を補正する技術を試してみた(仮) 登壇者: 株式会社Luup IoTチーム 高原健輔さん リアルタイムでの高精度測位が困難な場合、後から精度を引き上げる「PPK(Post-Processing Kinematic)」という選択肢があります。高価なRTK装置や特別な通信インフラを用いず、取得済みデータと基準局データを組み合わせて後処理する手法です。 ポイント: PPKのメリット: リアルタイムにこだわらず、後日精度向上が可能。初期投資を抑えながらセンチメートル級精度を最終的に実現。 コスト効率と拡張性: 将来的に需要が増すシナリオで後から精度改善を行える柔軟性。配送ロボット、ドローン、シェアモビリティなどで有効。 応用範囲: 地図整備、走行ログ高度化、インフラ検査など、事後分析が中心の領域で大きな価値を発揮。 https://speakerdeck.com/kensuketakahara/hou-chu-li-dewei-zhi-qing-bao-wobu-zheng-suruji-shu-woshi-sitemita 5. オンデマンドバスサービス導入前のシミュレーションロジックの構築(仮) 登壇者: トヨタコネクティッド株式会社 先行企画部 新技術開発室 Halufy(ハルフィ)さん オンデマンド交通は柔軟性が魅力ですが、収益性や持続可能性を確保するのは容易ではありません。このセッションでは事前シミュレーションによる精緻な需要予測や運行計画設計が紹介されました。 ポイント: 持続可能なモデル構築: 補助金頼みにならずに最適なステーション配置、運行台数、時間帯設定をデータで検証。 戦略的データ活用: 位置情報を中心にODデータや予約希望を統合し、需要予測や価格戦略、ルート最適化を試行。 長期的ビジョン: 他のモビリティ手段やインフラと連携し、都市全体の交通効率化や利便性向上を目指す土台となる。 今後扱いたいトピックとMobility Nightの展望 今回のMobility Night #1は、GPS・位置情報に特化することで、モビリティ業界の「現在地把握」技術に深く切り込みました。参加者からは「位置情報という身近なテーマがこんなに奥深いとは」「基礎から先端活用まで通して聞けるのは貴重」という声が多数寄せられています。 しかし、モビリティ業界にはGPS・位置情報以外にも多くのテーマが存在します。今後は、 IoTデバイス活用: センサーからのリアルタイムデータ収集・制御 データ分析: 需要予測や高度なオペレーション最適化 プロダクトデザイン: UX向上やユーザー満足度最大化 品質保証: 信頼性確保や安全基準遵守 といった領域も掘り下げ、業界全体のイノベーションを促す場にしていきたいと考えています。 Mobility Nightは、運営メンバーが企画するだけでなく、参加者からの登壇希望やテーマ提案も歓迎しています。Discordを通じて意見交換や共催募集が可能な環境を整え、誰もが関わりやすいコミュニティを目指します。 https://discord.com/invite/nn7QW5pn8B まとめ 「Mobility Night #1」では、GPS・位置情報技術を軸に、モビリティサービスの中核をなす技術的課題と、その克服による新たな価値創造の可能性が明確になりました。静的な地図を動的な情報基盤へアップデートする試み、環境に左右されるGPS精度を高度な手法で補正する取り組み、オンデマンド交通をデータ駆動型で計画する戦略など、多様なアプローチが交錯しました。 これらの知見は、今後のMobility Nightで扱うIoT、データ分析、プロダクトデザイン、品質保証などのテーマとも結びつき、業界全体の進歩を加速させるはずです。引き続きMobility Nightにご注目いただき、ともに学び、交流し、新たな価値を創造していきましょう!
Introduction Hello, we are Chang and Hosaka, in charge of the my route by KINTO iOS app development in the Mobile App Development Group. In our mobile app development group, we usually use GitHub Actions as a CI/CD tool. This time, we introduced Bitrise for the first time to the my route by KINTO iOS app, so we would like to talk about it. What is Bitrise ? Bitrise is a cloud-based CI/CD (continuous integration / continuous delivery) service for the automated building, testing, and deployment of mobile apps. Bitrise is designed to streamline mobile app development, and it supports major mobile app development frameworks such as iOS, Android, React Native, and Flutter. Some of the key features of Bitrise include: Build Automation Builds are automatically triggered when code in the repository is updated. Builds can be easily configured using the visual interface. Test Automation Tests are automatically run after the builds are complete. Bitrise supports integration with a variety of testing tools, allowing automation at different test levels, including unit testing and UI testing. Automated Deployment If the test passes, Bitrise will automatically take steps to deploy the app. Bitrise supports deployment to app stores such as the App Store and Google Play. Variety of Integration Bitrise supports integration with a variety of tools and services, including GitHub, Bitbucket, Slack, Jira, Firebase, and more. Cloud-based Services Bitrise is a cloud-based service that does not require infrastructure configuration or management. Developers can easily take advantage of Bitrise’s features. Bitrise is a powerful tool for streamlining and improving the quality of mobile app development, and is a very valuable CI/CD service to developers and development teams. The Reason for Introducing Bitrise There are two reasons why we implemented Bitrise in the my route by KINTO iOS app. We had an opportunity to hear from Bitrise Ltd. before building a CI/CD environment, and at that time all the team members had replaced their PCs from Intel to M1, so we were fascinated by Bitrise, which can be built in the same M1 environment. The results of the experiment below, comparing Bitrise and Github Actions on an Intel Medium machine (the lowest performance), show that the cost can be reduced by about 30% and the processing time can be shortened by about 50%. Bitrise and GitHub Actions performance comparison experiment (tested in a different app): Processing Time Comparison Experimental Attempt / Machine Name Bitrise (Intel Medium) Github Actions 1 07:48 16:24 2 11:42 16:18 3 06:53 16:09 Average 08:48 16:17 Cost Comparison Github Actions cost per minute is $0.08 Bitrise cost per minute is $0.106666 Bitrise calculation: Given that 1 credit (cr.) = elapsed minutes (min.) × machine spec (2)... (i), and $400/7,500 credit = apprrx. 0.05333($/cr)... (ii), For (i) and (ii), apprx. 0.05333($/cr.) × 2(cr./min.) = apprx. 0.106666 ($/min.) Experimental Count / Machine Name Bitrise (Intel Medium) Github Actions 1 $0.85 $1.36 2 $1.28 $1.36 3 $0.75 $1.36 Average $0.96 $1.36 Using Bitrise in my route by KINTO To implement Bitrise in my route KINTO, we signed up for Bitrise's Teams pricing plan and adopted an M1 Medium machine consuming 2 credits per minute. The Teams plan has a credit limit set according to the price, and exceeding that limit incurs additional costs, so we aimed to optimize costs by also using GitHub Actions. With GitHub Actions, Linux is 1/10 the cost of macOS . Therefore, we use GitHub Actions for steps that can run on Linux (no app build required) and Bitrise for steps that require macOS (app build required). Bitrise Workflow my route by KINTO, mainly automates the following: unit testing, deployment to App Store and TestFlight, and build result notifications to Slack. Currently, builds are triggered by pushing to the develop and release branches, and scheduled builds are done on weekday mornings. We have observed that a single build takes about 6-11 minutes (12-22 credits). GitHub Actions Workflow GitHub Actions automates the static analysis flow. SwiftLint: A static analysis tool for Swift that automatically points out any code violations in the PR. SonarQube: A static analysis tool that analyzes code duplication and other issues that SwiftLint cannot cover. Summary and Future Prospects Looking ahead, Bitrise is expected to continue to expand and improve its features to meet the needs of mobile app development. For example, we can expect more advanced testing and deployment options, more flexible workflow settings, and further expansion of cloud-based resources. It is also expected to provide a more seamless development experience, including collaboration with the developer community and improved integration with other tools. KINTO Technologies would like to keep a close eye on the trends and lead to further utilization of this technology. Here is the review. https://findy-tools.io/products/bitrise/18/39
この記事は KINTOテクノロジーズアドベントカレンダー2024 の10日目の記事です🎅🎄 「Mobility Night」は、モビリティ領域のソフトウェア技術者、ビジネスパーソン、研究者、プロダクトマネージャーなどが気軽に集まり、業界特有の知見や課題を共有する勉強会シリーズです。#0(初回)のクローズド開催を経て、いよいよ第1回(#1)をオープンな形で開催することができました。 今回のテーマは、モビリティサービスの基盤技術である「GPS・位置情報」。カーナビや地図アプリ、オンデマンド交通、そして将来の自動運転やスマートシティ基盤まで、「いまどこにいるか」を正確に把握し、活用することはサービス価値の根幹を支えます。 当日は以下の5つのセッションが行われ、GPS・位置情報技術を軸に、それぞれが独自の切り口から課題と可能性を示しました。 この記事は技術広報グループでMobility Nightの企画運営も行っている中西が執筆しています。 1. Exploring New Google Places API 登壇者: KINTOテクノロジーズ株式会社 numaさん Google Places APIは地図プラットフォームの中核機能の一つであり、周辺検索や施設情報取得を効率的に行うための重要なインターフェースです。このセッションでは、テキスト入力中から即時に候補を提示するAutocomplete機能の強化や、Fieldsパラメータによる必要情報の絞り込みなど、最新の改善点が紹介されました。 ポイント: パフォーマンスとコスト最適化: Fields指定で不要なデータ取得を削減し、APIコストを抑えるとともにレスポンス高速化が可能。 ユーザーエクスペリエンス向上: 欲しい情報に素早くアクセスできる体験は、移動中のユーザーにとって大きな利点。Autocomplete強化で検索負荷を軽減し、UXを磨き上げる。 将来への展望: 現在は位置情報取得が中心だが、将来的にはIoTセンサーや行動履歴分析と組み合わせたパーソナライズ戦略も期待できる。 https://speakerdeck.com/kintotechdev/exploring-new-google-places-api 2. AIドラレコサービスの走行データで作る位置情報データプロダクト 登壇者: GO株式会社 松浦慎平さん ドライブレコーダーは事故記録用のデバイスという印象が強いですが、このセッションでは「走行データ=街をセンサー化するプラットフォーム」として再解釈されました。映像+GPSデータをAI解析することで、道路標識や信号、舗装工事などの情報を動的に地図へ反映できる可能性が示唆されました。 ポイント: ダイナミックな地図更新: 静的だった地図を“生きた情報基盤”へ進化させ、道路インフラ変化をほぼリアルタイムで反映。 複数車両データの統合: 異なる車両から得られるデータを突き合わせることで、一時的な標識や工事箇所などを高精度に検出。 プライバシー対策: 個人情報が映り込む映像を適切に匿名化しつつ道路情報を保持する技術・運用が必須。 将来的応用: 自動運転用HDマップ整備、スマートシティ計画、新サービス創出など、多面的なビジネス展開が期待。 https://speakerdeck.com/pemugi/aidorarekosabisunozou-xing-deta-dezuo-ruwei-zhi-qing-bao-detapurodakuto-wei-zhi-qing-bao-jing-du-xiang-shang-nogong-fu 3. GPSモジュールを触って学ぶ、衛星測位技術の概要 登壇者: チャリチャリ株式会社 VP of Engineering 蛭田慎也さん GPSは当たり前に利用されていますが、都市環境では電波反射や視界不良、衛星数の偏りなど多くの実務的課題が存在します。このセッションでは基礎的な衛星測位技術を理解し、精度向上の可能性と対策を探りました。 ポイント: 環境依存課題: ビル街でのマルチパス、トンネル下での衛星ロストなど、ロケーションごとの特殊要件が精度を左右。 マルチGNSS活用: GPS単独でなくGLONASS、Galileo、BeiDou、みちびき(QZSS)など複数システムを組み合わせて精度底上げ。 ハイブリッド手法: 加速度・ジャイロセンサ、Wi-Fi/Bluetoothビーコン、マップマッチングなど補完技術で精度改善。 基礎知識が指針に: こうした理解が将来のプロダクトデザインや品質保証、データ分析を行う際の指針となる。 4. 後処理で位置情報を補正する技術を試してみた(仮) 登壇者: 株式会社Luup IoTチーム 高原健輔さん リアルタイムでの高精度測位が困難な場合、後から精度を引き上げる「PPK(Post-Processing Kinematic)」という選択肢があります。高価なRTK装置や特別な通信インフラを用いず、取得済みデータと基準局データを組み合わせて後処理する手法です。 ポイント: PPKのメリット: リアルタイムにこだわらず、後日精度向上が可能。初期投資を抑えながらセンチメートル級精度を最終的に実現。 コスト効率と拡張性: 将来的に需要が増すシナリオで後から精度改善を行える柔軟性。配送ロボット、ドローン、シェアモビリティなどで有効。 応用範囲: 地図整備、走行ログ高度化、インフラ検査など、事後分析が中心の領域で大きな価値を発揮。 https://speakerdeck.com/kensuketakahara/hou-chu-li-dewei-zhi-qing-bao-wobu-zheng-suruji-shu-woshi-sitemita 5. オンデマンドバスサービス導入前のシミュレーションロジックの構築(仮) 登壇者: トヨタコネクティッド株式会社 先行企画部 新技術開発室 Halufy(ハルフィ)さん オンデマンド交通は柔軟性が魅力ですが、収益性や持続可能性を確保するのは容易ではありません。このセッションでは事前シミュレーションによる精緻な需要予測や運行計画設計が紹介されました。 ポイント: 持続可能なモデル構築: 補助金頼みにならずに最適なステーション配置、運行台数、時間帯設定をデータで検証。 戦略的データ活用: 位置情報を中心にODデータや予約希望を統合し、需要予測や価格戦略、ルート最適化を試行。 長期的ビジョン: 他のモビリティ手段やインフラと連携し、都市全体の交通効率化や利便性向上を目指す土台となる。 今後扱いたいトピックとMobility Nightの展望 今回のMobility Night #1は、GPS・位置情報に特化することで、モビリティ業界の「現在地把握」技術に深く切り込みました。参加者からは「位置情報という身近なテーマがこんなに奥深いとは」「基礎から先端活用まで通して聞けるのは貴重」という声が多数寄せられています。 しかし、モビリティ業界にはGPS・位置情報以外にも多くのテーマが存在します。今後は、 IoTデバイス活用: センサーからのリアルタイムデータ収集・制御 データ分析: 需要予測や高度なオペレーション最適化 プロダクトデザイン: UX向上やユーザー満足度最大化 品質保証: 信頼性確保や安全基準遵守 といった領域も掘り下げ、業界全体のイノベーションを促す場にしていきたいと考えています。 Mobility Nightは、運営メンバーが企画するだけでなく、参加者からの登壇希望やテーマ提案も歓迎しています。Discordを通じて意見交換や共催募集が可能な環境を整え、誰もが関わりやすいコミュニティを目指します。 https://discord.com/invite/nn7QW5pn8B まとめ 「Mobility Night #1」では、GPS・位置情報技術を軸に、モビリティサービスの中核をなす技術的課題と、その克服による新たな価値創造の可能性が明確になりました。静的な地図を動的な情報基盤へアップデートする試み、環境に左右されるGPS精度を高度な手法で補正する取り組み、オンデマンド交通をデータ駆動型で計画する戦略など、多様なアプローチが交錯しました。 これらの知見は、今後のMobility Nightで扱うIoT、データ分析、プロダクトデザイン、品質保証などのテーマとも結びつき、業界全体の進歩を加速させるはずです。引き続きMobility Nightにご注目いただき、ともに学び、交流し、新たな価値を創造していきましょう!
Introduction My name is PannNuWai and I work in the Global Development Group at KINTO Technologies. In Global Development Group test automation team, I build and maintain test automation environments for the product development teams, and write test scripts with the product test team. In my previous company, I was involved in testing, but after joining KINTO Technologies, I had my first experience with automation testing using Appium, which provided me with valuable learning opportunities. I didn’t have any experience in Appium, so I had to start studying from scratch. However, I am now capable of handling everything from initial configuration to designing server architecture. In this automation testing role, I primarily focused on testing smartphone apps and will outline the issues I resolved during the process. In this article, I would like to talk about how to switch to dark mode using Appium version 1.22.3 for automation testing. What is automation testing? Software testing is the process of identifying issues in software to ensure that defective products are not released. In this article, 'automation testing' refers to the use of tools that support and automate the software testing process. Benefits of automation testing [^1] Early fault detection Improve quality while keeping costs down Tests can be performed even with a lack of human resources Tests can be performed more quickly Human error can be eliminated Tests can be performed outside of business hours [^1]: https://products.sint.co.jp/obpm/blog/test_automation What is Appium? It is an open source tool for testing native, web views, and hybrid apps on iOS, Android, and desktop platforms. [^2] [^2]: https://appium.io Appium supports Java, PHP, and Python programming languages, so it is an automation testing tool that testers can easily use while choosing their preferred language. There are three components: Appium Client Appium Server End Device in the architecture of Appium. The mobile device and app details are set up in the Appium Client. The Appium Server uses Node.js language to connect the simulator (iOS) or emulator (Android) while launching the json file. Finally, the end device is managed through the Appium server that has been launched. What is Appium Inspector? Appium Inspector is a standard procedure for uniquely identifying the UI elements of a mobile app. It works on both actual devices or simulators (iOS) or emulators (Android). Note - the Appium Inspector tool is specifically designed to retrieve only native mobile application attributes, so it does not support finding locators in a web browser (Chrome). The Appium desktop application is a combination of the Appium server itself and the Element inspector, designed to detect all visible elements of mobile applications while developing test scripts. [^3] [^3]: https://www.kobiton.com/book/chapter-5-the-appium-inspector-2 What is Dark Mode? Dark mode is a display setting for the user interface of smartphones, laptops, etc. Instead of displaying dark text (dark mode) on a bright screen, light text (light mode) is displayed on a black screen. In addition to the existing dark mode feature on both Android and iOS phones, we often use the dark mode feature in our apps. When testing mobile app automation, testing the dark mode feature was also a key checking point. So, I would like to talk about the dark mode of mobile apps using Appium. Problem There is a problem when using Appium to test dark mode. For example, when testing the login screen to see if the characters of username and password are displayed, the Appium inspector retrieves the location of the element for username and password . Normally, you only need to check that the element is displayed as follows. AssertTrue(driver.findElementByXPath("USER_NAME").isDisplayed()); AssertTrue(driver.findElementByXPath("PASSWORD").isDisplayed()); However, in dark mode, it is not enough to just retrieve the location of the element and check its display. Checking that the screen has changed to black is an important part of dark mode. You need to check the hexadecimal values for the black and white colors. Test Method Now, let’s actually write the source code of the dark mode test case using Appium. changeToDarkTheme Step 1 Retrieve the location (ElementId) of the element from the Appium inspector. Step 2 Use assertElementColorMode(MobileElement elementId, ColorMode colorMode) to confirm if light mode is the Default setting. Step 3 Press the dark mode button. Step 4 Use assertElementColorMode(MobileElement elementId, ColorMode colorMode) to confirm if the Display setting changes to dark mode. public class DisplayChangePage extends Base { public static final String THEME_CELL_ID = "id/theme_parent"; @Test(groups = "DisplayChangePage", dependsOnGroups = "Setting") public void changeToDarkTheme() { driver.manage().timeouts().implicitlyWait(60, TimeUnit.SECONDS); MobileElement themeCell = getDriver().findElementById(THEME_CELL_ID); assertElementColorMode(themeCell, ColorMode.LIGHT); themeCell.click(); driver.manage().timeouts().implicitlyWait(60, TimeUnit.SECONDS); tapElement( findElementByTextContains(ViewType.CHECKED_TEXT, resourceText("darkTheme")) ); themeCell = getDriver().findElementById(THEME_CELL_ID); assertElementColorMode(themeCell, ColorMode.DARK); } } assertElementColorMode Set the ElementId where the Theme cell is located and the ColorMode you want to change as parameters. Step 1 Retrieve evidence of the Element where the Theme cell is located. Use getElementBufferedImage(MobileElement element) to save the evidence as an image file. Step 2 Check that the saved image file does not become null . Step 3 Get the color from the (x = 10, y = 10) image file point and check the color of the dark mode you want to change. public interface AppiumHelpersInterface extends FindElementsInterface { AppiumDriver<MobileElement> getDriver(); Device getDevice(); /** * Get buffered image of mobile element * * @param element Mobile element * @return Buffered image */ default BufferedImage getElementBufferedImage(MobileElement element) { File image = element.getScreenshotAs(OutputType.FILE); try { return ImageIO.read(image); } catch (IOException e) { return null; } } /** * Assert element's color mode * * @param element Mobile Element * @param mode Color mode */ default void assertElementColorMode(MobileElement element, ColorMode mode) { BufferedImage image = getElementBufferedImage(element); Assert.assertNotNull(image); Assert.assertTrue(Utils.getColorString(image, 10, 10).matches(mode.cellRegex())); } } getColorString Change the color of x-point and y-point of the acquired image to hexadecimal and return the array. /** * Get color string from image at point x and y * * @param image BufferedImage * @param x int * @param y int * @return Hexadecimal Color String */ public static String getColorString(BufferedImage image, int x, int y) { int rgba = image.getRGB(x, y); int[] rgb = new int[]{ (rgba >> 16) & 0xff, (rgba >> 8) & 0xff, (rgba) & 0xff }; return String.format("%02x%02x%02x", rgb[0], rgb[1], rgb[2]); } cellRegex Determine the values for dark and light modes. public enum ColorMode { LIGHT, DARK; public String cellRegex() { // 22222 - lighter black if (this == DARK) return "2[(0-9|a-f)]2[(0-9|a-f)]2[(0-9|a-f)]"; // ffffff return "f[(0-9|a-f)]f[(0-9|a-f)]f[(0-9|a-f)]"; } } public interface ColorModeInterface { String darkModeScript(); Map<String, Object> darkModeSettings(); Map<String, Object> lightModeSettings(); default void configureDarkMode(ColorMode mode) { getDriver().executeScript(darkModeScript(), mode == ColorMode.DARK ? darkModeSettings() : lightModeSettings()); } } Caution This case involves using Appium, so only the native app's dark mode feature can be utilized. Summary In this article, I have explained how to switch to dark mode. It can be used on both iOS (version 13 and up) and Android (version 5.0 and up) for dark mode automation testing. This time, I tested the Native App's dark mode feature, but I would also like to explore testing the Web App's dark mode feature in the future Since December, the number of members on the test automation team in Global Development has increased. In the future, I hope to collaborate with team members on automation testing using not only Appium but also other tools like Katalon. Reference DarkMode Appium Architecture
この記事は KINTOテクノロジーズアドベントカレンダー2024 の9日目の記事です🎅🎄 弊社KINTOテクノロジーズは、300人を超えるエンジニア中心の組織です。複雑化する事業環境の中で、私たちは常に組織の効率性と創造性のバランスを模索し続けています。拠点の分散やリモートワークの増加により、部門を超えたコミュニケーションは限定的なものとなり、この課題に真剣に向き合う必要がありました。 この記事では日々のコミュニケーションで利用しているSlackを軸にどのように組織を活性化するのか、そしてSlackをベースにタレントサーチを構築してどのようなことを実現しようとしているのか、その取組の第一歩をご紹介すべく技術広報グループの中西が書いています。 タレントサーチとは社員のスキル情報をデータ化して検索できる仕組みのことです なぜタレントサーチが必要だったのか 効率的に業務をこなすことは大切ですが、イノベーションを生み出すためには、偶然の出会いや「無駄」に思える会話が実は重要な役割を果たします。私たちの組織では、日々の業務に集中するあまり、「暗黙知」や「潜在的な可能性」を見落としがちでした。 例えば、「このスキルを持つ人はいないだろうか」と思っても、誰に相談すればいいか分からない。組織の規模が大きくなるにつれ、こうした情報の非対称性は深刻な課題となっていました。そこで私たちは、Slackプロフィールを戦略的に活用し、この課題に正面から取り組むことにしたのです。 Slackプロフィール活用のメリット 技術広報グループの視点 これまで目立たなかった人材の発見 組織内には、その才能や可能性に気づかれていない人材が多く存在します。技術広報グループのメンバーは日々社内の皆さんとコミュニケーションを取っておりますが、それでも全ての社員の皆さんを深く知ることは難しく、Slackプロフィールは、そうした「隠れた人材」を可視化する新しい手段となっていきます。 プロジェクト支援の迅速化 適切なスキルを持つ人材を素早く特定できることで、プロジェクトの立ち上げや課題解決のスピードが劇的に向上することを期待しています。今までは、〇〇というスキルを持っている人居ないですか?などと社内でも口伝てで探し回ったりしていますが我々のような組織のハブとなる組織を介さずにコミュニケーションが取れるネットワークを構築していくことは今後の組織の成長にとってとても重要なことです 部署間のコラボレーション促進 これまで技術広報グループでは、社内の勉強会や交流イベント、社外講師をお招きしての勉強会など、様々な企画を実施し、それまで接点のなかった部署間でのつながりを作り続けることで社内でも自然にコミュニケーションが発生し、一度繋がったところから数珠つなぎにネットワークが構築され、日々新たなコラボレーションが生まれています。今回のSlack施策もそれに拍車をかけていくことでしょう 全社員にとってのメリット キャリア成長の機会拡大 自分のスキルや興味を明確に表現することで、これまで気づかなかった新たな可能性が開かれます。自分では思わぬキーワードでつながることで、草の根で様々な機会が生まれてきます。これは単に業務に限らず、共通の悩みを持つ方々がお互いに学んで成長する機会が生まれてきます。 スキルを持つ同僚への素早いアクセス 具体的には、新入社員が「Next.jsに詳しいフロントエンドエンジニア」を探す際、Slack上ですぐに簡単に検索できるようになり、学習や課題解決における大きな助けとなっていきます。Slack上でメッセージを検索する延長線上に社内のタレントデータベースが構築されて検索できるようになります。 自然な社内交流の活性化 社内には様々な趣味の草の根活動も存在しています。それが趣味と呼べるかどうかは別にして興味領域ごとに、腰の健康に関するチャンネルから簡単に作れるレシピをシェアするチャンネル。各種スポーツやゲーム、もちろん新しい技術に関するチャンネルもありますし、これらに個人がより紐づけやすくなってきて、業務以外でのつながりがあることで、業務で発生した緊急時の対応でもスムーズに執り行えるということもあります。 プロフィール作成をサポートする仕組み 「何を書けばいいか分からない」という声に応えるため、技術広報グループが積極的にサポートしています。このアプローチは、単なる情報収集ではなく、社員一人ひとりの可能性を引き出すための丁寧な対話プロセスです。 弊社はテックブログを開始した当初より社員の皆さんの才能を見つけ出せるようにインタビューを実施させていただいたり、伴走しながら記事の執筆や登壇資料の作成、イベント企画や運営、勉強会の運営サポートなど行っています。今回のプロフィール作成に関しても、この記事を読んでいる社員の方でまだプロフィールを埋めていないという方や、何を書いたらよいかわからないという方はぜひお声がけください。一緒にあなたの魅力を見つけて社内で発信していきましょう! サポート内容: 個別ヒアリングによる経験や興味の引き出し 1対1の対話を通じて、本人も気づいていない潜在的な強みを探ります。 プロフィール作成用のテンプレート 自己表現が苦手な方でも、安心して記入できます。 自己表現が苦手な方への言語化支援 専門スタッフが寄り添いながら、自分の強みや興味を適切に表現する手伝いをします。 テンプレート: 検索結果 今後の展望 現在は手動でのタレントサーチですが、将来的にはAIを活用したスキルマッチングシステムの構築を目指しています。蓄積されたデータを効果的に活用し、より効率的で戦略的な人材活用の実現を視野に入れています。将来的に個々の社員の可能性をさらに深く理解し、最適な機会と結びつけることができるでしょう。 おわりに Slackプロフィールは、単なる自己紹介欄ではありません。それは、人と人とを結びつける組織の潜在能力を引き出すための戦略的なツールであり、一人ひとりの可能性を解放する鍵なのです。 あなたの興味、スキル、可能性を積極的に発信することで、組織全体の可能性を広げることができます。私たちは、この小さな一歩が、やがて大きな変革につながると信じています。
この記事は KINTOテクノロジーズアドベントカレンダー2024 の9日目の記事です🎅🎄 弊社KINTOテクノロジーズは、300人を超えるエンジニア中心の組織です。複雑化する事業環境の中で、私たちは常に組織の効率性と創造性のバランスを模索し続けています。拠点の分散やリモートワークの増加により、部門を超えたコミュニケーションは限定的なものとなり、この課題に真剣に向き合う必要がありました。 この記事では日々のコミュニケーションで利用しているSlackを軸にどのように組織を活性化するのか、そしてSlackをベースにタレントサーチを構築してどのようなことを実現しようとしているのか、その取組の第一歩をご紹介すべく技術広報グループの中西が書いています。 タレントサーチとは社員のスキル情報をデータ化して検索できる仕組みのことです なぜタレントサーチが必要だったのか 効率的に業務をこなすことは大切ですが、イノベーションを生み出すためには、偶然の出会いや「無駄」に思える会話が実は重要な役割を果たします。私たちの組織では、日々の業務に集中するあまり、「暗黙知」や「潜在的な可能性」を見落としがちでした。 例えば、「このスキルを持つ人はいないだろうか」と思っても、誰に相談すればいいか分からない。組織の規模が大きくなるにつれ、こうした情報の非対称性は深刻な課題となっていました。そこで私たちは、Slackプロフィールを戦略的に活用し、この課題に正面から取り組むことにしたのです。 Slackプロフィール活用のメリット 技術広報グループの視点 これまで目立たなかった人材の発見 組織内には、その才能や可能性に気づかれていない人材が多く存在します。技術広報グループのメンバーは日々社内の皆さんとコミュニケーションを取っておりますが、それでも全ての社員の皆さんを深く知ることは難しく、Slackプロフィールは、そうした「隠れた人材」を可視化する新しい手段となっていきます。 プロジェクト支援の迅速化 適切なスキルを持つ人材を素早く特定できることで、プロジェクトの立ち上げや課題解決のスピードが劇的に向上することを期待しています。今までは、〇〇というスキルを持っている人居ないですか?などと社内でも口伝てで探し回ったりしていますが我々のような組織のハブとなる組織を介さずにコミュニケーションが取れるネットワークを構築していくことは今後の組織の成長にとってとても重要なことです 部署間のコラボレーション促進 これまで技術広報グループでは、社内の勉強会や交流イベント、社外講師をお招きしての勉強会など、様々な企画を実施し、それまで接点のなかった部署間でのつながりを作り続けることで社内でも自然にコミュニケーションが発生し、一度繋がったところから数珠つなぎにネットワークが構築され、日々新たなコラボレーションが生まれています。今回のSlack施策もそれに拍車をかけていくことでしょう 全社員にとってのメリット キャリア成長の機会拡大 自分のスキルや興味を明確に表現することで、これまで気づかなかった新たな可能性が開かれます。自分では思わぬキーワードでつながることで、草の根で様々な機会が生まれてきます。これは単に業務に限らず、共通の悩みを持つ方々がお互いに学んで成長する機会が生まれてきます。 スキルを持つ同僚への素早いアクセス 具体的には、新入社員が「Next.jsに詳しいフロントエンドエンジニア」を探す際、Slack上ですぐに簡単に検索できるようになり、学習や課題解決における大きな助けとなっていきます。Slack上でメッセージを検索する延長線上に社内のタレントデータベースが構築されて検索できるようになります。 自然な社内交流の活性化 社内には様々な趣味の草の根活動も存在しています。それが趣味と呼べるかどうかは別にして興味領域ごとに、腰の健康に関するチャンネルから簡単に作れるレシピをシェアするチャンネル。各種スポーツやゲーム、もちろん新しい技術に関するチャンネルもありますし、これらに個人がより紐づけやすくなってきて、業務以外でのつながりがあることで、業務で発生した緊急時の対応でもスムーズに執り行えるということもあります。 プロフィール作成をサポートする仕組み 「何を書けばいいか分からない」という声に応えるため、技術広報グループが積極的にサポートしています。このアプローチは、単なる情報収集ではなく、社員一人ひとりの可能性を引き出すための丁寧な対話プロセスです。 弊社はテックブログを開始した当初より社員の皆さんの才能を見つけ出せるようにインタビューを実施させていただいたり、伴走しながら記事の執筆や登壇資料の作成、イベント企画や運営、勉強会の運営サポートなど行っています。今回のプロフィール作成に関しても、この記事を読んでいる社員の方でまだプロフィールを埋めていないという方や、何を書いたらよいかわからないという方はぜひお声がけください。一緒にあなたの魅力を見つけて社内で発信していきましょう! サポート内容: 個別ヒアリングによる経験や興味の引き出し 1対1の対話を通じて、本人も気づいていない潜在的な強みを探ります。 プロフィール作成用のテンプレート 自己表現が苦手な方でも、安心して記入できます。 自己表現が苦手な方への言語化支援 専門スタッフが寄り添いながら、自分の強みや興味を適切に表現する手伝いをします。 テンプレート: 検索結果 今後の展望 現在は手動でのタレントサーチですが、将来的にはAIを活用したスキルマッチングシステムの構築を目指しています。蓄積されたデータを効果的に活用し、より効率的で戦略的な人材活用の実現を視野に入れています。将来的に個々の社員の可能性をさらに深く理解し、最適な機会と結びつけることができるでしょう。 おわりに Slackプロフィールは、単なる自己紹介欄ではありません。それは、人と人とを結びつける組織の潜在能力を引き出すための戦略的なツールであり、一人ひとりの可能性を解放する鍵なのです。 あなたの興味、スキル、可能性を積極的に発信することで、組織全体の可能性を広げることができます。私たちは、この小さな一歩が、やがて大きな変革につながると信じています。
この記事は KINTOテクノロジーズアドベントカレンダー2024 の9日目の記事です🎅🎄 はじめに こんにちは、DX開発グループでアプリケーションエンジニアを行っている亀山です。 近年、生成AIはさまざまな分野で活用されており、私たちの開発チームでも生成AIを活用したシステムの構築が行われています。 さらに、私たちの開発チーム内で広く使用されているJavaは、既存の知見やツールを活かしながら生成AIのインターフェースを効率的に構築することができると考えました。本記事では、こうした背景を踏まえ、Javaを用いて生成AIを呼び出し、結果を処理する方法について解説します。 今回はその導入編として、Javaコードを用いてAzure OpenAIのライブラリを用いて、呼び出す基本的な実装について、シンプルなコード例を用いてお話したいと思います。Azure OpenAIはOpenAIに比べ、高いスケーラビリティや信頼性に優れ、規模の大きい業務システムとも親和性が高いプラットフォームとされています。 Azure Open AIのセットアップ Azure サブスクリプションに登録します。 https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account?icid=ai-services&azure-portal=true あとは下記ページの説明に従ってエンドポイントとキーを取得します。 https://learn.microsoft.com/ja-jp/azure/ai-services/openai/chatgpt-quickstart?tabs=command-line%2Cjavascript-keyless%2Ctypescript-keyless%2Cpython-new&pivots=programming-language-java 取得先のAzureコンソールは こちら になります。 ※先ほど登録したアカウントでのログインが必要なページになります。 OpenAIライブラリのセットアップ 今回Azure OpenAIをコールするにあたり、AzureのSDKライブラリを使用します。 Azure OpenAIではこのSDKライブラリによって簡単に生成AIを呼び出すためのコーディングを行うことができます。 Gradleの場合 dependencies { implementation 'com.azure:azure-ai-openai:1.0.0-beta.12' implementation 'com.azure:azure-core-http-okhttp:1.7.8' } Mavenの場合 <dependencies> <dependency> <groupId>com.azure</groupId> <artifactId>azure-ai-openai</artifactId> <version>1.0.0-beta.12</version> </dependency> <dependency> <groupId>com.azure</groupId> <artifactId>azure-core-http-okhttp</artifactId> <version>1.7.8</version> </dependency> </dependencies> 使用バージョンは執筆時点のものであるため、最新バージョンついては公式ドキュメントでご確認ください。 特に今回使用するAzure OpenAI client library for Javaは現在ベータ版であるため、今後安定版がリリースされた場合にはそちらを使用していただくことをおすすめします。 実際にAzure OpenAIのチャットモデルを呼び出してみる 参考: https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/openai/azure-ai-openai /src/main/resource/config.properties endpoint=https://{リソース名}.openai.azure.com/ apiKey={APIキー} 取得したAzure OpenAIのエンドポイントとAPIキーを別ファイルで管理するためこちらに入力してください。 シークレット情報の管理方法はご自身またはチームの方針に合わせていただいても大丈夫です。 /src/main/java/com/sample/app/Main.java package com.sample.app; // ご自身のパッケージ名に合わせてください import com.azure.ai.openai.OpenAIClient; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.ai.openai.models.*; import com.azure.core.credential.AzureKeyCredential; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { // プロパティファイルを読み込む ※別の方法でキー情報を管理する場合は適宜変更してください Properties properties = new Properties(); try (InputStream input = Main.class.getClassLoader().getResourceAsStream("config.properties")) { if (input == null) { System.out.println("config.properties ファイルが見つかりません。"); return; } properties.load(input); } catch (IOException ex) { System.out.println(ex.getMessage()); return; } // プロパティから設定値を取得 String endpoint = properties.getProperty("endpoint"); String apiKey = properties.getProperty("apiKey"); // OpenAIクライアントを作成 var client = new OpenAIClientBuilder() .endpoint(endpoint) .credential(new AzureKeyCredential(apiKey)) .buildClient(); // プロンプトを準備 List<ChatRequestMessage> messages = new ArrayList<>() .setTemperature(0.7) // 応答のランダム性、高いほど多様(0.0~2.0) .setMaxTokens(100) // 応答の最大トークン数 .setFrequencyPenalty(0.0) // 頻出する単語に対するペナルティ(-2.0~2.0) .setPresencePenalty(0.6); // 既存のトピックに関連させるものに対するペナルティ(-2.0~2.0) messages.add(new ChatRequestSystemMessage("あなたは優秀なAIアシスタントです。")); messages.add(new ChatRequestUserMessage("初心者向けに、Javaのクラスとオブジェクトの違いを説明してください。")); // リクエストオプションを設定 var options = new ChatCompletionsOptions(messages); var chatCompletions = client.getChatCompletions("gpt-4o", options); // 使用するデプロイ名または生成AIモデル名を指定してください // リクエストを送信して結果を取得 for (ChatChoice choice : chatCompletions.getChoices()) { ChatResponseMessage message = choice.getMessage(); System.out.printf("Index: %d, Chat Role: %s.%n", choice.getIndex(), message.getRole()); System.out.println("Message:"); System.out.println(message.getContent()); } } } 生成AIの呼び出しにあたり、様々なパラメータを設定することができます。普段使用するChatGPTアプリケーションでは現在設定できないパラメータで、こういったパラメータを調整できるのもプログラムから生成AIを呼び出すメリットの1つです。今回は4つのパラメータ(temperature、maxTokens、frequencyPenalty、presencePenalty)を設定しましたが、他にも様々なパラメータがあります。詳細は こちら を参照してください。 また、messagesの部分で下記2種類のメッセージをセットしました。前者のChatRequestSystemMessageはセットしなくても実行可能です。 ChatRequestSystemMessage 生成AIモデルの振る舞いや役割を設定するためのメッセージで、会話のトーンや応答の仕方を定義します。 ChatRequestUserMessage ユーザーからの具体的な質問や指示をAIに伝えるためのメッセージで、このメッセージに対する回答が返却値としてOpenAIから返されます。 getChatCompletionsの第1引数はデプロイ名またはモデル名を入力します。 デプロイ名はAzureポータルで取得します。Azure以外のOpenAIを使用する場合はモデル名「gpt-4o」「gpt-3.5-turbo」などを入力します(上記の例だとモデル名を入力しています)。 .gitignore /src/main/resources/config.properties 今回ご説明するように/src/main/resource/config.propertiesで管理する場合は.gitignoreに上記1行を追加してください。 特にリポジトリで管理する際はAPIキーなどのシークレット情報の取り扱いにはくれぐれもご注意ください 実行結果 下記のようなレスポンスをOpenAIから得ることができました。(実際にはMarkdown形式の文字列です) Index: 0, Chat Role: assistant. Message: Javaにおけるクラスとオブジェクトの違いは、初めてプログラミングを学ぶ人にとって重要な概念です。以下に分かりやすく説明します。 クラス 設計図 : クラスはオブジェクトを作成するための設計図やテンプレートと考えることができます。クラスにはオブジェクトの属性(フィールド)や動作(メソッド)が定義されています。 宣言 : Javaではクラスを定義するために`class`キーワードを使用します。例えば、車を表すクラスは以下のように定義できます。 public class Car { // フィールド(属性) String color; int year; // メソッド(動作) void drive() { System.out.println("The car is driving"); } } オブジェクト インスタンス : オブジェクトは、クラスから生成された実体(インスタンス)です。オブジェクトは特定のデータを持ち、そのデータに対する操作を行うことができます。 生成 : Javaでは`new`キーワードを使用してクラスからオブジェクトを生成します。例えば、`Car`クラスからオブジェクトを作成する場合は次のようになります。 public class Main { public static void main(String[] args) { // Carクラスのインスタンスを作成 Car myCar = new Car(); myCar.color = "Red"; myCar.year = 2020; // オブジェクトのメソッドを呼び出す myCar.drive(); } } まとめ クラス はオブジェクトを作成するためのテンプレートであり、属性や動作を定義しています。 オブジェクト はそのクラスの実際のインスタンスであり、具体的なデータを持ち、定義された動作を実行できます。 この基本的な関係を理解することで、より複雑なプログラム構築を始めることができます。 終わりに 今回の記事では、JavaでAzure OpenAIを利用する基本的な方法について紹介しました。 JavaによるOpenAIの利用に関する情報はまだ少ないため、本記事が皆様のお役に立てれば幸いです。 次回はより高度な活用方法について解説していきたいと思っておりますので、引き続きよろしくお願いいたします。
Hello everyone, this is Martin from the Mobile Development Group here at KINTO Technologies! With this guide I hope to give you a quick overview on how to build your TFLite (TensorFlow Lite) models from scratch so let's dive straight into it. This article is the entry for December 9th in the KINTO Technologies Advent Calendar 2024 🎅🎄 Preparation There are basically two ways to prepare your dataset. One is to do the annotation process locally and the other is to annotate your dataset online whilst collaborating and sharing the initial workload better with your team members. This guide tries to emphasize the use of Roboflow ( https://roboflow.com/ ). Roboflow's model export functionality allows you to export trained models in various formats, making it easy to deploy them in your own applications or further fine-tune them. In our case we want to train TFlite models so we would want to export to the TFRecord format as shown in the image below. However, in case you are not using any third party online annotation tools such as Roboflow to annotate your images online and you want to annotate locally, you could try out the free Python library labelImg: https://github.com/HumanSignal/labelImg In general, either locally or online, first we need to collect the dataset of images and label them to get the corresponding bounding box classification meta data (xml) files. (in our case creating the VOC [Visual Object Classes] Pascal meta data) more information about Pascal VOC can be found here: https://roboflow.com/formats/pascal-voc-xml After creating a Google Cloud Platform standard runtime instance you will need to connect it to your Colab notebook. In essence, Google Colab provides a secure, scalable, and collaborative platform for data science and machine learning teams within organizations. Once that is done we first need to import the necessary libraries to get Tensorflow going (Step 1) Creation of GCP standard instance: Creation of Colab Enterprise ( https://cloud.google.com/colab/docs ) notebook: Connect your Google Cloud bucket (Step 4) Execution Install the TensorFlow Object Detection API (Step 5 in this guide) Generate the TFRecord files required for training. (need generate_tfrecord.py script to produce csv files for this) Edit the model pipeline config file and download the pre-trained model checkpoint Train and evaluate the model Export and convert the model into TFlite(TensorFlow Lite) format Deployment Deploy the TFlite model on Android / iOS / IoT devices So now get's started Here are the steps that you should undergo within your Colab Enterprise notebook in detail: 1) Import Libraries !pip install tensorflow==2.13.0 import os import glob import xml.etree.ElementTree as ET import pandas as pd import tensorflow as tf print(tf.__version__) * 2) Create customTF2 , training and data folders in your GCP cloud storage bucket (Necessary only the first time) * Create a folder named customTF2 in your GCP cloud storage bucket Create two sub-folders called training and data inside the customTF2 folder (The training folder is where the checkpoints will be saved during training) Creation of folder structure in your GCS bucket: 3) Download, save and upload the following as generate_tfrecord.py file to the customTF2 folder to your bucket. (Necessary only for the first time) from __future__ import division from __future__ import print_function from __future__ import absolute_import import os import io import pandas as pd import tensorflow as tf import argparse from PIL import Image from tqdm import tqdm from object_detection.utils import dataset_util from collections import namedtuple, OrderedDict def __split(df, group): data = namedtuple('data', ['filename', 'object']) gb = df.groupby(group) return [data(filename, gb.get_group(x)) for filename, x in zip(gb.groups.keys(), gb.groups)] def create_tf_example(group, path, class_dict): with tf.io.gfile.GFile(os.path.join(path, '{}'.format(group.filename)), 'rb') as fid: encoded_jpg = fid.read() encoded_jpg_io = io.BytesIO(encoded_jpg) image = Image.open(encoded_jpg_io) width, height = image.size filename = group.filename.encode('utf8') image_format = b'jpg' xmins = [] xmaxs = [] ymins = [] ymaxs = [] classes_text = [] classes = [] for index, row in group.object.iterrows(): if set(['xmin_rel', 'xmax_rel', 'ymin_rel', 'ymax_rel']).issubset(set(row.index)): xmin = row['xmin_rel'] xmax = row['xmax_rel'] ymin = row['ymin_rel'] ymax = row['ymax_rel'] elif set(['xmin', 'xmax', 'ymin', 'ymax']).issubset(set(row.index)): xmin = row['xmin'] / width xmax = row['xmax'] / width ymin = row['ymin'] / height ymax = row['ymax'] / height xmins.append(xmin) xmaxs.append(xmax) ymins.append(ymin) ymaxs.append(ymax) classes_text.append(str(row['class']).encode('utf8')) classes.append(class_dict[str(row['class'])]) tf_example = tf.train.Example(features=tf.train.Features( feature={ 'image/height': dataset_util.int64_feature(height), 'image/width': dataset_util.int64_feature(width), 'image/filename': dataset_util.bytes_feature(filename), 'image/source_id': dataset_util.bytes_feature(filename), 'image/encoded': dataset_util.bytes_feature(encoded_jpg), 'image/format': dataset_util.bytes_feature(image_format), 'image/object/bbox/xmin': dataset_util.float_list_feature(xmins), 'image/object/bbox/xmax': dataset_util.float_list_feature(xmaxs), 'image/object/bbox/ymin': dataset_util.float_list_feature(ymins), 'image/object/bbox/ymax': dataset_util.float_list_feature(ymaxs), 'image/object/class/text': dataset_util.bytes_list_feature(classes_text), 'image/object/class/label': dataset_util.int64_list_feature(classes), })) return tf_example def class_dict_from_pbtxt(pbtxt_path): # open file, strip \n, trim lines and keep only # lines beginning with id or display_name with open(pbtxt_path, 'r', encoding='utf-8-sig') as f: data = f.readlines() name_key = None if any('display_name:' in s for s in data): name_key = 'display_name:' elif any('name:' in s for s in data): name_key = 'name:' if name_key is None: raise ValueError( "label map does not have class names, provided by values with the 'display_name' or 'name' keys in the contents of the file" ) data = [l.rstrip('\n').strip() for l in data if 'id:' in l or name_key in l] ids = [int(l.replace('id:', '')) for l in data if l.startswith('id')] names = [ l.replace(name_key, '').replace('"', '').replace("'", '').strip() for l in data if l.startswith(name_key)] # join ids and display_names into a single dictionary class_dict = {} for i in range(len(ids)): class_dict[names[i]] = ids[i] return class_dict if __name__ == '__main__': parser = argparse.ArgumentParser( description='Create a TFRecord file for use with the TensorFlow Object Detection API.', formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('csv_input', metavar='csv_input', type=str, help='Path to the CSV input') parser.add_argument('pbtxt_input', metavar='pbtxt_input', type=str, help='Path to a pbtxt file containing class ids and display names') parser.add_argument('image_dir', metavar='image_dir', type=str, help='Path to the directory containing all images') parser.add_argument('output_path', metavar='output_path', type=str, help='Path to output TFRecord') args = parser.parse_args() class_dict = class_dict_from_pbtxt(args.pbtxt_input) writer = tf.compat.v1.python_io.TFRecordWriter(args.output_path) path = os.path.join(args.image_dir) examples = pd.read_csv(args.csv_input) grouped = __split(examples, 'filename') for group in tqdm(grouped, desc='groups'): tf_example = create_tf_example(group, path, class_dict) writer.write(tf_example.SerializeToString()) writer.close() output_path = os.path.join(os.getcwd(), args.output_path) print('Successfully created the TFRecords: {}'.format(output_path)) 4) Mount your GCS bucket, install GCSFUSE and link your folder from google.colab import auth auth.authenticate_user() !echo "deb https://packages.cloud.google.com/apt gcsfuse-bionic main" > /etc/apt/sources.list.d/gcsfuse.list !curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - !apt -qq update !apt -qq install gcsfuse !gsutil ls -r gs://your-cloud-storage-bucket-name !mkdir customTF2 !gcsfuse --implicit-dirs your-cloud-storage-bucket-name customTF2 5) Clone the tensorflow models git repository & Install TensorFlow Object Detection API %cd /content # clone the tensorflow models on the colab cloud vm !git clone --q https://github.com/tensorflow/models.git #navigate to /models/research folder to compile protos %cd models/research # Compile protos. !protoc object_detection/protos/*.proto --python_out=. # Install TensorFlow Object Detection API. !cp object_detection/packages/tf2/setup.py . !python -m pip install . 6) Test the model builder (Suggested) %cd /content/models/research # testing the model builder !pip install 'tf-models-official >=2.5.1, <2.16.0' !python object_detection/builders/model_builder_tf2_test.py 7) Download pre-trained model checkpoint (Necessary only for the first time) Current working directory is /content/customTF2/customTF2/data/ Download ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz into the data folder & unzip it. A list of detection checkpoints for other tensorflow 2.x can be found here . %cd /content/customTF2/customTF2/data/ #Download the pre-trained model ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz into the data folder & unzip it. !wget http://download.tensorflow.org/models/object_detection/tf2/20200711/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz !tar -xzvf ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz 8) Get the model pipeline config file, make changes to it and put it inside the data folder (Necessary every time and when you change the amount of class numbers) Current working directory is /content/customTF2/customTF2/data/ Download ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config from /content/models/research/object_detection/configs/tf2 . Make the required changes to it and upload it to the /content/customTF2/customTF2/data/ folder. OR Edit the config file from /content/models/research/object_detection/configs/tf2 in colab and copy the edited config file to the /content/customTF2/customTF2/data folder. You can also find the pipeline config file inside the model checkpoint folder we just downloaded in the previous step. You need to make the following changes: change num_classes to the number of your classes change test.record path, train.record path & labelmap path to the paths where you have created these files (paths should be relative to your current working directory while training) change fine_tune_checkpoint to the path of the directory where the downloaded checkpoint from step 12 is change fine_tune_checkpoint_type with value classification or detection depending on your classification type change batch_size to any multiple of 8 depending upon the capability of your GPU (eg:- 24,128,...,512) - usually 24 for smaller datasets and 32 for larger datasets works well with a standard colab enterprise instance change num_steps to number of steps you want the detector to train. #copy the edited config file from the configs/tf2 directory to the data/ folder in your GCP storage !cp /content/models/research/object_detection/configs/tf2/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config /content/customTF2/customTF2/data In the next step we want to make use of the official TensorBoard tool to visualize our runs and graphs to inspect learning and classification loss over time. More information on how to read graphs and how to use the tool can be found here: https://www.tensorflow.org/tensorboard/get_started#:~:text=TensorBoard%20is%20a%20tool%20for,during%20the%20machine%20learning%20workflow . 9) Load TensorBoard (Recommended) # cload tensorboard %cd /content/customTF2/customTF2/training # !pip install tensorboard # tensorboard --inspect --logdir /content/customTF2/customTF2/training # !gcloud init # !gcloud auth application-default login %reload_ext tensorboard %tensorboard --logdir '/content/customTF2/customTF2/training' 10) Train the model Navigate to the object_detection folder in colab vm %cd /content/models/research/object_detection 10 (a) Training using model_main_tf2.py (Suggested method) Here {PIPELINE_CONFIG_PATH} points to the pipeline config and {MODEL_DIR} points to the directory in which training checkpoints and events will be written. For best results, you should stop the training when the loss is less than 0.1 if possible, else train the model until the loss does not show any significant change for a while. The ideal loss should be below 0.05 (Try to get the loss as low as possible without overfitting the model. Don’t go too high on training steps to try and lower the loss if the model has already converged viz. if it does not reduce loss significantly any further and takes a while to go down. ) !pip install tensorflow==2.13.0 # Run the command below from the content/models/research/object_detection directory """ PIPELINE_CONFIG_PATH=path/to/pipeline.config MODEL_DIR=path to training checkpoints directory NUM_TRAIN_STEPS=50000 SAMPLE_1_OF_N_EVAL_EXAMPLES=1 python model_main_tf2.py -- \ --model_dir=$MODEL_DIR --num_train_steps=$NUM_TRAIN_STEPS \ --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \ --pipeline_config_path=$PIPELINE_CONFIG_PATH \ --alsologtostderr """ !python model_main_tf2.py --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --model_dir=/content/customTF2/customTF2/training --alsologtostderr 10 (b) Evaluation using model_main_tf2.py (Optional, just if you want more customization) You can run this in parallel by opening another colab notebook and running this command simultaneously along with the training command above (don't forget to mount your gcp storage, clone the TF git repo and install the TF2 object detection API there as well). This will give you validation loss, mAP, etc so you have a better idea of how your model is performing. Here {CHECKPOINT_DIR} points to the directory with checkpoints produced by the training job. Evaluation events are written to {MODEL_DIR/eval} . # Run the command below from the content/models/research/object_detection directory """ PIPELINE_CONFIG_PATH=path/to/pipeline.config MODEL_DIR=path to training checkpoints directory CHECKPOINT_DIR=${MODEL_DIR} NUM_TRAIN_STEPS=50000 SAMPLE_1_OF_N_EVAL_EXAMPLES=1 python model_main_tf2.py -- \ --model_dir=$MODEL_DIR --num_train_steps=$NUM_TRAIN_STEPS \ --checkpoint_dir=${CHECKPOINT_DIR} \ --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \ --pipeline_config_path=$PIPELINE_CONFIG_PATH \ --alsologtostderr """ !python model_main_tf2.py --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --model_dir=/content/customTF2/customTF2/training/ --checkpoint_dir=/content/customTF2/customTF2/training/ --alsologtostderr Retraining your model (In case you get disconnected) If you get disconnected or lose your session on colab vm, you can start your training where you left off as the checkpoint is saved on your cloud storage inside the training folder. To restart the training simply run steps 1, 4, 5, 6, 9 and 10. Note that since we have all the files required for training like the record files, our edited pipeline config file, the label_map file and the model checkpoint folder, we do not need to create these again. The model_main_tf2.py script saves the checkpoint every 1000 steps. The training automatically restarts from the last saved checkpoint itself. However, if you see that it doesn't restart training from the last checkpoint you can make 1 change in the pipeline config file. Change fine_tune_checkpoint to where your latest trained checkpoints have been written and have it point to the latest checkpoint as shown below: fine_tune_checkpoint: "/content/customTF2/customTF2/training/ckpt-X" (where ckpt-X is the latest checkpoint) 11) Test your trained model Export inference graph Current working directory is /content/models/research/object_detection %cd /content/models/research/object_detection !pip install tensorflow==2.13.0 ##Export inference graph !python exporter_main_v2.py --trained_checkpoint_dir=/content/customTF2/customTF2/training --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --output_directory /content/customTF2/customTF2/data/inference_graph Test your trained Object Detection model on images (Provide test image of your liking and adjust image_path) Current working directory is /content/models/research/object_detection %cd /content/models/research/object_detection # Different font-type for labels text.(This step is optional) !wget https://www.freefontspro.com/d/14454/arial.zip !unzip arial.zip -d . %cd utils/ !sed -i "s/font = ImageFont.truetype('arial.ttf', 24)/font = ImageFont.truetype('arial.ttf', 50)/" visualization_utils.py %cd .. %cd /content/models/research/object_detection !pip install tensorflow=="2.12.0" #Loading the saved_model import tensorflow as tf import time import numpy as np import warnings warnings.filterwarnings('ignore') from PIL import Image from google.colab.patches import cv2_imshow from object_detection.utils import label_map_util from object_detection.utils import visualization_utils as viz_utils IMAGE_SIZE = (12, 8) # Output display size as you want import matplotlib.pyplot as plt PATH_TO_SAVED_MODEL="/content/customTF2/customTF2/data/inference_graph/saved_model" print('Loading model...', end='') # Load saved model and build the detection function detect_fn=tf.saved_model.load(PATH_TO_SAVED_MODEL) print('Done!') #Loading the label_map category_index=label_map_util.create_category_index_from_labelmap("/content/customTF2/customTF2/data/label_map.pbtxt",use_display_name=True) def load_image_into_numpy_array(path): return np.array(Image.open(path)) # Replace with your test image image_path = "/content/customTF2/customTF2/data/images/your_test.jpg" #print('Running inference for {}... '.format(image_path), end='') image_np = load_image_into_numpy_array(image_path) # The input needs to be a tensor, convert it using `tf.convert_to_tensor`. input_tensor = tf.convert_to_tensor(image_np) # The model expects a batch of images, so add an axis with `tf.newaxis`. input_tensor = input_tensor[tf.newaxis, ...] detections = detect_fn(input_tensor) # All outputs are batches tensors. # Convert to numpy arrays, and take index [0] to remove the batch dimension. # We're only interested in the first num_detections. num_detections = int(detections.pop('num_detections')) detections = {key: value[0, :num_detections].numpy() for key, value in detections.items()} detections['num_detections'] = num_detections # detection_classes should be ints. detections['detection_classes'] = detections['detection_classes'].astype(np.int64) image_np_with_detections = image_np.copy() viz_utils.visualize_boxes_and_labels_on_image_array( image_np_with_detections, detections['detection_boxes'], detections['detection_classes'], detections['detection_scores'], category_index, use_normalized_coordinates=True, max_boxes_to_draw=200, min_score_thresh=.8, # Adjust this value to set the minimum probability boxes to be classified as True agnostic_mode=False) %matplotlib inline plt.figure(figsize=IMAGE_SIZE, dpi=200) plt.axis("off") plt.imshow(image_np_with_detections) plt.show() Converting trained SSD (Single Shot Detector) model to TFLite model 12) Install tf-nightly TFLite converter works better with tf-nightly. %cd /content/models/research/object_detection !pip install tensorflow=="2.12.0" !pip install numpy==1.26.4 !pip install tf-nightly 13) Export SSD TFLite graph Current working directory is /content/models/research/object_detection # !pip3 uninstall keras # !pip3 install keras==2.14.0 !pip3 install --upgrade tensorflow keras !pip3 install tensorflow=="2.12.0" # !pip3 install --upgrade tensorflow keras # !pip3 install tensorflow=="2.13.1" # !pip3 install numpy --upgrade # !pip3 uninstall numpy # !pip3 install numpy=="1.22.0" # !pip3 install tensorflow --upgrade #!python --version %cd /content/models/research/object_detection !python export_tflite_graph_tf2.py --pipeline_config_path /content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --trained_checkpoint_dir /content/customTF2/customTF2/training --output_directory /content/customTF2/customTF2/data/tflite 14) Convert TF saved model to TFLite model Current working directory is /mydrive/customTF2/data/ %cd /content/customTF2/customTF2/data/ Check input and output tensor names !saved_model_cli show --dir /content/customTF2/customTF2/data/tflite/saved_model --tag_set serve --all Converting to TFlite: Use either Method (a) or Method (b). METHOD (a) Using command-line tool tflite_convert - (Basic model conversion) # The default inference type is Floating-point. %cd /content/customTF2/customTF2/data/ !tflite_convert --saved_model_dir=tflite/saved_model --output_file=tflite/detect.tflite METHOD (b) Using Python API - (For advanced model conversion with optimizations etc) %cd /mydrive/customTF2/data/ #'''******************************** # FOR FLOATING-POINT INFERENCE #*********************************''' #import tensorflow as tf saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' #converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) #tflite_model = converter.convert() #open("/content/customTF2/customTF2/data/tflite/detect.tflite", "wb").write(tflite_model) #'''************************************************** # FOR FLOATING-POINT INFERENCE WITH OPTIMIZATIONS #***************************************************''' import tensorflow as tf converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir,signature_keys=['serving_default']) converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.experimental_new_converter = True converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS] tflite_model = converter.convert() with tf.io.gfile.GFile('/mydrive/customTF2/data/tflite/detect.tflite', 'wb') as f: f.write(tflite_model) #'''********************************** # FOR DYNAMIC RANGE QUANTIZATION #************************************* # The model is now a bit smaller with quantized weights, but other variable data is still in float format.''' # import tensorflow as tf # converter = tf.lite.TFLiteConverter.from_saved_model('/content/customTF2/customTF2/data/tflite/saved_model',signature_keys=['serving_default']) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # tflite_quant_model = converter.convert() # with tf.io.gfile.GFile('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model) # '''*********************************************************************** # FOR INTEGER WITH FLOAT FALLBACK QUANTIZATION WITH DEFAULT OPTMIZATIONS # ************************************************************************** # Now all weights and variable data are quantized, and the model is significantly smaller compared to the original TensorFlow Lite model. # However, to maintain compatibility with applications that traditionally use float model input and output tensors, # the TensorFlow Lite Converter leaves the model input and output tensors in float''' # import tensorflow as tf # import numpy as np # saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' # def representative_dataset(): # for _ in range(100): # data = np.random.rand(1, 320, 320, 3) # yield [data.astype(np.float32)] # converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # converter.representative_dataset = representative_dataset # tflite_quant_model = converter.convert() # with open('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model) # '''********************************* # FOR FULL INTEGER QUANTIZATION # ************************************ # The internal quantization remains the same as previous float fallback quantization method, # but you can see the input and output tensors here are also now integer format''' # import tensorflow as tf # import numpy as np # saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' # def representative_dataset(): # for _ in range(100): # data = np.random.rand(1, 320, 320, 3) # yield [data.astype(np.float32)] # converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # converter.representative_dataset = representative_dataset # converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] # converter.inference_input_type = tf.uint8 # converter.inference_output_type = tf.uint8 # tflite_quant_model_full_int = converter.convert() # with open('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model_full_int) Read more about post-training quantization here . You can also read about these in this colab notebook. 15) Create TFLite metadata !pip install tflite_support_nightly %cd /content/customTF2/customTF2/data/ %cd tflite/ !mkdir tflite_with_metadata %cd .. Create a labelmap.txt file with the names of the classes written in each line inside the data folder. Finally run the following cell to create the detect.tflite model with metadata attached to it. Current working directory is /content/customTF2/customTF2/data/ %cd /content/customTF2/customTF2/data/ !pip uninstall tensorflow !pip install tensorflow=="2.13.1" # Attach Metadata to TFLite from tflite_support.metadata_writers import object_detector from tflite_support.metadata_writers import writer_utils import flatbuffers import platform from tensorflow_lite_support.metadata import metadata_schema_py_generated from tensorflow_lite_support.metadata import schema_py_generated from tensorflow_lite_support.metadata.python import metadata from tensorflow_lite_support.metadata.python import metadata_writers import flatbuffers import os from tensorflow_lite_support.metadata import metadata_schema_py_generated as _metadata_fb from tensorflow_lite_support.metadata.python import metadata as _metadata from tensorflow_lite_support.metadata.python.metadata_writers import metadata_info from tensorflow_lite_support.metadata.python.metadata_writers import metadata_writer from tensorflow_lite_support.metadata.python.metadata_writers import writer_utils ObjectDetectorWriter = object_detector.MetadataWriter _MODEL_PATH = "/content/customTF2/customTF2/data/tflite/detect.tflite" _LABEL_FILE = "/content/customTF2/customTF2/data/labelmap.txt" _SAVE_TO_PATH = "/content/customTF2/customTF2/data/tflite/tflite_with_metadata/detect.tflite" writer = ObjectDetectorWriter.create_for_inference( writer_utils.load_file(_MODEL_PATH), [127.5], [127.5], [_LABEL_FILE]) writer_utils.save_file(writer.populate(), _SAVE_TO_PATH) # Verify the populated metadata and associated files. displayer = metadata.MetadataDisplayer.with_model_file(_SAVE_TO_PATH) print("Metadata populated:") print(displayer.get_metadata_json()) print("Associated file(s) populated:") print(displayer.get_packed_associated_file_list()) model_meta = _metadata_fb.ModelMetadataT() model_meta.name = "SSD_Detector" model_meta.description = ( "Identify which of a known set of objects might be present and provide " "information about their positions within the given image or a video " "stream.") # Creates input info. input_meta = _metadata_fb.TensorMetadataT() input_meta.name = "image" input_meta.content = _metadata_fb.ContentT() input_meta.content.contentProperties = _metadata_fb.ImagePropertiesT() input_meta.content.contentProperties.colorSpace = ( _metadata_fb.ColorSpaceType.RGB) input_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.ImageProperties) input_normalization = _metadata_fb.ProcessUnitT() input_normalization.optionsType = ( _metadata_fb.ProcessUnitOptions.NormalizationOptions) input_normalization.options = _metadata_fb.NormalizationOptionsT() input_normalization.options.mean = [127.5] input_normalization.options.std = [127.5] input_meta.processUnits = [input_normalization] input_stats = _metadata_fb.StatsT() input_stats.max = [255] input_stats.min = [0] input_meta.stats = input_stats # Creates outputs info. output_location_meta = _metadata_fb.TensorMetadataT() output_location_meta.name = "location" output_location_meta.description = "The locations of the detected boxes." output_location_meta.content = _metadata_fb.ContentT() output_location_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.BoundingBoxProperties) output_location_meta.content.contentProperties = ( _metadata_fb.BoundingBoxPropertiesT()) output_location_meta.content.contentProperties.index = [1, 0, 3, 2] output_location_meta.content.contentProperties.type = ( _metadata_fb.BoundingBoxType.BOUNDARIES) output_location_meta.content.contentProperties.coordinateType = ( _metadata_fb.CoordinateType.RATIO) output_location_meta.content.range = _metadata_fb.ValueRangeT() output_location_meta.content.range.min = 2 output_location_meta.content.range.max = 2 output_class_meta = _metadata_fb.TensorMetadataT() output_class_meta.name = "category" output_class_meta.description = "The categories of the detected boxes." output_class_meta.content = _metadata_fb.ContentT() output_class_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_class_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) output_class_meta.content.range = _metadata_fb.ValueRangeT() output_class_meta.content.range.min = 2 output_class_meta.content.range.max = 2 label_file = _metadata_fb.AssociatedFileT() label_file.name = os.path.basename("labelmap.txt") label_file.description = "Label of objects that this model can recognize." label_file.type = _metadata_fb.AssociatedFileType.TENSOR_VALUE_LABELS output_class_meta.associatedFiles = [label_file] output_score_meta = _metadata_fb.TensorMetadataT() output_score_meta.name = "score" output_score_meta.description = "The scores of the detected boxes." output_score_meta.content = _metadata_fb.ContentT() output_score_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_score_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) output_score_meta.content.range = _metadata_fb.ValueRangeT() output_score_meta.content.range.min = 2 output_score_meta.content.range.max = 2 output_number_meta = _metadata_fb.TensorMetadataT() output_number_meta.name = "number of detections" output_number_meta.description = "The number of the detected boxes." output_number_meta.content = _metadata_fb.ContentT() output_number_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_number_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) # Creates subgraph info. group = _metadata_fb.TensorGroupT() group.name = "detection result" group.tensorNames = [ output_location_meta.name, output_class_meta.name, output_score_meta.name ] subgraph = _metadata_fb.SubGraphMetadataT() subgraph.inputTensorMetadata = [input_meta] subgraph.outputTensorMetadata = [ output_location_meta, output_class_meta, output_score_meta, output_number_meta ] subgraph.outputTensorGroups = [group] model_meta.subgraphMetadata = [subgraph] b = flatbuffers.Builder(0) b.Finish( model_meta.Pack(b), _metadata.MetadataPopulator.METADATA_FILE_IDENTIFIER) metadata_buf = b.Output() When asked, proceed with 'Y' 16) Download the TFLite model Congrats, you are done! Final thoughts Google Colab Enterprise is a powerful cloud-based platform for machine learning, making it an ideal environment for building TensorFlow Lite models. After over a year of using this platform, I've found that the most time-consuming part of the process is data preparation and the initial trial-and-error phase. This stage requires significant iteration and testing to identify challenges in recognizing specific parts of the dataset and to address false positives, where images are incorrectly classified. *The Android robot header image was reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License.
この記事は KINTOテクノロジーズアドベントカレンダー2024 の8日目の記事です🎅🎄 はじめに こんにちは。モバイルアプリ開発グループの中本です。 普段は、大阪にいながら東京のメンバーと協力して、 KINTO Unlimitedアプリ のiOS開発をしています。 本記事では、KINTO Unlimitedアプリ(iOS)のアーキテクチャ改善の過程について詳しく説明します。 このアプリのアーキテクチャは、1st → 2nd → 3rdと段階的に進化し、最終的には独自のアーキテクチャへと移行しました。 それぞれの段階における設計や課題について、以下でお話しします。 1st Generation Architecture VIPERアーキテクチャを採用 すべての画面をUIKit + xib/storyboardで実装 Combineを使用してViewを更新 ファーストリリースに向けて納期が短かったこともあり、社内で実績のあったアーキテクチャを採用 1stの設計 flowchart TD id1(ViewController) -- publish --> id2(ViewModel) -- subscribe --> id1 id2 -- request --> id3(Interactor) id1 -- apply view property --> id4(UIView) id1 -- transition --> id5(Router) ViewController ViewModelにイベントを通知 ViewModelからのイベントに基づいてアウトプットを購読 購読結果に応じてViewを更新し、Routerを呼び出して画面遷移を実施 ViewModel Combineを使用してリアクティブに状態を変化 イベントPublisherを変換し、Viewの状態をPublisher経由でアウトプット Interactor APIや内部DBにリクエストを実施 Router 他の画面への遷移処理を実施 UIView コード/xib/storyboardを使用してレイアウト 1stの課題 UIKitを使用したレイアウトは開発コストが高く、特にxib/storyboardを使用した場合は変更が容易ではない → SwiftUIへ移行したい! 2nd Generation Architecture UIKitからSwiftUIへ移行 UIKitによるレイアウトをSwiftUIに置き換え、開発効率を改善 UIHostingControllerを使用してSwiftUIのViewをViewControllerに注入 画面遷移は従来通りUIKitで実施 当時、SwiftUIの画面遷移APIは不安定だったためUIKitのまま SwiftUIへの移行に専念する 一度にたくさん変更すると、機能仕様のデグレが懸念されるため 2ndの設計 flowchart TD id1(ViewController) -- input --> id2(ViewModel) -- output --> id1 id2 -- request --> id6(Interactor) id1 -- mapping --> id3(ScreenModel) -- publish --> id1 id3 -- publish --> id4(View) -- publish --> id3 id1 -- transit --> id5(Router) ViewController HostingControllerInjectableプロトコルを実装し、SwiftUI Viewを追加 ViewModelのアウトプットを購読し、ScreenModel(ObservableObject)に反映 ViewModelのアウトプットやScreenModelのPublisherを購読し、Routerを用いて画面遷移を実施 ScreenModel Viewの状態を保持するObservableObject ViewModel / Interactor / Router 1st Generationと同様の機能 2ndの課題 状態管理がViewModelとScreenModelの両方で行われるため、ロジックが分散し、開発・保守コストが増加 1stからの課題 Combineによるリアクティブな状態変化の実装は保守性に懸念があり、コード量が多く可読性に難がある 1画面につき1つのViewModelであるため、機能の多い画面ではViewModelが巨大化 → CombineやViewModelから脱却したい! 3rd Generation Architecture Combineを用いたViewModelから状態を集中管理するViewStoreを中心としたアーキテクチャへ移行 イベントの結果をAnyPublisherを経ずに直接ObservableObjectに反映できる仕組みを実現 Combineを使用せず、async/awaitを用いてリアクティブな状態変更を実現 状態管理ロジックを機能ごとに分割可能 3rdの設計 flowchart TD subgraph ViewStore id1(ActionHandler) -- update --> id2(State) end id2 -- bind --> id5(View) -- publish action --> id1 id1 -- publish routing --> id3(ViewController) -- publish action --> id1 id3 -- transit --> id4(Router) id1 -- request --> id6(Interactor) ViewStore State Viewの状態を保持するObservableObjectで、SwiftUIのViewで使用 Action 従来のViewModelのtransformメソッドにおけるINPUTに相当する機能を提供するenum ActionHandler Actionを引数にとり、Stateを更新するハンドラー async/awaitを使用して実装 ViewController routerSubjectを購読し、Routerを用いて画面遷移を実施 Interactor / Router 2nd Generationと同様 ActionHandlerの分割 機能の多い画面では、ActionHandlerとStateを分割することで、コードの可読性や保守性を向上させることができる StateのactionPublisherを他のStateにバインドすることで、あるViewから他のViewにアクションを送ることが可能 flowchart TD subgraph ViewStore id2 -- action --> id1 id1(ActionHandler1) -- update --> id2(State1) id5 -- action --> id4 id4(ActionHandler2) -- update --> id5(State2) id8 -- action --> id7 id7(ActionHandler3) -- update --> id8(State3) end subgraph Parent View id3 id6 id9 end id2 -- bind --> id3(View1) id5 -- bind --> id6(View2) id8 -- bind --> id9(View3) おわりに 今回の取り組みは、機能開発と並行して1年以上かけて進めてきました。 現在では、ほぼすべてのソースコードが3rd Generation Architectureに置き換わっています。 その結果、コードの可読性や保守性が向上し、今後の開発が非常にやりやすくなったと感じています。 引き続き、さらなる改善を重ねていければと思います!
この記事は KINTOテクノロジーズアドベントカレンダー2024 の8日目の記事です🎅🎄 こんにちは、学びの道の駅チームのHOKAです。 学びの道の駅が発足されてからそろそろ1年が経過するので、ちょっとした振り返りBlogを書いてみます。 一年前に考えていたこと 学びの道の駅チームはちょうど1年前の2023年11月末に、きんちゃん、中西、HOKAの3人が集まって「このたくさんの勉強会をもっと盛り上げていきたい」というところからスタートしました。 そこで、私たちは2024年の年明けすぐに集まり、インセプションデッキを作りました。 その結果がこちら↓ 社内の「勉強会」と「勉強会」が交わる「道の駅」として、勉強会を軸にした社内活性を支援します。 社内広報活動 今度、こういうテーマで勉強会やるよ! 気になるあの勉強会、どんな感じなんだろう? 勉強会の支援 勉強会を始めてみたいけど、どうやって始めると良いのか? 勉強会の運営しているけど、盛り上がらない… などの、お悩み相談 詳しくは「 はじまりのテックブログ 」に記載しております。 はじめの6か月でやったこと まず、月に一度開催されるKTCの全社MTG(通称:本部会)で学びの道の駅の活動を毎月報告しました。 おそらく、下記のスライドは社内でお馴染みになったかと思います。 活動サマリー 勉強会全般の相談​:勉強会を始めたい!相談したい!等「勉強会に関するご相談」を承る場​ 突撃!​となりの勉強会:事務局が「みなさんの勉強会」を見学する活動​ KTC Podcast​:勉強会の運営者にインタビュー!音声で空気感ごと社内にお届け​ TechBlog​:TechBlogで「KTCにこんな勉強会あるよ、参加したよ」を伝えます​ 新たな活動 ーその1ー 6か月を過ぎた頃から、私たちの活動に賛同してくれた方が現れ始めました。 「どんな勉強会があるかを検索できるようにしたい」という課題を抱えていたのですが、モバイルアプリ開発グループのエンジニアがSlackを活用した検索システムを作ってくれました。 これにより、勉強会検索方法は、Slackチャンネルでキャラクターまなびぃにメンションすると、勉強会を見つけられることになったのです。 詳しくは開発者のBlogをご覧ください https://blog.kinto-technologies.com/posts/2024-12-04_manabyi/ 新たな活動 ーその2ー 「勉強会の資料や動画を誰でもいつでも閲覧できる状態にしたい」という課題もありました。 そんな中、コーポレートITグループのエンジニアが自ら名乗り出て、Sharepointを活用した勉強会のポータルサイトを作ってくれました。 その名も「学びの道の駅 Portal」 無味乾燥だったフォルダが、まるでYouTubeのようになりました。 動画や資料がそろったことにより、 「当日参加できなくなってしまった勉強会、資料だけでも見ておこう」 「先日の勉強会を復習したいな、動画を見るか」 といったムーブメントができるようになりました。 その他、うれしかったこと 勉強会を運営中の方から相談の依頼が来るようになったこと。 Podcastの出演依頼をすると、皆さん快諾してくれること。 全社イベントのポスターに「学びの道の駅」という言葉が使われていたこと。 グループ会社の社内向け資料に、KTCを紹介する文脈で「学びの道の駅」が紹介されたこと。 まだ1年も経っていないのに、社内だけでなくグループ会社まで私たちの活動が届くようになるとは、1年前の自分たちでは想像さえしておりませんでした。 ふとした時に、社員の方から「道の駅、めっちゃ良いね!」と言ってもらえるのも大きな励みとなっています。 技術広報グループにジョイン そんな学びの道の駅チームですが、2024年9月から技術広報グループの傘下に入ることになりました。 技術広報グループとは 2022年にKINTOテクノロジーズ TechBlogを立ち上げ、社外に向けたイベントや、社内の勉強会の開催などエンジニアがアウトプットをする場づくりをしてきました。そもそも業務で成果を出すこともアウトプットですが、勉強会やTechBlogもエンジニアにとっては重要なアウトプットの場だと私たちは考えております。技術広報グループでは「業務と業務の間」とも言えるアウトプットの場づくりをしてきました。 2023年末に私たちの始めた「学びの道の駅」は、「業務と業務の間」に必要とされる学びの場=インプットの場を作っており、技術広報グループと非常に近い存在だったのです。 ※そもそも技術広報グループ発起人の中西は、「インプットからアウトプットまで下支えすることでエンジニアの成長を促す」という構想だったので、別々のものではなかったのかもしれません。 今後の学びの道の駅チームは ぜひ中西によるTechBlogをご覧いただければ幸いです。 https://blog.kinto-technologies.com/posts/2024-12-03-the-next-goal/
This article is part of day 12 of KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello. Yamayuki here, from the Producer Team in KINTO Technologies’ Mobile Development Group.  Our company offers many opportunities to explore the use generative AI.  While I am not an engineer, I took the initiative to experiment with generative AI to develop an operational tool for business-side staff. This tool simplifies the creation of HTML announcements. When I share this with others, they often get the idea that I pulled off the amazing feat of building generative AI into it, but that is not the case. What I actually did was use generative AI to help develop it. This article is about that process. Even non-engineers can develop solutions to improve work! Having experienced this firsthand, I wanted to share it as a concrete example. Background: Why I Decided to Do It For the apps I am responsible for, tapping a push notification opens the announcement details page. This page is a web view that displays announcements by uploading an HTML file to the web. The operation looks as follows: (1) Business-side staff submit an Excel or Word manuscript of the content they want to display. (2) Someone on the development side codes the HTML file. (3) The business-side staff review the HTML file. (4) Someone on the development side makes corrections (Steps 3 and 4 were repeat in some cases.) (5) The content is reflected on the web.  Manuscripts were submitted at a pace of approximately two per week. A challenge with this process was that the business-side staff submitting the manuscripts did not have the final version ready for review, leading to repeated requests for corrections. Additionally, the frequent need for "a bit of coding" to handle submissions and corrections became a significant burden for the development team as well. Overall, the process was far from efficient or smooth.  We wanted to solve these issues! But at the same time, we were unable to do any drastic changes like introducing a CMS. That is when I decided to leverage generative AI to develop the operational tool I want to discuss in this article: a tool designed to create HTML for announcements.  - Issues we wanted to solve (Summary) Allow manuscript submitters to have the finalized version available for review during submission (eliminating the need for corrections) and remove the necessity for HTML coding (reducing the workload). - Why neither using Excel/Word’s HTML file output nor relying on generative AI to create HTML files during manuscript submission provided an effective solution While these methods can generate HTML files, the requirements in this case went beyond simple text. The HTML needed to include complex elements such as in-app buttons, embedded YouTube videos, and intricately designed sections for specific topics. Additionally, it was essential to produce a high-quality HTML file in a single step, allowing staff with no web expertise to have the finalized version readily available for review. As a result, I concluded that relying on these methods would complicate the operation. Step 1: Define the Minimum Features Required The first thing I did was write down what the minimum features it would require were. I could have written a virtually never-ending want-list, but since I am a non-engineer, I decided to aim for an MVP to ensure that developing the tool would not end up as just a dream. - The minimum required features I wrote down (1) You can input content via a form and get it output as an HTML file. (2) The required input fields are the title, date, headings, etc. (3) Users can add or delete input fields at will. (4) The outputted HTML file reflects the specified design. (5) The outputted HTML file reflects the specified measurement parameters. Step 2: Write a Prompt Based on the minimum required functions I wrote down in step 1, I wrote the following prompt to give instructions to the generative AI. I used Copilot, but I doubt it really matters which one I had used (notable alternatives being ChatGPT and Gemini). - The prompt I wrote Please take announcement content inputted by the user, and create a web page of it that can be downloaded as an HTML file. # Required input fields Title Date Headings Subheadings Paragraphs Images Buttons YouTube embedding tags # Instructions (1) Content inputted via a form can be outputted as an HTML file. (2) See above for the required input fields. (3) Users can add or delete any number of input fields at will. (4) The specified design is reflected in the outputted HTML file. (5) The specified measurement parameters are reflected in the outputted HTML file. # Prescribed design (CSS) https://... # Measurement parameters ... Step 3: Make Adjustments By inputting the prompt from step 2 into the generative AI should produce output that is more than adequate for a first attempt. Based on that, you then adjust the parts that are a slightly off or could use a little tweaking, engaging with the generative AI as you go. Example Please improve the previous form based on the instructions below. # Instructions Make it so that instead of having to enter a URL into the image input fields, you can upload the images right there and then. Preferably, anyone should be able to include images without having to go through a difficult procedure. - The key point when making adjustments When making adjustments, I suggest providing instructions to focus on one improvement at a time. For instance, if you give instructions like, "Images should..., button behavior should..., and input fields should...," you’re asking generative AI to handle multiple improvements at once. If it makes a mistake, it can be challenging to pinpoint where things went wrong. For the same reason, I also recommend maintaining a version-numbered history of the generated code and prompts. Make adjustments gradually, engaging in a step-by-step conversation as you go! things little by little, talking to it as you go! - Through persistent fine-tuning, you can achieve results that surpass your original expectations! In my case, this adjustment process not only allowed me to achieve the minimum required features but also successfully generate and incorporate the following additional ones. Preview the inputted content. Let the order of the input fields be changed later. Import and edit previously outputted HTML files. If an image is excessively large, display an error message and provide instructions on how to resize it - Adjust and polish up the design. Finally, I adjusted the CSS to make it all look nicer. You can adjust the CSS by reviewing the results and providing feedback to the generative AI as you go. However, if that feels cumbersome, you might prefer to provide instructions to the generative AI using a prompt like this: Example What kinds of designs make for easy-to-use general input forms? Okay, please reflect those designs in the previous form. - Finished example Here is the operation tool that I actually developed using generative AI! https://www.youtube.com/watch?v=F-eyKyS8HSo Conclusion Developing this operational tool successfully reduced the initial five-step process to just two. Before: (1) Business-side staff submit an Excel or Word manuscript of the content they want to display. (2) Someone on the development side does codes the HTML file. (3) The business-side staff check the HTML file. (4) Someone on the development side makes corrections (Steps 3 and 4 were often repeated.) (5) The content is reflected on the web. ↓ After: (1) Business-side staff submit an HTML manuscript of the content they want to display. (2) The content is reflected on the web. Additionally, I received positive feedback from the business side, noting that submitting manuscripts had become much easier. Both the business and development teams were pleased, making it a win-win improvement for everyone involved! What particularly stood out to me was when the back-end engineers on the same project pointed out that, in the past, it would have taken about two weeks and a significant amount of money to develop the same functionality manuall. I’m amazed that, as a non-engineer, I was able to independently develop something so valuable. Generative AI can feel intimidating, but letting fear hold me back won’t help, so I’m determined to make the most of it! This experience truly made me reconsider my approach to generative AI in a serious and thoughtful way. This applies to everything, but I believe the key is to keep trying lol. Thank you for reading all the way to the end!
この記事は KINTOテクノロジーズアドベントカレンダー2024 の12日目の記事です🎅🎄 はじめに こんにちは。KINTOテクノロジーズ モバイル開発グループ プロデューサーチームの やまゆき です。  弊社では生成AIの使い方を学ぶ機会が、たくさん提供されています。  私はエンジニアではありませんが、今回、生成AIでビジネスサイド向けの運用ツール「お知らせHTML作成ツール」を開発してみました。 この話をすると、よく「え!?生成AIを組み込んだの!?」と勘違いをされるのですが、 そうではなく「生成AIを道具として使って開発した」というのがこの記事の内容です。  非エンジニアでも、自分で開発して業務を改善できる!そのことを実感したので、事例としてご紹介したいと思います。 背景:やろうと思った理由 私の担当アプリでは、PUSH通知をタップすると「お知らせの詳細ページ」が開きます。このページはWeb Viewになっており、HTMLファイルをWebにアップロードすることでお知らせを表示できます。 そのため、次のようなステップで運用されていました。 (1)ビジネスサイドから、表示したい内容をExcel・Word形式で入稿 (2)開発サイドの誰かがコーディングしてHTMLファイルを作成 (3)ビジネスサイドでHTMLファイルをチェック (4)開発サイドの誰かが修正 [場合によって(3)(4)をくり返し] (5)Web反映  入稿は、週に1〜2回のペース。 入稿元のビジネスサイドでは、入稿時に手元で完成形を確認できないため、何度も修正依頼をせざるを得ないという課題がありました。また、開発サイドとしても、修正を含め ”ちょっとしたコーディング” が頻発することで業務が圧迫され、スムーズな運用とは言い難い状況でした。  これらの課題を解決したい!ただ、CMS導入など大掛かりなことはできない・・・。 そこで、生成AIを使って今回開発した運用ツール「お知らせHTML作成ツール」を作るに至りました。  - 解決したかった課題(まとめ) 入稿時に手元で完成形を確認できるようにする(=修正を無くす) HTMLコーディング作業を無くす(=業務圧迫の改善) - 「Excel・WordのHTMLファイル出力」や「入稿時に生成AIでHTMLファイル化」ではダメだった理由 これらの方法でもHTMLファイルを作成できますが、ここで必要とされるHTMLファイルは単純な文章だけでなく、アプリ内のボタンやYouTubeの埋め込み動画、デザインが施されたトピックエリアなど、複雑な内容を含んでいました。また、Webの知識がない担当者でも手元で完成形を確認できるように、一発で高い品質のHTMLファイルを作成する必要があったため、これらの方法では運用が難しいと判断しました。 手順1:必要最小限の機能を定義する まずやったことは、必要最小限の機能は何か?を書き出すことです。あれやこれや理想を言い出すとキリがないものですが、 非エンジニアの自分が一人で開発するのですから、ツール開発を夢で終わらせないためMVP開発を目指すことにしました。 - 私が書き出した必要最小限の機能 (1)フォーム形式で入力した内容を、HTMLファイルとして出力できる (2)必要な入力欄は「タイトル・日付・見出しetc…」 (3)入力欄はユーザーの任意で、いくつでも追加・削除できる (4)出力するHTMLファイルには、指定のデザインを反映する (5)出力するHTMLファイルには、指定の計測パラメーターを反映する 手順2:プロンプトを書く 手順1で書き出した必要最小限の機能をもとに、以下のようなプロンプトを書いて生成AIへ指示を出しました。 私はCopilotを使いましたが、ChatGPT・Geminiなど、何を使っても問題ないと思います。 - 私が書いたプロンプト ユーザーが入力した"お知らせ"の内容を、 HTMLファイルとしてダウンロードできるWEBページを作成してください。 # 必要な入力欄 タイトル 日付 見出し サブ見出し 段落 画像 ボタン Youtube埋め込みタグ # 指示 (1)フォーム形式で入力した内容を、HTMLファイルとして出力できる (2)必要な入力欄は上記を参照 (3)入力欄はユーザーの任意で、いくつでも追加・削除できる (4)出力するHTMLファイルには、指定のデザインを反映する (5)出力するHTMLファイルには、指定の計測パラメーターを反映する # 規定のデザイン(CSS) https://〜 # 計測パラメーター 〜〜〜 手順3:調整する 手順2のプロンプトを生成AIへ入力すると、初手としては十分すぎるアウトプットが返ってくるはずです。それを元に、何だかちょっと違うな?あと少しこうだったら、という部分を生成AIと会話しながら調整します。 例 先ほどのフォームを、以下の指示を元に改善してください。 # 指示 画像の入力欄はURLではなく、その場でアップロードできるようにしてください。 難しい手順は踏まず、誰でも画像を反映できる状態が望ましいです。 - 調整する時のポイント 調整時は、改善点を1つずつ指示して進めることをおすすめします。例えば、「画像は〜」「ボタンの動作は〜」「入力欄は〜」と複数の改善点を一気に伝えてしまうと、生成が失敗した際に、どこで躓いたか分かりにくくなるためです。また、同じ理由で、生成されたコードやプロンプトの履歴は、バージョンを付けて残しておくことをおすすめします。 会話をしながら、少しづつ調整していきましょう! - 粘り強く調整すれば、想像以上のものを生成できる! 私の場合、この調整手順で必要最小限の機能を実現したのはもちろんですが、最終的には以下のような機能まで生成し、組み込むことに成功しました。 入力内容をプレビューする 入力欄の順番を後から入れ替えられるようにする 過去に出力したHTMLファイルを読み込んで編集する 画像サイズが極端に大きい場合にエラーを出し、リサイズ方法を案内する - デザインを調整して仕上げる 最後に見栄えがよくなるようCSSを調整しました。CSSは調べたり、これもまた生成AIに聞いたりして調整が可能ですが、面倒な方は以下のようなプロンプトで生成AIに指示してもいいかもしれません。 例 一般的な入力フォームとして使いやすいデザインとは、どのようなデザインですか? それでは、それらのデザインを先ほどのフォームに反映してください。 - 完成例 こちらが、実際に私が生成AIを使って開発した運用ツールです! https://www.youtube.com/watch?v=F-eyKyS8HSo さいごに この運用ツール開発によって、当初あった5ステップを2ステップに削減することに成功しました。 (1)ビジネスサイドから、表示したい内容をExcel・Word形式で入稿 (2)開発サイドの誰かがコーディングしてHTMLファイルを作成 (3)ビジネスサイドでHTMLファイルをチェック (4)開発サイドの誰かが修正 (場合によって(3)(4)をくり返し) (5)Web反映 ↓ (1)ビジネスサイドから、表示したい内容をHTMLファイルで入稿 (2)Web反映 また、ビジネスサイドからは入稿が楽になったと嬉しい声をもらうことができ、ビジネスサイドも開発サイドも嬉しい、WIN-WINな業務改善となりました! 私個人としては、同じプロジェクトのバックエンドエンジニアに「昔ならこれと同じものを人の手で開発するのに、2週間ほどの時間とお金がもらえたのに…」と嘆かれたことが印象的でした。エンジニアではない私が、それだけの価値あるものを一人で開発できたことに驚いています。 生成AIには怖さすら感じることがありますが、怖がっていても何もならないので、たくさん活用したい!そう強く思い直すきっかけになりました。 何事も同じですが、コツは何度もチャレンジすることだと思います(笑)最後までご覧いただき、ありがとうございました。