- 投稿日:2020-02-22T23:35:30+09:00
「.swiftlint.yml」のルールが機能しなくハマったこと
.swiftlint.ymlに
line_length
のルールを記載したのに反映されないことがありハマりました。
AppDelegateなどでコメントのwarningがでていたのでLintに以下のルールを追加しましたが、反映されませんでした。.swiftlint.ymlline_length: warning: 300 ignores_comments: trueSwiftLintのバージョンが古いとか、RULEに半角スペースが3つあるとかでも反映されないような現象があるみたいですが、
私の場合は違いました。試したこと
- Homebrewをupgradeしてswiftlintの入れ直し。
- YAMLが正しく書いているか確認。
- YAMLlint - The YAML Validator http://www.yamllint.com/
- swiftlintのupgrade / swiftlintのuninstall
結論
.swiftlint.yml
はxxx.xcodeproj
と同じ階層にないと反映されない。
私の場合、githubからcloneしてきたので以下のディレクトリの構成になっていました。beforeSampleProject ├── SampleProject │ ├── SampleProject/... │ ├── SampleProject.xcodeproj │ └── SampleProjectTests ├── .swiftlint.yml ├── README.md ├── .gitignore └── .git以下のようにルートディレクトリに
xxx.xcode.proj
と.swiftlint.yml
を同じ階層に置けばルールが適応されました。afterSampleProject ├── SampleProject/... ├── SampleProject.xcodeproj ├── SampleProjectTests ├── .swiftlint.yml ├── README.md ├── .gitignore └── .gitcloneしてからプロジェクトファイルを作るとプロジェクト名のディレクトリがダブるので、
プロジェクトファイルを作成してから、git clone http://xxxxxx.com/xxxxxx.git .
をした方がよさそうと思った。
最後の.
はリポジトリと同名のディレクトリを作らないためにつける。参考
How to use swiftlint command and Xcode Script when .swiftlint.yml file not in the same folder with project.xcodeproj? · Issue #2673 · realm/SwiftLint
https://github.com/realm/SwiftLint/issues/2673git clone でディレクトリを作らない | 株式会社Orfool
https://orfool.com/programing/2206/SwiftLintを試してみた - Qiita
https://qiita.com/ushisantoasobu/items/b494c9cf7d78a968b373SwiftLintのdisabled_rules一覧(実例付き) - Qiita
https://qiita.com/akatsuki174/items/6da27f7ccdd0572f8927SwiftLintの運用ノウハウ - Qiita
https://qiita.com/shtnkgm/items/6dd756aa14926736c6f5
- 投稿日:2020-02-22T23:17:10+09:00
Flutter Form系Widgetの使い方 〜 すべてのWidgetを学習
はじめに
自身の勉強を兼ねて、
Widget Catalog
で公開されている全てのWidgetについての解説やソースコードを交えた使い方などをまとめています。基本方針として、掲載しているサンプルコードは公式サイトのサンプルを流用しています。しかし、公式サイトにはサンプルコードがないものが多くあるため、その場合には動作確認済みの自身のコードを掲載しています。間違い等ありましたらご指摘頂けると助かります。
カテゴリ Widgetの種類 Qiitaまとめ記事 Accesibility アクセシビリティに関するWidget Coming Soon Animation and Motion アニメーションに関するWidget Coming Soon Assets, Images, and Icons Asset/Icon/画像表示に関するWidget Coming Soon Async Asyncパターン (FutureBuilder, StreamBuilder) Coming Soon Basics 最低限抑えるべき基本的なWidget Coming Soon Cupertino (iOS-style widgets) iOS風の各種Widget Coming Soon Input フォームやキー入力等の入力に関するWidget 今回 Interaction Models タッチイベントやその他操作の応答に関するWidget Coming Soon Layout レイアウトに関するWidget Coming Soon Material Components Android マテリアルデザインの各種Widget Coming Soon Painting and effects ペイント, エフェクト効果等のWidget Coming Soon Scrolling GridView等のスクロール機能を保有するようなWidget Coming Soon Styling テーマ等のスタイリングに関わるWidget Coming Soon Text テキスト表示に関わるWidget Flutter Text Widgetの使い方 〜 すべてのWidgetを学習 Form系 Widgets
テキスト入力用途のフォームやキー入力検出に関する以下のWidgetについて解説します。
Widget名 用途 Form FormField用のコンテナWidgetで複数のFormFieldをまとめて管理する時に利用します。 TextFormField テキスト入力のFormField Widgetです。 FormField Form内で利用するベースとなるWidgetです。これを直接利用することはなく、TextFormFieldのように、FormFieldを継承して独自のFormFieldを作成する時に利用します。 RawKeyboardListener キー入力検出用のリスナーです。 Form
後述の複数の
FormFiled
Widgetをグループ化して管理するためのコンテナWidgetです。グループ化することで、ユーザが入力データの送信ボタンを押した時に、フォーム内の各入力データの形式チェックするValidator
を一括して呼び出すことが出来ます。Formクラスconst Form({ Key key, @required this.child, this.autovalidate = false, this.onWillPop, this.onChanged, }) : assert(child != null), super(key: key);
Form
は子クラス (後述のTextFormFiled
など) の最上位のコンテナとして定義します。なお、
Validator
を利用する場合には、第一引数のKey
にGlobalKey<FormState>
を指定する必要があります。このKey
については別の記事にて解説予定のため、ここでは以下のサンプルコードのように必要なものだレベルで覚えてるに留めておいてください。TextFormField
別途解説予定の
TextField
Widgetを後述のFormField
に適用したWidgetです。つまり、Form
におけるText入力のWidgetです。サンプルコード
サンプルとして、2つのテキスト入力を持ったフォームの例を示します。Submitボタンを押した時に各テキスト入力をチェックし、空白以外ならOKというロジックになっています。
サンプルコードclass FormSampleState extends State<WidgetSample> { final _formKey = GlobalKey<FormState>(); @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ TextFormField( decoration: const InputDecoration( labelText: "Email address form", // ラベル hintText: 'Enter your email', // 入力ヒント ), autovalidate: false, // 入力変化しても自動でチェックしない。trueにすると初期状態および入力が変化する毎に自動でvalidatorがコールされる validator: (value) { // _formKey.currentState.validate()でコールされる if (value.isEmpty) { return 'Please enter some text'; // エラー表示のメッセージを返す } return null; // 問題ない場合はnullを返す }, onSaved: (value) => () { // this._formKey.currentState.save()でコールされる print('$value'); }, ), TextFormField( decoration: const InputDecoration( icon: Icon(Icons.email), border: OutlineInputBorder(), // 外枠付きデザイン filled: true, // fillColorで指定した色で塗り潰し fillColor: Colors.greenAccent, labelText: "Email address form", hintText: 'Enter your email', ), autovalidate: false, validator: (value) { if (value.isEmpty) { return 'Please enter some text'; } return null; }, ), Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: RaisedButton( onPressed: () { // 各Fieldのvalidatorを呼び出す if (_formKey.currentState.validate()) { // 入力データが正常な場合の処理 this._formKey.currentState.save(); } }, child: Text('Submit'), ), ), ], ), ); } }FormField
FormField
はForm
内で利用するベースとなるWidgetです。後述のTextFormField
もこれを継承しており、その他Form
内で利用する独自定義Widgetを作成するなどに利用します。サンプル
サンプルとして、独自定義Widgetを利用したフォーム入力の例を示します。初期値0のカウンタを+/-ボタンで増減するフォームを作成しています。Submitボタンを押した時に-1以外であればResult表示を更新し、それ以外はFieldにエラーメッセージが表示されます。
サンプルコードclass FormFieldSampleState extends State<WidgetSample> { final _formKey = GlobalKey<FormState>(); int _count = 0; @override Widget build(BuildContext context) { return Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ CounterFormField( // 独自定義のFormField autovalidate: false, validator: (value) { if (value < 0) return 'Negative values not supported'; return null; }, onSaved: (value) => setState(() { _count = value; }), ), FlatButton( child: Text('Submit'), color: Colors.blue, onPressed: () { if (this._formKey.currentState.validate()) { this._formKey.currentState.save(); } }, ), SizedBox(height: 20.0), Text('Result = $_count') ])); } } // int型のデータを保有する独自FormLieldクラスを作成 class CounterFormField extends FormField<int> { CounterFormField( {FormFieldSetter<int> onSaved, FormFieldValidator<int> validator, int initialValue = 0, bool autovalidate = false}) : super( onSaved: onSaved, validator: validator, initialValue: initialValue, autovalidate: autovalidate, builder: (FormFieldState<int> state) { return Column(children: <Widget>[ Row( mainAxisSize: MainAxisSize.min, children: <Widget>[ IconButton( icon: Icon(Icons.remove), onPressed: () { // Fieldの値を変化させる state.didChange(state.value - 1); }, ), Text(state.value.toString()), IconButton( icon: Icon(Icons.add), onPressed: () { state.didChange(state.value + 1); }, ), ], ), state.hasError // Validatorの結果がエラー時の表示をここで対応 ? Text( state.errorText, style: TextStyle(color: Colors.red), ) : Container(), ]); }); }RawKeyboardListener
キーボード入力イベントをキャッチするためのWidgetです。iOS (かつエミュレータのみ?) 以外は問題なく動いてそうですが、iOS (かつエミュレータのみ?) だと反応が無いです。2016年の時点でissueが出ていますね。。
RawKeyboardListener doesn't receive any events from keyboard in ios simulatorサンプル
サンプルコードclass RawKeyboardListenerSampleState extends State<WidgetSample> { final FocusNode _focusNode = FocusNode(); String _message; @override void dispose() { _focusNode.dispose(); super.dispose(); } void _handleKeyEvent(RawKeyEvent event) { setState(() { _message = 'Key: ${event.logicalKey.debugName}, KeyId: ${event.logicalKey.keyId}'; }); } @override Widget build(BuildContext context) { return RawKeyboardListener( focusNode: _focusNode, onKey: _handleKeyEvent, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ TextField(), Text(_message ?? 'Press a key'), ])); } }
- 投稿日:2020-02-22T19:48:11+09:00
iOS と Android で Opus 形式のオーディオを再生しようとしたらコンテナの違いにはまった話し
Android と iOS で Opus を扱うにあたって チョットワカッタ -> 完全に理解した(慢心)を20ループくらいしたので知見をメモします。
やりたかったこと
.wav
(の中のPCM)音源を品質と圧縮率のバランス良い形のフォーマットに変換して、Android (android.media.MediaPlayer
) と iOS (AVKit
) からよしなに再生したい。オーディオファイルのフォーマットとしては Opus が良さそうでこれに目を付ける。結論
Android 向けには Matroskaコンテナ、iOS 向けには CAFコンテナの中に
libopus
で圧縮した Opus 形式の音声データを格納すればよい。1ファイルで両プラットフォーム対応は無理なので諦めよう。FFmpeg を使う場合以下のようになる。
# for Android ffmpeg -i input.wav -vn -ac 2 -c:a libopus -b:a 24k output.mkv # for iOS ffmpeg -i input.wav -vn -ac 2 -c:a libopus -b:a 24k output.cafinput.wav が 140MB 程度で、エンコードしたあとは概ね 1MB程度。(上記コマンドのとおり ビットレートを 24k に指定した場合)
何が大変だったのか
Android は公式に Opus コーデック(のエンコード)をサポートしているが1、コンテナが適切でないと再生はできるけどシークが適切に行えない、などの問題があった。ローカルファイルだと行えてストリーミングだと無理、などの場合もあり問題の切り分けに苦労しつつ試行錯誤の末コンテナを Matroska にする必要がありそうなことに気付く(というか公式ドキュメントに書いてあった)。この間 .ogg, .oga, libopus, libvorbis などの組み合わせを無限に総当たりで試し続ける。
iOS は iOS11 から Opus コーデックをサポートするという話しがあったが結局どうなってしまったのかよくわかっていない。Androidでいけた
.mkv
ファイルがうまく再生できず詰んだ!ここは旧世代の遺物AACか〜 からの、afconvert
コマンドで適当に.caf
ファイルを作ってみたらなぜか再生できることに気づき、コンテナを CAF にする必要がありそうなことに気付く。ちなみに Opus と CAF については Wikipedia - Opus を見ると
(Limited container support)
という表記とともにOn iOS 11:Core Audio Format (.caf)
と書いてあるのでつまりそういうことなんだろう。ということはこの記事を書いていて気付きました。アウトプット大事...。何で Opus ?
音楽ファイルが96kbpsになる日――Opus音声コーデックの実力 - Qiita とか、YouTube で Opus vs MP3 で検索した結果 とかを参照すると良いと思います。感覚的な表現をすると、低ビットレートなMP3における音のこもり感が劇的に解消されている感じ。劇的に。これにより高品質で高圧縮なオーディオファイルをつくることができる。特に最新の技術ではない。
- 投稿日:2020-02-22T19:01:09+09:00
AirDropは使えるのにContinuityが使えない時の解決策
問題
AirDrop
は使えるのに、Continuity
が使えない。
Continuity
とは、iOSとmacOSの連携機能のこと。
詳しくはこちら。
https://support.apple.com/ja-jp/HT204681AppleIDのログインし直し、再起動、何度試しても解決しない。
ハードウェア的に対応していないわけではない。……ということがあったので、自分の場合の解決策を載せます。
解決策
Wi-FiとEthernetの接続を片方ずつにしてみる。
据置PCだったのでWi-FiとEthernet両方に繋いでいたのですが、Ethernetの方に問題があった模様。
一旦Ethernetを無効にしてWi-Fiのみにしたら、見事Continuityが使えるようになりました。一度アクティベートされると、繋ぎ直しても問題なさそうで、自分は優先順位をWi-Fi > Ethernetにしていますが、今のところ問題ないです。
というだけのお話
実はこれ、Appleサポートの方に教えていただきました。
Continuityは2段階認証やら無線モジュールやらの制約が厳しいので原因が分かりづらいのですが、まさかネットワークの問題だったとは。
どこのサイトにも載っていなかったので、忘備録として記事にしました。……にしても最近、技術的な記事書いてないなぁ。。。
- 投稿日:2020-02-22T18:52:47+09:00
[Apple] 写経 Start Developing iOS Apps (Swift)
Apple公式ドキュメント Start Developing iOS Apps (Swift) の一部を写経します。
appデリゲートソースファイル
AppDelegate.swift
には、2つの主要な関数があります。
AppDelegate
クラスを定義します。appデリゲートは、コンテンツが描画されるウィンドウを作成し、アプリケーション内の状態遷移を受け付けるための場所を提供します。- アプリケーションのエントリポイントと、入力イベントをアプリケーションへ渡す実行ループを作成します。これはファイル先頭にある
UIApplicationMain
属性(@UIApplicationMain
)によって行われます。UIApplicationMain
属性は、デリゲートクラスとしてAppDelegate
クラスをUIApplicationMain
へ渡すことと同義です。戻り値の中で、システムはアプリケーションオブジェクトを作成します。アプリケーションオブジェクトはアプリケーションのライフサイクルを管理する責任を負います。システムはAppDelegate
クラスのインスタンスもまた作成しアプリケーションオブジェクトへ割り当てます。最終的にシステムはアプリケーションを起動します。わかったこと:
アプリケーションの実行時に、アプリケーションオブジェクトとappデリゲートが作成される。
UIApplicationMain
属性をつけることで、アプリケーションのエントリポイントとappデリゲートであることをシステムに認識させる。
AppDelegate
クラスはプロパティを一つ含みます。var window: UIWindow?このプロパティはアプリケーションのウィンドウへの参照を格納しています。ウィンドウはアプリケーションのビュー階層の根元です。コンテンツはこの中に描画されます。このwindowプロパティはオプショナルであることに注意が必要です。ある時点では値が入っていません(nilです)。
AppDelegate
クラスは以下のメソッドのスタブ実装も含んでいます。func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool func applicationWillResignActive(_ application: UIApplication) func applicationDidEnterBackground(_ application: UIApplication) func applicationWillEnterForeground(_ application: UIApplication) func applicationDidBecomeActive(_ application: UIApplication) func applicationWillTerminate(_ application: UIApplication)これらのメソッドは、アプリケーションオブジェクトにappデリゲートとやりとりさせるためのものです。
いずれのデリゲートメソッドも既定の振る舞いを持っています。メソッドが空か存在しないと、既定の振る舞いをします。
わかったこと:
AppDelegate
プロトコルを実装することでアプリケーションオブジェクトとやりとりする。
AppDelegate
プロトコルの中身を実装しないと、既定の振る舞いをする。
- 投稿日:2020-02-22T16:31:28+09:00
Amazon TranscribeとSwiftから連携してみた
Amazon Transcribeとは
音声ファイルから自動的に文字を起こしてくれるAmazonのサービスです。
https://aws.amazon.com/jp/transcribe/
類似のものに、Google CloudのSpeech-to-Textがあります。
https://cloud.google.com/speech-to-text?hl=ja今回説明しないもの
・AWSの設定の詳細
・S3へのアップロード、ダウンロードの詳細
・動画や音声の詳細準備
AWS
・Cognito
iOSアプリからAWSにアクセスするためのやつ。
・S3
Transcribeに使う音声ファイルやjobファイルを置くストレージ。
・Transcribe
本丸。Create Jobから特に困ることなく設定可能です。iOS
・AWS SDK for iOS
AWSS3とAWSTranscribeだけあれば大丈夫です。(AWSCoreもついてくるので。)おおまかな流れ
iOSから音声(動画)ファイルをS3にアップロード
-> iOSからTranscribeのjobを実行命令
-> AWSのTranscribeがjobを実行し、先ほどアップロードしたS3のファイルを文字化
-> Transcribeの結果取得
-> 変換結果のjsonファイルをS3からダウンロード実装
初期設定
SceneDelegate.swiftfunc scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // AWS Cognito & S3 & Transcribe registration let credentialProvider = AWSCognitoCredentialsProvider(regionType: .APNortheast1, identityPoolId: "ap-northeast-1:*******************") if let configuration = AWSServiceConfiguration(region:.APNortheast1, credentialsProvider:credentialProvider) { AWSS3TransferUtility.register(with:configuration!, forKey: "MY_S3") AWSTranscribe.register(with:configuration!, forKey: "MY_Transcribe") } }SceneDelegate(AppDelegate)のおなじみの起動ファンクションでAWS初期設定します。
Cognitoでregionと設定の際に発行されたIdentity Pool IDを設定します。
設定後、Transcribe、 AWSS3TransferUtilityへの登録も行っておきます。S3へ該当ファイルをアップロード
let bucketName = "mybucket" let mp4URL = URL(fileURLWithPath: "your/video.mp4") if let awss3 = AWSS3TransferUtility.s3TransferUtility(forKey: "MY_S3") { do { let videoData = try Data(contentsOf: mp4URL) let videoName = "s3video.mp4" awss3.uploadData( videoData, bucket: bucketName, key: videoName, contentType: "mp4", expression: nil, // 途中経過task nullable completionHandler: { task, error in if let error = error { print("s3 upload error \(error)") } else { print("success upload") } } catch let error { print("data convert error \(error)") } }今回はmp4ファイルで行います。Transcribeがフォローしているファイルタイプは、flac, mp3, mp4, wavの4タイプです。(2020/2/20現在)
録画なりして端末に入ってるmp4ファイルをData型で取得し、AWSS3TransferUtility.uploadDataでアップロードします。
ここでのbucketはCognitoで設定したものと同じでなくてはなりません。
expressionはアップロードの途中経過を取得できるtaskですが今回は省きます。
また、uploadData自体もTaskとして登録できるものもあるので使い分けてください。TranscribeのJobを実行
let awstrans = AWSTranscribe(forKey: "MY_Transcribe") let jobName = "MY_JOB" if let startRequest = AWSTranscribeStartTranscriptionJobRequest() { startRequest.languageCode = .jaJP // language code結構いっぱいある let media = AWSTranscribeMedia() media?.mediaFileUri = "https://s3-ap-northeast-1.amazonaws.com/\(bucketName)/\(videoName)" startRequest.media = media // 先ほどアップロードしたs3のファイルのurlを指定する startRequest.mediaFormat = .mp4 // flac, mp3, mp4, wav startRequest.mediaSampleRateHertz = 44100 // いらないかも startRequest.transcriptionJobName = jobName startRequest.outputBucketName = bucketName // Job実行 awstrans.startTranscriptionJob(startRequest, completionHandler: {response, error in if let error = error { print("start job error \(error)") } else { print("success start job") } } }AWSTranscribeStartTranscriptionJobRequestでJobのステータスを設定します。
ここで勘違いしていたのが、startTranscriptionJobのcompletionがJob完了時に呼ばれるものだと思ってましたが、これはあくまでJobがスタートした時に呼ばれるものでした。TranscribeのJobが完了するまで待って取得
// timer使うので DispatchQueue.main.async { self.timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true, block: { timer in if let getJobRequest = AWSTranscribeGetTranscriptionJobRequest() { getJobRequest.transcriptionJobName = projectId awstrans.getTranscriptionJob(getJobRequest, completionHandler: {response, error in if let error = error { print("get job error \(error)") self.timer.invalidate() } if let reason = response?.transcriptionJob?.failureReason { print("job failed \(reason)") } if response?.transcriptionJob?.transcriptionJobStatus == .completed { // 完了後、awsにアップロードされた、結果の記載されたjsonのuriが取得できる print(response?.transcriptionJob?.transcript?.transcriptFileUri) self.timer.invalidate() } }) } }) }探してみたところ、Jobの完了通知をしてくれるものは見当たらなかったので、取り急ぎTimerで完了するまでgetし続けるという原始的なことをしました。しかも、mp4ファイルだと3MBぐらいのサイズでも40秒とかかかったので、interval=10としました。
JobStatus=completeとなった段階で、transcriptionJobにいろいろな値がセットされて返却されるので、結果の記載されたjsonのURIを取得する。
余談ですが、ハンドラ内でTimerを実行する際は実行スレッドに注意。S3からTranscribeの結果jsonを取得
awss3.downloadData( fromBucket: bucketName, key: projectId + ".json", expression: nil, completionHandler:{task, location, data, error in if let error = error { print("s3 download error \(error)") } else { if let data = data { do { let jsonDecoder = JSONDecoder() let transcribeData = try jsonDecoder.decode(AmazonTranscribe.self, from: data) print(transcribeData.results.transcripts) } catch let error { print("decode error \(error)") } } } } )AmazonTranscribe.swiftstruct AmazonTranscribe: Decodable { let jobName:String let accountId:String let results: AmazonTranscribeResults let status: String } struct AmazonTranscribeResults: Decodable { let transcripts: [AmazonTranscribeTranscripts] let items: [AmazonTranscribeItem] } struct AmazonTranscribeTranscripts: Decodable { let transcript: String // 全文 } struct AmazonTranscribeItem: Decodable { let startTime: Double let endTime: Double let alternatives: [AmazonTranscribeAlternatives] let type: String private enum CodingKeys: String, CodingKey { case startTime = "start_time" case endTime = "end_time" case alternatives, type } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) guard let startTimeDouble = Double(try values.decode(String.self, forKey: .startTime)) else { fatalError("The start time is not an Double") } guard let endTimeDouble = Double(try values.decode(String.self, forKey: .endTime)) else { fatalError("The end time is not an Double") } startTime = startTimeDouble endTime = endTimeDouble alternatives = try values.decode([AmazonTranscribeAlternatives].self, forKey: .alternatives) type = try values.decode(String.self, forKey: .type) } } struct AmazonTranscribeAlternatives: Decodable { let confidence: Double let content: String private enum CodingKeys: String, CodingKey { case confidence, content } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) guard let confidenceDouble = Double(try values.decode(String.self, forKey: .confidence)) else { fatalError("The confidence is not an Double") } confidence = confidenceDouble content = try values.decode(String.self, forKey: .content) } }おまけでjsonをDecodeするのにつかったDecodableも載せておきます。返却値はこんな(AmazonTranscribe.swift)感じです。
各値の意味は深く調べてませんが、全文(なぜリスト?)と代替候補っぽいのはありました。感想
正直なところ、日本語の文字起こし精度はさほど高くないように感じられました。
ファイルの形式等を調整したらもうちょい精度あがりそうですが、
試しにGoogleHomeとの、「ねぇGoogle、明日の天気は?」「明日の新宿は最高気温15度、最低気温7度で晴れるでしょう」というやりとり動画を送信してみたら、
「めぐる 明日 の 天気 は 明日 の 新宿 は 最高 気温 十 五 度 再 激 音 など で 買える でしょ」
というアウトプットがきました。この場合の使える情報は、明日の新宿が最高気温15度ってことだけでしょう。
英語はしゃべれないし、発音も悪いので試してません。
- 投稿日:2020-02-22T16:31:28+09:00
Amazon TranscribeにSwiftアプリから連携してみた
Amazon Transcribeとは
音声ファイルから自動的に文字を起こしてくれるAmazonのサービスです。
https://aws.amazon.com/jp/transcribe/
類似のものに、Google CloudのSpeech-to-Textがあります。
https://cloud.google.com/speech-to-text?hl=ja今回説明しないもの
・AWSの設定の詳細
・S3へのアップロード、ダウンロードの詳細
・動画や音声の詳細準備
AWS
・Cognito
iOSアプリからAWSにアクセスするためのやつ。
・S3
Transcribeに使う音声ファイルやjobファイルを置くストレージ。
・Transcribe
本丸。Create Jobから特に困ることなく設定可能です。iOS
・AWS SDK for iOS
AWSS3とAWSTranscribeだけあれば大丈夫です。(AWSCoreもついてくるので。)おおまかな流れ
iOSから音声(動画)ファイルをS3にアップロード
-> iOSからTranscribeのjobを実行命令
-> AWSのTranscribeがjobを実行し、先ほどアップロードしたS3のファイルを文字化
-> Transcribeの結果取得
-> 変換結果のjsonファイルをS3からダウンロード実装
初期設定
SceneDelegate.swiftfunc scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // AWS Cognito & S3 & Transcribe registration let credentialProvider = AWSCognitoCredentialsProvider(regionType: .APNortheast1, identityPoolId: "ap-northeast-1:*******************") if let configuration = AWSServiceConfiguration(region:.APNortheast1, credentialsProvider:credentialProvider) { AWSS3TransferUtility.register(with:configuration!, forKey: "MY_S3") AWSTranscribe.register(with:configuration!, forKey: "MY_Transcribe") } }SceneDelegate(AppDelegate)のおなじみの起動ファンクションでAWS初期設定します。
Cognitoでregionと設定の際に発行されたIdentity Pool IDを設定します。
設定後、Transcribe、 AWSS3TransferUtilityへの登録も行っておきます。S3へ該当ファイルをアップロード
let bucketName = "mybucket" let mp4URL = URL(fileURLWithPath: "your/video.mp4") if let awss3 = AWSS3TransferUtility.s3TransferUtility(forKey: "MY_S3") { do { let videoData = try Data(contentsOf: mp4URL) let videoName = "s3video.mp4" awss3.uploadData( videoData, bucket: bucketName, key: videoName, contentType: "mp4", expression: nil, // 途中経過task nullable completionHandler: { task, error in if let error = error { print("s3 upload error \(error)") } else { print("success upload") } } catch let error { print("data convert error \(error)") } }今回はmp4ファイルで行います。Transcribeがフォローしているファイルタイプは、flac, mp3, mp4, wavの4タイプです。(2020/2/20現在)
録画なりして端末に入ってるmp4ファイルをData型で取得し、AWSS3TransferUtility.uploadDataでアップロードします。
ここでのbucketはCognitoで設定したものと同じでなくてはなりません。
expressionはアップロードの途中経過を取得できるtaskですが今回は省きます。
また、uploadData自体もTaskとして登録できるものもあるので使い分けてください。TranscribeのJobを実行
let awstrans = AWSTranscribe(forKey: "MY_Transcribe") let jobName = "MY_JOB" if let startRequest = AWSTranscribeStartTranscriptionJobRequest() { startRequest.languageCode = .jaJP // language code結構いっぱいある let media = AWSTranscribeMedia() media?.mediaFileUri = "https://s3-ap-northeast-1.amazonaws.com/\(bucketName)/\(videoName)" startRequest.media = media // 先ほどアップロードしたs3のファイルのurlを指定する startRequest.mediaFormat = .mp4 // flac, mp3, mp4, wav startRequest.mediaSampleRateHertz = 44100 // いらないかも startRequest.transcriptionJobName = jobName startRequest.outputBucketName = bucketName // Job実行 awstrans.startTranscriptionJob(startRequest, completionHandler: {response, error in if let error = error { print("start job error \(error)") } else { print("success start job") } } }AWSTranscribeStartTranscriptionJobRequestでJobのステータスを設定します。
ここで勘違いしていたのが、startTranscriptionJobのcompletionがJob完了時に呼ばれるものだと思ってましたが、これはあくまでJobがスタートした時に呼ばれるものでした。TranscribeのJobが完了するまで待って取得
// timer使うので DispatchQueue.main.async { self.timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: true, block: { timer in if let getJobRequest = AWSTranscribeGetTranscriptionJobRequest() { getJobRequest.transcriptionJobName = projectId awstrans.getTranscriptionJob(getJobRequest, completionHandler: {response, error in if let error = error { print("get job error \(error)") self.timer.invalidate() } if let reason = response?.transcriptionJob?.failureReason { print("job failed \(reason)") } if response?.transcriptionJob?.transcriptionJobStatus == .completed { // 完了後、awsにアップロードされた、結果の記載されたjsonのuriが取得できる print(response?.transcriptionJob?.transcript?.transcriptFileUri) self.timer.invalidate() } }) } }) }探してみたところ、Jobの完了通知をしてくれるものは見当たらなかったので、取り急ぎTimerで完了するまでgetし続けるという原始的なことをしました。しかも、mp4ファイルだと3MBぐらいのサイズでも40秒とかかかったので、interval=10としました。
JobStatus=completeとなった段階で、transcriptionJobにいろいろな値がセットされて返却されるので、結果の記載されたjsonのURIを取得する。
余談ですが、ハンドラ内でTimerを実行する際は実行スレッドに注意。S3からTranscribeの結果jsonを取得
awss3.downloadData( fromBucket: bucketName, key: projectId + ".json", expression: nil, completionHandler:{task, location, data, error in if let error = error { print("s3 download error \(error)") } else { if let data = data { do { let jsonDecoder = JSONDecoder() let transcribeData = try jsonDecoder.decode(AmazonTranscribe.self, from: data) print(transcribeData.results.transcripts) } catch let error { print("decode error \(error)") } } } } )AmazonTranscribe.swiftstruct AmazonTranscribe: Decodable { let jobName:String let accountId:String let results: AmazonTranscribeResults let status: String } struct AmazonTranscribeResults: Decodable { let transcripts: [AmazonTranscribeTranscripts] let items: [AmazonTranscribeItem] } struct AmazonTranscribeTranscripts: Decodable { let transcript: String // 全文 } struct AmazonTranscribeItem: Decodable { let startTime: Double let endTime: Double let alternatives: [AmazonTranscribeAlternatives] let type: String private enum CodingKeys: String, CodingKey { case startTime = "start_time" case endTime = "end_time" case alternatives, type } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) guard let startTimeDouble = Double(try values.decode(String.self, forKey: .startTime)) else { fatalError("The start time is not an Double") } guard let endTimeDouble = Double(try values.decode(String.self, forKey: .endTime)) else { fatalError("The end time is not an Double") } startTime = startTimeDouble endTime = endTimeDouble alternatives = try values.decode([AmazonTranscribeAlternatives].self, forKey: .alternatives) type = try values.decode(String.self, forKey: .type) } } struct AmazonTranscribeAlternatives: Decodable { let confidence: Double let content: String private enum CodingKeys: String, CodingKey { case confidence, content } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) guard let confidenceDouble = Double(try values.decode(String.self, forKey: .confidence)) else { fatalError("The confidence is not an Double") } confidence = confidenceDouble content = try values.decode(String.self, forKey: .content) } }おまけでjsonをDecodeするのにつかったDecodableも載せておきます。返却値はこんな(AmazonTranscribe.swift)感じです。
各値の意味は深く調べてませんが、全文(なぜリスト?)と代替候補っぽいのはありました。感想
正直なところ、日本語の文字起こし精度はさほど高くないように感じられました。
ファイルの形式等を調整したらもうちょい精度あがりそうですが、
試しにGoogleHomeとの、「ねぇGoogle、明日の天気は?」「明日の新宿は最高気温15度、最低気温7度で晴れるでしょう」というやりとり動画を送信してみたら、
「めぐる 明日 の 天気 は 明日 の 新宿 は 最高 気温 十 五 度 再 激 音 など で 買える でしょ」
というアウトプットがきました。この場合の使える情報は、明日の新宿が最高気温15度ってことだけでしょう。
英語はしゃべれないし、発音も悪いので試してません。
- 投稿日:2020-02-22T16:11:42+09:00
CombineのflatMapで実行数を制御する
この文章の結論として、例えばWeb APIによる通信処理でPOST/DELETEやファイルアクセスによる作成/削除などでflatMapを使う際に、引数として
.flatMap(maxPublishers: .max(1))
を利用し、バックプレッシャーでリクエスト数を1として利用するほうが良さそうなんじゃないか?という文章です。もし、「そりゃ違うよ」などのご意見があればコメントで頂けると幸いです。
はじめに
RxSwiftではflatMapLatestを使ったが、CombineではflatMapLatestがない。つまり連続するストリームのイベントに対してなるべく無駄なく新しいストリームを作りたい場合に、オペレータで制御する方法がないのかなと思っていました。
しかし、CombineのflatMapはパラメータに
maxPublishers: Subscribers.Demand
があり、リファレンスではThe maximum number of publishers produced by this method
とあります。これを試してみるとflatMapのmaxの数より多くの新しいストリームはつくられません。つまりflatMapLatestの完全な代用にはならないけれど使えてむしろflatMapLatestより良い部分もあるんじゃないかと思ってます。(まあdisposeの仕組みでキャンセルとかflatMapLatestのほうが良いこともありそうだけど)。
あとSwiftUI ガイドブック発売中です。
検証したいこと
言葉で説明しても分からないと思うので、そのflatMapする際の検証コードを載せておきます。
- 検証1
- やること
- 1...3でイベントを流してflatMapで非同期のイベントを流してみる
- 検証コード1
- flatMapを
.unlimited
にして実行を観測- 検証コード2
.max(1)
にして実行を観測- 検証2
- やること
.unlimited
では上流自体が止められていないことを確かめたいのでエラー流す- 非同期ではないほうがわかりやすいはず
- 検証コード1
- flatMapを
.unlimited
にして実行を観測検証1:
検証コードの仕様
- Coldなストリームとして1, 2, 3が流れる
- flatMapを挟む
- 1つ目のイベントでは1秒後に遅らせてイベントを文字列にして流す
- 1つ目でなければ非同期でイベントを文字列にして流す
- sinkで観測
何がやりたいかと言うと、flatMapで一発目のストリームの終了時間を遅くして次のストリームまでの実行が遅れるかどうかが知りたい。
先に結果
- maxPublishersが
.unlimited
ならflatMapで作成したストリームは非同期に実行される- maxPublishersが
.max(1)
ならflatMapで作成したストリームは同期的に実行される検証コード
デフォルトの
.unlimited
での例引数maxPublishersを省略すると、
.flatMap(maxPublishers: .unlimited)
になります。import Foundation import Combine import PlaygroundSupport let page = PlaygroundPage.current page.needsIndefiniteExecution = true let cancelable = (1...3).publisher .flatMap { value -> Future<String, Never> in print("?flatMap: \(value)") if value == 1 { return Future<String, Never> { promise in let v = value // 1発目だけ遅らせたい DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { print("?after: \(v)") promise(.success("\(v)")) } } } else { return Future<String, Never> { promise in let v = value DispatchQueue.main.async { print("?after : \(v)") promise(.success("\(v)")) } } } } .sink(receiveCompletion: { completion in switch completion { case .finished: print("?sink finished:", completion) case .failure(let error): print("?sink failure:", error) } }) { value in print("?sink received: \(String(describing: value))") } DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { print("終わったやろ") page.finishExecution() }出力は次の通り。解説を
//
で加えます?flatMap: 1 ?flatMap: 2 // 1の購読を待たず2のflatMap開始 ?flatMap: 3 // 2の購読も待たず ?after : 2 ?sink received: 2 ?after : 3 ?sink received: 3 ?after: 1 ?sink received: 1 // 1のflatMapのストリームが購読された ?sink finished: finished 終わったやろflatMapは呼び出されそのままflatMap内でストリームが実行されました。
- デフォルトのflatMapはすでに実行されたストリームの購読の終了を待たない
.max(1)での例
.flatMap(maxPublishers: .max(1))
だけ変更します。先程のコードから次のように変更してください
.flatMap { value -> Future<String, Error> in
↓
.flatMap(maxPublishers: .max(1)) { value -> Future<String, Error> in
出力を見ると、1が購読されるまで遅延させられていたのが分かるはずです。
//
で解説を加えています?flatMap: 1 ?after: 1 ?sink received: 1 // 1の購読が終わり ?flatMap: 2 // flatMapの2つ目が遅延させられていた ?after: 2 ?sink received: 2 ?flatMap: 3 ?after: 3 ?sink received: 3 ?sink finished: finished 終わったやろこれでflatMapはストリームのイベント発生とそのsinkが終わるまで次のは実行されていないのがわかります
検証2: flatMapでエラーを流す場合上流は止まってる?
次にflatMapでエラーを流してみるとどうなるか、というのをやってみたいと思います。
なぜかというと、flatMapでエラーを流してその下流は止められるんですが、上流は止まらないのです。デフォルトの
.unlimited
での例import Foundation import Combine import PlaygroundSupport let page = PlaygroundPage.current page.needsIndefiniteExecution = true let cancelable = (1...3).publisher .handleEvents(receiveOutput: { print("❗handle \($0)") }, receiveCompletion: { _ in print("❗handle completion") }, receiveCancel: { print("❗handle cancel") }) .setFailureType(to: Error.self) .flatMap { value -> Future<String, Error> in print("?flatMap: \(value)") if value == 1 { return Future<String, Error> { promise in let v = value // 1発目からエラー print("error : \(v)") promise(.failure(NSError(domain: "test", code: 1))) } } else { return Future<String, Error> { promise in let v = value print("success : \(v)") promise(.success("\(v)")) } } } .sink(receiveCompletion: { completion in switch completion { case .finished: print("?sink finished:", completion) case .failure(let error): print("?sink failure:", error) } }) { value in print("?sink received: \(String(describing: value))") } DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { print("終わったやろ") page.finishExecution() }出力は次の通り、handleが出力されているので止まってないと思えます。
❗handle 1 ?flatMap: 1 error : 1 ?sink failure: Error Domain=test Code=1 "(null)" ❗handle 2 // flatMapより上流は動作している ❗handle 3 // これも ❗handle completion // エラーが起きてるのにそれは関係なく 終わったやろこれはすごく不思議な気がします。エラーになってるのに上流は止まっていない。
.max(1)での例
.max(1)にするも念の為やってみましょう
先程のコードから次のように変更してください
.flatMap { value -> Future<String, Error> in
↓
.flatMap(maxPublishers: .max(1)) { value -> Future<String, Error> in
結果は次の通り、1発目のエラーで終了していますが、handleも出力していないため上流も止めています。
❗handle 1 ?flatMap: 1 error : 1 ?sink failure: Error Domain=test Code=1 "(null)" 終わったやろ上流を止めていると言うか、coldなストリームを下流のflatMapで購読していないと思えるのでこれが普通な気がします。
つまり、flatMapはエラーにしてるのにストリームをdisposeしてない。RxSwiftだとdisposeされて数珠つなぎになった購読が止まると思うんですが、Combineはそうなっておらず、そもそもdisposeという概念がないため、ストリームの上流を止めるにも使っていけということですかね?
そしてこれがバックプレッシャーを使うということでしょうかね?
.flatMap(maxPublishers: .max(1))が向いてない処理は
インクリメンタルサーチには向いてない
インクリメンタルサーチは次々に入力が来て、古い検索は気にせず新しい検索を投げたい処理です。
そのため、.flatMap(maxPublishers: .max(1))
は処理を待ってしまうので向いてなさそうです。まとめ
言いたいことが2つになっちゃってますが、次のようなことを確認しました。
- flatMapの
maxPublishers: Subscribers.Demand
引数
- maxの数より多くflatMapで新しいストリームはつくられない
- Combineのストリームはエラーになっても上流は止まらない
- エラーになった場合にmaxPublishersに指定数超えて新しくリクエストしないのでflatMapの上流も処理を止められる
感想
感想としては
.max(1)
でWeb API利用時やファイルアクセス時には積極的にバックプレッシャー使えば良さそうなんじゃない?と思います。デフォルトでバックプレッシャーを使わず無制限なのはRxJavaでもデフォルトだから仕様としてそうなってるんだとは思いますが...。かなり自信がない考察
おそらく、
.max(1)
は実行が完了するとあらためて1つをリクエストすることで、RxJavaでいうとFlowable
に対してデータを受ける側のSubscription
がrequest(1)
にしているのと同じ?RxJavaの資料から次のrequestがそれにあたるのかな。
そうなると
.max(1)
は要求したデータ数1を受け取ったらさらに1を要求するということですかね?
- 投稿日:2020-02-22T01:00:14+09:00
【iOS】UICircularProgressRingを試す
UICircularProgressRingとは?
↓ こちらを見れば一目瞭然ですが、よく見かける丸型のProgressを簡単に実現できるライブラリです。
環境構築
UICircularProgressRing
自体はCocoaPods
やCarthage
でも導入できます。
今回はCarthage
で導入します。
まずは以下の内容でCartfile
を作成。github "luispadron/UICircularProgressRing"次に以下コマンドで手元にダウンロード&ビルドします。
$ carthage update --platform iOSXcodeに戻ってTargetsのGeneralの「Framework, Libraries, and Embedded Content」に
./Carthage/Build/iOS/UICircularProgressRing.framework
を追加。
次に「Build Phases」で新規に「New Run Script Phases」を追加し
/usr/local/bin/carthage copy-frameworks上記を設定し、
Input Files
に$(SRCROOT)/Carthage/Build/iOS/UICircularProgressRing.framework
を設定します。
ここまででライブラリの導入は完了になります。実装
簡単なサンプル
README に乗っていた単純なサンプルを少し修正して試してみます。
import UIKit import UICircularProgressRing class ViewController: UIViewController { private let progressRing = UICircularProgressRing() override func viewDidLoad() { super.viewDidLoad() progressRing.maxValue = 50 progressRing.frame = CGRect(x: 0, y: 0, width: 200, height: 200) progressRing.center = self.view.center self.view.addSubview(progressRing) } // Viewのレイアウトが決定した後じゃないとアニメーションされない override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) progressRing.startProgress(to: 50, duration: 2.0) { print("Done animating!") } } }
UICircularRingStyle
UICircularProgressRing
で設定できるスタイルとして以下があります。
- inside (デフォルト ↑と同じ)
中のテキストを非表示にする
以下のプロパティ
shouldShowValueText
をfalse
に設定すれば非表示になります。progressRing.shouldShowValueText = false
リングカラーを設定する
progressRing.outerRingColor = UIColor.red progressRing.innerRingColor = UIColor.blue
開始アングルを設定する
startAngle
で設定可能。デフォルトは0
で設定されています。
UICircularTimerRing
Timerを設定できる様にしたクラスです。
import UIKit import UICircularProgressRing class ViewController: UIViewController { private let timerRing = UICircularTimerRing() override func viewDidLoad() { super.viewDidLoad() timerRing.frame = CGRect(x: 0, y: 0, width: 200, height: 200) timerRing.center = self.view.center self.view.addSubview(timerRing) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) timerRing.startTimer(to: 10) { state in print("state: \(state)") } } }↑のサンプルでは10秒後にリングが閉じる様に設定しています。
参考になったURL