Spaces:
Sleeping
Sleeping
matt HOFFNER
commited on
Commit
·
faa5faf
1
Parent(s):
7df6d3d
fixes to recurring recordings
Browse files- app/BlobFix.ts +549 -0
- app/hooks/useAudioManager.ts +0 -48
- app/input.tsx +141 -97
app/BlobFix.ts
ADDED
@@ -0,0 +1,549 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/*
|
2 |
+
* There is a bug where `navigator.mediaDevices.getUserMedia` + `MediaRecorder`
|
3 |
+
* creates WEBM files without duration metadata. See:
|
4 |
+
* - https://bugs.chromium.org/p/chromium/issues/detail?id=642012
|
5 |
+
* - https://stackoverflow.com/a/39971175/13989043
|
6 |
+
*
|
7 |
+
* This file contains a function that fixes the duration metadata of a WEBM file.
|
8 |
+
* - Answer found: https://stackoverflow.com/a/75218309/13989043
|
9 |
+
* - Code adapted from: https://github.com/mat-sz/webm-fix-duration
|
10 |
+
* (forked from https://github.com/yusitnikov/fix-webm-duration)
|
11 |
+
*/
|
12 |
+
|
13 |
+
/*
|
14 |
+
* This is the list of possible WEBM file sections by their IDs.
|
15 |
+
* Possible types: Container, Binary, Uint, Int, String, Float, Date
|
16 |
+
*/
|
17 |
+
interface Section {
|
18 |
+
name: string;
|
19 |
+
type: string;
|
20 |
+
}
|
21 |
+
|
22 |
+
const sections: Record<number, Section> = {
|
23 |
+
0xa45dfa3: { name: "EBML", type: "Container" },
|
24 |
+
0x286: { name: "EBMLVersion", type: "Uint" },
|
25 |
+
0x2f7: { name: "EBMLReadVersion", type: "Uint" },
|
26 |
+
0x2f2: { name: "EBMLMaxIDLength", type: "Uint" },
|
27 |
+
0x2f3: { name: "EBMLMaxSizeLength", type: "Uint" },
|
28 |
+
0x282: { name: "DocType", type: "String" },
|
29 |
+
0x287: { name: "DocTypeVersion", type: "Uint" },
|
30 |
+
0x285: { name: "DocTypeReadVersion", type: "Uint" },
|
31 |
+
0x6c: { name: "Void", type: "Binary" },
|
32 |
+
0x3f: { name: "CRC-32", type: "Binary" },
|
33 |
+
0xb538667: { name: "SignatureSlot", type: "Container" },
|
34 |
+
0x3e8a: { name: "SignatureAlgo", type: "Uint" },
|
35 |
+
0x3e9a: { name: "SignatureHash", type: "Uint" },
|
36 |
+
0x3ea5: { name: "SignaturePublicKey", type: "Binary" },
|
37 |
+
0x3eb5: { name: "Signature", type: "Binary" },
|
38 |
+
0x3e5b: { name: "SignatureElements", type: "Container" },
|
39 |
+
0x3e7b: { name: "SignatureElementList", type: "Container" },
|
40 |
+
0x2532: { name: "SignedElement", type: "Binary" },
|
41 |
+
0x8538067: { name: "Segment", type: "Container" },
|
42 |
+
0x14d9b74: { name: "SeekHead", type: "Container" },
|
43 |
+
0xdbb: { name: "Seek", type: "Container" },
|
44 |
+
0x13ab: { name: "SeekID", type: "Binary" },
|
45 |
+
0x13ac: { name: "SeekPosition", type: "Uint" },
|
46 |
+
0x549a966: { name: "Info", type: "Container" },
|
47 |
+
0x33a4: { name: "SegmentUID", type: "Binary" },
|
48 |
+
0x3384: { name: "SegmentFilename", type: "String" },
|
49 |
+
0x1cb923: { name: "PrevUID", type: "Binary" },
|
50 |
+
0x1c83ab: { name: "PrevFilename", type: "String" },
|
51 |
+
0x1eb923: { name: "NextUID", type: "Binary" },
|
52 |
+
0x1e83bb: { name: "NextFilename", type: "String" },
|
53 |
+
0x444: { name: "SegmentFamily", type: "Binary" },
|
54 |
+
0x2924: { name: "ChapterTranslate", type: "Container" },
|
55 |
+
0x29fc: { name: "ChapterTranslateEditionUID", type: "Uint" },
|
56 |
+
0x29bf: { name: "ChapterTranslateCodec", type: "Uint" },
|
57 |
+
0x29a5: { name: "ChapterTranslateID", type: "Binary" },
|
58 |
+
0xad7b1: { name: "TimecodeScale", type: "Uint" },
|
59 |
+
0x489: { name: "Duration", type: "Float" },
|
60 |
+
0x461: { name: "DateUTC", type: "Date" },
|
61 |
+
0x3ba9: { name: "Title", type: "String" },
|
62 |
+
0xd80: { name: "MuxingApp", type: "String" },
|
63 |
+
0x1741: { name: "WritingApp", type: "String" },
|
64 |
+
// 0xf43b675: { name: 'Cluster', type: 'Container' },
|
65 |
+
0x67: { name: "Timecode", type: "Uint" },
|
66 |
+
0x1854: { name: "SilentTracks", type: "Container" },
|
67 |
+
0x18d7: { name: "SilentTrackNumber", type: "Uint" },
|
68 |
+
0x27: { name: "Position", type: "Uint" },
|
69 |
+
0x2b: { name: "PrevSize", type: "Uint" },
|
70 |
+
0x23: { name: "SimpleBlock", type: "Binary" },
|
71 |
+
0x20: { name: "BlockGroup", type: "Container" },
|
72 |
+
0x21: { name: "Block", type: "Binary" },
|
73 |
+
0x22: { name: "BlockVirtual", type: "Binary" },
|
74 |
+
0x35a1: { name: "BlockAdditions", type: "Container" },
|
75 |
+
0x26: { name: "BlockMore", type: "Container" },
|
76 |
+
0x6e: { name: "BlockAddID", type: "Uint" },
|
77 |
+
0x25: { name: "BlockAdditional", type: "Binary" },
|
78 |
+
0x1b: { name: "BlockDuration", type: "Uint" },
|
79 |
+
0x7a: { name: "ReferencePriority", type: "Uint" },
|
80 |
+
0x7b: { name: "ReferenceBlock", type: "Int" },
|
81 |
+
0x7d: { name: "ReferenceVirtual", type: "Int" },
|
82 |
+
0x24: { name: "CodecState", type: "Binary" },
|
83 |
+
0x35a2: { name: "DiscardPadding", type: "Int" },
|
84 |
+
0xe: { name: "Slices", type: "Container" },
|
85 |
+
0x68: { name: "TimeSlice", type: "Container" },
|
86 |
+
0x4c: { name: "LaceNumber", type: "Uint" },
|
87 |
+
0x4d: { name: "FrameNumber", type: "Uint" },
|
88 |
+
0x4b: { name: "BlockAdditionID", type: "Uint" },
|
89 |
+
0x4e: { name: "Delay", type: "Uint" },
|
90 |
+
0x4f: { name: "SliceDuration", type: "Uint" },
|
91 |
+
0x48: { name: "ReferenceFrame", type: "Container" },
|
92 |
+
0x49: { name: "ReferenceOffset", type: "Uint" },
|
93 |
+
0x4a: { name: "ReferenceTimeCode", type: "Uint" },
|
94 |
+
0x2f: { name: "EncryptedBlock", type: "Binary" },
|
95 |
+
0x654ae6b: { name: "Tracks", type: "Container" },
|
96 |
+
0x2e: { name: "TrackEntry", type: "Container" },
|
97 |
+
0x57: { name: "TrackNumber", type: "Uint" },
|
98 |
+
0x33c5: { name: "TrackUID", type: "Uint" },
|
99 |
+
0x3: { name: "TrackType", type: "Uint" },
|
100 |
+
0x39: { name: "FlagEnabled", type: "Uint" },
|
101 |
+
0x8: { name: "FlagDefault", type: "Uint" },
|
102 |
+
0x15aa: { name: "FlagForced", type: "Uint" },
|
103 |
+
0x1c: { name: "FlagLacing", type: "Uint" },
|
104 |
+
0x2de7: { name: "MinCache", type: "Uint" },
|
105 |
+
0x2df8: { name: "MaxCache", type: "Uint" },
|
106 |
+
0x3e383: { name: "DefaultDuration", type: "Uint" },
|
107 |
+
0x34e7a: { name: "DefaultDecodedFieldDuration", type: "Uint" },
|
108 |
+
0x3314f: { name: "TrackTimecodeScale", type: "Float" },
|
109 |
+
0x137f: { name: "TrackOffset", type: "Int" },
|
110 |
+
0x15ee: { name: "MaxBlockAdditionID", type: "Uint" },
|
111 |
+
0x136e: { name: "Name", type: "String" },
|
112 |
+
0x2b59c: { name: "Language", type: "String" },
|
113 |
+
0x6: { name: "CodecID", type: "String" },
|
114 |
+
0x23a2: { name: "CodecPrivate", type: "Binary" },
|
115 |
+
0x58688: { name: "CodecName", type: "String" },
|
116 |
+
0x3446: { name: "AttachmentLink", type: "Uint" },
|
117 |
+
0x1a9697: { name: "CodecSettings", type: "String" },
|
118 |
+
0x1b4040: { name: "CodecInfoURL", type: "String" },
|
119 |
+
0x6b240: { name: "CodecDownloadURL", type: "String" },
|
120 |
+
0x2a: { name: "CodecDecodeAll", type: "Uint" },
|
121 |
+
0x2fab: { name: "TrackOverlay", type: "Uint" },
|
122 |
+
0x16aa: { name: "CodecDelay", type: "Uint" },
|
123 |
+
0x16bb: { name: "SeekPreRoll", type: "Uint" },
|
124 |
+
0x2624: { name: "TrackTranslate", type: "Container" },
|
125 |
+
0x26fc: { name: "TrackTranslateEditionUID", type: "Uint" },
|
126 |
+
0x26bf: { name: "TrackTranslateCodec", type: "Uint" },
|
127 |
+
0x26a5: { name: "TrackTranslateTrackID", type: "Binary" },
|
128 |
+
0x60: { name: "Video", type: "Container" },
|
129 |
+
0x1a: { name: "FlagInterlaced", type: "Uint" },
|
130 |
+
0x13b8: { name: "StereoMode", type: "Uint" },
|
131 |
+
0x13c0: { name: "AlphaMode", type: "Uint" },
|
132 |
+
0x13b9: { name: "OldStereoMode", type: "Uint" },
|
133 |
+
0x30: { name: "PixelWidth", type: "Uint" },
|
134 |
+
0x3a: { name: "PixelHeight", type: "Uint" },
|
135 |
+
0x14aa: { name: "PixelCropBottom", type: "Uint" },
|
136 |
+
0x14bb: { name: "PixelCropTop", type: "Uint" },
|
137 |
+
0x14cc: { name: "PixelCropLeft", type: "Uint" },
|
138 |
+
0x14dd: { name: "PixelCropRight", type: "Uint" },
|
139 |
+
0x14b0: { name: "DisplayWidth", type: "Uint" },
|
140 |
+
0x14ba: { name: "DisplayHeight", type: "Uint" },
|
141 |
+
0x14b2: { name: "DisplayUnit", type: "Uint" },
|
142 |
+
0x14b3: { name: "AspectRatioType", type: "Uint" },
|
143 |
+
0xeb524: { name: "ColourSpace", type: "Binary" },
|
144 |
+
0xfb523: { name: "GammaValue", type: "Float" },
|
145 |
+
0x383e3: { name: "FrameRate", type: "Float" },
|
146 |
+
0x61: { name: "Audio", type: "Container" },
|
147 |
+
0x35: { name: "SamplingFrequency", type: "Float" },
|
148 |
+
0x38b5: { name: "OutputSamplingFrequency", type: "Float" },
|
149 |
+
0x1f: { name: "Channels", type: "Uint" },
|
150 |
+
0x3d7b: { name: "ChannelPositions", type: "Binary" },
|
151 |
+
0x2264: { name: "BitDepth", type: "Uint" },
|
152 |
+
0x62: { name: "TrackOperation", type: "Container" },
|
153 |
+
0x63: { name: "TrackCombinePlanes", type: "Container" },
|
154 |
+
0x64: { name: "TrackPlane", type: "Container" },
|
155 |
+
0x65: { name: "TrackPlaneUID", type: "Uint" },
|
156 |
+
0x66: { name: "TrackPlaneType", type: "Uint" },
|
157 |
+
0x69: { name: "TrackJoinBlocks", type: "Container" },
|
158 |
+
0x6d: { name: "TrackJoinUID", type: "Uint" },
|
159 |
+
0x40: { name: "TrickTrackUID", type: "Uint" },
|
160 |
+
0x41: { name: "TrickTrackSegmentUID", type: "Binary" },
|
161 |
+
0x46: { name: "TrickTrackFlag", type: "Uint" },
|
162 |
+
0x47: { name: "TrickMasterTrackUID", type: "Uint" },
|
163 |
+
0x44: { name: "TrickMasterTrackSegmentUID", type: "Binary" },
|
164 |
+
0x2d80: { name: "ContentEncodings", type: "Container" },
|
165 |
+
0x2240: { name: "ContentEncoding", type: "Container" },
|
166 |
+
0x1031: { name: "ContentEncodingOrder", type: "Uint" },
|
167 |
+
0x1032: { name: "ContentEncodingScope", type: "Uint" },
|
168 |
+
0x1033: { name: "ContentEncodingType", type: "Uint" },
|
169 |
+
0x1034: { name: "ContentCompression", type: "Container" },
|
170 |
+
0x254: { name: "ContentCompAlgo", type: "Uint" },
|
171 |
+
0x255: { name: "ContentCompSettings", type: "Binary" },
|
172 |
+
0x1035: { name: "ContentEncryption", type: "Container" },
|
173 |
+
0x7e1: { name: "ContentEncAlgo", type: "Uint" },
|
174 |
+
0x7e2: { name: "ContentEncKeyID", type: "Binary" },
|
175 |
+
0x7e3: { name: "ContentSignature", type: "Binary" },
|
176 |
+
0x7e4: { name: "ContentSigKeyID", type: "Binary" },
|
177 |
+
0x7e5: { name: "ContentSigAlgo", type: "Uint" },
|
178 |
+
0x7e6: { name: "ContentSigHashAlgo", type: "Uint" },
|
179 |
+
0xc53bb6b: { name: "Cues", type: "Container" },
|
180 |
+
0x3b: { name: "CuePoint", type: "Container" },
|
181 |
+
0x33: { name: "CueTime", type: "Uint" },
|
182 |
+
0x37: { name: "CueTrackPositions", type: "Container" },
|
183 |
+
0x77: { name: "CueTrack", type: "Uint" },
|
184 |
+
0x71: { name: "CueClusterPosition", type: "Uint" },
|
185 |
+
0x70: { name: "CueRelativePosition", type: "Uint" },
|
186 |
+
0x32: { name: "CueDuration", type: "Uint" },
|
187 |
+
0x1378: { name: "CueBlockNumber", type: "Uint" },
|
188 |
+
0x6a: { name: "CueCodecState", type: "Uint" },
|
189 |
+
0x5b: { name: "CueReference", type: "Container" },
|
190 |
+
0x16: { name: "CueRefTime", type: "Uint" },
|
191 |
+
0x17: { name: "CueRefCluster", type: "Uint" },
|
192 |
+
0x135f: { name: "CueRefNumber", type: "Uint" },
|
193 |
+
0x6b: { name: "CueRefCodecState", type: "Uint" },
|
194 |
+
0x941a469: { name: "Attachments", type: "Container" },
|
195 |
+
0x21a7: { name: "AttachedFile", type: "Container" },
|
196 |
+
0x67e: { name: "FileDescription", type: "String" },
|
197 |
+
0x66e: { name: "FileName", type: "String" },
|
198 |
+
0x660: { name: "FileMimeType", type: "String" },
|
199 |
+
0x65c: { name: "FileData", type: "Binary" },
|
200 |
+
0x6ae: { name: "FileUID", type: "Uint" },
|
201 |
+
0x675: { name: "FileReferral", type: "Binary" },
|
202 |
+
0x661: { name: "FileUsedStartTime", type: "Uint" },
|
203 |
+
0x662: { name: "FileUsedEndTime", type: "Uint" },
|
204 |
+
0x43a770: { name: "Chapters", type: "Container" },
|
205 |
+
0x5b9: { name: "EditionEntry", type: "Container" },
|
206 |
+
0x5bc: { name: "EditionUID", type: "Uint" },
|
207 |
+
0x5bd: { name: "EditionFlagHidden", type: "Uint" },
|
208 |
+
0x5db: { name: "EditionFlagDefault", type: "Uint" },
|
209 |
+
0x5dd: { name: "EditionFlagOrdered", type: "Uint" },
|
210 |
+
0x36: { name: "ChapterAtom", type: "Container" },
|
211 |
+
0x33c4: { name: "ChapterUID", type: "Uint" },
|
212 |
+
0x1654: { name: "ChapterStringUID", type: "String" },
|
213 |
+
0x11: { name: "ChapterTimeStart", type: "Uint" },
|
214 |
+
0x12: { name: "ChapterTimeEnd", type: "Uint" },
|
215 |
+
0x18: { name: "ChapterFlagHidden", type: "Uint" },
|
216 |
+
0x598: { name: "ChapterFlagEnabled", type: "Uint" },
|
217 |
+
0x2e67: { name: "ChapterSegmentUID", type: "Binary" },
|
218 |
+
0x2ebc: { name: "ChapterSegmentEditionUID", type: "Uint" },
|
219 |
+
0x23c3: { name: "ChapterPhysicalEquiv", type: "Uint" },
|
220 |
+
0xf: { name: "ChapterTrack", type: "Container" },
|
221 |
+
0x9: { name: "ChapterTrackNumber", type: "Uint" },
|
222 |
+
0x0: { name: "ChapterDisplay", type: "Container" },
|
223 |
+
0x5: { name: "ChapString", type: "String" },
|
224 |
+
0x37c: { name: "ChapLanguage", type: "String" },
|
225 |
+
0x37e: { name: "ChapCountry", type: "String" },
|
226 |
+
0x2944: { name: "ChapProcess", type: "Container" },
|
227 |
+
0x2955: { name: "ChapProcessCodecID", type: "Uint" },
|
228 |
+
0x50d: { name: "ChapProcessPrivate", type: "Binary" },
|
229 |
+
0x2911: { name: "ChapProcessCommand", type: "Container" },
|
230 |
+
0x2922: { name: "ChapProcessTime", type: "Uint" },
|
231 |
+
0x2933: { name: "ChapProcessData", type: "Binary" },
|
232 |
+
0x254c367: { name: "Tags", type: "Container" },
|
233 |
+
0x3373: { name: "Tag", type: "Container" },
|
234 |
+
0x23c0: { name: "Targets", type: "Container" },
|
235 |
+
0x28ca: { name: "TargetTypeValue", type: "Uint" },
|
236 |
+
0x23ca: { name: "TargetType", type: "String" },
|
237 |
+
0x23c5: { name: "TagTrackUID", type: "Uint" },
|
238 |
+
0x23c9: { name: "TagEditionUID", type: "Uint" },
|
239 |
+
0x23c4: { name: "TagChapterUID", type: "Uint" },
|
240 |
+
0x23c6: { name: "TagAttachmentUID", type: "Uint" },
|
241 |
+
0x27c8: { name: "SimpleTag", type: "Container" },
|
242 |
+
0x5a3: { name: "TagName", type: "String" },
|
243 |
+
0x47a: { name: "TagLanguage", type: "String" },
|
244 |
+
0x484: { name: "TagDefault", type: "Uint" },
|
245 |
+
0x487: { name: "TagString", type: "String" },
|
246 |
+
0x485: { name: "TagBinary", type: "Binary" },
|
247 |
+
};
|
248 |
+
|
249 |
+
class WebmBase<T> {
|
250 |
+
source?: Uint8Array;
|
251 |
+
data?: T;
|
252 |
+
|
253 |
+
constructor(private name = "Unknown", private type = "Unknown") {}
|
254 |
+
|
255 |
+
updateBySource() {}
|
256 |
+
|
257 |
+
setSource(source: Uint8Array) {
|
258 |
+
this.source = source;
|
259 |
+
this.updateBySource();
|
260 |
+
}
|
261 |
+
|
262 |
+
updateByData() {}
|
263 |
+
|
264 |
+
setData(data: T) {
|
265 |
+
this.data = data;
|
266 |
+
this.updateByData();
|
267 |
+
}
|
268 |
+
}
|
269 |
+
|
270 |
+
class WebmUint extends WebmBase<string> {
|
271 |
+
constructor(name: string, type: string) {
|
272 |
+
super(name, type || "Uint");
|
273 |
+
}
|
274 |
+
|
275 |
+
updateBySource() {
|
276 |
+
// use hex representation of a number instead of number value
|
277 |
+
this.data = "";
|
278 |
+
for (let i = 0; i < this.source!.length; i++) {
|
279 |
+
const hex = this.source![i].toString(16);
|
280 |
+
this.data += padHex(hex);
|
281 |
+
}
|
282 |
+
}
|
283 |
+
|
284 |
+
updateByData() {
|
285 |
+
const length = this.data!.length / 2;
|
286 |
+
this.source = new Uint8Array(length);
|
287 |
+
for (let i = 0; i < length; i++) {
|
288 |
+
const hex = this.data!.substr(i * 2, 2);
|
289 |
+
this.source[i] = parseInt(hex, 16);
|
290 |
+
}
|
291 |
+
}
|
292 |
+
|
293 |
+
getValue() {
|
294 |
+
return parseInt(this.data!, 16);
|
295 |
+
}
|
296 |
+
|
297 |
+
setValue(value: number) {
|
298 |
+
this.setData(padHex(value.toString(16)));
|
299 |
+
}
|
300 |
+
}
|
301 |
+
|
302 |
+
function padHex(hex: string) {
|
303 |
+
return hex.length % 2 === 1 ? "0" + hex : hex;
|
304 |
+
}
|
305 |
+
|
306 |
+
class WebmFloat extends WebmBase<number> {
|
307 |
+
constructor(name: string, type: string) {
|
308 |
+
super(name, type || "Float");
|
309 |
+
}
|
310 |
+
|
311 |
+
getFloatArrayType() {
|
312 |
+
return this.source && this.source.length === 4
|
313 |
+
? Float32Array
|
314 |
+
: Float64Array;
|
315 |
+
}
|
316 |
+
updateBySource() {
|
317 |
+
const byteArray = this.source!.reverse();
|
318 |
+
const floatArrayType = this.getFloatArrayType();
|
319 |
+
const floatArray = new floatArrayType(byteArray.buffer);
|
320 |
+
this.data! = floatArray[0];
|
321 |
+
}
|
322 |
+
updateByData() {
|
323 |
+
const floatArrayType = this.getFloatArrayType();
|
324 |
+
const floatArray = new floatArrayType([this.data!]);
|
325 |
+
const byteArray = new Uint8Array(floatArray.buffer);
|
326 |
+
this.source = byteArray.reverse();
|
327 |
+
}
|
328 |
+
getValue() {
|
329 |
+
return this.data;
|
330 |
+
}
|
331 |
+
setValue(value: number) {
|
332 |
+
this.setData(value);
|
333 |
+
}
|
334 |
+
}
|
335 |
+
|
336 |
+
interface ContainerData {
|
337 |
+
id: number;
|
338 |
+
idHex?: string;
|
339 |
+
data: WebmBase<any>;
|
340 |
+
}
|
341 |
+
|
342 |
+
class WebmContainer extends WebmBase<ContainerData[]> {
|
343 |
+
offset: number = 0;
|
344 |
+
data: ContainerData[] = [];
|
345 |
+
|
346 |
+
constructor(name: string, type: string) {
|
347 |
+
super(name, type || "Container");
|
348 |
+
}
|
349 |
+
|
350 |
+
readByte() {
|
351 |
+
return this.source![this.offset++];
|
352 |
+
}
|
353 |
+
readUint() {
|
354 |
+
const firstByte = this.readByte();
|
355 |
+
const bytes = 8 - firstByte.toString(2).length;
|
356 |
+
let value = firstByte - (1 << (7 - bytes));
|
357 |
+
for (let i = 0; i < bytes; i++) {
|
358 |
+
// don't use bit operators to support x86
|
359 |
+
value *= 256;
|
360 |
+
value += this.readByte();
|
361 |
+
}
|
362 |
+
return value;
|
363 |
+
}
|
364 |
+
updateBySource() {
|
365 |
+
let end: number | undefined = undefined;
|
366 |
+
this.data = [];
|
367 |
+
for (
|
368 |
+
this.offset = 0;
|
369 |
+
this.offset < this.source!.length;
|
370 |
+
this.offset = end
|
371 |
+
) {
|
372 |
+
const id = this.readUint();
|
373 |
+
const len = this.readUint();
|
374 |
+
end = Math.min(this.offset + len, this.source!.length);
|
375 |
+
const data = this.source!.slice(this.offset, end);
|
376 |
+
|
377 |
+
const info = sections[id] || { name: "Unknown", type: "Unknown" };
|
378 |
+
let ctr: any = WebmBase;
|
379 |
+
switch (info.type) {
|
380 |
+
case "Container":
|
381 |
+
ctr = WebmContainer;
|
382 |
+
break;
|
383 |
+
case "Uint":
|
384 |
+
ctr = WebmUint;
|
385 |
+
break;
|
386 |
+
case "Float":
|
387 |
+
ctr = WebmFloat;
|
388 |
+
break;
|
389 |
+
}
|
390 |
+
const section = new ctr(info.name, info.type);
|
391 |
+
section.setSource(data);
|
392 |
+
this.data.push({
|
393 |
+
id: id,
|
394 |
+
idHex: id.toString(16),
|
395 |
+
data: section,
|
396 |
+
});
|
397 |
+
}
|
398 |
+
}
|
399 |
+
writeUint(x: number, draft = false) {
|
400 |
+
for (
|
401 |
+
var bytes = 1, flag = 0x80;
|
402 |
+
x >= flag && bytes < 8;
|
403 |
+
bytes++, flag *= 0x80
|
404 |
+
) {}
|
405 |
+
|
406 |
+
if (!draft) {
|
407 |
+
let value = flag + x;
|
408 |
+
for (let i = bytes - 1; i >= 0; i--) {
|
409 |
+
// don't use bit operators to support x86
|
410 |
+
const c = value % 256;
|
411 |
+
this.source![this.offset! + i] = c;
|
412 |
+
value = (value - c) / 256;
|
413 |
+
}
|
414 |
+
}
|
415 |
+
|
416 |
+
this.offset += bytes;
|
417 |
+
}
|
418 |
+
|
419 |
+
writeSections(draft = false) {
|
420 |
+
this.offset = 0;
|
421 |
+
for (let i = 0; i < this.data.length; i++) {
|
422 |
+
const section = this.data[i],
|
423 |
+
content = section.data.source,
|
424 |
+
contentLength = content!.length;
|
425 |
+
this.writeUint(section.id, draft);
|
426 |
+
this.writeUint(contentLength, draft);
|
427 |
+
if (!draft) {
|
428 |
+
this.source!.set(content!, this.offset);
|
429 |
+
}
|
430 |
+
this.offset += contentLength;
|
431 |
+
}
|
432 |
+
return this.offset;
|
433 |
+
}
|
434 |
+
|
435 |
+
updateByData() {
|
436 |
+
// run without accessing this.source to determine total length - need to know it to create Uint8Array
|
437 |
+
const length = this.writeSections(true);
|
438 |
+
this.source = new Uint8Array(length);
|
439 |
+
// now really write data
|
440 |
+
this.writeSections();
|
441 |
+
}
|
442 |
+
|
443 |
+
getSectionById(id: number) {
|
444 |
+
for (let i = 0; i < this.data.length; i++) {
|
445 |
+
const section = this.data[i];
|
446 |
+
if (section.id === id) {
|
447 |
+
return section.data;
|
448 |
+
}
|
449 |
+
}
|
450 |
+
|
451 |
+
return undefined;
|
452 |
+
}
|
453 |
+
}
|
454 |
+
|
455 |
+
class WebmFile extends WebmContainer {
|
456 |
+
constructor(source: Uint8Array) {
|
457 |
+
super("File", "File");
|
458 |
+
this.setSource(source);
|
459 |
+
}
|
460 |
+
|
461 |
+
fixDuration(duration: number) {
|
462 |
+
const segmentSection = this.getSectionById(0x8538067) as WebmContainer;
|
463 |
+
if (!segmentSection) {
|
464 |
+
return false;
|
465 |
+
}
|
466 |
+
|
467 |
+
const infoSection = segmentSection.getSectionById(
|
468 |
+
0x549a966,
|
469 |
+
) as WebmContainer;
|
470 |
+
if (!infoSection) {
|
471 |
+
return false;
|
472 |
+
}
|
473 |
+
|
474 |
+
const timeScaleSection = infoSection.getSectionById(
|
475 |
+
0xad7b1,
|
476 |
+
) as WebmFloat;
|
477 |
+
if (!timeScaleSection) {
|
478 |
+
return false;
|
479 |
+
}
|
480 |
+
|
481 |
+
let durationSection = infoSection.getSectionById(0x489) as WebmFloat;
|
482 |
+
if (durationSection) {
|
483 |
+
if (durationSection.getValue()! <= 0) {
|
484 |
+
durationSection.setValue(duration);
|
485 |
+
} else {
|
486 |
+
return false;
|
487 |
+
}
|
488 |
+
} else {
|
489 |
+
// append Duration section
|
490 |
+
durationSection = new WebmFloat("Duration", "Float");
|
491 |
+
durationSection.setValue(duration);
|
492 |
+
infoSection.data.push({
|
493 |
+
id: 0x489,
|
494 |
+
data: durationSection,
|
495 |
+
});
|
496 |
+
}
|
497 |
+
|
498 |
+
// set default time scale to 1 millisecond (1000000 nanoseconds)
|
499 |
+
timeScaleSection.setValue(1000000);
|
500 |
+
infoSection.updateByData();
|
501 |
+
segmentSection.updateByData();
|
502 |
+
this.updateByData();
|
503 |
+
|
504 |
+
return true;
|
505 |
+
}
|
506 |
+
|
507 |
+
toBlob(type = "video/webm") {
|
508 |
+
return new Blob([this.source!.buffer], { type });
|
509 |
+
}
|
510 |
+
}
|
511 |
+
|
512 |
+
/**
|
513 |
+
* Fixes duration on MediaRecorder output.
|
514 |
+
* @param blob Input Blob with incorrect duration.
|
515 |
+
* @param duration Correct duration (in milliseconds).
|
516 |
+
* @param type Output blob mimetype (default: video/webm).
|
517 |
+
* @returns
|
518 |
+
*/
|
519 |
+
export const webmFixDuration = (
|
520 |
+
blob: Blob,
|
521 |
+
duration: number,
|
522 |
+
type = "video/webm",
|
523 |
+
): Promise<Blob> => {
|
524 |
+
return new Promise((resolve, reject) => {
|
525 |
+
try {
|
526 |
+
const reader = new FileReader();
|
527 |
+
|
528 |
+
reader.addEventListener("loadend", () => {
|
529 |
+
try {
|
530 |
+
const result = reader.result as ArrayBuffer;
|
531 |
+
const file = new WebmFile(new Uint8Array(result));
|
532 |
+
if (file.fixDuration(duration)) {
|
533 |
+
resolve(file.toBlob(type));
|
534 |
+
} else {
|
535 |
+
resolve(blob);
|
536 |
+
}
|
537 |
+
} catch (ex) {
|
538 |
+
reject(ex);
|
539 |
+
}
|
540 |
+
});
|
541 |
+
|
542 |
+
reader.addEventListener("error", () => reject());
|
543 |
+
|
544 |
+
reader.readAsArrayBuffer(blob);
|
545 |
+
} catch (ex) {
|
546 |
+
reject(ex);
|
547 |
+
}
|
548 |
+
});
|
549 |
+
};
|
app/hooks/useAudioManager.ts
DELETED
@@ -1,48 +0,0 @@
|
|
1 |
-
"use client";
|
2 |
-
|
3 |
-
import { useState, useCallback } from 'react';
|
4 |
-
import constants from '../constants';
|
5 |
-
|
6 |
-
const useAudioManager = () => {
|
7 |
-
const [progress, setProgress] = useState<number | undefined>(undefined);
|
8 |
-
const [audioData, setAudioData] = useState<{
|
9 |
-
buffer: AudioBuffer;
|
10 |
-
url: string;
|
11 |
-
source: any;
|
12 |
-
mimeType: string;
|
13 |
-
} | undefined>(undefined);
|
14 |
-
|
15 |
-
// Reset the audio data
|
16 |
-
const resetAudio = useCallback(() => {
|
17 |
-
setAudioData(undefined);
|
18 |
-
}, []);
|
19 |
-
|
20 |
-
// Set audio from a Blob (e.g., from recording)
|
21 |
-
const setAudioFromRecording = useCallback(async (data: Blob) => {
|
22 |
-
resetAudio();
|
23 |
-
setProgress(0);
|
24 |
-
const blobUrl = URL.createObjectURL(data);
|
25 |
-
const audioCTX = new AudioContext({sampleRate: constants.SAMPLING_RATE, });
|
26 |
-
const arrayBuffer = await data.arrayBuffer();
|
27 |
-
const decoded = await audioCTX.decodeAudioData(arrayBuffer);
|
28 |
-
setProgress(undefined);
|
29 |
-
setAudioData({
|
30 |
-
buffer: decoded,
|
31 |
-
url: blobUrl,
|
32 |
-
source: "RECORDING",
|
33 |
-
mimeType: data.type,
|
34 |
-
});
|
35 |
-
}, [resetAudio]);
|
36 |
-
|
37 |
-
// Other functionalities (e.g., setAudioFromDownload, downloadAudioFromUrl)
|
38 |
-
// can be added similarly based on your requirements
|
39 |
-
|
40 |
-
return {
|
41 |
-
audioData,
|
42 |
-
progress,
|
43 |
-
setAudioFromRecording,
|
44 |
-
// Export other functions as needed
|
45 |
-
};
|
46 |
-
};
|
47 |
-
|
48 |
-
export default useAudioManager;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/input.tsx
CHANGED
@@ -1,24 +1,13 @@
|
|
1 |
import React, { useState, useEffect, useRef } from 'react';
|
2 |
-
import MicIcon from '@mui/icons-material/Mic';
|
3 |
-
import StopIcon from '@mui/icons-material/Stop';
|
4 |
import styles from './page.module.css';
|
5 |
-
|
6 |
import useSpeechRecognition from './hooks/useSpeechRecognition';
|
7 |
-
import useAudioManager from './hooks/useAudioManager';
|
8 |
import { useMicVAD } from "@ricky0123/vad-react";
|
9 |
-
|
10 |
import * as ort from "onnxruntime-web";
|
11 |
-
|
|
|
|
|
12 |
|
13 |
-
|
14 |
-
const types = ["audio/webm", "audio/mp4", "audio/ogg", "audio/wav", "audio/aac"];
|
15 |
-
for (let type of types) {
|
16 |
-
if (MediaRecorder.isTypeSupported(type)) {
|
17 |
-
return type;
|
18 |
-
}
|
19 |
-
}
|
20 |
-
return null;
|
21 |
-
};
|
22 |
|
23 |
interface VoiceInputFormProps {
|
24 |
handleSubmit: any;
|
@@ -26,22 +15,40 @@ interface VoiceInputFormProps {
|
|
26 |
setInput: React.Dispatch<React.SetStateAction<string>>;
|
27 |
}
|
28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
const VoiceInputForm: React.FC<VoiceInputFormProps> = ({ handleSubmit, input, setInput }) => {
|
30 |
-
const [
|
31 |
-
const
|
32 |
-
const
|
33 |
|
|
|
34 |
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
35 |
-
const
|
36 |
-
|
37 |
-
const cleanupRecording = () => {
|
38 |
-
if (mediaRecorderRef.current) {
|
39 |
-
mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
|
40 |
-
mediaRecorderRef.current = null;
|
41 |
-
}
|
42 |
-
audioChunksRef.current = [];
|
43 |
-
};
|
44 |
|
|
|
45 |
|
46 |
useEffect(() => {
|
47 |
if (recognizedText) {
|
@@ -49,84 +56,122 @@ const VoiceInputForm: React.FC<VoiceInputFormProps> = ({ handleSubmit, input, se
|
|
49 |
}
|
50 |
}, [recognizedText, setInput]);
|
51 |
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
let recorderOptions = {};
|
59 |
-
|
60 |
-
// Check if the mimeType is supported; if so, use it
|
61 |
-
const mimeType = getMimeType();
|
62 |
-
if (mimeType && MediaRecorder.isTypeSupported(mimeType)) {
|
63 |
-
recorderOptions = { mimeType };
|
64 |
-
}
|
65 |
-
|
66 |
-
mediaRecorderRef.current = new MediaRecorder(stream, recorderOptions);
|
67 |
-
|
68 |
-
mediaRecorderRef.current.ondataavailable = (event: BlobEvent) => {
|
69 |
-
audioChunksRef.current.push(event.data);
|
70 |
-
};
|
71 |
-
|
72 |
-
mediaRecorderRef.current.start();
|
73 |
-
} catch (err) {
|
74 |
-
console.error("Error accessing media devices:", err);
|
75 |
-
}
|
76 |
-
};
|
77 |
-
|
78 |
|
79 |
-
|
80 |
-
|
81 |
-
const recorder = mediaRecorderRef.current;
|
82 |
-
if (recorder && recorder.state === "recording") {
|
83 |
-
recorder.onstop = () => {
|
84 |
-
const audioBlob = new Blob(audioChunksRef.current, { 'type': recorder.mimeType });
|
85 |
-
audioChunksRef.current = [];
|
86 |
-
resolve(audioBlob);
|
87 |
-
};
|
88 |
-
recorder.stop();
|
89 |
-
} else {
|
90 |
-
reject(new Error("MediaRecorder is not recording"));
|
91 |
}
|
92 |
-
}
|
93 |
-
|
|
|
|
|
94 |
|
95 |
const vad = useMicVAD({
|
96 |
modelURL: "/_next/static/chunks/silero_vad.onnx",
|
97 |
workletURL: "/_next/static/chunks/vad.worklet.bundle.min.js",
|
98 |
startOnLoad: false,
|
99 |
-
onSpeechEnd: async (
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
startListening(audioBuffer);
|
107 |
-
setIsRecording(!isRecording);
|
108 |
}
|
109 |
},
|
110 |
});
|
111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
112 |
|
113 |
-
const
|
114 |
-
if
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
|
|
|
|
|
|
|
|
121 |
}
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
126 |
}
|
127 |
-
setIsRecording(!isRecording);
|
128 |
};
|
129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
|
131 |
return (
|
132 |
<div>
|
@@ -139,17 +184,16 @@ const VoiceInputForm: React.FC<VoiceInputFormProps> = ({ handleSubmit, input, se
|
|
139 |
placeholder="Speak or type..."
|
140 |
/>
|
141 |
</form>
|
142 |
-
<button
|
143 |
-
|
144 |
-
|
|
|
|
|
|
|
|
|
145 |
</div>
|
146 |
);
|
147 |
};
|
148 |
|
149 |
-
const convertBlobToAudioBuffer = async (blob: Blob): Promise<AudioBuffer> => {
|
150 |
-
const audioContext = new AudioContext();
|
151 |
-
const arrayBuffer = await blob.arrayBuffer();
|
152 |
-
return await audioContext.decodeAudioData(arrayBuffer);
|
153 |
-
};
|
154 |
|
155 |
export default VoiceInputForm;
|
|
|
1 |
import React, { useState, useEffect, useRef } from 'react';
|
|
|
|
|
2 |
import styles from './page.module.css';
|
|
|
3 |
import useSpeechRecognition from './hooks/useSpeechRecognition';
|
|
|
4 |
import { useMicVAD } from "@ricky0123/vad-react";
|
|
|
5 |
import * as ort from "onnxruntime-web";
|
6 |
+
import MicIcon from '@mui/icons-material/Mic';
|
7 |
+
import StopIcon from '@mui/icons-material/Stop';
|
8 |
+
import { webmFixDuration } from './BlobFix';
|
9 |
|
10 |
+
ort.env.wasm.wasmPaths = "/_next/static/chunks/";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
|
12 |
interface VoiceInputFormProps {
|
13 |
handleSubmit: any;
|
|
|
15 |
setInput: React.Dispatch<React.SetStateAction<string>>;
|
16 |
}
|
17 |
|
18 |
+
function getMimeType() {
|
19 |
+
const types = [
|
20 |
+
"audio/webm",
|
21 |
+
"audio/mp4",
|
22 |
+
"audio/ogg",
|
23 |
+
"audio/wav",
|
24 |
+
"audio/aac",
|
25 |
+
];
|
26 |
+
for (let i = 0; i < types.length; i++) {
|
27 |
+
if (MediaRecorder.isTypeSupported(types[i])) {
|
28 |
+
return types[i];
|
29 |
+
}
|
30 |
+
}
|
31 |
+
return undefined;
|
32 |
+
}
|
33 |
+
|
34 |
+
const convertBlobToAudioBuffer = async (blob: Blob): Promise<AudioBuffer> => {
|
35 |
+
const audioContext = new AudioContext();
|
36 |
+
const arrayBuffer = await blob.arrayBuffer();
|
37 |
+
return await audioContext.decodeAudioData(arrayBuffer);
|
38 |
+
};
|
39 |
+
|
40 |
+
|
41 |
const VoiceInputForm: React.FC<VoiceInputFormProps> = ({ handleSubmit, input, setInput }) => {
|
42 |
+
const [recording, setRecording] = useState(false);
|
43 |
+
const [duration, setDuration] = useState(0);
|
44 |
+
const [recordedBlob, setRecordedBlob] = useState<Blob | null>(null);
|
45 |
|
46 |
+
const streamRef = useRef<MediaStream | null>(null);
|
47 |
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
48 |
+
const chunksRef = useRef<Blob[]>([]);
|
49 |
+
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
50 |
|
51 |
+
const { startListening, recognizedText } = useSpeechRecognition();
|
52 |
|
53 |
useEffect(() => {
|
54 |
if (recognizedText) {
|
|
|
56 |
}
|
57 |
}, [recognizedText, setInput]);
|
58 |
|
59 |
+
useEffect(() => {
|
60 |
+
const processRecording = async () => {
|
61 |
+
if (recordedBlob) {
|
62 |
+
// Process the blob for transcription
|
63 |
+
const audioBuffer = await convertBlobToAudioBuffer(recordedBlob);
|
64 |
+
startListening(audioBuffer); // Start the transcription process
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
|
66 |
+
// Reset the blob state if you want to prepare for a new recording
|
67 |
+
setRecordedBlob(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
}
|
69 |
+
};
|
70 |
+
|
71 |
+
processRecording();
|
72 |
+
}, [recordedBlob, startListening]);
|
73 |
|
74 |
const vad = useMicVAD({
|
75 |
modelURL: "/_next/static/chunks/silero_vad.onnx",
|
76 |
workletURL: "/_next/static/chunks/vad.worklet.bundle.min.js",
|
77 |
startOnLoad: false,
|
78 |
+
onSpeechEnd: async () => {
|
79 |
+
if (recording) {
|
80 |
+
await stopRecording(); // Stop the recording
|
81 |
+
|
82 |
+
console.log('input', input);
|
83 |
+
|
84 |
+
setRecording(!recording); // Update the recording state
|
|
|
|
|
85 |
}
|
86 |
},
|
87 |
});
|
88 |
|
89 |
+
const stopRecording = () => {
|
90 |
+
if (
|
91 |
+
mediaRecorderRef.current &&
|
92 |
+
mediaRecorderRef.current.state === "recording"
|
93 |
+
) {
|
94 |
+
mediaRecorderRef.current.stop(); // set state to inactive
|
95 |
+
setDuration(0);
|
96 |
+
setRecording(false);
|
97 |
+
vad.pause();
|
98 |
+
}
|
99 |
+
};
|
100 |
|
101 |
+
const startRecording = async () => {
|
102 |
+
// Reset recording (if any)
|
103 |
+
setRecordedBlob(null);
|
104 |
+
vad.start();
|
105 |
+
|
106 |
+
let startTime = Date.now();
|
107 |
+
|
108 |
+
try {
|
109 |
+
if (!streamRef.current) {
|
110 |
+
streamRef.current = await navigator.mediaDevices.getUserMedia({
|
111 |
+
audio: true,
|
112 |
+
});
|
113 |
}
|
114 |
+
|
115 |
+
const mimeType = getMimeType();
|
116 |
+
const mediaRecorder = new MediaRecorder(streamRef.current, {
|
117 |
+
mimeType,
|
118 |
+
});
|
119 |
+
|
120 |
+
mediaRecorderRef.current = mediaRecorder;
|
121 |
+
|
122 |
+
mediaRecorder.addEventListener("dataavailable", async (event) => {
|
123 |
+
if (event.data.size > 0) {
|
124 |
+
chunksRef.current.push(event.data);
|
125 |
+
}
|
126 |
+
if (mediaRecorder.state === "inactive") {
|
127 |
+
const duration = Date.now() - startTime;
|
128 |
+
|
129 |
+
// Received a stop event
|
130 |
+
let blob = new Blob(chunksRef.current, { type: mimeType });
|
131 |
+
|
132 |
+
if (mimeType === "audio/webm") {
|
133 |
+
blob = await webmFixDuration(blob, duration, blob.type);
|
134 |
+
}
|
135 |
+
|
136 |
+
setRecordedBlob(blob);
|
137 |
+
|
138 |
+
chunksRef.current = [];
|
139 |
+
}
|
140 |
+
});
|
141 |
+
mediaRecorder.start();
|
142 |
+
setRecording(true);
|
143 |
+
} catch (error) {
|
144 |
+
console.error("Error accessing microphone:", error);
|
145 |
}
|
|
|
146 |
};
|
147 |
|
148 |
+
useEffect(() => {
|
149 |
+
let stream: MediaStream | null = null;
|
150 |
+
|
151 |
+
if (recording) {
|
152 |
+
const timer = setInterval(() => {
|
153 |
+
setDuration((prevDuration) => prevDuration + 1);
|
154 |
+
}, 1000);
|
155 |
+
|
156 |
+
return () => {
|
157 |
+
clearInterval(timer);
|
158 |
+
};
|
159 |
+
}
|
160 |
+
|
161 |
+
return () => {
|
162 |
+
if (stream) {
|
163 |
+
stream.getTracks().forEach((track) => track.stop());
|
164 |
+
}
|
165 |
+
};
|
166 |
+
}, [recording]);
|
167 |
+
|
168 |
+
const handleToggleRecording = () => {
|
169 |
+
if (recording) {
|
170 |
+
stopRecording();
|
171 |
+
} else {
|
172 |
+
startRecording();
|
173 |
+
}
|
174 |
+
};
|
175 |
|
176 |
return (
|
177 |
<div>
|
|
|
184 |
placeholder="Speak or type..."
|
185 |
/>
|
186 |
</form>
|
187 |
+
<button
|
188 |
+
type='button'
|
189 |
+
className={styles.button}
|
190 |
+
onClick={handleToggleRecording}
|
191 |
+
>
|
192 |
+
{recording ? <StopIcon /> : <MicIcon />}
|
193 |
+
</button>
|
194 |
</div>
|
195 |
);
|
196 |
};
|
197 |
|
|
|
|
|
|
|
|
|
|
|
198 |
|
199 |
export default VoiceInputForm;
|