├── .gitignore
├── LICENSE
├── README.md
├── WhispererKeyboard.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── WhispererKeyboard.xcscheme
│ └── keyboard.xcscheme
├── WhispererKeyboard
├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
├── Audio.swift
├── Info.plist
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Transcription.swift
├── WhispererKeyboard.entitlements
└── WhispererKeyboardApp.swift
├── example.jpg
└── keyboard
├── Info.plist
├── KeyboardViewController.swift
└── keyboard.entitlements
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | ##
3 | build/
4 | DerivedData/
5 | *.moved-aside
6 | *.xcuserstate
7 |
8 | # User interface state
9 | xcuserdata/
10 |
11 | # Swift Package Manager
12 | .build/
13 |
14 | # Carthage
15 | Carthage/Build
16 |
17 | # CocoaPods
18 | Pods/
19 | Podfile.lock
20 |
21 | # Fastlane
22 | fastlane/report.xml
23 | fastlane/Preview.html
24 | fastlane/screenshots/**/*.png
25 | fastlane/test_output
26 |
27 | # Code Injection
28 | injected_container/
29 |
30 | # macOS
31 | .DS_Store
32 | .AppleDouble
33 | .LSOverride
34 |
35 | # Other common ignores
36 | *.log
37 | *.swp
38 | *.bak
39 | *.tmp
40 | *~
41 |
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Experimental project created for personal use.
2 |
3 | ### Custom iOS keyboard using OpenAI Whisperer API for speech-to-text conversion
4 |
5 | 1. Captures audio from microphone
6 | 2. sends to OpenAI API for transcription
7 | 3. Inserts result into the active text edit
8 |
9 |
10 | ### Example using in Notes, comparison with iOS built-in transcription
11 |
12 |
13 |
--------------------------------------------------------------------------------
/WhispererKeyboard.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 5F5263092AB027D90087E46B /* WhispererKeyboardApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F5263082AB027D90087E46B /* WhispererKeyboardApp.swift */; };
11 | 5F52630D2AB027DA0087E46B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F52630C2AB027DA0087E46B /* Assets.xcassets */; };
12 | 5F5263102AB027DA0087E46B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F52630F2AB027DA0087E46B /* Preview Assets.xcassets */; };
13 | 5F52631D2AB029F60087E46B /* KeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F52631C2AB029F60087E46B /* KeyboardViewController.swift */; };
14 | 5F5263212AB029F60087E46B /* keyboard.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 5F52631A2AB029F60087E46B /* keyboard.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
15 | 5F6738962ACB8BEF00F83932 /* Transcription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F6738952ACB8BEF00F83932 /* Transcription.swift */; };
16 | 5F6738982ACB8C8900F83932 /* Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F6738972ACB8C8900F83932 /* Audio.swift */; };
17 | /* End PBXBuildFile section */
18 |
19 | /* Begin PBXContainerItemProxy section */
20 | 5F52631F2AB029F60087E46B /* PBXContainerItemProxy */ = {
21 | isa = PBXContainerItemProxy;
22 | containerPortal = 5F5262FD2AB027D90087E46B /* Project object */;
23 | proxyType = 1;
24 | remoteGlobalIDString = 5F5263192AB029F60087E46B;
25 | remoteInfo = keyboard;
26 | };
27 | /* End PBXContainerItemProxy section */
28 |
29 | /* Begin PBXCopyFilesBuildPhase section */
30 | 5F5263252AB029F60087E46B /* Embed Foundation Extensions */ = {
31 | isa = PBXCopyFilesBuildPhase;
32 | buildActionMask = 2147483647;
33 | dstPath = "";
34 | dstSubfolderSpec = 13;
35 | files = (
36 | 5F5263212AB029F60087E46B /* keyboard.appex in Embed Foundation Extensions */,
37 | );
38 | name = "Embed Foundation Extensions";
39 | runOnlyForDeploymentPostprocessing = 0;
40 | };
41 | /* End PBXCopyFilesBuildPhase section */
42 |
43 | /* Begin PBXFileReference section */
44 | 5F5263052AB027D90087E46B /* WhispererKeyboard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WhispererKeyboard.app; sourceTree = BUILT_PRODUCTS_DIR; };
45 | 5F5263082AB027D90087E46B /* WhispererKeyboardApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhispererKeyboardApp.swift; sourceTree = ""; };
46 | 5F52630C2AB027DA0087E46B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
47 | 5F52630F2AB027DA0087E46B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
48 | 5F52631A2AB029F60087E46B /* keyboard.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = keyboard.appex; sourceTree = BUILT_PRODUCTS_DIR; };
49 | 5F52631C2AB029F60087E46B /* KeyboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardViewController.swift; sourceTree = ""; };
50 | 5F52631E2AB029F60087E46B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
51 | 5F5263342AB17A300087E46B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
52 | 5F5DF0812AB6398C00BAE883 /* WhispererKeyboard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WhispererKeyboard.entitlements; sourceTree = ""; };
53 | 5F5DF0822AB643FA00BAE883 /* keyboard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = keyboard.entitlements; sourceTree = ""; };
54 | 5F6738952ACB8BEF00F83932 /* Transcription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transcription.swift; sourceTree = ""; };
55 | 5F6738972ACB8C8900F83932 /* Audio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Audio.swift; sourceTree = ""; };
56 | /* End PBXFileReference section */
57 |
58 | /* Begin PBXFrameworksBuildPhase section */
59 | 5F5263022AB027D90087E46B /* Frameworks */ = {
60 | isa = PBXFrameworksBuildPhase;
61 | buildActionMask = 2147483647;
62 | files = (
63 | );
64 | runOnlyForDeploymentPostprocessing = 0;
65 | };
66 | 5F5263172AB029F60087E46B /* Frameworks */ = {
67 | isa = PBXFrameworksBuildPhase;
68 | buildActionMask = 2147483647;
69 | files = (
70 | );
71 | runOnlyForDeploymentPostprocessing = 0;
72 | };
73 | /* End PBXFrameworksBuildPhase section */
74 |
75 | /* Begin PBXGroup section */
76 | 5F5262FC2AB027D90087E46B = {
77 | isa = PBXGroup;
78 | children = (
79 | 5F5263072AB027D90087E46B /* WhispererKeyboard */,
80 | 5F52631B2AB029F60087E46B /* keyboard */,
81 | 5F5263062AB027D90087E46B /* Products */,
82 | 5F5DF0862AB6589F00BAE883 /* Frameworks */,
83 | );
84 | sourceTree = "";
85 | };
86 | 5F5263062AB027D90087E46B /* Products */ = {
87 | isa = PBXGroup;
88 | children = (
89 | 5F5263052AB027D90087E46B /* WhispererKeyboard.app */,
90 | 5F52631A2AB029F60087E46B /* keyboard.appex */,
91 | );
92 | name = Products;
93 | sourceTree = "";
94 | };
95 | 5F5263072AB027D90087E46B /* WhispererKeyboard */ = {
96 | isa = PBXGroup;
97 | children = (
98 | 5F5DF0812AB6398C00BAE883 /* WhispererKeyboard.entitlements */,
99 | 5F5263342AB17A300087E46B /* Info.plist */,
100 | 5F5263082AB027D90087E46B /* WhispererKeyboardApp.swift */,
101 | 5F52630C2AB027DA0087E46B /* Assets.xcassets */,
102 | 5F52630E2AB027DA0087E46B /* Preview Content */,
103 | 5F6738952ACB8BEF00F83932 /* Transcription.swift */,
104 | 5F6738972ACB8C8900F83932 /* Audio.swift */,
105 | );
106 | path = WhispererKeyboard;
107 | sourceTree = "";
108 | };
109 | 5F52630E2AB027DA0087E46B /* Preview Content */ = {
110 | isa = PBXGroup;
111 | children = (
112 | 5F52630F2AB027DA0087E46B /* Preview Assets.xcassets */,
113 | );
114 | path = "Preview Content";
115 | sourceTree = "";
116 | };
117 | 5F52631B2AB029F60087E46B /* keyboard */ = {
118 | isa = PBXGroup;
119 | children = (
120 | 5F5DF0822AB643FA00BAE883 /* keyboard.entitlements */,
121 | 5F52631C2AB029F60087E46B /* KeyboardViewController.swift */,
122 | 5F52631E2AB029F60087E46B /* Info.plist */,
123 | );
124 | path = keyboard;
125 | sourceTree = "";
126 | };
127 | 5F5DF0862AB6589F00BAE883 /* Frameworks */ = {
128 | isa = PBXGroup;
129 | children = (
130 | );
131 | name = Frameworks;
132 | sourceTree = "";
133 | };
134 | /* End PBXGroup section */
135 |
136 | /* Begin PBXNativeTarget section */
137 | 5F5263042AB027D90087E46B /* WhispererKeyboard */ = {
138 | isa = PBXNativeTarget;
139 | buildConfigurationList = 5F5263132AB027DA0087E46B /* Build configuration list for PBXNativeTarget "WhispererKeyboard" */;
140 | buildPhases = (
141 | 5F5263012AB027D90087E46B /* Sources */,
142 | 5F5263022AB027D90087E46B /* Frameworks */,
143 | 5F5263032AB027D90087E46B /* Resources */,
144 | 5F5263252AB029F60087E46B /* Embed Foundation Extensions */,
145 | );
146 | buildRules = (
147 | );
148 | dependencies = (
149 | 5F5263202AB029F60087E46B /* PBXTargetDependency */,
150 | );
151 | name = WhispererKeyboard;
152 | packageProductDependencies = (
153 | );
154 | productName = WhispererKeyboard;
155 | productReference = 5F5263052AB027D90087E46B /* WhispererKeyboard.app */;
156 | productType = "com.apple.product-type.application";
157 | };
158 | 5F5263192AB029F60087E46B /* keyboard */ = {
159 | isa = PBXNativeTarget;
160 | buildConfigurationList = 5F5263222AB029F60087E46B /* Build configuration list for PBXNativeTarget "keyboard" */;
161 | buildPhases = (
162 | 5F5263162AB029F60087E46B /* Sources */,
163 | 5F5263172AB029F60087E46B /* Frameworks */,
164 | 5F5263182AB029F60087E46B /* Resources */,
165 | );
166 | buildRules = (
167 | );
168 | dependencies = (
169 | );
170 | name = keyboard;
171 | packageProductDependencies = (
172 | );
173 | productName = keyboard;
174 | productReference = 5F52631A2AB029F60087E46B /* keyboard.appex */;
175 | productType = "com.apple.product-type.app-extension";
176 | };
177 | /* End PBXNativeTarget section */
178 |
179 | /* Begin PBXProject section */
180 | 5F5262FD2AB027D90087E46B /* Project object */ = {
181 | isa = PBXProject;
182 | attributes = {
183 | BuildIndependentTargetsInParallel = 1;
184 | LastSwiftUpdateCheck = 1430;
185 | LastUpgradeCheck = 1500;
186 | TargetAttributes = {
187 | 5F5263042AB027D90087E46B = {
188 | CreatedOnToolsVersion = 14.3.1;
189 | };
190 | 5F5263192AB029F60087E46B = {
191 | CreatedOnToolsVersion = 14.3.1;
192 | };
193 | };
194 | };
195 | buildConfigurationList = 5F5263002AB027D90087E46B /* Build configuration list for PBXProject "WhispererKeyboard" */;
196 | compatibilityVersion = "Xcode 14.0";
197 | developmentRegion = en;
198 | hasScannedForEncodings = 0;
199 | knownRegions = (
200 | en,
201 | Base,
202 | );
203 | mainGroup = 5F5262FC2AB027D90087E46B;
204 | packageReferences = (
205 | );
206 | productRefGroup = 5F5263062AB027D90087E46B /* Products */;
207 | projectDirPath = "";
208 | projectRoot = "";
209 | targets = (
210 | 5F5263042AB027D90087E46B /* WhispererKeyboard */,
211 | 5F5263192AB029F60087E46B /* keyboard */,
212 | );
213 | };
214 | /* End PBXProject section */
215 |
216 | /* Begin PBXResourcesBuildPhase section */
217 | 5F5263032AB027D90087E46B /* Resources */ = {
218 | isa = PBXResourcesBuildPhase;
219 | buildActionMask = 2147483647;
220 | files = (
221 | 5F5263102AB027DA0087E46B /* Preview Assets.xcassets in Resources */,
222 | 5F52630D2AB027DA0087E46B /* Assets.xcassets in Resources */,
223 | );
224 | runOnlyForDeploymentPostprocessing = 0;
225 | };
226 | 5F5263182AB029F60087E46B /* Resources */ = {
227 | isa = PBXResourcesBuildPhase;
228 | buildActionMask = 2147483647;
229 | files = (
230 | );
231 | runOnlyForDeploymentPostprocessing = 0;
232 | };
233 | /* End PBXResourcesBuildPhase section */
234 |
235 | /* Begin PBXSourcesBuildPhase section */
236 | 5F5263012AB027D90087E46B /* Sources */ = {
237 | isa = PBXSourcesBuildPhase;
238 | buildActionMask = 2147483647;
239 | files = (
240 | 5F6738982ACB8C8900F83932 /* Audio.swift in Sources */,
241 | 5F6738962ACB8BEF00F83932 /* Transcription.swift in Sources */,
242 | 5F5263092AB027D90087E46B /* WhispererKeyboardApp.swift in Sources */,
243 | );
244 | runOnlyForDeploymentPostprocessing = 0;
245 | };
246 | 5F5263162AB029F60087E46B /* Sources */ = {
247 | isa = PBXSourcesBuildPhase;
248 | buildActionMask = 2147483647;
249 | files = (
250 | 5F52631D2AB029F60087E46B /* KeyboardViewController.swift in Sources */,
251 | );
252 | runOnlyForDeploymentPostprocessing = 0;
253 | };
254 | /* End PBXSourcesBuildPhase section */
255 |
256 | /* Begin PBXTargetDependency section */
257 | 5F5263202AB029F60087E46B /* PBXTargetDependency */ = {
258 | isa = PBXTargetDependency;
259 | target = 5F5263192AB029F60087E46B /* keyboard */;
260 | targetProxy = 5F52631F2AB029F60087E46B /* PBXContainerItemProxy */;
261 | };
262 | /* End PBXTargetDependency section */
263 |
264 | /* Begin XCBuildConfiguration section */
265 | 5F5263112AB027DA0087E46B /* Debug */ = {
266 | isa = XCBuildConfiguration;
267 | buildSettings = {
268 | ALWAYS_SEARCH_USER_PATHS = NO;
269 | CLANG_ANALYZER_NONNULL = YES;
270 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
271 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
272 | CLANG_ENABLE_MODULES = YES;
273 | CLANG_ENABLE_OBJC_ARC = YES;
274 | CLANG_ENABLE_OBJC_WEAK = YES;
275 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
276 | CLANG_WARN_BOOL_CONVERSION = YES;
277 | CLANG_WARN_COMMA = YES;
278 | CLANG_WARN_CONSTANT_CONVERSION = YES;
279 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
280 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
281 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
282 | CLANG_WARN_EMPTY_BODY = YES;
283 | CLANG_WARN_ENUM_CONVERSION = YES;
284 | CLANG_WARN_INFINITE_RECURSION = YES;
285 | CLANG_WARN_INT_CONVERSION = YES;
286 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
287 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
288 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
289 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
290 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
291 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
292 | CLANG_WARN_STRICT_PROTOTYPES = YES;
293 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
294 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
295 | CLANG_WARN_UNREACHABLE_CODE = YES;
296 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
297 | COPY_PHASE_STRIP = NO;
298 | DEBUG_INFORMATION_FORMAT = dwarf;
299 | ENABLE_STRICT_OBJC_MSGSEND = YES;
300 | ENABLE_TESTABILITY = YES;
301 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
302 | GCC_C_LANGUAGE_STANDARD = gnu11;
303 | GCC_DYNAMIC_NO_PIC = NO;
304 | GCC_NO_COMMON_BLOCKS = YES;
305 | GCC_OPTIMIZATION_LEVEL = 0;
306 | GCC_PREPROCESSOR_DEFINITIONS = (
307 | "DEBUG=1",
308 | "$(inherited)",
309 | );
310 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
311 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
312 | GCC_WARN_UNDECLARED_SELECTOR = YES;
313 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
314 | GCC_WARN_UNUSED_FUNCTION = YES;
315 | GCC_WARN_UNUSED_VARIABLE = YES;
316 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
317 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
318 | MTL_FAST_MATH = YES;
319 | ONLY_ACTIVE_ARCH = YES;
320 | SDKROOT = iphoneos;
321 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
322 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
323 | };
324 | name = Debug;
325 | };
326 | 5F5263122AB027DA0087E46B /* Release */ = {
327 | isa = XCBuildConfiguration;
328 | buildSettings = {
329 | ALWAYS_SEARCH_USER_PATHS = NO;
330 | CLANG_ANALYZER_NONNULL = YES;
331 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
332 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
333 | CLANG_ENABLE_MODULES = YES;
334 | CLANG_ENABLE_OBJC_ARC = YES;
335 | CLANG_ENABLE_OBJC_WEAK = YES;
336 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
337 | CLANG_WARN_BOOL_CONVERSION = YES;
338 | CLANG_WARN_COMMA = YES;
339 | CLANG_WARN_CONSTANT_CONVERSION = YES;
340 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
341 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
342 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
343 | CLANG_WARN_EMPTY_BODY = YES;
344 | CLANG_WARN_ENUM_CONVERSION = YES;
345 | CLANG_WARN_INFINITE_RECURSION = YES;
346 | CLANG_WARN_INT_CONVERSION = YES;
347 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
348 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
349 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
350 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
351 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
352 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
353 | CLANG_WARN_STRICT_PROTOTYPES = YES;
354 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
355 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
356 | CLANG_WARN_UNREACHABLE_CODE = YES;
357 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
358 | COPY_PHASE_STRIP = NO;
359 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
360 | ENABLE_NS_ASSERTIONS = NO;
361 | ENABLE_STRICT_OBJC_MSGSEND = YES;
362 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
363 | GCC_C_LANGUAGE_STANDARD = gnu11;
364 | GCC_NO_COMMON_BLOCKS = YES;
365 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
366 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
367 | GCC_WARN_UNDECLARED_SELECTOR = YES;
368 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
369 | GCC_WARN_UNUSED_FUNCTION = YES;
370 | GCC_WARN_UNUSED_VARIABLE = YES;
371 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
372 | MTL_ENABLE_DEBUG_INFO = NO;
373 | MTL_FAST_MATH = YES;
374 | SDKROOT = iphoneos;
375 | SWIFT_COMPILATION_MODE = wholemodule;
376 | SWIFT_OPTIMIZATION_LEVEL = "-O";
377 | VALIDATE_PRODUCT = YES;
378 | };
379 | name = Release;
380 | };
381 | 5F5263142AB027DA0087E46B /* Debug */ = {
382 | isa = XCBuildConfiguration;
383 | buildSettings = {
384 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
385 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
386 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
387 | CODE_SIGN_ENTITLEMENTS = WhispererKeyboard/WhispererKeyboard.entitlements;
388 | CODE_SIGN_STYLE = Automatic;
389 | CURRENT_PROJECT_VERSION = 1;
390 | DEVELOPMENT_ASSET_PATHS = "\"WhispererKeyboard/Preview Content\"";
391 | DEVELOPMENT_TEAM = C9A7ZND9S4;
392 | ENABLE_PREVIEWS = YES;
393 | GENERATE_INFOPLIST_FILE = YES;
394 | INFOPLIST_FILE = WhispererKeyboard/Info.plist;
395 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Grant permission to access microphone";
396 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
397 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
398 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
399 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
400 | LD_RUNPATH_SEARCH_PATHS = (
401 | "$(inherited)",
402 | "@executable_path/Frameworks",
403 | );
404 | MARKETING_VERSION = 1.0;
405 | PRODUCT_BUNDLE_IDENTIFIER = lcf.WhispererKeyboard;
406 | PRODUCT_NAME = "$(TARGET_NAME)";
407 | SWIFT_EMIT_LOC_STRINGS = YES;
408 | SWIFT_VERSION = 5.0;
409 | TARGETED_DEVICE_FAMILY = "1,2";
410 | };
411 | name = Debug;
412 | };
413 | 5F5263152AB027DA0087E46B /* Release */ = {
414 | isa = XCBuildConfiguration;
415 | buildSettings = {
416 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
417 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
418 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
419 | CODE_SIGN_ENTITLEMENTS = WhispererKeyboard/WhispererKeyboard.entitlements;
420 | CODE_SIGN_STYLE = Automatic;
421 | CURRENT_PROJECT_VERSION = 1;
422 | DEVELOPMENT_ASSET_PATHS = "\"WhispererKeyboard/Preview Content\"";
423 | DEVELOPMENT_TEAM = C9A7ZND9S4;
424 | ENABLE_PREVIEWS = YES;
425 | GENERATE_INFOPLIST_FILE = YES;
426 | INFOPLIST_FILE = WhispererKeyboard/Info.plist;
427 | INFOPLIST_KEY_NSMicrophoneUsageDescription = "Grant permission to access microphone";
428 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
429 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
430 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
431 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
432 | LD_RUNPATH_SEARCH_PATHS = (
433 | "$(inherited)",
434 | "@executable_path/Frameworks",
435 | );
436 | MARKETING_VERSION = 1.0;
437 | PRODUCT_BUNDLE_IDENTIFIER = lcf.WhispererKeyboard;
438 | PRODUCT_NAME = "$(TARGET_NAME)";
439 | SWIFT_EMIT_LOC_STRINGS = YES;
440 | SWIFT_VERSION = 5.0;
441 | TARGETED_DEVICE_FAMILY = "1,2";
442 | };
443 | name = Release;
444 | };
445 | 5F5263232AB029F60087E46B /* Debug */ = {
446 | isa = XCBuildConfiguration;
447 | buildSettings = {
448 | CODE_SIGN_ENTITLEMENTS = keyboard/keyboard.entitlements;
449 | CODE_SIGN_STYLE = Automatic;
450 | CURRENT_PROJECT_VERSION = 1;
451 | DEVELOPMENT_TEAM = C9A7ZND9S4;
452 | GENERATE_INFOPLIST_FILE = YES;
453 | INFOPLIST_FILE = keyboard/Info.plist;
454 | INFOPLIST_KEY_CFBundleDisplayName = keyboard;
455 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
456 | LD_RUNPATH_SEARCH_PATHS = (
457 | "$(inherited)",
458 | "@executable_path/Frameworks",
459 | "@executable_path/../../Frameworks",
460 | );
461 | MARKETING_VERSION = 1.0;
462 | PRODUCT_BUNDLE_IDENTIFIER = lcf.WhispererKeyboard.keyboard;
463 | PRODUCT_NAME = "$(TARGET_NAME)";
464 | SKIP_INSTALL = YES;
465 | SWIFT_EMIT_LOC_STRINGS = YES;
466 | SWIFT_VERSION = 5.0;
467 | TARGETED_DEVICE_FAMILY = "1,2";
468 | };
469 | name = Debug;
470 | };
471 | 5F5263242AB029F60087E46B /* Release */ = {
472 | isa = XCBuildConfiguration;
473 | buildSettings = {
474 | CODE_SIGN_ENTITLEMENTS = keyboard/keyboard.entitlements;
475 | CODE_SIGN_STYLE = Automatic;
476 | CURRENT_PROJECT_VERSION = 1;
477 | DEVELOPMENT_TEAM = C9A7ZND9S4;
478 | GENERATE_INFOPLIST_FILE = YES;
479 | INFOPLIST_FILE = keyboard/Info.plist;
480 | INFOPLIST_KEY_CFBundleDisplayName = keyboard;
481 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
482 | LD_RUNPATH_SEARCH_PATHS = (
483 | "$(inherited)",
484 | "@executable_path/Frameworks",
485 | "@executable_path/../../Frameworks",
486 | );
487 | MARKETING_VERSION = 1.0;
488 | PRODUCT_BUNDLE_IDENTIFIER = lcf.WhispererKeyboard.keyboard;
489 | PRODUCT_NAME = "$(TARGET_NAME)";
490 | SKIP_INSTALL = YES;
491 | SWIFT_EMIT_LOC_STRINGS = YES;
492 | SWIFT_VERSION = 5.0;
493 | TARGETED_DEVICE_FAMILY = "1,2";
494 | };
495 | name = Release;
496 | };
497 | /* End XCBuildConfiguration section */
498 |
499 | /* Begin XCConfigurationList section */
500 | 5F5263002AB027D90087E46B /* Build configuration list for PBXProject "WhispererKeyboard" */ = {
501 | isa = XCConfigurationList;
502 | buildConfigurations = (
503 | 5F5263112AB027DA0087E46B /* Debug */,
504 | 5F5263122AB027DA0087E46B /* Release */,
505 | );
506 | defaultConfigurationIsVisible = 0;
507 | defaultConfigurationName = Release;
508 | };
509 | 5F5263132AB027DA0087E46B /* Build configuration list for PBXNativeTarget "WhispererKeyboard" */ = {
510 | isa = XCConfigurationList;
511 | buildConfigurations = (
512 | 5F5263142AB027DA0087E46B /* Debug */,
513 | 5F5263152AB027DA0087E46B /* Release */,
514 | );
515 | defaultConfigurationIsVisible = 0;
516 | defaultConfigurationName = Release;
517 | };
518 | 5F5263222AB029F60087E46B /* Build configuration list for PBXNativeTarget "keyboard" */ = {
519 | isa = XCConfigurationList;
520 | buildConfigurations = (
521 | 5F5263232AB029F60087E46B /* Debug */,
522 | 5F5263242AB029F60087E46B /* Release */,
523 | );
524 | defaultConfigurationIsVisible = 0;
525 | defaultConfigurationName = Release;
526 | };
527 | /* End XCConfigurationList section */
528 | };
529 | rootObject = 5F5262FD2AB027D90087E46B /* Project object */;
530 | }
531 |
--------------------------------------------------------------------------------
/WhispererKeyboard.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/WhispererKeyboard.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/WhispererKeyboard.xcodeproj/xcshareddata/xcschemes/WhispererKeyboard.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/WhispererKeyboard.xcodeproj/xcshareddata/xcschemes/keyboard.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
16 |
22 |
23 |
24 |
30 |
36 |
37 |
38 |
39 |
40 |
46 |
47 |
59 |
63 |
64 |
65 |
71 |
72 |
73 |
74 |
82 |
84 |
90 |
91 |
92 |
93 |
95 |
96 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/WhispererKeyboard/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/WhispererKeyboard/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/WhispererKeyboard/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/WhispererKeyboard/Audio.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Audio.swift
3 | // WhispererKeyboard
4 | //
5 | // Created by Alexander Steshenko on 10/2/23.
6 | //
7 |
8 | import Foundation
9 | import AVFoundation
10 |
11 |
12 | /// Records audio from microphone to a predefine file "recording.m4a".
13 | /// To get file name use function getFilename
14 | /// Use start() and stop() to operate the recorder
15 | class Audio {
16 | var recorder: AVAudioRecorder?
17 |
18 | class AudioRecorderDelegate: NSObject, AVAudioRecorderDelegate {
19 | var onError: ((Error?) -> Void)?
20 |
21 | func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
22 | onError?(error)
23 | }
24 | }
25 |
26 | private let audioRecorderDelegate = AudioRecorderDelegate()
27 |
28 | let audioSettings: [String: Any] = [
29 | AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
30 | AVSampleRateKey: 44100,
31 | AVNumberOfChannelsKey: 1,
32 | AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
33 | ]
34 |
35 | func stop() {
36 | recorder?.stop()
37 | }
38 |
39 | func start() {
40 | requestMicrophonePermission() // only requests permissions if not previously granted
41 | let audioSession = AVAudioSession.sharedInstance()
42 | do {
43 | try audioSession.setCategory(.record, mode: .default)
44 | try audioSession.setActive(true)
45 | } catch {
46 | print("Failed to set up audio session: \(error)")
47 | return
48 | }
49 |
50 | do {
51 | recorder = try AVAudioRecorder(url: getFilename(), settings: audioSettings)
52 | recorder?.delegate = audioRecorderDelegate
53 | recorder?.record()
54 |
55 | } catch {
56 | print("Could not start recording: \(error)")
57 | }
58 | }
59 |
60 | func getFilename() -> URL {
61 | return URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("recording.m4a")
62 | }
63 |
64 | func requestMicrophonePermission() {
65 | let audioSession = AVAudioSession.sharedInstance()
66 |
67 | switch audioSession.recordPermission {
68 | case .granted:
69 | // Microphone permission already granted
70 | break
71 | case .denied:
72 | print("Microphone access has been denied.")
73 | case .undetermined:
74 | audioSession.requestRecordPermission { allowed in
75 | DispatchQueue.main.async {
76 | if !allowed {
77 | // Handle the denial.
78 | print("Microphone access was denied.")
79 | }
80 | }
81 | }
82 | @unknown default:
83 | print("Unknown microphone access status.")
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/WhispererKeyboard/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleURLTypes
6 |
7 |
8 | CFBundleTypeRole
9 | Editor
10 | CFBundleURLSchemes
11 |
12 | WhispererKeyboardApp
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/WhispererKeyboard/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/WhispererKeyboard/Transcription.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Transcription.swift
3 | // WhispererKeyboard
4 | //
5 | // Created by Alexander Steshenko on 10/2/23.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | /// Perform transcription of a given audio file using OpenAI Whisperer API
12 | /// Results are stored in shared group.WhispererKeyboardSharing storage
13 | ///
14 | /// Maintains internal "status" property to show status of transcription, useful when transcription takes a few seconds
15 | ///
16 | class Transcription : ObservableObject {
17 |
18 | enum TranscriptionStatus {
19 | case recording
20 | case transcribing
21 | case finished
22 | case error
23 | }
24 |
25 | // Default status, before transcription is called audio is recorded
26 | @Published var status: TranscriptionStatus = .recording
27 |
28 | // This shared container is necessary to pass data between the main app and the keyboard extension
29 | // Since it's not possible to access microphone from within the keyboard itself
30 | let sharedDefaults = UserDefaults(suiteName: "group.WhispererKeyboardSharing")
31 |
32 | func transcribe(_ audioFilename : URL) {
33 | self.status = .transcribing
34 | do {
35 | sendRequestToOpenAI(file: try Data(contentsOf: audioFilename)) {
36 | (result:Result) in
37 | switch result {
38 | case .success(let text):
39 | // On successful transcription using OpenAI Whisperer, store the results into shared storage
40 | // so that the Keyboard extension can find it and insert into the application under edit
41 | self.sharedDefaults?.set(text, forKey: "transcribedText")
42 | case .failure(let failure):
43 | print("\(failure.localizedDescription)")
44 | }
45 | DispatchQueue.main.async {
46 | self.status = .finished
47 | }
48 | }
49 | } catch {
50 | print(error)
51 | status = .error
52 | return
53 | }
54 | }
55 |
56 | // TODO: This should be moved to iOS secret management solution (not a real API key here)
57 | private let OPENAI_API_KEY = "s-kLYgLIU693MfwDxiEnX9TRlB3Fbk6dJzBCaPtuCI2I3kyoJu2"
58 |
59 | struct WhispererResponse: Codable {
60 | public let text: String
61 | }
62 |
63 | func sendRequestToOpenAI(file: Data, completion: @escaping (Result) -> Void) {
64 | let url = URL(string: "https://api.openai.com/v1/audio/transcriptions")!
65 | var request = URLRequest(url: url)
66 |
67 | request.httpMethod = "POST"
68 | request.addValue("application/json", forHTTPHeaderField: "Accept")
69 | request.addValue("multipart/form-data", forHTTPHeaderField: "Content-Type")
70 | request.setValue("Bearer \(OPENAI_API_KEY)", forHTTPHeaderField: "Authorization")
71 | request.addValue("gzip", forHTTPHeaderField: "Accept-Encoding")
72 |
73 | // Audio file is sent to OpenAI as multipart form data. There is probably an easier way to do this with a built-in library
74 | let boundary = UUID().uuidString
75 | request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
76 |
77 | var formData = Data()
78 | formData.append("--\(boundary)\r\n".data(using: .utf8)!)
79 | formData.append("Content-Disposition: form-data; name=\"file\"; filename=\"recording.m4a\"\r\n".data(using: .utf8)!)
80 | formData.append("\r\n".data(using: .utf8)!)
81 | formData.append(file)
82 | formData.append("\r\n".data(using: .utf8)!)
83 |
84 | // This specifies the model to use "whisper-1"
85 | formData.append("--\(boundary)\r\n".data(using: .utf8)!)
86 | formData.append("Content-Disposition: form-data; name=\"model\"\r\n\r\nwhisper-1\r\n".data(using: .utf8)!)
87 |
88 | formData.append("--\(boundary)--\r\n".data(using: .utf8)!)
89 |
90 | request.httpBody = formData
91 |
92 | // Below makes the http request and passes the resulting text to the callback function
93 | let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
94 | do {
95 | let response = try JSONDecoder().decode(WhispererResponse.self, from: data!)
96 | completion(.success(response.text))
97 | } catch let decodingError {
98 |
99 | completion(.failure(decodingError))
100 | }
101 | }
102 | task.resume()
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/WhispererKeyboard/WhispererKeyboard.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.WhispererKeyboardSharing
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/WhispererKeyboard/WhispererKeyboardApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WhispererKeyboardApp.swift
3 | // WhispererKeyboard
4 | //
5 | // Created by Alexander Steshenko on 9/11/23.
6 | //
7 |
8 | import SwiftUI
9 | import Foundation
10 |
11 | /// This is a full screen view that opens up when "Record audio" button is clicked in the keyboard extension
12 | /// When open, the app automatically begins recording. Once finished, the application requests transcriptiong using OpenAI Whisperer API
13 | /// The app then suggests the user to return to the app that had the keyboard open. Unfortunately found no way to return user automatically.
14 | @main
15 | struct WhispererKeyboardApp: App {
16 |
17 | // contains logic for capturing audio from the microphone and saving into a temporary file
18 | private var audio = Audio()
19 |
20 | // contains logic for sending data for transcription to OpenAI and storing results into shared app storage
21 | @StateObject private var transcription = Transcription()
22 |
23 | // Necessary to detect when application becomes active. Begin recording immediately
24 | @Environment(\.scenePhase) var scenePhase
25 |
26 | var body: some Scene {
27 | // Will show one clickable text at at the bottom of the screen to control recording
28 | // Positioned at the bottom so it's convenient to swipe back to the previous app
29 | WindowGroup {
30 | VStack {
31 | Spacer()
32 | Text(getTranscriptionStatusMessage())
33 | .onTapGesture(count: 1, perform: {
34 | if self.transcription.status != .finished {
35 | // Request to transcribe is what stops the audio recording
36 | self.audio.stop()
37 | self.transcription.transcribe(audio.getFilename())
38 | }
39 | })
40 | }
41 | .padding()
42 | .onChange(of: scenePhase) { newPhase in
43 | if newPhase == .active {
44 | // When app loads, start recording immediately.
45 | // This auxiliary appliation is to workaround iOS restrictions to record audio from within keyboard extension
46 | transcription.status = .recording
47 | self.audio.start()
48 | }
49 | }
50 | }
51 | }
52 |
53 | func getTranscriptionStatusMessage() -> String {
54 | switch transcription.status {
55 | case .recording:
56 | return "Press to stop recording"
57 | case .transcribing:
58 | return "Transcribing ..."
59 | case .finished:
60 | return "Finished. Return to the application"
61 | case .error:
62 | return "Error. Try again later"
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/example.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lcf/WhispererKeyboard/7d4bacdb944f2c1e4d8f4cc6a90b2608ba641237/example.jpg
--------------------------------------------------------------------------------
/keyboard/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionAttributes
8 |
9 | IsASCIICapable
10 |
11 | PrefersRightToLeft
12 |
13 | PrimaryLanguage
14 | en-US
15 | RequestsOpenAccess
16 |
17 |
18 | NSExtensionPointIdentifier
19 | com.apple.keyboard-service
20 | NSExtensionPrincipalClass
21 | $(PRODUCT_MODULE_NAME).KeyboardViewController
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/keyboard/KeyboardViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyboardViewController.swift
3 | // keyboard
4 | //
5 | // Created by Alexander Steshenko on 9/11/23.
6 | //
7 |
8 | import UIKit
9 | import AVFoundation
10 | import SwiftUI
11 |
12 | /// Custom keyboard view. This keyboard only has one button in the center which begins recording once pressed
13 | /// Recording is performed by the main app "WhispererKeyboardApp" immediately when it opens
14 | /// After recording is processed by OpenAI, the keyboard inserts the text into the text edit field that is in focus
15 | class KeyboardViewController: UIInputViewController {
16 |
17 | // The following block is required to keep the height of the keyboard from fluctuating.
18 | var keyboardHeight: CGFloat = 155
19 | var KeyboardVCHeightConstraint: NSLayoutConstraint!
20 | var containerViewHeight: CGFloat = 0
21 |
22 | override func viewWillAppear(_ animated: Bool) {
23 | super.viewWillAppear(animated)
24 | self.view.removeConstraint(KeyboardVCHeightConstraint)
25 | self.view.addConstraint(self.KeyboardVCHeightConstraint)
26 | }
27 |
28 | // This shared container is necessary to pass data between the main app and the keyboard extension
29 | // Since it's not possible to access microphone from within the keyboard itself
30 | let sharedDefaults = UserDefaults(suiteName: "group.WhispererKeyboardSharing")
31 |
32 | // This function is called every time when keyboard becomes visible
33 | // It may be after transcription just finished in the main app
34 | // Keyboard extension checks if data is available in the shared container
35 | // and inserts it into the text edit field
36 | override func viewDidAppear(_ animated: Bool) {
37 | super.viewDidAppear(animated)
38 |
39 | // Whenever keyboard becomes visible on the screen, it may be after transcription has finished.
40 | if let transcribedText = sharedDefaults?.string(forKey: "transcribedText") {
41 | self.textDocumentProxy.insertText(transcribedText)
42 | sharedDefaults?.removeObject(forKey: "transcribedText")
43 | }
44 | }
45 |
46 | // The keyboard has the only button that opens the main app when clicked
47 | // Configuration of the button is in the viewDidLoad method
48 | let recordButton: UIButton = UIButton(type: .system)
49 |
50 | override func viewDidLoad() {
51 | super.viewDidLoad()
52 | // Keyboard height settings
53 | self.KeyboardVCHeightConstraint = NSLayoutConstraint(
54 | item: self.view!, attribute: .height, relatedBy: .equal, toItem: nil,
55 | attribute: .notAnAttribute, multiplier: 1, constant: keyboardHeight+containerViewHeight)
56 |
57 | // Add record button to the keyboard view
58 | self.view.addSubview(recordButton)
59 | recordButton.setTitle("Record audio", for: .normal)
60 | recordButton.sizeToFit()
61 | recordButton.translatesAutoresizingMaskIntoConstraints = false
62 |
63 | recordButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive=true
64 | recordButton.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive=true
65 |
66 | // Add tap gesture recognizer
67 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleButtonTap))
68 | recordButton.addGestureRecognizer(tapGesture)
69 |
70 |
71 | }
72 |
73 | // The following block of functions was generated by ChatGPT. It opens the main application when the record button is tapped.
74 |
75 | // the Application scheme for interlinking is configered in Info.plist file or in XCode UI
76 | @objc func handleButtonTap() {
77 | self.openURL(url: NSURL(string:"WhispererKeyboardApp://")!)
78 | }
79 |
80 | func openURL(url: NSURL) -> Bool {
81 | do {
82 | let application = try self.sharedApplication()
83 | application.performSelector(inBackground: "openURL:", with: url)
84 | return true
85 | }
86 | catch {
87 | return false
88 | }
89 | }
90 |
91 | func sharedApplication() throws -> UIApplication {
92 | var responder: UIResponder? = self
93 | while responder != nil {
94 | if let application = responder as? UIApplication {
95 | return application
96 | }
97 |
98 | responder = responder?.next
99 | }
100 |
101 | throw NSError(domain: "UIInputViewController+sharedApplication.swift", code: 1, userInfo: nil)
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/keyboard/keyboard.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.WhispererKeyboardSharing
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------