@capgo/capacitor-speech-recognition 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ var core = require('@capacitor/core');
4
+
5
+ const SpeechRecognition = core.registerPlugin('SpeechRecognition', {
6
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.SpeechRecognitionWeb()),
7
+ });
8
+
9
+ class SpeechRecognitionWeb extends core.WebPlugin {
10
+ available() {
11
+ throw this.unimplemented('Speech recognition is not available on the web.');
12
+ }
13
+ start(_options) {
14
+ throw this.unimplemented('Speech recognition is not available on the web.');
15
+ }
16
+ stop() {
17
+ throw this.unimplemented('Speech recognition is not available on the web.');
18
+ }
19
+ getSupportedLanguages() {
20
+ throw this.unimplemented('Speech recognition is not available on the web.');
21
+ }
22
+ isListening() {
23
+ throw this.unimplemented('Speech recognition is not available on the web.');
24
+ }
25
+ checkPermissions() {
26
+ throw this.unimplemented('Speech recognition permissions are not handled on the web.');
27
+ }
28
+ requestPermissions() {
29
+ throw this.unimplemented('Speech recognition permissions are not handled on the web.');
30
+ }
31
+ }
32
+
33
+ var web = /*#__PURE__*/Object.freeze({
34
+ __proto__: null,
35
+ SpeechRecognitionWeb: SpeechRecognitionWeb
36
+ });
37
+
38
+ exports.SpeechRecognition = SpeechRecognition;
39
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SpeechRecognition = registerPlugin('SpeechRecognition', {\n web: () => import('./web').then((m) => new m.SpeechRecognitionWeb()),\n});\nexport * from './definitions';\nexport { SpeechRecognition };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SpeechRecognitionWeb extends WebPlugin {\n available() {\n throw this.unimplemented('Speech recognition is not available on the web.');\n }\n start(_options) {\n throw this.unimplemented('Speech recognition is not available on the web.');\n }\n stop() {\n throw this.unimplemented('Speech recognition is not available on the web.');\n }\n getSupportedLanguages() {\n throw this.unimplemented('Speech recognition is not available on the web.');\n }\n isListening() {\n throw this.unimplemented('Speech recognition is not available on the web.');\n }\n checkPermissions() {\n throw this.unimplemented('Speech recognition permissions are not handled on the web.');\n }\n requestPermissions() {\n throw this.unimplemented('Speech recognition permissions are not handled on the web.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,iBAAiB,GAAGA,mBAAc,CAAC,mBAAmB,EAAE;AAC9D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;AACxE,CAAC;;ACFM,MAAM,oBAAoB,SAASC,cAAS,CAAC;AACpD,IAAI,SAAS,GAAG;AAChB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC;AACnF,IAAI;AACJ,IAAI,KAAK,CAAC,QAAQ,EAAE;AACpB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC;AACnF,IAAI;AACJ,IAAI,IAAI,GAAG;AACX,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC;AACnF,IAAI;AACJ,IAAI,qBAAqB,GAAG;AAC5B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC;AACnF,IAAI;AACJ,IAAI,WAAW,GAAG;AAClB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC;AACnF,IAAI;AACJ,IAAI,gBAAgB,GAAG;AACvB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ,IAAI,kBAAkB,GAAG;AACzB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;AAC9F,IAAI;AACJ;;;;;;;;;"}
package/dist/plugin.js ADDED
@@ -0,0 +1,42 @@
1
+ var capacitorCapacitorSpeechRecognition = (function (exports, core) {
2
+ 'use strict';
3
+
4
+ const SpeechRecognition = core.registerPlugin('SpeechRecognition', {
5
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.SpeechRecognitionWeb()),
6
+ });
7
+
8
+ class SpeechRecognitionWeb extends core.WebPlugin {
9
+ available() {
10
+ throw this.unimplemented('Speech recognition is not available on the web.');
11
+ }
12
+ start(_options) {
13
+ throw this.unimplemented('Speech recognition is not available on the web.');
14
+ }
15
+ stop() {
16
+ throw this.unimplemented('Speech recognition is not available on the web.');
17
+ }
18
+ getSupportedLanguages() {
19
+ throw this.unimplemented('Speech recognition is not available on the web.');
20
+ }
21
+ isListening() {
22
+ throw this.unimplemented('Speech recognition is not available on the web.');
23
+ }
24
+ checkPermissions() {
25
+ throw this.unimplemented('Speech recognition permissions are not handled on the web.');
26
+ }
27
+ requestPermissions() {
28
+ throw this.unimplemented('Speech recognition permissions are not handled on the web.');
29
+ }
30
+ }
31
+
32
+ var web = /*#__PURE__*/Object.freeze({
33
+ __proto__: null,
34
+ SpeechRecognitionWeb: SpeechRecognitionWeb
35
+ });
36
+
37
+ exports.SpeechRecognition = SpeechRecognition;
38
+
39
+ return exports;
40
+
41
+ })({}, capacitorExports);
42
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SpeechRecognition = registerPlugin('SpeechRecognition', {\n web: () => import('./web').then((m) => new m.SpeechRecognitionWeb()),\n});\nexport * from './definitions';\nexport { SpeechRecognition };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SpeechRecognitionWeb extends WebPlugin {\n available() {\n throw this.unimplemented('Speech recognition is not available on the web.');\n }\n start(_options) {\n throw this.unimplemented('Speech recognition is not available on the web.');\n }\n stop() {\n throw this.unimplemented('Speech recognition is not available on the web.');\n }\n getSupportedLanguages() {\n throw this.unimplemented('Speech recognition is not available on the web.');\n }\n isListening() {\n throw this.unimplemented('Speech recognition is not available on the web.');\n }\n checkPermissions() {\n throw this.unimplemented('Speech recognition permissions are not handled on the web.');\n }\n requestPermissions() {\n throw this.unimplemented('Speech recognition permissions are not handled on the web.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,iBAAiB,GAAGA,mBAAc,CAAC,mBAAmB,EAAE;IAC9D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,oBAAoB,EAAE,CAAC;IACxE,CAAC;;ICFM,MAAM,oBAAoB,SAASC,cAAS,CAAC;IACpD,IAAI,SAAS,GAAG;IAChB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC;IACnF,IAAI;IACJ,IAAI,KAAK,CAAC,QAAQ,EAAE;IACpB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC;IACnF,IAAI;IACJ,IAAI,IAAI,GAAG;IACX,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC;IACnF,IAAI;IACJ,IAAI,qBAAqB,GAAG;IAC5B,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC;IACnF,IAAI;IACJ,IAAI,WAAW,GAAG;IAClB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,iDAAiD,CAAC;IACnF,IAAI;IACJ,IAAI,gBAAgB,GAAG;IACvB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ,IAAI,kBAAkB,GAAG;IACzB,QAAQ,MAAM,IAAI,CAAC,aAAa,CAAC,4DAA4D,CAAC;IAC9F,IAAI;IACJ;;;;;;;;;;;;;;;"}
@@ -0,0 +1,282 @@
1
+ import AVFoundation
2
+ import Capacitor
3
+ import Foundation
4
+ import Speech
5
+
6
+ private enum PermissionState: String {
7
+ case granted
8
+ case denied
9
+ case prompt
10
+ }
11
+
12
+ @objc(SpeechRecognitionPlugin)
13
+ public final class SpeechRecognitionPlugin: CAPPlugin, CAPBridgedPlugin {
14
+ public let identifier = "SpeechRecognitionPlugin"
15
+ public let jsName = "SpeechRecognition"
16
+ public let pluginMethods: [CAPPluginMethod] = [
17
+ CAPPluginMethod(name: "available", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "getSupportedLanguages", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "isListening", returnType: CAPPluginReturnPromise),
22
+ CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise)
24
+ ]
25
+
26
+ private let audioEngine = AVAudioEngine()
27
+ private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
28
+ private var recognitionTask: SFSpeechRecognitionTask?
29
+ private var speechRecognizer: SFSpeechRecognizer?
30
+ private var activeCall: CAPPluginCall?
31
+ private var currentOptions: RecognitionOptions?
32
+ private var hasInstalledTap = false
33
+
34
+ private let maxDefaultResults = 5
35
+
36
+ @objc func available(_ call: CAPPluginCall) {
37
+ let locale = Locale(identifier: call.getString("language") ?? Locale.current.identifier)
38
+ let recognizer = SFSpeechRecognizer(locale: locale)
39
+ call.resolve(["available": recognizer?.isAvailable ?? false])
40
+ }
41
+
42
+ @objc func start(_ call: CAPPluginCall) {
43
+ if self.audioEngine.isRunning || recognitionTask != nil {
44
+ call.reject("Speech recognition is already running.")
45
+ return
46
+ }
47
+
48
+ guard isSpeechPermissionGranted else {
49
+ call.reject("Missing speech recognition permission.")
50
+ return
51
+ }
52
+
53
+ let options = RecognitionOptions(
54
+ language: call.getString("language") ?? Locale.current.identifier,
55
+ maxResults: call.getInt("maxResults") ?? maxDefaultResults,
56
+ partialResults: call.getBool("partialResults") ?? false,
57
+ addPunctuation: call.getBool("addPunctuation") ?? false
58
+ )
59
+
60
+ self.activeCall = call
61
+ self.currentOptions = options
62
+
63
+ AVAudioSession.sharedInstance().requestRecordPermission { granted in
64
+ guard granted else {
65
+ DispatchQueue.main.async {
66
+ call.reject("User denied microphone access.")
67
+ self.cleanupRecognition(notifyStop: false)
68
+ }
69
+ return
70
+ }
71
+
72
+ DispatchQueue.main.async {
73
+ self.beginRecognition(call: call, options: options)
74
+ }
75
+ }
76
+ }
77
+
78
+ @objc func stop(_ call: CAPPluginCall) {
79
+ cleanupRecognition(notifyStop: true)
80
+ call.resolve()
81
+ }
82
+
83
+ @objc func isListening(_ call: CAPPluginCall) {
84
+ call.resolve(["listening": audioEngine.isRunning])
85
+ }
86
+
87
+ @objc func getSupportedLanguages(_ call: CAPPluginCall) {
88
+ let identifiers = SFSpeechRecognizer
89
+ .supportedLocales()
90
+ .map { $0.identifier }
91
+ .sorted()
92
+ call.resolve(["languages": identifiers])
93
+ }
94
+
95
+ @objc override public func checkPermissions(_ call: CAPPluginCall) {
96
+ call.resolve(["speechRecognition": permissionState.rawValue])
97
+ }
98
+
99
+ @objc override public func requestPermissions(_ call: CAPPluginCall) {
100
+ SFSpeechRecognizer.requestAuthorization { status in
101
+ switch status {
102
+ case .authorized:
103
+ AVAudioSession.sharedInstance().requestRecordPermission { granted in
104
+ DispatchQueue.main.async {
105
+ let result: PermissionState = granted ? .granted : .denied
106
+ call.resolve(["speechRecognition": result.rawValue])
107
+ }
108
+ }
109
+ case .denied, .restricted:
110
+ DispatchQueue.main.async {
111
+ call.resolve(["speechRecognition": PermissionState.denied.rawValue])
112
+ }
113
+ case .notDetermined:
114
+ DispatchQueue.main.async {
115
+ call.resolve(["speechRecognition": PermissionState.prompt.rawValue])
116
+ }
117
+ @unknown default:
118
+ DispatchQueue.main.async {
119
+ call.resolve(["speechRecognition": PermissionState.prompt.rawValue])
120
+ }
121
+ }
122
+ }
123
+ }
124
+
125
+ private func beginRecognition(call: CAPPluginCall, options: RecognitionOptions) {
126
+ guard let recognizer = SFSpeechRecognizer(locale: Locale(identifier: options.language)) else {
127
+ call.reject("Unsupported locale: \(options.language)")
128
+ cleanupRecognition(notifyStop: false)
129
+ return
130
+ }
131
+
132
+ guard recognizer.isAvailable else {
133
+ call.reject("Speech recognizer is currently unavailable.")
134
+ cleanupRecognition(notifyStop: false)
135
+ return
136
+ }
137
+
138
+ speechRecognizer = recognizer
139
+
140
+ do {
141
+ try configureAudioSession()
142
+ } catch {
143
+ call.reject("Failed to configure audio session: \(error.localizedDescription)")
144
+ cleanupRecognition(notifyStop: false)
145
+ return
146
+ }
147
+
148
+ let recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
149
+ recognitionRequest.shouldReportPartialResults = options.partialResults
150
+ if #available(iOS 16.0, *) {
151
+ recognitionRequest.addsPunctuation = options.addPunctuation
152
+ }
153
+ self.recognitionRequest = recognitionRequest
154
+
155
+ let inputNode = audioEngine.inputNode
156
+ let recordingFormat = inputNode.outputFormat(forBus: 0)
157
+ inputNode.removeTap(onBus: 0)
158
+ inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { [weak self] buffer, _ in
159
+ self?.recognitionRequest?.append(buffer)
160
+ }
161
+ hasInstalledTap = true
162
+
163
+ audioEngine.prepare()
164
+
165
+ do {
166
+ try audioEngine.start()
167
+ notifyListeners("listeningState", data: ["status": "started"])
168
+ } catch {
169
+ call.reject("Unable to start audio engine: \(error.localizedDescription)")
170
+ cleanupRecognition(notifyStop: false)
171
+ return
172
+ }
173
+
174
+ if options.partialResults {
175
+ call.resolve()
176
+ }
177
+
178
+ recognitionTask = recognizer.recognitionTask(with: recognitionRequest) { [weak self] result, error in
179
+ guard let self else { return }
180
+ if let result {
181
+ let matches = self.buildMatches(from: result, maxResults: options.maxResults)
182
+ if options.partialResults {
183
+ DispatchQueue.main.async {
184
+ self.notifyListeners("partialResults", data: ["matches": matches])
185
+ }
186
+ } else if result.isFinal {
187
+ DispatchQueue.main.async {
188
+ self.activeCall?.resolve(["matches": matches])
189
+ }
190
+ }
191
+
192
+ if result.isFinal {
193
+ self.cleanupRecognition(notifyStop: true)
194
+ }
195
+ }
196
+
197
+ if let error {
198
+ self.handleRecognitionError(error)
199
+ }
200
+ }
201
+ }
202
+
203
+ private func configureAudioSession() throws {
204
+ let session = AVAudioSession.sharedInstance()
205
+ try session.setCategory(.playAndRecord, options: [.defaultToSpeaker, .duckOthers])
206
+ try session.setMode(.measurement)
207
+ try session.setActive(true, options: .notifyOthersOnDeactivation)
208
+ }
209
+
210
+ private func cleanupRecognition(notifyStop: Bool) {
211
+ DispatchQueue.main.async {
212
+ if self.audioEngine.isRunning {
213
+ self.audioEngine.stop()
214
+ }
215
+
216
+ if self.hasInstalledTap {
217
+ self.audioEngine.inputNode.removeTap(onBus: 0)
218
+ self.hasInstalledTap = false
219
+ }
220
+
221
+ self.recognitionRequest?.endAudio()
222
+ self.recognitionRequest = nil
223
+ self.recognitionTask?.cancel()
224
+ self.recognitionTask = nil
225
+ self.speechRecognizer = nil
226
+ self.currentOptions = nil
227
+ self.activeCall = nil
228
+
229
+ if notifyStop {
230
+ self.notifyListeners("listeningState", data: ["status": "stopped"])
231
+ }
232
+ }
233
+ }
234
+
235
+ private func handleRecognitionError(_ error: Error) {
236
+ DispatchQueue.main.async {
237
+ self.cleanupRecognition(notifyStop: true)
238
+ self.activeCall?.reject(error.localizedDescription)
239
+ }
240
+ }
241
+
242
+ private func buildMatches(from result: SFSpeechRecognitionResult, maxResults: Int) -> [String] {
243
+ var matches: [String] = []
244
+ for transcription in result.transcriptions where matches.count < maxResults {
245
+ matches.append(transcription.formattedString)
246
+ }
247
+ return matches
248
+ }
249
+
250
+ private var isSpeechPermissionGranted: Bool {
251
+ switch SFSpeechRecognizer.authorizationStatus() {
252
+ case .authorized:
253
+ return true
254
+ case .notDetermined, .denied, .restricted:
255
+ return false
256
+ @unknown default:
257
+ return false
258
+ }
259
+ }
260
+
261
+ private var permissionState: PermissionState {
262
+ let speechStatus = SFSpeechRecognizer.authorizationStatus()
263
+ let micStatus = AVAudioSession.sharedInstance().recordPermission
264
+
265
+ if speechStatus == .denied || speechStatus == .restricted || micStatus == .denied {
266
+ return .denied
267
+ }
268
+
269
+ if speechStatus == .notDetermined || micStatus == .undetermined {
270
+ return .prompt
271
+ }
272
+
273
+ return .granted
274
+ }
275
+ }
276
+
277
+ private struct RecognitionOptions {
278
+ let language: String
279
+ let maxResults: Int
280
+ let partialResults: Bool
281
+ let addPunctuation: Bool
282
+ }
@@ -0,0 +1,7 @@
1
+ import XCTest
2
+
3
+ final class SpeechRecognitionPluginTests: XCTestCase {
4
+ func testExample() {
5
+ XCTAssertTrue(true)
6
+ }
7
+ }
package/package.json ADDED
@@ -0,0 +1,87 @@
1
+ {
2
+ "name": "@capgo/capacitor-speech-recognition",
3
+ "version": "7.0.0",
4
+ "description": "Modern Capacitor plugin for on-device and cloud speech recognition with live partial results.",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "Package.swift",
16
+ "CapgoCapacitorSpeechRecognition.podspec"
17
+ ],
18
+ "author": "Martin Donadieu <martin@capgo.app>",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Cap-go/capacitor-speech-recognition.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/Cap-go/capacitor-speech-recognition/issues"
26
+ },
27
+ "keywords": [
28
+ "capacitor",
29
+ "plugin",
30
+ "speech",
31
+ "recognition",
32
+ "voice",
33
+ "transcription"
34
+ ],
35
+ "scripts": {
36
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
37
+ "verify:ios": "xcodebuild -scheme CapgoCapacitorSpeechRecognition -destination generic/platform=iOS",
38
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
39
+ "verify:web": "npm run build",
40
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
41
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
42
+ "eslint": "eslint . --ext .ts",
43
+ "prettier": "prettier \"**/*.{css,html,ts,js,java,kt}\" --plugin=prettier-plugin-java",
44
+ "swiftlint": "node-swiftlint",
45
+ "docgen": "docgen --api SpeechRecognitionPlugin --output-readme README.md --output-json dist/docs.json",
46
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
47
+ "clean": "rimraf ./dist",
48
+ "watch": "tsc --watch",
49
+ "prepublishOnly": "npm run build"
50
+ },
51
+ "devDependencies": {
52
+ "@capacitor/android": "^7.0.0",
53
+ "@capacitor/cli": "^7.0.0",
54
+ "@capacitor/core": "^7.0.0",
55
+ "@capacitor/docgen": "^0.3.0",
56
+ "@capacitor/ios": "^7.0.0",
57
+ "@ionic/eslint-config": "^0.4.0",
58
+ "@ionic/prettier-config": "^4.0.0",
59
+ "@ionic/swiftlint-config": "^2.0.0",
60
+ "@types/node": "^22.13.1",
61
+ "eslint": "^8.57.0",
62
+ "eslint-plugin-import": "^2.31.0",
63
+ "husky": "^9.1.7",
64
+ "prettier": "^3.4.2",
65
+ "prettier-plugin-java": "^2.6.7",
66
+ "rimraf": "^6.0.1",
67
+ "rollup": "^4.34.6",
68
+ "swiftlint": "^2.0.0",
69
+ "typescript": "^5.7.3"
70
+ },
71
+ "peerDependencies": {
72
+ "@capacitor/core": ">=7.0.0"
73
+ },
74
+ "eslintConfig": {
75
+ "extends": "@ionic/eslint-config/recommended"
76
+ },
77
+ "prettier": "@ionic/prettier-config",
78
+ "swiftlint": "@ionic/swiftlint-config",
79
+ "capacitor": {
80
+ "ios": {
81
+ "src": "ios"
82
+ },
83
+ "android": {
84
+ "src": "android"
85
+ }
86
+ }
87
+ }